Add transcript backup tracking system and pre-compact hook improvements

- pre-compact-backup.sh: derive transcript path from session_id+cwd (no
  longer relies on transcript_path field that Claude Code stopped providing);
  register each backup in tracking.json after saving
- extract-transcripts.py: new script managing tracking.json — register,
  list, extract conversation text, mark-processed modes
- list-transcripts-here.sh: thin wrapper for extract-transcripts --list
  using $(pwd); needed because SKILL.md bang commands reject $() substitution
- install-hooks.sh: now also symlinks skill-helper scripts into
  ~/.claude/scripts/ via a curated SKILL_HELPERS list
- Memory docs: new script-extract-transcripts.md, script-list-transcripts-here.md;
  updated skill-log.md, script-install-hooks.md, MEMORY.md index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul O'Reilly
2026-04-13 14:06:09 +12:00
parent 52b0f2b5c6
commit 6fd0dac218
9 changed files with 439 additions and 24 deletions

View File

@@ -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) - [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 - [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 - [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 ## 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 - [/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-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 - [/reflect](memory/skill-reflect.md) — Milestone reflection: reviews conversation + git log, produces structured artifacts

View File

@@ -1,17 +1,36 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Pre-compact hook: save a copy of the session transcript before compaction. # 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 set -euo pipefail
# Parse stdin JSON for transcript path and session ID # Parse stdin JSON
INPUT="$(cat)" 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)" 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 if [ -z "$SESSION_ID" ]; then
echo "pre-compact-backup: no transcript found, skipping" >&2 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 exit 0
fi fi
@@ -20,11 +39,21 @@ BACKUP_DIR="$HOME/.claude/transcript-backups"
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR"
TIMESTAMP="$(date +%Y%m%d-%H%M%S)" 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" cp "$TRANSCRIPT_PATH" "$BACKUP_FILE"
# Prune backups older than 30 days # Prune backups older than 30 days
find "$BACKUP_DIR" -name "*.jsonl" -mtime +30 -delete 2>/dev/null || true 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

View File

@@ -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`

View File

@@ -4,7 +4,7 @@
## Purpose ## 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 ## Usage
@@ -13,15 +13,27 @@ cd ~/dev/claude/projects/claude-foundations
scripts/install-hooks.sh 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 ## How it works
1. Iterates over `hooks/*.sh` 1. Iterates over `hooks/*.sh`, creates symlinks in `~/.claude/hooks/` (force-overwrites)
2. Creates symlinks in `~/.claude/hooks/` (force-overwrites existing) 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 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 ## Gotchas
- The printed JSON must be manually added to `settings.json` — the script doesn't edit it automatically. - 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.

View File

@@ -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

View File

@@ -12,22 +12,31 @@ End-of-session logging. Captures key decisions, gotchas, open questions, and pro
/log /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 ## How it works
1. Creates `memory/log/` if needed 1. Creates `memory/log/` if needed
2. Reviews the conversation and extracts: Summary, Decisions, Gotchas (tagged with `[topic]`), Open Questions, Key Context, Process Notes 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 3. Writes a structured in-context log — 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) 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 ## Part of the knowledge pipeline
`/log``/reflect-logs``/distill-best-practices` `/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 ## Gotchas
- Trivial sessions can be skipped — the skill says so if nothing worth logging happened - 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` - `[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

265
scripts/extract-transcripts.py Executable file
View File

@@ -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()

View File

@@ -1,26 +1,49 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/ # Symlink all hooks from claude-foundations/hooks/ into ~/.claude/hooks/,
# and print the settings.json configuration to add. # and symlink skill-helper scripts from claude-foundations/scripts/ into ~/.claude/scripts/.
# Prints the settings.json configuration to add for hooks.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
HOOKS_SRC="$(cd "$SCRIPT_DIR/../hooks" && pwd)" HOOKS_SRC="$(cd "$SCRIPT_DIR/../hooks" && pwd)"
HOOKS_DST="$HOME/.claude/hooks" HOOKS_DST="$HOME/.claude/hooks"
SCRIPTS_DST="$HOME/.claude/scripts"
# ---------------------------------------------------------------------------
# Hooks
# ---------------------------------------------------------------------------
mkdir -p "$HOOKS_DST" mkdir -p "$HOOKS_DST"
echo "Installing hooks: $HOOKS_SRC$HOOKS_DST"
echo "Installing hooks from $HOOKS_SRC$HOOKS_DST"
echo ""
for HOOK in "$HOOKS_SRC"/*.sh; do for HOOK in "$HOOKS_SRC"/*.sh; do
NAME="$(basename "$HOOK")" NAME="$(basename "$HOOK")"
ln -sf "$HOOK" "$HOOKS_DST/$NAME" ln -sf "$HOOK" "$HOOKS_DST/$NAME"
echo "$NAME" echo "$NAME"
done 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 ""
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 "" echo ""
cat <<'EOF' cat <<'EOF'
{ {
@@ -50,3 +73,6 @@ cat <<'EOF'
} }
} }
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."

View File

@@ -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)"