feat: OpenBrain integration for session hooks

SessionStart: query OpenBrain for recent activity + project context,
inject into Claude's context window on every new session.

Stop: async save session context to OpenBrain when git changes exist.

PreCompact: save working state to OpenBrain before context compaction.

API key read from ~/.claude/brain.key or CORE_BRAIN_KEY env var.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-14 11:44:04 +00:00
parent 2e8e071d75
commit 93415257f3
4 changed files with 208 additions and 36 deletions

View file

@ -55,6 +55,20 @@
"description": "Warn about uncommitted work after git commit"
}
],
"Stop": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-save.sh",
"async": true,
"timeout": 10
}
],
"description": "Save session context to OpenBrain (async, only when git changes exist)"
}
],
"PreCompact": [
{
"matcher": "*",

View file

@ -1,23 +1,24 @@
#!/bin/bash
# Pre-compact: Save minimal state for Claude to resume after auto-compact
# Pre-compact: Save state locally + to OpenBrain before context compaction
#
# Captures:
# - Working directory + branch
# - Git status (files touched)
# - Todo state (in_progress items)
# - Context facts (decisions, actionables)
# - Saves to OpenBrain so the memory survives across sessions
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
BRAIN_URL="${CORE_BRAIN_URL:-https://api.lthn.sh}"
BRAIN_KEY="${CORE_BRAIN_KEY:-}"
BRAIN_KEY_FILE="${HOME}/.claude/brain.key"
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)
# Load API key
if [[ -z "$BRAIN_KEY" && -f "$BRAIN_KEY_FILE" ]]; then
BRAIN_KEY=$(cat "$BRAIN_KEY_FILE" 2>/dev/null | tr -d '[:space:]')
fi
# Get git status
@ -28,12 +29,18 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
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
# Detect project
PROJECT=""
case "$CWD" in
*/core/go-*) PROJECT=$(basename "$CWD" | sed 's/^go-//') ;;
*/core/php-*) PROJECT=$(basename "$CWD" | sed 's/^php-//') ;;
*/core/*) PROJECT=$(basename "$CWD") ;;
*/host-uk/*) PROJECT=$(basename "$CWD") ;;
*/lthn/*) PROJECT=$(basename "$CWD") ;;
*/snider/*) PROJECT=$(basename "$CWD") ;;
esac
# Save local scratchpad
cat > "$STATE_FILE" << EOF
---
timestamp: ${TIMESTAMP}
@ -53,17 +60,31 @@ You were mid-task. Do NOT assume work is complete.
${GIT_STATUS:-none}
\`\`\`
## Todos (in_progress = NOT done)
\`\`\`json
${TODOS:-check /todos}
\`\`\`
## Context (decisions & actionables)
${CONTEXT:-none captured}
## Next
Continue the in_progress todo.
Continue the in_progress todo. Check OpenBrain for context.
EOF
echo "[PreCompact] Snapshot saved" >&2
echo "[PreCompact] Local snapshot saved" >&2
# Save to OpenBrain
if [[ -n "$BRAIN_KEY" && -n "$GIT_STATUS" ]]; then
CHANGED_FILES=$(echo "$GIT_STATUS" | awk '{print $2}' | tr '\n' ', ')
CONTENT="Context compaction during work on ${PROJECT:-unknown} (branch: ${BRANCH:-none}). Changed files: ${CHANGED_FILES}"
curl -s --max-time 5 "${BRAIN_URL}/v1/brain/remember" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer ${BRAIN_KEY}" \
-d "{
\"content\": $(echo "$CONTENT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))'),
\"type\": \"context\",
\"project\": \"${PROJECT:-unknown}\",
\"agent_id\": \"cladius\",
\"tags\": [\"pre-compact\", \"session\"]
}" >/dev/null 2>&1
echo "[PreCompact] Saved to OpenBrain" >&2
fi
exit 0

View file

@ -0,0 +1,53 @@
#!/bin/bash
# Stop hook: Save session context to OpenBrain after each response
# Runs async so it doesn't block the conversation
#
# Only saves if there are git changes (indicates real work happened)
BRAIN_URL="${CORE_BRAIN_URL:-https://api.lthn.sh}"
BRAIN_KEY="${CORE_BRAIN_KEY:-}"
BRAIN_KEY_FILE="${HOME}/.claude/brain.key"
# Load API key
if [[ -z "$BRAIN_KEY" && -f "$BRAIN_KEY_FILE" ]]; then
BRAIN_KEY=$(cat "$BRAIN_KEY_FILE" 2>/dev/null | tr -d '[:space:]')
fi
[[ -z "$BRAIN_KEY" ]] && exit 0
# Only save if there are uncommitted changes (real work happened)
GIT_STATUS=$(git status --short 2>/dev/null | head -5)
[[ -z "$GIT_STATUS" ]] && exit 0
# Detect project
PROJECT=""
CWD=$(pwd)
case "$CWD" in
*/core/go-*) PROJECT=$(basename "$CWD" | sed 's/^go-//') ;;
*/core/php-*) PROJECT=$(basename "$CWD" | sed 's/^php-//') ;;
*/core/*) PROJECT=$(basename "$CWD") ;;
*/host-uk/*) PROJECT=$(basename "$CWD") ;;
*/lthn/*) PROJECT=$(basename "$CWD") ;;
*/snider/*) PROJECT=$(basename "$CWD") ;;
esac
[[ -z "$PROJECT" ]] && PROJECT="unknown"
BRANCH=$(git branch --show-current 2>/dev/null || echo "none")
CHANGED_FILES=$(git diff --name-only 2>/dev/null | head -10 | tr '\n' ', ')
CONTENT="Working on ${PROJECT} (branch: ${BRANCH}). Changed files: ${CHANGED_FILES}"
curl -s --max-time 5 "${BRAIN_URL}/v1/brain/remember" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer ${BRAIN_KEY}" \
-d "{
\"content\": $(echo "$CONTENT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))'),
\"type\": \"context\",
\"project\": \"${PROJECT}\",
\"agent_id\": \"cladius\",
\"tags\": [\"auto-save\", \"session\"]
}" >/dev/null 2>&1
exit 0

View file

@ -1,32 +1,116 @@
#!/bin/bash
# Session start: Read scratchpad if recent, otherwise start fresh
# 3 hour window - if older, you've moved on mentally
# Session start: Load OpenBrain context + recent scratchpad
#
# 1. Query OpenBrain for project-relevant memories
# 2. Read local scratchpad if recent (<3h)
# 3. Output to stdout → injected into Claude's context
BRAIN_URL="${CORE_BRAIN_URL:-https://api.lthn.sh}"
BRAIN_KEY="${CORE_BRAIN_KEY:-}"
BRAIN_KEY_FILE="${HOME}/.claude/brain.key"
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
THREE_HOURS=10800 # seconds
THREE_HOURS=10800
# Load API key from file if not in env
if [[ -z "$BRAIN_KEY" && -f "$BRAIN_KEY_FILE" ]]; then
BRAIN_KEY=$(cat "$BRAIN_KEY_FILE" 2>/dev/null | tr -d '[:space:]')
fi
# --- OpenBrain Recall ---
if [[ -n "$BRAIN_KEY" ]]; then
# Detect project from CWD
PROJECT=""
CWD=$(pwd)
case "$CWD" in
*/core/go-*) PROJECT=$(basename "$CWD" | sed 's/^go-//') ;;
*/core/php-*) PROJECT=$(basename "$CWD" | sed 's/^php-//') ;;
*/core/*) PROJECT=$(basename "$CWD") ;;
*/host-uk/*) PROJECT=$(basename "$CWD") ;;
*/lthn/*) PROJECT=$(basename "$CWD") ;;
*/snider/*) PROJECT=$(basename "$CWD") ;;
esac
echo "[SessionStart] OpenBrain: querying memories..." >&2
# 1. Recent session summaries (what did we do recently?)
RECENT=$(curl -s --max-time 5 "${BRAIN_URL}/v1/brain/recall" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer ${BRAIN_KEY}" \
-d "{\"query\": \"session summary milestone recent work completed\", \"top_k\": 3, \"agent_id\": \"cladius\"}" 2>/dev/null)
# 2. Project-specific context (if we're in a project dir)
PROJECT_CTX=""
if [[ -n "$PROJECT" ]]; then
PROJECT_CTX=$(curl -s --max-time 5 "${BRAIN_URL}/v1/brain/recall" \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H "Authorization: Bearer ${BRAIN_KEY}" \
-d "{\"query\": \"architecture decisions conventions for ${PROJECT}\", \"top_k\": 3, \"agent_id\": \"cladius\", \"project\": \"${PROJECT}\"}" 2>/dev/null)
fi
# Output to stdout (injected into context)
RECENT_COUNT=$(echo "$RECENT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('memories',[])))" 2>/dev/null || echo "0")
if [[ "$RECENT_COUNT" -gt 0 ]]; then
echo ""
echo "## OpenBrain — Recent Activity"
echo ""
echo "$RECENT" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for m in data.get('memories', []):
t = m.get('type', '?')
p = m.get('project', '?')
content = m.get('content', '')[:300]
print(f'**[{t}]** ({p}): {content}')
print()
" 2>/dev/null
fi
if [[ -n "$PROJECT" && -n "$PROJECT_CTX" ]]; then
PROJECT_COUNT=$(echo "$PROJECT_CTX" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('memories',[])))" 2>/dev/null || echo "0")
if [[ "$PROJECT_COUNT" -gt 0 ]]; then
echo ""
echo "## OpenBrain — ${PROJECT} Context"
echo ""
echo "$PROJECT_CTX" | python3 -c "
import json, sys
data = json.load(sys.stdin)
for m in data.get('memories', []):
t = m.get('type', '?')
content = m.get('content', '')[:300]
print(f'**[{t}]**: {content}')
print()
" 2>/dev/null
fi
fi
echo "[SessionStart] OpenBrain: ${RECENT_COUNT} recent + ${PROJECT_COUNT:-0} project memories loaded" >&2
else
echo "[SessionStart] OpenBrain: no API key (set CORE_BRAIN_KEY or create ~/.claude/brain.key)" >&2
fi
# --- Local Scratchpad ---
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
echo "[SessionStart] Scratchpad: $(($AGE / 60)) min old" >&2
echo ""
echo "## Recent Scratchpad ($(($AGE / 60)) min ago)"
echo ""
cat "$STATE_FILE"
else
# Stale - delete and start fresh
rm -f "$STATE_FILE"
echo "[SessionStart] Previous session >3h old - starting fresh" >&2
echo "[SessionStart] Scratchpad: >3h old, cleared" >&2
fi
else
# No timestamp, delete it
rm -f "$STATE_FILE"
fi
fi