diff --git a/MEMORY.md b/MEMORY.md index bc33e56..12a3cb4 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -13,9 +13,12 @@ - [git-status-report](memory/script-git-status-report.md) — Scans directories for git repos, reports uncommitted changes and remote sync (lives in small-scripts) - [statusline](memory/script-statusline.md) — Status bar scripts: statusline.sh (renderer) and set-topic.sh (topic setter) for per-session topic display - [require-plan-file](memory/script-require-plan-file.md) — PreToolUse hook: blocks ExitPlanMode unless a *-PLAN.md file exists in the project root +- [extract-transcripts](memory/script-extract-transcripts.md) — Manages transcript backup tracking: register, list, extract, mark-processed. Used by /log and pre-compact hook +- [list-transcripts-here](memory/script-list-transcripts-here.md) — Wrapper calling extract-transcripts.py --list $(pwd); exists to avoid $() in SKILL.md bang commands ## Skills +- [/end-session](memory/skill-end-session.md) — End-of-session wrap-up: session log + CONTEXT.md update for seamless resumption - [/log](memory/skill-log.md) — End-of-session logging to memory/log/ for later reflection - [/reflect-logs](memory/skill-reflect-logs.md) — Processes session logs into topic memory files (gotchas, decisions, process lessons) - [/reflect](memory/skill-reflect.md) — Milestone reflection: reviews conversation + git log, produces structured artifacts diff --git a/hooks/pre-compact-backup.sh b/hooks/pre-compact-backup.sh index 999f4e3..096f160 100755 --- a/hooks/pre-compact-backup.sh +++ b/hooks/pre-compact-backup.sh @@ -1,17 +1,36 @@ #!/usr/bin/env bash # Pre-compact hook: save a copy of the session transcript before compaction. -# Claude Code pipes JSON to stdin with session_id, transcript_path, cwd, etc. +# Claude Code pipes JSON with session_id, cwd, hook_event_name (no transcript_path). +# Derives the transcript location from session_id + cwd. +# After saving, registers the backup in ~/.claude/transcript-backups/tracking.json. set -euo pipefail -# Parse stdin JSON for transcript path and session ID +# Parse stdin JSON INPUT="$(cat)" -TRANSCRIPT_PATH="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null)" SESSION_ID="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)" -TRIGGER="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('trigger','unknown'))" 2>/dev/null)" +CWD="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('cwd',''))" 2>/dev/null)" +PROVIDED_PATH="$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null)" -if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then - echo "pre-compact-backup: no transcript found, skipping" >&2 +if [ -z "$SESSION_ID" ]; then + echo "pre-compact-backup: missing session_id in hook input, skipping" >&2 + exit 0 +fi + +# Try provided path first (older Claude Code versions), then derive from session_id + cwd +if [ -n "$PROVIDED_PATH" ] && [ -f "$PROVIDED_PATH" ]; then + TRANSCRIPT_PATH="$PROVIDED_PATH" +elif [ -n "$CWD" ]; then + # Derive transcript path: /home/paul/dev/claude -> -home-paul-dev-claude + PROJECT_DIR="$(echo "$CWD" | tr '/' '-')" + TRANSCRIPT_PATH="$HOME/.claude/projects/${PROJECT_DIR}/${SESSION_ID}.jsonl" +else + echo "pre-compact-backup: cannot locate transcript (no cwd or transcript_path), skipping" >&2 + exit 0 +fi + +if [ ! -f "$TRANSCRIPT_PATH" ]; then + echo "pre-compact-backup: transcript not found at ${TRANSCRIPT_PATH}, skipping" >&2 exit 0 fi @@ -20,11 +39,21 @@ BACKUP_DIR="$HOME/.claude/transcript-backups" mkdir -p "$BACKUP_DIR" TIMESTAMP="$(date +%Y%m%d-%H%M%S)" -BACKUP_FILE="${BACKUP_DIR}/${TIMESTAMP}-${SESSION_ID:0:8}-${TRIGGER}.jsonl" +BACKUP_NAME="${TIMESTAMP}-${SESSION_ID:0:8}.jsonl" +BACKUP_FILE="${BACKUP_DIR}/${BACKUP_NAME}" cp "$TRANSCRIPT_PATH" "$BACKUP_FILE" # Prune backups older than 30 days find "$BACKUP_DIR" -name "*.jsonl" -mtime +30 -delete 2>/dev/null || true -echo "pre-compact-backup: saved $(wc -l < "$BACKUP_FILE") lines to ${BACKUP_FILE##*/}" >&2 +# Register backup in tracking.json using the register helper +python3 "$HOME/.claude/scripts/extract-transcripts.py" \ + --register "$BACKUP_NAME" \ + --session-id "$SESSION_ID" \ + --cwd "$CWD" \ + 2>/dev/null \ + && TRACKED=" [tracked]" \ + || TRACKED=" [tracking failed]" + +echo "pre-compact-backup: saved $(wc -l < "$BACKUP_FILE") lines to ${BACKUP_NAME} [project: ${CWD}]${TRACKED}" >&2 diff --git a/memory/script-extract-transcripts.md b/memory/script-extract-transcripts.md new file mode 100644 index 0000000..186e97c --- /dev/null +++ b/memory/script-extract-transcripts.md @@ -0,0 +1,41 @@ +# script: extract-transcripts + +**Location:** `claude-foundations/scripts/extract-transcripts.py` +**Symlinked to:** `~/.claude/scripts/extract-transcripts.py` + +## Purpose + +Manages the transcript backup tracking system. Registers pre-compaction JSONL backups, lists unprocessed ones per project, extracts readable conversation text for analysis, and marks entries as processed after a log is written. + +## Usage + +```bash +# Register a new backup (called by pre-compact-backup.sh hook) +python3 ~/.claude/scripts/extract-transcripts.py \ + --register BACKUP_NAME --session-id SESSION_ID --cwd PROJECT_CWD + +# List unprocessed transcripts for a project (used by /log skill) +python3 ~/.claude/scripts/extract-transcripts.py --list /home/paul/dev/claude/projects/foo +# → compact JSON array of {backup, session_id, saved_at, path, exists} + +# Extract readable conversation from one backup (used by /log Sonnet subagent) +python3 ~/.claude/scripts/extract-transcripts.py --extract BACKUP_NAME + +# Mark all unprocessed backups for a project as done +python3 ~/.claude/scripts/extract-transcripts.py \ + --mark-all-processed /home/paul/dev/claude/projects/foo --log-file memory/log/YYYY-MM-DD.HHMMSS-transcripts.md +``` + +## How it works + +- Tracking state lives in `~/.claude/transcript-backups/tracking.json`, keyed by backup filename +- Each entry: `{session_id, cwd, saved_at, processed, log_file, processed_at}` +- `--list` filters by `cwd == project_cwd` and `processed == false` — output is metadata only (no content), safe for skill pre-gathering +- `--extract` reads the JSONL, skips sidechain entries, extracts `user`/`assistant` message text, truncates per-message at 3000 chars +- Tool use blocks are summarised as `[tool: ToolName(key=...)]` rather than shown in full + +## Gotchas + +- `sys.exit(0)` inside a bare `except: pass` block is caught as SystemExit — always use specific exception types or `break` when early exit is needed inside exception handlers +- The JSONL entries for `type: "user"` without a `message` field (e.g., file-history snapshots) are skipped silently +- Old backups with `-auto` suffix (from before this system) are not in tracking.json and will never appear in `--list` diff --git a/memory/script-install-hooks.md b/memory/script-install-hooks.md index 097f51b..5b57672 100644 --- a/memory/script-install-hooks.md +++ b/memory/script-install-hooks.md @@ -4,7 +4,7 @@ ## Purpose -Symlinks all hook scripts from `claude-foundations/hooks/` into `~/.claude/hooks/` and prints the `settings.json` configuration to add. +Symlinks all hook scripts from `claude-foundations/hooks/` into `~/.claude/hooks/`, and symlinks skill-helper scripts from `claude-foundations/scripts/` into `~/.claude/scripts/`. Also prints the `settings.json` configuration to add. ## Usage @@ -13,15 +13,27 @@ cd ~/dev/claude/projects/claude-foundations scripts/install-hooks.sh ``` -One-time setup. Re-run after adding new hooks. +One-time setup. Re-run after adding new hooks or skill-helper scripts. ## How it works -1. Iterates over `hooks/*.sh` -2. Creates symlinks in `~/.claude/hooks/` (force-overwrites existing) +1. Iterates over `hooks/*.sh`, creates symlinks in `~/.claude/hooks/` (force-overwrites) +2. Iterates over a curated list of skill-helper scripts, creates symlinks in `~/.claude/scripts/` 3. Prints the JSON config for `~/.claude/settings.json` covering PreCompact, PostToolUse, and PreToolUse matchers +## Skill-helper scripts vs regular scripts + +Not all `claude-foundations/scripts/` go into `~/.claude/scripts/`. Only scripts that skills reference via `~/.claude/scripts/` are installed there. The `SKILL_HELPERS` array in `install-hooks.sh` is the authoritative list. Currently: `extract-transcripts.py`, `list-transcripts-here.sh`. + +Regular scripts (`statusline.sh`, `set-topic.sh`, etc.) are accessed via their full path in `claude-foundations/scripts/`. + +## Profile notes + +- Hooks: `~/.claude/hooks/` is referenced in all profile `settings.json` files — install once, works everywhere +- Scripts: `~/.claude/scripts/` is the only location skills reference — no per-profile script dirs needed + ## Gotchas - The printed JSON must be manually added to `settings.json` — the script doesn't edit it automatically. -- Symlinks mean the hook code stays in the repo; updates take effect immediately without re-running the script. +- Symlinks mean the hook/script code stays in the repo; updates take effect immediately without re-running the script. +- Adding a new skill-helper script requires updating the `SKILL_HELPERS` array in `install-hooks.sh` and re-running it. diff --git a/memory/script-list-transcripts-here.md b/memory/script-list-transcripts-here.md new file mode 100644 index 0000000..7a7f022 --- /dev/null +++ b/memory/script-list-transcripts-here.md @@ -0,0 +1,26 @@ +# script: list-transcripts-here + +**Location:** `claude-foundations/scripts/list-transcripts-here.sh` +**Symlinked to:** `~/.claude/scripts/list-transcripts-here.sh` + +## Purpose + +Thin wrapper around `extract-transcripts.py --list "$(pwd)"`. Exists because SKILL.md bang commands (`!`command``) cannot use `$()` substitution — the Claude Code permission checker rejects it. The wrapper runs the substitution internally (in bash, where it's fine) and outputs the result. + +## Usage + +```bash +# From within a project directory +bash ~/.claude/scripts/list-transcripts-here.sh +# → JSON array of unprocessed transcript backups for the current cwd +``` + +## How it works + +Single line: `python3 "$HOME/.claude/scripts/extract-transcripts.py" --list "$(pwd)"` + +## Gotchas + +- Must be called with `bash list-transcripts-here.sh` (not `python3`) — it's a shell wrapper, not a Python script +- Output is the same as `extract-transcripts.py --list`, so check that script's docs for the JSON format +- The `$()` substitution is the entire reason this wrapper exists; if that restriction is ever lifted from SKILL.md bang commands, this wrapper can be removed diff --git a/memory/skill-log.md b/memory/skill-log.md index f751559..11dd104 100644 --- a/memory/skill-log.md +++ b/memory/skill-log.md @@ -12,22 +12,31 @@ End-of-session logging. Captures key decisions, gotchas, open questions, and pro /log ``` -No arguments. Reviews the full conversation history automatically. +No arguments. Reviews the full conversation history and any pre-compaction transcript backups automatically. ## How it works 1. Creates `memory/log/` if needed 2. Reviews the conversation and extracts: Summary, Decisions, Gotchas (tagged with `[topic]`), Open Questions, Key Context, Process Notes -3. Writes a structured log file — empty sections are omitted -4. Prunes old logs: deletes reflected logs older than `retention_days` (default 7), warns about unreflected logs older than `warn_unreflected_days` (default 14) +3. Writes a structured in-context log — empty sections are omitted +4. **Transcript analysis (when backups exist):** Checks `~/.claude/transcript-backups/tracking.json` for unprocessed pre-compaction snapshots for the current project. If found, spawns a **Sonnet** subagent to read the JSONL backups (via `extract-transcripts.py --extract`) and write a companion log `HHMMSS-transcripts.md`. The subagent then marks backups as processed in `tracking.json`. Sonnet is used (not Haiku) because gotcha detection requires judgment about backtracking and failed attempts. +5. Prunes old logs: deletes reflected logs older than `retention_days` (default 7), warns about unreflected logs older than `warn_unreflected_days` (default 14) + +## Dependencies + +- `~/.claude/scripts/list-transcripts-here.sh` — pre-gathers unprocessed transcript metadata (small JSON, no context overflow) +- `~/.claude/scripts/extract-transcripts.py` — reads JSONL backups and manages tracking.json (used by the Sonnet subagent) +- Both scripts live in `claude-foundations/scripts/`, symlinked into `~/.claude/scripts/` ## Part of the knowledge pipeline `/log` → `/reflect-logs` → `/distill-best-practices` -Raw session logs are input for `/reflect-logs`, which routes entries into topic memory files. +Raw session logs (and transcript companion logs) are input for `/reflect-logs`, which routes entries into topic memory files. ## Gotchas - Trivial sessions can be skipped — the skill says so if nothing worth logging happened - `[topic]` tags on gotchas should match existing memory file topics for routing by `/reflect-logs` +- The Sonnet subagent writes to the project's absolute `memory/log/` path; if it uses a relative path from the wrong cwd, the file ends up in the wrong place — verify on first real run +- Pre-compaction backups from before the tracking system existed (files with `-auto` in the name) are not in `tracking.json` and won't be processed; they'll be pruned by the 30-day cleanup in the hook diff --git a/scripts/extract-transcripts.py b/scripts/extract-transcripts.py new file mode 100755 index 0000000..a62a9a7 --- /dev/null +++ b/scripts/extract-transcripts.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +Manage and extract session transcript backups for the /log skill. + +Modes: + --register BACKUP_NAME --session-id SID --cwd CWD + Register a new backup file in tracking.json (called by pre-compact hook). + + --list CWD + List unprocessed transcripts for a project as JSON (small output, for /log skill). + + --extract BACKUP_NAME + Extract readable conversation text from one backup (for subagent use). + + --mark-processed BACKUP_NAME LOG_FILE + Mark a backup as processed, recording which log file captured it. + + --mark-all-processed CWD LOG_FILE + Mark ALL unprocessed backups for a cwd as processed. + +Tracking file: ~/.claude/transcript-backups/tracking.json +""" + +import json +import os +import sys +from datetime import datetime, timezone +from pathlib import Path + +BACKUP_DIR = Path.home() / ".claude" / "transcript-backups" +TRACKING_FILE = BACKUP_DIR / "tracking.json" +MAX_TEXT_LEN = 3000 # max chars per message before truncating + + +def load_tracking(): + if TRACKING_FILE.exists(): + with open(TRACKING_FILE) as f: + return json.load(f) + return {} + + +def save_tracking(tracking): + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + with open(TRACKING_FILE, "w") as f: + json.dump(tracking, f, indent=2) + + +def now_utc(): + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def extract_text_from_content(content): + """Extract plain text from a message content field (string or list of blocks).""" + if isinstance(content, str): + return content.strip() + if isinstance(content, list): + parts = [] + for block in content: + if not isinstance(block, dict): + continue + btype = block.get("type") + if btype == "text": + text = block.get("text", "").strip() + if text: + parts.append(text) + elif btype == "tool_use": + name = block.get("name", "?") + inp = block.get("input", {}) + keys = list(inp.keys())[:3] if isinstance(inp, dict) else [] + summary = ", ".join(f"{k}=..." for k in keys) if keys else "..." + parts.append(f"[tool: {name}({summary})]") + return " ".join(parts) + return "" + + +def extract_conversation(jsonl_path): + """Read a JSONL transcript; return list of (role, text) tuples.""" + turns = [] + try: + with open(jsonl_path) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + + if entry.get("type") not in ("user", "assistant"): + continue + if entry.get("isSidechain"): + continue + + msg = entry.get("message", {}) + if not isinstance(msg, dict): + continue + + role = msg.get("role", entry.get("type", "?")) + content = msg.get("content", "") + text = extract_text_from_content(content) + if not text: + continue + + if len(text) > MAX_TEXT_LEN: + text = text[:MAX_TEXT_LEN] + f"... [truncated, {len(text)} chars total]" + + turns.append((role, text)) + except Exception as e: + turns.append(("error", f"Failed to read transcript: {e}")) + return turns + + +# --------------------------------------------------------------------------- +# Modes +# --------------------------------------------------------------------------- + +def cmd_register(args): + """Register a new backup in tracking.json.""" + try: + idx = args.index("--register") + backup_name = args[idx + 1] + session_id = args[args.index("--session-id") + 1] + cwd = args[args.index("--cwd") + 1] + except (ValueError, IndexError) as e: + print(f"Usage: --register BACKUP_NAME --session-id SID --cwd CWD", file=sys.stderr) + sys.exit(1) + + tracking = load_tracking() + tracking[backup_name] = { + "session_id": session_id, + "cwd": cwd, + "saved_at": now_utc(), + "processed": False, + "log_file": None, + } + save_tracking(tracking) + print(f"Registered {backup_name} for {cwd}") + + +def cmd_list(cwd): + """List unprocessed transcripts for a cwd as compact JSON.""" + tracking = load_tracking() + unprocessed = [ + { + "backup": fname, + "session_id": entry["session_id"][:8], + "saved_at": entry.get("saved_at", "?"), + "path": str(BACKUP_DIR / fname), + "exists": (BACKUP_DIR / fname).exists(), + } + for fname, entry in sorted(tracking.items(), key=lambda x: x[1].get("saved_at", "")) + if entry.get("cwd") == cwd and not entry.get("processed", False) + ] + print(json.dumps(unprocessed, indent=2)) + + +def cmd_extract(backup_name): + """Extract readable conversation from one backup file.""" + backup_path = BACKUP_DIR / backup_name + if not backup_path.exists(): + print(f"Backup not found: {backup_path}", file=sys.stderr) + sys.exit(1) + + tracking = load_tracking() + entry = tracking.get(backup_name, {}) + + print(f"{'='*72}") + print(f"Transcript: {backup_name} (pre-compaction snapshot)") + print(f"Project: {entry.get('cwd', 'unknown')}") + print(f"Session: {entry.get('session_id', '?')[:8]} | Saved: {entry.get('saved_at', '?')}") + print(f"{'='*72}") + + turns = extract_conversation(backup_path) + if not turns: + print("[no readable conversation found]") + return + + for role, text in turns: + label = "USER" if role == "user" else "ASST" + print(f"\n[{label}] {text}") + + print(f"\n[{len(turns)} turns extracted]") + + +def cmd_mark_processed(backup_name, log_file): + """Mark one backup as processed.""" + tracking = load_tracking() + if backup_name not in tracking: + print(f"Warning: {backup_name} not found in tracking, adding entry", file=sys.stderr) + tracking[backup_name] = {} + tracking[backup_name]["processed"] = True + tracking[backup_name]["processed_at"] = now_utc() + tracking[backup_name]["log_file"] = log_file + save_tracking(tracking) + print(f"Marked {backup_name} as processed → {log_file}") + + +def cmd_mark_all_processed(cwd, log_file): + """Mark all unprocessed backups for a cwd as processed.""" + tracking = load_tracking() + count = 0 + for fname, entry in tracking.items(): + if entry.get("cwd") == cwd and not entry.get("processed", False): + tracking[fname]["processed"] = True + tracking[fname]["processed_at"] = now_utc() + tracking[fname]["log_file"] = log_file + count += 1 + save_tracking(tracking) + print(f"Marked {count} transcript(s) as processed → {log_file}") + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + args = sys.argv[1:] + + if not args: + print(__doc__) + sys.exit(1) + + if "--register" in args: + cmd_register(args) + elif "--list" in args: + idx = args.index("--list") + cwd = args[idx + 1] if idx + 1 < len(args) else "" + if not cwd: + print("Usage: --list CWD", file=sys.stderr) + sys.exit(1) + cmd_list(cwd) + elif "--extract" in args: + idx = args.index("--extract") + backup_name = args[idx + 1] if idx + 1 < len(args) else "" + if not backup_name: + print("Usage: --extract BACKUP_NAME", file=sys.stderr) + sys.exit(1) + cmd_extract(backup_name) + elif "--mark-all-processed" in args: + idx = args.index("--mark-all-processed") + cwd = args[idx + 1] if idx + 1 < len(args) else "" + log_idx = args.index("--log-file") if "--log-file" in args else -1 + log_file = args[log_idx + 1] if log_idx >= 0 and log_idx + 1 < len(args) else "" + if not cwd or not log_file: + print("Usage: --mark-all-processed CWD --log-file LOG_FILE", file=sys.stderr) + sys.exit(1) + cmd_mark_all_processed(cwd, log_file) + elif "--mark-processed" in args: + idx = args.index("--mark-processed") + backup_name = args[idx + 1] if idx + 1 < len(args) else "" + log_idx = args.index("--log-file") if "--log-file" in args else -1 + log_file = args[log_idx + 1] if log_idx >= 0 and log_idx + 1 < len(args) else "" + if not backup_name or not log_file: + print("Usage: --mark-processed BACKUP_NAME --log-file LOG_FILE", file=sys.stderr) + sys.exit(1) + cmd_mark_processed(backup_name, log_file) + else: + print(f"Unknown arguments: {args}", file=sys.stderr) + print(__doc__) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh index b2f716f..b5d6ca7 100755 --- a/scripts/install-hooks.sh +++ b/scripts/install-hooks.sh @@ -1,26 +1,49 @@ #!/usr/bin/env bash -# Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/ -# and print the settings.json configuration to add. +# Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/, +# and symlink skill-helper scripts from claude-foundations/scripts/ into ~/.claude/scripts/. +# Prints the settings.json configuration to add for hooks. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" HOOKS_SRC="$(cd "$SCRIPT_DIR/../hooks" && pwd)" HOOKS_DST="$HOME/.claude/hooks" +SCRIPTS_DST="$HOME/.claude/scripts" +# --------------------------------------------------------------------------- +# Hooks +# --------------------------------------------------------------------------- mkdir -p "$HOOKS_DST" - -echo "Installing hooks from $HOOKS_SRC → $HOOKS_DST" -echo "" - +echo "Installing hooks: $HOOKS_SRC → $HOOKS_DST" for HOOK in "$HOOKS_SRC"/*.sh; do NAME="$(basename "$HOOK")" ln -sf "$HOOK" "$HOOKS_DST/$NAME" echo " ✓ $NAME" done +# --------------------------------------------------------------------------- +# Skill-helper scripts (scripts that skills reference via ~/.claude/scripts/) +# Not all claude-foundations scripts go here — only the ones used by skills. +# --------------------------------------------------------------------------- +mkdir -p "$SCRIPTS_DST" echo "" -echo "Hooks symlinked. Ensure your ~/.claude/settings.json includes:" +echo "Installing skill-helper scripts: $SCRIPT_DIR → $SCRIPTS_DST" +SKILL_HELPERS=( + "extract-transcripts.py" + "list-transcripts-here.sh" +) +for SCRIPT in "${SKILL_HELPERS[@]}"; do + SRC="$SCRIPT_DIR/$SCRIPT" + if [ -f "$SRC" ]; then + ln -sf "$SRC" "$SCRIPTS_DST/$SCRIPT" + echo " ✓ $SCRIPT" + else + echo " ✗ $SCRIPT (not found at $SRC)" + fi +done + +echo "" +echo "Done. Ensure your ~/.claude/settings.json (and profile settings.json files) include:" echo "" cat <<'EOF' { @@ -50,3 +73,6 @@ cat <<'EOF' } } EOF +echo "" +echo "Note: hooks are referenced as ~/.claude/hooks/ (default profile path) in all profile settings.json files." +echo "Note: skill-helper scripts are installed to ~/.claude/scripts/ only — profiles reference this shared path." diff --git a/scripts/list-transcripts-here.sh b/scripts/list-transcripts-here.sh new file mode 100755 index 0000000..60c009a --- /dev/null +++ b/scripts/list-transcripts-here.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Wrapper for extract-transcripts.py --list, passing the current working directory. +# Used by the /log skill's pre-gathered context (bang commands can't use $() substitution). +python3 "$HOME/.claude/scripts/extract-transcripts.py" --list "$(pwd)"