-
-
Notifications
You must be signed in to change notification settings - Fork 31
Expand file tree
/
Copy pathpalinode-session-start.sh
More file actions
executable file
·124 lines (106 loc) · 4.83 KB
/
Copy pathpalinode-session-start.sh
File metadata and controls
executable file
·124 lines (106 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/bin/bash
# palinode-session-start.sh — warm + inject Palinode context on session start.
#
# Fires on Claude Code SessionStart (startup and /clear by default). Two
# actions, both fail-silent:
#
# 1. POST /context/prime — warms server-side session context for this CWD
# (ADR-012 Layer 4 + ADR-009 Layer 1). The endpoint returns the
# scope-aware context digest; this hook discards the body and injects
# via the /list digest below. An older server (pre-0.9.3) 404s
# harmlessly.
# 2. GET /list?core_only=true — injects a bounded digest of core memories
# into the session as additionalContext, with a deterministic recall
# reminder. This is the "sessions start smart" half: grounding that does
# not depend on the agent remembering to search.
#
# Fail-silent by design — never block session start. API down → no output,
# exit 0. The agent-side pull path (palinode_search) is unaffected either way.
#
# Install:
# 1. Copy to .claude/hooks/palinode-session-start.sh (or ~/.claude/hooks/…)
# 2. chmod +x .claude/hooks/palinode-session-start.sh
# 3. Register in .claude/settings.json — see ./settings.json in this dir.
#
# Or just run: `palinode init` — it installs all of this for you.
set -euo pipefail
# No jq → no way to parse the hook payload or build JSON. Bail silently.
command -v jq >/dev/null 2>&1 || exit 0
PALINODE_API="${PALINODE_API_URL:-http://localhost:6340}"
# SessionStart blocks the session becoming interactive — keep timeouts tight.
# This is per-curl total time; the settings.json hook timeout must exceed 2x.
HOOK_TIMEOUT="${PALINODE_HOOK_START_TIMEOUT:-8}"
# Sources to fire on. startup + clear = fresh context that needs grounding.
# resume and compact are excluded by default (prior context usually still
# carries the injection); extend via PALINODE_HOOK_START_SOURCES if you want
# re-injection after compaction, e.g. "startup clear compact".
ALLOWED_SOURCES="${PALINODE_HOOK_START_SOURCES:-startup clear}"
# Injection bounds. MAX_FILES=0 disables injection entirely (prime-only mode).
MAX_FILES="${PALINODE_HOOK_INJECT_MAX_FILES:-10}"
MAX_CHARS="${PALINODE_HOOK_INJECT_MAX_CHARS:-4000}"
# Optional bearer auth for token-protected deployments (PALINODE_API_TOKEN).
# The ${AUTH[@]+…} expansion is the bash-3.2-safe empty-array idiom (set -u).
AUTH=()
if [ -n "${PALINODE_API_TOKEN:-}" ]; then
AUTH=(-H "Authorization: Bearer ${PALINODE_API_TOKEN}")
fi
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"')
# Word-boundary match on a space-padded allowlist so substrings don't
# false-positive (same pattern as palinode-session-end.sh).
case " $ALLOWED_SOURCES " in
*" $SOURCE "*) ;;
*) exit 0 ;;
esac
# Dry-run: print what would happen, touch nothing.
if [ "${PALINODE_HOOK_DRYRUN:-0}" = "1" ]; then
echo "[palinode-session-start DRYRUN] would POST ${PALINODE_API}/context/prime (cwd=${CWD}, session=${SESSION_ID}) and GET ${PALINODE_API}/list?core_only=true"
exit 0
fi
# 1. Warm server-side session context (/context/prime — ADR-012 Layer 4 +
# ADR-009 Layer 1). No -f: an older server (pre-0.9.3) without the
# endpoint 404s harmlessly; only connection errors fail, and those are
# swallowed.
PRIME_PAYLOAD=$(jq -n --arg cwd "$CWD" --arg session_id "$SESSION_ID" \
'{cwd: $cwd, session_id: $session_id}')
curl -s -o /dev/null \
-X POST "${PALINODE_API}/context/prime" \
${AUTH[@]+"${AUTH[@]}"} \
-H "Content-Type: application/json" \
-d "$PRIME_PAYLOAD" \
--connect-timeout 2 \
--max-time "${HOOK_TIMEOUT}" 2>/dev/null || true
# 2. Inject a bounded core-memory digest as session context.
if [ "$MAX_FILES" -le 0 ]; then
exit 0
fi
CORE_JSON=$(curl -s -f \
${AUTH[@]+"${AUTH[@]}"} \
"${PALINODE_API}/list?core_only=true" \
--connect-timeout 2 \
--max-time "${HOOK_TIMEOUT}" 2>/dev/null) || exit 0
# Build "- [file] name — summary" lines inside jq (string concatenation, no
# shell loop). /list sorts newest-first, so [:$max] keeps the freshest files.
DIGEST=$(echo "$CORE_JSON" | jq -r --argjson max "$MAX_FILES" '
if type == "array" and length > 0 then
.[:$max]
| map("- [" + .file + "] " + (.name // "untitled")
+ (if (.summary // "") != "" then " — " + .summary else "" end))
| join("\n")
else empty end' 2>/dev/null) || exit 0
if [ -z "$DIGEST" ]; then
exit 0
fi
CONTEXT="## Palinode memory (session start)
Persistent memory is connected. Recall details with the palinode_search /
palinode_read MCP tools — they read the live store; session notes are NOT
files in this repo.
Core memories:
${DIGEST}"
# Bound total size so a pathological store can't flood the context window.
CONTEXT="${CONTEXT:0:${MAX_CHARS}}"
jq -n --arg ctx "$CONTEXT" \
'{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
exit 0