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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
41
memory/script-extract-transcripts.md
Normal file
41
memory/script-extract-transcripts.md
Normal 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`
|
||||
@@ -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.
|
||||
|
||||
26
memory/script-list-transcripts-here.md
Normal file
26
memory/script-list-transcripts-here.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
265
scripts/extract-transcripts.py
Executable file
265
scripts/extract-transcripts.py
Executable 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()
|
||||
@@ -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."
|
||||
|
||||
4
scripts/list-transcripts-here.sh
Executable file
4
scripts/list-transcripts-here.sh
Executable 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)"
|
||||
Reference in New Issue
Block a user