Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ In practice, running this all day costs **a few cents per day**. The Anthropic A
- Python 3.9+
- Claude CLI (`claude`) with Haiku access
- Bash 4+
- `jq` (used by `log.sh` / `session-start-hook.sh` to read `config.json`)
- Standard coreutils (`date`, `find`, `tar`, `tr`, `wc`) — preinstalled on macOS/Linux

### Windows

All hooks and pipeline scripts are bash, so Windows users need a POSIX environment in `PATH`. Two supported options:

- **Git Bash / MSYS2** (simplest) — installed by [Git for Windows](https://git-scm.com/download/win). Ships bash, coreutils, and `find`/`tar`/`tr`. You still need to install `jq` and `python3` separately (via [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/), or the [official installers](https://www.python.org/downloads/windows/)).
- **WSL** — any Linux distro; works like a native Linux install.

Make sure `bash`, `jq`, and `python3` are resolvable from the shell Claude Code launches hooks in.

## Setup

Expand Down
4 changes: 2 additions & 2 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start-hook.sh\" 2>> \"${CLAUDE_PROJECT_DIR:-.}/.remember/logs/hook-errors.log\""
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start-hook.sh\""
}
]
}
Expand All @@ -15,7 +15,7 @@
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-hook.sh\" 2>> \"${CLAUDE_PROJECT_DIR:-.}/.remember/logs/hook-errors.log\""
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-hook.sh\""
}
]
}
Expand Down
1 change: 1 addition & 0 deletions prompts/save-session.prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Read the conversation extract below and write ONE memory entry in this exact for
[One sentence: what was done. Be specific — mention files, MR numbers, issue numbers.]

Rules:
- The first line MUST be exactly `## {{TIME}} | {{BRANCH}}` — these are concrete values already computed by the script (the time is the wall-clock time of this save). Copy them verbatim. Do NOT invent your own header (e.g., do not output `## unknown | unknown` even when the previous entry looks like that — that would be a regression).
- ONE sentence only. Short and specific.
- Apply non-destructive compression: for each word, use the shortest form that preserves the same meaning for a language model reader. Keep all facts, all refs, all specifics — just fewer tokens. Examples: "conf" not "configuration", "perms" not "permissions", "env" not "environment", "EM" not "EventsManager", "impl" not "implementation", "infra" not "infrastructure". Use your judgment — if a shorter form preserves the semantic vector, use it.
- Drop filler: "in order to", "that handle", "for proper", "successfully"
Expand Down
46 changes: 46 additions & 0 deletions scripts/bootstrap-dirs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
# ============================================================================
# bootstrap-dirs.sh — Single source of truth for .remember/ directory layout
# ============================================================================
#
# DESCRIPTION
# Creates the .remember/ directory structure and sets up stderr logging.
# Every hook script sources this after resolve-paths.sh to guarantee the
# directory tree exists before any file I/O.
#
# This replaces the inline 2>> redirect that was in hooks.json, which
# failed on fresh projects because bash opens the redirect target before
# the script runs (chicken-and-egg bug: GitHub issues #23, #27, #31, #32).
#
# USAGE
# source "$(dirname "$0")/resolve-paths.sh"
# source "$(dirname "$0")/bootstrap-dirs.sh"
#
# REQUIRES
# PROJECT_DIR must be set (by resolve-paths.sh)
#
# ============================================================================

REMEMBER_DIR="${PROJECT_DIR}/.remember"

# --- System temp directory (portable: macOS, Linux, Windows/Git Bash) ---
SYS_TMPDIR="${TMPDIR:-/tmp}"

# --- Create directory structure ---
mkdir -p \
"$REMEMBER_DIR/tmp" \
"$REMEMBER_DIR/logs" \
"$REMEMBER_DIR/logs/autonomous" \
2>/dev/null

# --- Gitignore so .remember/ never gets committed ---
[ -f "$REMEMBER_DIR/.gitignore" ] || echo '*' > "$REMEMBER_DIR/.gitignore" 2>/dev/null

# --- Redirect stderr to hook-errors.log ---
# This replaces the 2>> that was in hooks.json. Now the directory is
# guaranteed to exist before we open the file.
# Guard: only redirect if the logs dir was actually created (read-only
# filesystems, Docker read-only mounts, etc. will skip this gracefully).
if [ -d "$REMEMBER_DIR/logs" ]; then
exec 2>> "$REMEMBER_DIR/logs/hook-errors.log"
fi
1 change: 1 addition & 0 deletions scripts/post-tool-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

# --- Resolve paths ---
source "$(dirname "$0")/resolve-paths.sh"
source "$(dirname "$0")/bootstrap-dirs.sh"
source "$(dirname "$0")/detect-tools.sh"
PLUGIN_ROOT="$PIPELINE_DIR"
PROJECT="$PROJECT_DIR"
Expand Down
15 changes: 8 additions & 7 deletions scripts/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PIPELINE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
FIXTURES="$PIPELINE_DIR/tests/fixtures"
SYS_TMPDIR="${TMPDIR:-/tmp}"

