diff --git a/claude/code/hooks.json b/claude/code/hooks.json index 646ac42..0390a99 100644 --- a/claude/code/hooks.json +++ b/claude/code/hooks.json @@ -7,6 +7,10 @@ "hooks": [ { "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-history-capture.sh" + } + ], + "description": "Capture session history before each tool use" "command": "${CLAUDE_PLUGIN_ROOT}/scripts/detect-module.sh" } ], @@ -94,25 +98,13 @@ "description": "Warn about uncommitted work after git commit" } ], - "PreCompact": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.sh" - } - ], - "description": "Save state before auto-compact to prevent amnesia" - } - ], "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-start.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-history-restore.sh" } ], "description": "Restore recent session context on startup" diff --git a/claude/code/scripts/pre-compact.sh b/claude/code/scripts/pre-compact.sh deleted file mode 100755 index bb9d841..0000000 --- a/claude/code/scripts/pre-compact.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Pre-compact: Save minimal state for Claude to resume after auto-compact -# -# Captures: -# - Working directory + branch -# - Git status (files touched) -# - Todo state (in_progress items) -# - Context facts (decisions, actionables) - -STATE_FILE="${HOME}/.claude/sessions/scratchpad.md" -CONTEXT_FILE="${HOME}/.claude/sessions/context.json" -TIMESTAMP=$(date '+%s') -CWD=$(pwd) - -mkdir -p "${HOME}/.claude/sessions" - -# Get todo state -TODOS="" -if [[ -f "${HOME}/.claude/todos/current.json" ]]; then - TODOS=$(cat "${HOME}/.claude/todos/current.json" 2>/dev/null | head -50) -fi - -# Get git status -GIT_STATUS="" -BRANCH="" -if git rev-parse --git-dir > /dev/null 2>&1; then - GIT_STATUS=$(git status --short 2>/dev/null | head -15) - BRANCH=$(git branch --show-current 2>/dev/null) -fi - -# Get context facts -CONTEXT="" -if [[ -f "$CONTEXT_FILE" ]]; then - CONTEXT=$(jq -r '.[] | "- [\(.source)] \(.fact)"' "$CONTEXT_FILE" 2>/dev/null | tail -10) -fi - -cat > "$STATE_FILE" << EOF ---- -timestamp: ${TIMESTAMP} -cwd: ${CWD} -branch: ${BRANCH:-none} ---- - -# Resume After Compact - -You were mid-task. Do NOT assume work is complete. - -## Project -\`${CWD}\` on \`${BRANCH:-no branch}\` - -## Files Changed -\`\`\` -${GIT_STATUS:-none} -\`\`\` - -## Todos (in_progress = NOT done) -\`\`\`json -${TODOS:-check /todos} -\`\`\` - -## Context (decisions & actionables) -${CONTEXT:-none captured} - -## Next -Continue the in_progress todo. -EOF - -echo "[PreCompact] Snapshot saved" >&2 -exit 0 diff --git a/claude/code/scripts/session-history-capture.sh b/claude/code/scripts/session-history-capture.sh new file mode 100644 index 0000000..c13807d --- /dev/null +++ b/claude/code/scripts/session-history-capture.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# capture-session-history.sh +# Captures session context, focusing on git status, and saves it to history.json. + +HISTORY_FILE="${HOME}/.claude/sessions/history.json" +SESSION_TIMEOUT=10800 # 3 hours + +# Ensure session directory exists +mkdir -p "${HOME}/.claude/sessions" + +# Initialize history file if it doesn't exist +if [[ ! -f "$HISTORY_FILE" ]]; then + echo '{"sessions": []}' > "$HISTORY_FILE" +fi + +# --- Get Session Identifiers --- +MODULE=$(basename "$(pwd)") +BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +NOW=$(date '+%s') + +# --- Read and Find Current Session --- +HISTORY_CONTENT=$(cat "$HISTORY_FILE") +SESSION_INDEX=$(echo "$HISTORY_CONTENT" | jq \ + --arg module "$MODULE" \ + --arg branch "$BRANCH" \ + --argjson now "$NOW" \ + --argjson timeout "$SESSION_TIMEOUT" ' + .sessions | to_entries | + map(select(.value.module == $module and .value.branch == $branch and ($now - .value.last_updated < $timeout))) | + .[-1].key +') + +# --- Extract Key Actions from Git --- +# Get list of modified/new files. `git status --short` gives entries like " M path/file.txt". +# We'll format them into more readable strings. +ACTIONS_LIST=() +while read -r line; do + status=$(echo "$line" | cut -c 1-2) + path=$(echo "$line" | cut -c 4-) + action="" + case "$status" in + " M") action="Modified: $path" ;; + "A ") action="Added: $path" ;; + "D ") action="Deleted: $path" ;; + "R ") action="Renamed: $path" ;; + "C ") action="Copied: $path" ;; + "??") action="Untracked: $path" ;; + esac + if [[ -n "$action" ]]; then + ACTIONS_LIST+=("$action") + fi +done < <(git status --short) + +KEY_ACTIONS_JSON=$(printf '%s\n' "${ACTIONS_LIST[@]}" | jq -R . | jq -s .) + +# --- Update or Create Session --- +if [[ "$SESSION_INDEX" != "null" ]]; then + # Update existing session + UPDATED_HISTORY=$(echo "$HISTORY_CONTENT" | jq \ + --argjson index "$SESSION_INDEX" \ + --argjson ts "$NOW" \ + --argjson actions "$KEY_ACTIONS_JSON" ' + .sessions[$index].last_updated = $ts | + .sessions[$index].key_actions = $actions + # Note: pending_tasks and decisions would be updated here from conversation + ' + ) +else + # Create new session + SESSION_ID="session_$(date '+%Y%m%d%H%M%S')_$$" + NEW_SESSION=$(jq -n \ + --arg id "$SESSION_ID" \ + --argjson ts "$NOW" \ + --arg module "$MODULE" \ + --arg branch "$BRANCH" \ + --argjson actions "$KEY_ACTIONS_JSON" ' + { + "id": $id, + "started": $ts, + "last_updated": $ts, + "module": $module, + "branch": $branch, + "key_actions": $actions, + "pending_tasks": [], + "decisions": [] + }' + ) + UPDATED_HISTORY=$(echo "$HISTORY_CONTENT" | jq --argjson new_session "$NEW_SESSION" '.sessions += [$new_session]') +fi + +# Write back to file +# Use a temp file for atomic write +TMP_FILE="${HISTORY_FILE}.tmp" +echo "$UPDATED_HISTORY" > "$TMP_FILE" && mv "$TMP_FILE" "$HISTORY_FILE" + +# This script does not produce output, it works in the background. +exit 0 diff --git a/claude/code/scripts/session-history-restore.sh b/claude/code/scripts/session-history-restore.sh new file mode 100644 index 0000000..3c0aa18 --- /dev/null +++ b/claude/code/scripts/session-history-restore.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# session-history-restore.sh +# Restores and displays the most recent session context from history.json. + +HISTORY_FILE="${HOME}/.claude/sessions/history.json" +PRUNE_AGE_DAYS=7 # Prune sessions older than 7 days + +# Ensure the history file exists, otherwise exit silently. +if [[ ! -f "$HISTORY_FILE" ]]; then + exit 0 +fi + +# --- Prune Old Sessions --- +NOW=$(date '+%s') +PRUNE_TIMESTAMP=$((NOW - PRUNE_AGE_DAYS * 86400)) +PRUNED_HISTORY=$(jq --argjson prune_ts "$PRUNE_TIMESTAMP" ' + .sessions = (.sessions | map(select(.last_updated >= $prune_ts))) +' "$HISTORY_FILE") + +# Atomically write the pruned history back to the file +TMP_FILE="${HISTORY_FILE}.tmp" +echo "$PRUNED_HISTORY" > "$TMP_FILE" && mv "$TMP_FILE" "$HISTORY_FILE" + +# --- Read the Most Recent Session --- +# Get the last session from the (potentially pruned) history +LAST_SESSION=$(echo "$PRUNED_HISTORY" | jq '.sessions[-1]') + +# If no sessions, exit. +if [[ "$LAST_SESSION" == "null" ]]; then + exit 0 +fi + +# --- Format and Display Session Context --- +MODULE=$(echo "$LAST_SESSION" | jq -r '.module') +BRANCH=$(echo "$LAST_SESSION" | jq -r '.branch') +LAST_UPDATED=$(echo "$LAST_SESSION" | jq -r '.last_updated') + +# Calculate human-readable "last active" time +AGE_SECONDS=$((NOW - LAST_UPDATED)) +if (( AGE_SECONDS < 60 )); then + LAST_ACTIVE="less than a minute ago" +elif (( AGE_SECONDS < 3600 )); then + LAST_ACTIVE="$((AGE_SECONDS / 60)) minutes ago" +elif (( AGE_SECONDS < 86400 )); then + LAST_ACTIVE="$((AGE_SECONDS / 3600)) hours ago" +else + LAST_ACTIVE="$((AGE_SECONDS / 86400)) days ago" +fi + +# --- Build the Output --- +# Using ANSI escape codes for formatting (bold, colors) +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Header +echo -e "${BLUE}${BOLD}📋 Previous Session Context${NC}" >&2 +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2 +echo -e "${BOLD}Module:${NC} ${MODULE} (${BRANCH})" >&2 +echo -e "${BOLD}Last active:${NC} ${LAST_ACTIVE}" >&2 +echo "" >&2 + +# Key Actions +KEY_ACTIONS=$(echo "$LAST_SESSION" | jq -r '.key_actions[]?') +if [[ -n "$KEY_ACTIONS" ]]; then + echo -e "${BOLD}Key actions:${NC}" >&2 + while read -r action; do + echo -e "• ${action}" >&2 + done <<< "$KEY_ACTIONS" + echo "" >&2 +fi + +# Pending Tasks +PENDING_TASKS=$(echo "$LAST_SESSION" | jq -r '.pending_tasks[]?') +if [[ -n "$PENDING_TASKS" ]]; then + echo -e "${BOLD}Pending tasks:${NC}" >&2 + while read -r task; do + echo -e "• ${task}" >&2 + done <<< "$PENDING_TASKS" + echo "" >&2 +fi + +# Decisions Made +DECISIONS=$(echo "$LAST_SESSION" | jq -r '.decisions[]?') +if [[ -n "$DECISIONS" ]]; then + echo -e "${BOLD}Decisions made:${NC}" >&2 + while read -r decision; do + echo -e "• ${decision}" >&2 + done <<< "$DECISIONS" + echo "" >&2 +fi + +exit 0 diff --git a/claude/code/scripts/session-start.sh b/claude/code/scripts/session-start.sh deleted file mode 100755 index 3a44d97..0000000 --- a/claude/code/scripts/session-start.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Session start: Read scratchpad if recent, otherwise start fresh -# 3 hour window - if older, you've moved on mentally - -STATE_FILE="${HOME}/.claude/sessions/scratchpad.md" -THREE_HOURS=10800 # seconds - -if [[ -f "$STATE_FILE" ]]; then - # Get timestamp from file - FILE_TS=$(grep -E '^timestamp:' "$STATE_FILE" 2>/dev/null | cut -d' ' -f2) - NOW=$(date '+%s') - - if [[ -n "$FILE_TS" ]]; then - AGE=$((NOW - FILE_TS)) - - if [[ $AGE -lt $THREE_HOURS ]]; then - # Recent - read it back - echo "[SessionStart] Found recent scratchpad ($(($AGE / 60)) min ago)" >&2 - echo "[SessionStart] Reading previous state..." >&2 - echo "" >&2 - cat "$STATE_FILE" >&2 - echo "" >&2 - else - # Stale - delete and start fresh - rm -f "$STATE_FILE" - echo "[SessionStart] Previous session >3h old - starting fresh" >&2 - fi - else - # No timestamp, delete it - rm -f "$STATE_FILE" - fi -fi - -exit 0