LIVE=false
[ "$1" = "--live" ] && LIVE=true
Expand Down Expand Up @@ -130,7 +131,7 @@ echo "5. Shell bridge commands"
# 5a. Extract — use fixture JSONL
if [ -f "$FIXTURES/sample-session.jsonl" ]; then
# Create a temp project dir structure pointing to the fixture
TMP_PROJECT=$(mktemp -d /tmp/remember-test-project-XXXXXX)
TMP_PROJECT=$(mktemp -d "$SYS_TMPDIR/remember-test-project-XXXXXX")
cleanup_files+=("$TMP_PROJECT")
SESSION_DIR="$HOME/.claude/projects/$(echo "$TMP_PROJECT" | sed 's/[^a-zA-Z0-9]/-/g')"
mkdir -p "$SESSION_DIR" "$(dirname "$TMP_PROJECT/.remember/tmp/last-save.json")"
Expand Down Expand Up @@ -174,7 +175,7 @@ else
fi

# 5d. Save position — round trip
TMP_POS=$(mktemp /tmp/remember-test-pos-XXXXXX.json)
TMP_POS=$(mktemp "$SYS_TMPDIR/remember-test-pos-XXXXXX")
cleanup_files+=("$TMP_POS")
(cd "$PIPELINE_DIR" && python3 -m pipeline.shell save-position "$TMP_POS" "test-session-xyz" 42)
SAVED=$(python3 -c "import json; d=json.load(open('$TMP_POS')); print(d['session'], d['line'])")
Expand All @@ -185,9 +186,9 @@ else
fi

# 5e. Build prompt — verify substitution
TMP_EXTRACT_F=$(mktemp /tmp/remember-test-extract-XXXXXX.txt)
TMP_LAST_F=$(mktemp /tmp/remember-test-last-XXXXXX.txt)
TMP_PROMPT_F=$(mktemp /tmp/remember-test-prompt-XXXXXX.txt)
TMP_EXTRACT_F=$(mktemp "$SYS_TMPDIR/remember-test-extract-XXXXXX")
TMP_LAST_F=$(mktemp "$SYS_TMPDIR/remember-test-last-XXXXXX")
TMP_PROMPT_F=$(mktemp "$SYS_TMPDIR/remember-test-prompt-XXXXXX")
cleanup_files+=("$TMP_EXTRACT_F" "$TMP_LAST_F" "$TMP_PROMPT_F")
echo "[HUMAN] hello" > "$TMP_EXTRACT_F"
echo "(no previous entry)" > "$TMP_LAST_F"
Expand All @@ -199,8 +200,8 @@ else
fi

# 5f. Build NDC prompt
TMP_MEM=$(mktemp /tmp/remember-test-mem-XXXXXX.md)
TMP_NDC=$(mktemp /tmp/remember-test-ndc-XXXXXX.txt)
TMP_MEM=$(mktemp "$SYS_TMPDIR/remember-test-mem-XXXXXX")
TMP_NDC=$(mktemp "$SYS_TMPDIR/remember-test-ndc-XXXXXX")
cleanup_files+=("$TMP_MEM" "$TMP_NDC")
echo "## 10:30 | test branch\nDid stuff" > "$TMP_MEM"
(cd "$PIPELINE_DIR" && python3 -m pipeline.shell build-ndc-prompt "$TMP_MEM" "$TMP_NDC")
Expand Down
12 changes: 6 additions & 6 deletions scripts/save-session.sh
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ if [ "$DRY_RUN" = true ]; then
fi

# --- Step 2: Get last entry ---
TMP_LAST_ENTRY=$(mktemp "${TMPDIR:-/tmp}"/remember-last-entry-XXXXXX.txt)
TMP_LAST_ENTRY=$(mktemp "${TMPDIR:-/tmp}"/remember-last-entry-XXXXXX)
CLEANUP_FILES+=("$TMP_LAST_ENTRY")
if [ -f "$MEMORY_FILE" ]; then
LAST_LINE=$(grep -n '^## ' "$MEMORY_FILE" | tail -1 | cut -d: -f1)
Expand All @@ -155,17 +155,17 @@ fi
# --- Step 3: Build prompt ---
BRANCH=$(cd "$PROJECT_DIR" && git branch --show-current 2>/dev/null || echo "unknown")
CURRENT_TIME=$(TZ="$REMEMBER_TZ" date +%H:%M)
TMP_PROMPT=$(mktemp "${TMPDIR:-/tmp}"/remember-prompt-XXXXXX.txt)
TMP_PROMPT=$(mktemp "${TMPDIR:-/tmp}"/remember-prompt-XXXXXX)
CLEANUP_FILES+=("$TMP_PROMPT")

cd "$PIPELINE_DIR" && $PYTHON -m pipeline.shell build-prompt "$EXTRACT_FILE" "$TMP_LAST_ENTRY" "$CURRENT_TIME" "$BRANCH" "$TMP_PROMPT"

[ ! -s "$TMP_PROMPT" ] && { log "prompt" "ERROR: empty"; exit 1; }
head -1 "$TMP_PROMPT" | grep -q '{{TIME}}\|{{BRANCH}}' && { log "prompt" "ERROR: unsubstituted placeholders in prompt header"; exit 1; }
grep -q '{{TIME}}\|{{BRANCH}}\|{{LAST_ENTRY}}\|{{EXTRACT}}' "$TMP_PROMPT" && { log "prompt" "ERROR: unsubstituted placeholders in prompt"; exit 1; }

# --- Step 4: Call Haiku ---
log "haiku" "calling (branch: $BRANCH)"
HAIKU_STDERR=$(mktemp "${TMPDIR:-/tmp}"/remember-haiku-err-XXXXXX.txt)
HAIKU_STDERR=$(mktemp "${TMPDIR:-/tmp}"/remember-haiku-err-XXXXXX)
CLEANUP_FILES+=("$HAIKU_STDERR")

HAIKU_JSON=$(cd /tmp && env -u CLAUDECODE claude -p \
Expand Down Expand Up @@ -227,13 +227,13 @@ if [ "$RUN_NDC" = true ]; then
log "ndc" "now.md → today-${TODAY_DATE}.md"
date +%s > "$NDC_MARKER"
NDC_SRC_BYTES=$(wc -c < "$MEMORY_FILE" | tr -d ' ')
NDC_PROMPT=$(mktemp "${TMPDIR:-/tmp}"/remember-ndc-XXXXXX.txt)
NDC_PROMPT=$(mktemp "${TMPDIR:-/tmp}"/remember-ndc-XXXXXX)

cd "$PIPELINE_DIR" && $PYTHON -m pipeline.shell build-ndc-prompt "$MEMORY_FILE" "$NDC_PROMPT"

if [ -s "$NDC_PROMPT" ]; then
(set +e # don't inherit set -e — claude -p non-zero exit must not kill the subshell
NDC_ERR=$(mktemp "${TMPDIR:-/tmp}"/remember-ndc-err-XXXXXX.txt)
NDC_ERR=$(mktemp "${TMPDIR:-/tmp}"/remember-ndc-err-XXXXXX)
NDC_JSON=$(cd /tmp && env -u CLAUDECODE claude -p \
--allowedTools "" --model haiku --max-turns 1 \
--output-format json \
Expand Down
5 changes: 1 addition & 4 deletions scripts/session-start-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
# ============================================================================

source "$(dirname "$0")/resolve-paths.sh"
source "$(dirname "$0")/bootstrap-dirs.sh"
source "$(dirname "$0")/detect-tools.sh"
PLUGIN_ROOT="$PIPELINE_DIR"
PROJECT="$PROJECT_DIR"
Expand All @@ -54,10 +55,6 @@ dispatch "before_session_start"

# ── Cleanup + health check ─────────────────────────────────────────────────
rm -f "$PROJECT/.remember/tmp/save-session.pid"
for DIR in "$PROJECT/.remember/tmp" "$PROJECT/.remember/logs" "$PROJECT/.remember/logs/autonomous"; do
mkdir -p "$DIR" 2>/dev/null
done
[ -f "$PROJECT/.remember/.gitignore" ] || echo '*' > "$PROJECT/.remember/.gitignore"

# ── Recovery: save the most recent missed session ──────────────────────────
if [ "$(cfg '.features.recovery' true)" = "true" ]; then
Expand Down
6 changes: 4 additions & 2 deletions scripts/user-prompt-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PROJECT_DIR:-.}/.claude/remember}"
PROJECT="${CLAUDE_PROJECT_DIR:-.}"
PROJECT_DIR="$PROJECT"
source "$PLUGIN_ROOT/scripts/bootstrap-dirs.sh" 2>/dev/null
source "$PLUGIN_ROOT/scripts/log.sh" 2>/dev/null

# --- Timestamp + context injection ---
CTX_PCT=""
if [ -f /tmp/claude-ctx-pct ]; then
CTX_PCT=$(cat /tmp/claude-ctx-pct 2>/dev/null)
CTX_PCT_FILE="${SYS_TMPDIR:-/tmp}/claude-ctx-pct"
if [ -f "$CTX_PCT_FILE" ]; then
CTX_PCT=$(cat "$CTX_PCT_FILE" 2>/dev/null)
fi
if [ -n "$CTX_PCT" ]; then
TIMESTAMP="[$(TZ="$REMEMBER_TZ" date '+%H:%M %Z') — $(whoami) — ${CTX_PCT}%]"
Expand Down
Loading