@suwujs/codex-vault 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/plugin/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.5.0
|
|
@@ -5,6 +5,10 @@ set -eo pipefail
|
|
|
5
5
|
# Injects vault context into the agent's prompt at session start.
|
|
6
6
|
# Works with any agent that supports SessionStart hooks (Claude Code, Codex CLI).
|
|
7
7
|
#
|
|
8
|
+
# Output contract (matches claude-mem pattern):
|
|
9
|
+
# stdout → JSON with hookSpecificOutput.additionalContext (agent context)
|
|
10
|
+
# stderr → visible banner (user terminal)
|
|
11
|
+
#
|
|
8
12
|
# Dynamic context: adapts git log window, reads full North Star,
|
|
9
13
|
# shows all active work, and includes uncommitted changes.
|
|
10
14
|
|
|
@@ -47,9 +51,13 @@ fi
|
|
|
47
51
|
echo ""
|
|
48
52
|
} >&2
|
|
49
53
|
|
|
50
|
-
# ---
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
# --- Collect context into variable ---
|
|
55
|
+
CONTEXT=$(
|
|
56
|
+
cat <<'CONTEXT_HEADER'
|
|
57
|
+
## Session Context
|
|
58
|
+
|
|
59
|
+
CONTEXT_HEADER
|
|
60
|
+
|
|
53
61
|
echo "### Date"
|
|
54
62
|
echo "$(date +%Y-%m-%d) ($(date +%A))"
|
|
55
63
|
echo ""
|
|
@@ -160,11 +168,9 @@ _key_files() {
|
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
if [ "$FILE_COUNT" -le 20 ]; then
|
|
163
|
-
# Tier 1: small vault — list everything
|
|
164
171
|
echo "$ALL_FILES"
|
|
165
172
|
|
|
166
173
|
elif [ "$FILE_COUNT" -le 50 ]; then
|
|
167
|
-
# Tier 2: medium vault — list hot folders, summarize cold storage
|
|
168
174
|
HOT_FILES=$(echo "$ALL_FILES" | grep -v -E "^\./sources/|^\./work/archive/" || true)
|
|
169
175
|
COLD_COUNT=$(echo "$ALL_FILES" | grep -E "^\./sources/|^\./work/archive/" | grep -c . 2>/dev/null || echo "0")
|
|
170
176
|
|
|
@@ -177,7 +183,6 @@ elif [ "$FILE_COUNT" -le 50 ]; then
|
|
|
177
183
|
fi
|
|
178
184
|
|
|
179
185
|
elif [ "$FILE_COUNT" -le 150 ]; then
|
|
180
|
-
# Tier 3: large vault — folder summary + recent + key files
|
|
181
186
|
echo "($FILE_COUNT files — showing summary)"
|
|
182
187
|
echo ""
|
|
183
188
|
_folder_summary
|
|
@@ -189,7 +194,6 @@ elif [ "$FILE_COUNT" -le 150 ]; then
|
|
|
189
194
|
_key_files
|
|
190
195
|
|
|
191
196
|
else
|
|
192
|
-
# Tier 4: very large vault — minimal footprint
|
|
193
197
|
echo "($FILE_COUNT files — showing summary)"
|
|
194
198
|
echo ""
|
|
195
199
|
_folder_summary
|
|
@@ -202,3 +206,16 @@ else
|
|
|
202
206
|
echo ""
|
|
203
207
|
echo "Use /recall <topic> to search the vault."
|
|
204
208
|
fi
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# --- Output structured JSON (stdout → agent via hookSpecificOutput) ---
|
|
212
|
+
python3 -c "
|
|
213
|
+
import json, sys
|
|
214
|
+
context = sys.stdin.read()
|
|
215
|
+
json.dump({
|
|
216
|
+
'hookSpecificOutput': {
|
|
217
|
+
'hookEventName': 'SessionStart',
|
|
218
|
+
'additionalContext': context
|
|
219
|
+
}
|
|
220
|
+
}, sys.stdout)
|
|
221
|
+
" <<< "$CONTEXT"
|
package/vault/.codex/hooks.json
CHANGED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Session-start hook — injects vault context into the agent's prompt.
|
|
3
|
+
|
|
4
|
+
Works with any agent that supports SessionStart hooks (Claude Code, Codex CLI).
|
|
5
|
+
Outputs structured JSON: additionalContext for LLM, systemMessage for terminal.
|
|
6
|
+
|
|
7
|
+
Dynamic context: adapts git log window, reads full North Star,
|
|
8
|
+
shows all active work, and includes uncommitted changes.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _find_vault_root():
|
|
21
|
+
"""Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
|
|
22
|
+
cwd = os.environ.get("CLAUDE_PROJECT_DIR",
|
|
23
|
+
os.environ.get("CODEX_PROJECT_DIR", os.getcwd()))
|
|
24
|
+
if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
|
|
25
|
+
return cwd
|
|
26
|
+
vault_sub = os.path.join(cwd, "vault")
|
|
27
|
+
if os.path.isdir(vault_sub) and (
|
|
28
|
+
os.path.isfile(os.path.join(vault_sub, "Home.md")) or
|
|
29
|
+
os.path.isdir(os.path.join(vault_sub, "brain"))
|
|
30
|
+
):
|
|
31
|
+
return vault_sub
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run_git(args, cwd, timeout=5):
|
|
36
|
+
"""Run git command and return stdout lines."""
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(
|
|
39
|
+
["git"] + args,
|
|
40
|
+
capture_output=True, text=True, cwd=cwd, timeout=timeout,
|
|
41
|
+
)
|
|
42
|
+
return result.stdout.strip().splitlines() if result.stdout.strip() else []
|
|
43
|
+
except Exception:
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _git_log_oneline(cwd, since=None, max_count=None):
|
|
48
|
+
"""Get git log --oneline entries."""
|
|
49
|
+
args = ["log", "--oneline", "--no-merges"]
|
|
50
|
+
if since:
|
|
51
|
+
args.append(f"--since={since}")
|
|
52
|
+
if max_count:
|
|
53
|
+
args.extend(["-n", str(max_count)])
|
|
54
|
+
return _run_git(args, cwd)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _git_status_short(cwd):
|
|
58
|
+
"""Get git status --short output."""
|
|
59
|
+
return _run_git(["status", "--short", "--", "."], cwd)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_file(path):
|
|
63
|
+
"""Read file content, return empty string on error."""
|
|
64
|
+
try:
|
|
65
|
+
return Path(path).read_text(encoding="utf-8")
|
|
66
|
+
except Exception:
|
|
67
|
+
return ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _find_md_files(vault_dir):
|
|
71
|
+
"""Find all .md files in vault, excluding non-vault directories."""
|
|
72
|
+
exclude = {".git", ".obsidian", "thinking", ".claude", ".codex",
|
|
73
|
+
".codex-vault", ".codex-mem", "node_modules"}
|
|
74
|
+
files = []
|
|
75
|
+
for root, dirs, filenames in os.walk(vault_dir):
|
|
76
|
+
dirs[:] = [d for d in dirs if d not in exclude]
|
|
77
|
+
for f in filenames:
|
|
78
|
+
if f.endswith(".md"):
|
|
79
|
+
rel = os.path.relpath(os.path.join(root, f), vault_dir)
|
|
80
|
+
files.append(f"./{rel}")
|
|
81
|
+
return sorted(files)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _folder_summary(all_files):
|
|
85
|
+
"""Generate folder summary with file counts."""
|
|
86
|
+
folders = Counter()
|
|
87
|
+
for f in all_files:
|
|
88
|
+
parts = f[2:].split("/") # strip ./
|
|
89
|
+
folders[parts[0] if len(parts) > 1 else "."] += 1
|
|
90
|
+
return [f" {folder}/ ({count} files)"
|
|
91
|
+
for folder, count in folders.most_common()]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _key_files(all_files):
|
|
95
|
+
"""Filter for key vault files."""
|
|
96
|
+
pattern = re.compile(
|
|
97
|
+
r"(Home|Index|North Star|Memories|Key Decisions|Patterns|log)\.md$")
|
|
98
|
+
return [f for f in all_files if pattern.search(f)]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _mtime_ok(vault_dir, rel_path, cutoff):
|
|
102
|
+
"""Check if file was modified after cutoff timestamp."""
|
|
103
|
+
try:
|
|
104
|
+
return os.path.getmtime(os.path.join(vault_dir, rel_path.lstrip("./"))) > cutoff
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _north_star_goal(vault_dir):
|
|
110
|
+
"""Extract first goal from North Star for banner display."""
|
|
111
|
+
ns_path = os.path.join(vault_dir, "brain", "North Star.md")
|
|
112
|
+
if not os.path.isfile(ns_path):
|
|
113
|
+
return None
|
|
114
|
+
content = _read_file(ns_path)
|
|
115
|
+
in_focus = False
|
|
116
|
+
for line in content.splitlines():
|
|
117
|
+
if re.match(r"^## Current Focus", line):
|
|
118
|
+
in_focus = True
|
|
119
|
+
continue
|
|
120
|
+
if in_focus and line.startswith("## "):
|
|
121
|
+
break
|
|
122
|
+
if in_focus and re.match(r"^- .+", line):
|
|
123
|
+
goal = line[2:].strip()
|
|
124
|
+
return goal[:40] if goal else None
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ── Context builder (→ additionalContext for LLM) ──────────────────────
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _build_context(vault_dir):
|
|
132
|
+
lines = []
|
|
133
|
+
lines.append("## Session Context")
|
|
134
|
+
lines.append("")
|
|
135
|
+
|
|
136
|
+
# Date
|
|
137
|
+
lines.append("### Date")
|
|
138
|
+
now = datetime.now()
|
|
139
|
+
lines.append(f"{now.strftime('%Y-%m-%d')} ({now.strftime('%A')})")
|
|
140
|
+
lines.append("")
|
|
141
|
+
|
|
142
|
+
# North Star — full file
|
|
143
|
+
lines.append("### North Star")
|
|
144
|
+
ns_path = os.path.join(vault_dir, "brain", "North Star.md")
|
|
145
|
+
if os.path.isfile(ns_path):
|
|
146
|
+
lines.append(_read_file(ns_path))
|
|
147
|
+
else:
|
|
148
|
+
lines.append("(No North Star found — create brain/North Star.md to set goals)")
|
|
149
|
+
lines.append("")
|
|
150
|
+
|
|
151
|
+
# Recent changes — adaptive window
|
|
152
|
+
lines.append("### Recent Changes")
|
|
153
|
+
commits_48h = _git_log_oneline(vault_dir, since="48 hours ago")
|
|
154
|
+
if commits_48h:
|
|
155
|
+
lines.append("(last 48 hours)")
|
|
156
|
+
lines.extend(commits_48h[:15])
|
|
157
|
+
else:
|
|
158
|
+
commits_7d = _git_log_oneline(vault_dir, since="7 days ago")
|
|
159
|
+
if commits_7d:
|
|
160
|
+
lines.append("(nothing in 48h — showing last 7 days)")
|
|
161
|
+
lines.extend(commits_7d[:15])
|
|
162
|
+
else:
|
|
163
|
+
lines.append("(nothing recent — showing last 5 commits)")
|
|
164
|
+
commits = _git_log_oneline(vault_dir, max_count=5)
|
|
165
|
+
lines.extend(commits if commits else ["(no git history)"])
|
|
166
|
+
lines.append("")
|
|
167
|
+
|
|
168
|
+
# Recent operations from log.md
|
|
169
|
+
lines.append("### Recent Operations")
|
|
170
|
+
log_path = os.path.join(vault_dir, "log.md")
|
|
171
|
+
if os.path.isfile(log_path):
|
|
172
|
+
entries = [l for l in _read_file(log_path).splitlines()
|
|
173
|
+
if l.startswith("## [")]
|
|
174
|
+
lines.extend(entries[-5:] if entries else ["(no entries in log.md)"])
|
|
175
|
+
else:
|
|
176
|
+
lines.append("(no log.md)")
|
|
177
|
+
lines.append("")
|
|
178
|
+
|
|
179
|
+
# Active work
|
|
180
|
+
lines.append("### Active Work")
|
|
181
|
+
active_dir = os.path.join(vault_dir, "work", "active")
|
|
182
|
+
if os.path.isdir(active_dir):
|
|
183
|
+
work_files = sorted(f for f in os.listdir(active_dir) if f.endswith(".md"))
|
|
184
|
+
if work_files:
|
|
185
|
+
for f in work_files:
|
|
186
|
+
lines.append(f.replace(".md", ""))
|
|
187
|
+
else:
|
|
188
|
+
lines.append("(none)")
|
|
189
|
+
else:
|
|
190
|
+
lines.append("(no work/active/ directory)")
|
|
191
|
+
lines.append("")
|
|
192
|
+
|
|
193
|
+
# Uncommitted changes
|
|
194
|
+
lines.append("### Uncommitted Changes")
|
|
195
|
+
changes = _git_status_short(vault_dir)
|
|
196
|
+
lines.extend(changes[:20] if changes else ["(working tree clean)"])
|
|
197
|
+
lines.append("")
|
|
198
|
+
|
|
199
|
+
# Recently modified brain files
|
|
200
|
+
lines.append("### Recently Modified Brain Files")
|
|
201
|
+
brain_dir = os.path.join(vault_dir, "brain")
|
|
202
|
+
if os.path.isdir(brain_dir):
|
|
203
|
+
cutoff = datetime.now().timestamp() - 7 * 86400
|
|
204
|
+
recent = sorted(
|
|
205
|
+
f.replace(".md", "")
|
|
206
|
+
for f in os.listdir(brain_dir)
|
|
207
|
+
if f.endswith(".md") and os.path.getmtime(os.path.join(brain_dir, f)) > cutoff
|
|
208
|
+
)
|
|
209
|
+
if recent:
|
|
210
|
+
lines.append("(modified in last 7 days)")
|
|
211
|
+
lines.extend(recent)
|
|
212
|
+
else:
|
|
213
|
+
lines.append("(no recent changes)")
|
|
214
|
+
lines.append("")
|
|
215
|
+
|
|
216
|
+
# Vault file listing — tiered
|
|
217
|
+
lines.append("### Vault Files")
|
|
218
|
+
all_files = _find_md_files(vault_dir)
|
|
219
|
+
n = len(all_files)
|
|
220
|
+
|
|
221
|
+
if n <= 20:
|
|
222
|
+
lines.extend(all_files)
|
|
223
|
+
|
|
224
|
+
elif n <= 50:
|
|
225
|
+
hot = [f for f in all_files
|
|
226
|
+
if not re.match(r"\./sources/|\./work/archive/", f)]
|
|
227
|
+
cold = n - len(hot)
|
|
228
|
+
lines.extend(hot)
|
|
229
|
+
if cold > 0:
|
|
230
|
+
lines.append("")
|
|
231
|
+
lines.append(f"(+ {cold} files in sources/ and work/archive/ — use /recall to search)")
|
|
232
|
+
|
|
233
|
+
elif n <= 150:
|
|
234
|
+
lines.append(f"({n} files — showing summary)")
|
|
235
|
+
lines.append("")
|
|
236
|
+
lines.extend(_folder_summary(all_files))
|
|
237
|
+
lines.append("")
|
|
238
|
+
lines.append("Recently modified (7 days):")
|
|
239
|
+
cutoff = datetime.now().timestamp() - 7 * 86400
|
|
240
|
+
recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
|
|
241
|
+
lines.extend(recent if recent else [" (none)"])
|
|
242
|
+
lines.append("")
|
|
243
|
+
lines.append("Key files:")
|
|
244
|
+
lines.extend(_key_files(all_files))
|
|
245
|
+
|
|
246
|
+
else:
|
|
247
|
+
lines.append(f"({n} files — showing summary)")
|
|
248
|
+
lines.append("")
|
|
249
|
+
lines.extend(_folder_summary(all_files))
|
|
250
|
+
lines.append("")
|
|
251
|
+
lines.append("Recently modified (3 days):")
|
|
252
|
+
cutoff = datetime.now().timestamp() - 3 * 86400
|
|
253
|
+
recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
|
|
254
|
+
lines.extend(recent if recent else [" (none)"])
|
|
255
|
+
lines.append("")
|
|
256
|
+
lines.append("Key files:")
|
|
257
|
+
lines.extend(_key_files(all_files))
|
|
258
|
+
lines.append("")
|
|
259
|
+
lines.append("Use /recall <topic> to search the vault.")
|
|
260
|
+
|
|
261
|
+
return "\n".join(lines)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ── ANSI colors (oh-my-codex style) ──────────────────────────────────
|
|
265
|
+
|
|
266
|
+
RESET = "\x1b[0m"
|
|
267
|
+
BOLD = "\x1b[1m"
|
|
268
|
+
DIM = "\x1b[2m"
|
|
269
|
+
CYAN = "\x1b[36m"
|
|
270
|
+
GREEN = "\x1b[32m"
|
|
271
|
+
YELLOW = "\x1b[33m"
|
|
272
|
+
SEP = f" {DIM}|{RESET} "
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _c(code, text):
|
|
276
|
+
return f"{code}{text}{RESET}"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ── Banner builder (→ systemMessage for terminal) ──────────────────────
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _build_banner(vault_dir):
|
|
283
|
+
# --- Line 1: statusline ---
|
|
284
|
+
elements = []
|
|
285
|
+
|
|
286
|
+
# Git branch
|
|
287
|
+
branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], vault_dir)
|
|
288
|
+
if branch:
|
|
289
|
+
elements.append(_c(CYAN, branch[0]))
|
|
290
|
+
|
|
291
|
+
# North Star goal
|
|
292
|
+
goal = _north_star_goal(vault_dir)
|
|
293
|
+
if goal:
|
|
294
|
+
elements.append(f"\U0001f3af {goal}")
|
|
295
|
+
else:
|
|
296
|
+
elements.append(_c(DIM, "\U0001f3af no goal set"))
|
|
297
|
+
|
|
298
|
+
# Active work
|
|
299
|
+
active_dir = os.path.join(vault_dir, "work", "active")
|
|
300
|
+
work_files = sorted(
|
|
301
|
+
f.replace(".md", "")
|
|
302
|
+
for f in os.listdir(active_dir) if f.endswith(".md")
|
|
303
|
+
) if os.path.isdir(active_dir) else []
|
|
304
|
+
if work_files:
|
|
305
|
+
names = ", ".join(work_files[:3])
|
|
306
|
+
suffix = f" +{len(work_files) - 3}" if len(work_files) > 3 else ""
|
|
307
|
+
elements.append(_c(GREEN, f"active:{len(work_files)}") + _c(DIM, f" {names}{suffix}"))
|
|
308
|
+
else:
|
|
309
|
+
elements.append(_c(DIM, "active:0"))
|
|
310
|
+
|
|
311
|
+
# Changes
|
|
312
|
+
changes = _git_status_short(vault_dir)
|
|
313
|
+
if changes:
|
|
314
|
+
elements.append(_c(YELLOW, f"changes:{len(changes)}"))
|
|
315
|
+
else:
|
|
316
|
+
elements.append(_c(GREEN, "clean"))
|
|
317
|
+
|
|
318
|
+
label = _c(BOLD, "[Vault]")
|
|
319
|
+
statusline = label + " " + SEP.join(elements)
|
|
320
|
+
|
|
321
|
+
# --- Line 2+: recent commits ---
|
|
322
|
+
lines = ["", statusline]
|
|
323
|
+
commits = _git_log_oneline(vault_dir, since="7 days ago")
|
|
324
|
+
if not commits:
|
|
325
|
+
commits = _git_log_oneline(vault_dir, max_count=3)
|
|
326
|
+
if commits:
|
|
327
|
+
for c in commits[:3]:
|
|
328
|
+
# hash in cyan, message in dim
|
|
329
|
+
parts = c.split(" ", 1)
|
|
330
|
+
if len(parts) == 2:
|
|
331
|
+
lines.append(f" {_c(DIM, parts[0])} {_c(DIM, parts[1])}")
|
|
332
|
+
else:
|
|
333
|
+
lines.append(f" {_c(DIM, c)}")
|
|
334
|
+
|
|
335
|
+
# --- Line: vault file count ---
|
|
336
|
+
all_files = _find_md_files(vault_dir)
|
|
337
|
+
lines.append(_c(DIM, f" {len(all_files)} notes"))
|
|
338
|
+
lines.append("")
|
|
339
|
+
|
|
340
|
+
return "\n".join(lines)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ── Main ───────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def main():
|
|
347
|
+
vault_dir = _find_vault_root()
|
|
348
|
+
if not vault_dir:
|
|
349
|
+
output = {
|
|
350
|
+
"hookSpecificOutput": {
|
|
351
|
+
"hookEventName": "SessionStart",
|
|
352
|
+
"additionalContext": "## Session Context\n\n(No vault found)"
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
json.dump(output, sys.stdout)
|
|
356
|
+
sys.exit(0)
|
|
357
|
+
|
|
358
|
+
context = _build_context(vault_dir)
|
|
359
|
+
banner = _build_banner(vault_dir)
|
|
360
|
+
|
|
361
|
+
output = {
|
|
362
|
+
"hookSpecificOutput": {
|
|
363
|
+
"hookEventName": "SessionStart",
|
|
364
|
+
"additionalContext": context
|
|
365
|
+
},
|
|
366
|
+
"systemMessage": banner
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
json.dump(output, sys.stdout)
|
|
370
|
+
sys.stdout.flush()
|
|
371
|
+
sys.exit(0)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
try:
|
|
376
|
+
main()
|
|
377
|
+
except Exception:
|
|
378
|
+
# Never block session start
|
|
379
|
+
sys.exit(0)
|
|
@@ -5,6 +5,10 @@ set -eo pipefail
|
|
|
5
5
|
# Injects vault context into the agent's prompt at session start.
|
|
6
6
|
# Works with any agent that supports SessionStart hooks (Claude Code, Codex CLI).
|
|
7
7
|
#
|
|
8
|
+
# Output contract (matches claude-mem pattern):
|
|
9
|
+
# stdout → JSON with hookSpecificOutput.additionalContext (agent context)
|
|
10
|
+
# stderr → visible banner (user terminal)
|
|
11
|
+
#
|
|
8
12
|
# Dynamic context: adapts git log window, reads full North Star,
|
|
9
13
|
# shows all active work, and includes uncommitted changes.
|
|
10
14
|
|
|
@@ -47,9 +51,13 @@ fi
|
|
|
47
51
|
echo ""
|
|
48
52
|
} >&2
|
|
49
53
|
|
|
50
|
-
# ---
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
# --- Collect context into variable ---
|
|
55
|
+
CONTEXT=$(
|
|
56
|
+
cat <<'CONTEXT_HEADER'
|
|
57
|
+
## Session Context
|
|
58
|
+
|
|
59
|
+
CONTEXT_HEADER
|
|
60
|
+
|
|
53
61
|
echo "### Date"
|
|
54
62
|
echo "$(date +%Y-%m-%d) ($(date +%A))"
|
|
55
63
|
echo ""
|
|
@@ -160,11 +168,9 @@ _key_files() {
|
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
if [ "$FILE_COUNT" -le 20 ]; then
|
|
163
|
-
# Tier 1: small vault — list everything
|
|
164
171
|
echo "$ALL_FILES"
|
|
165
172
|
|
|
166
173
|
elif [ "$FILE_COUNT" -le 50 ]; then
|
|
167
|
-
# Tier 2: medium vault — list hot folders, summarize cold storage
|
|
168
174
|
HOT_FILES=$(echo "$ALL_FILES" | grep -v -E "^\./sources/|^\./work/archive/" || true)
|
|
169
175
|
COLD_COUNT=$(echo "$ALL_FILES" | grep -E "^\./sources/|^\./work/archive/" | grep -c . 2>/dev/null || echo "0")
|
|
170
176
|
|
|
@@ -177,7 +183,6 @@ elif [ "$FILE_COUNT" -le 50 ]; then
|
|
|
177
183
|
fi
|
|
178
184
|
|
|
179
185
|
elif [ "$FILE_COUNT" -le 150 ]; then
|
|
180
|
-
# Tier 3: large vault — folder summary + recent + key files
|
|
181
186
|
echo "($FILE_COUNT files — showing summary)"
|
|
182
187
|
echo ""
|
|
183
188
|
_folder_summary
|
|
@@ -189,7 +194,6 @@ elif [ "$FILE_COUNT" -le 150 ]; then
|
|
|
189
194
|
_key_files
|
|
190
195
|
|
|
191
196
|
else
|
|
192
|
-
# Tier 4: very large vault — minimal footprint
|
|
193
197
|
echo "($FILE_COUNT files — showing summary)"
|
|
194
198
|
echo ""
|
|
195
199
|
_folder_summary
|
|
@@ -202,3 +206,16 @@ else
|
|
|
202
206
|
echo ""
|
|
203
207
|
echo "Use /recall <topic> to search the vault."
|
|
204
208
|
fi
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# --- Output structured JSON (stdout → agent via hookSpecificOutput) ---
|
|
212
|
+
python3 -c "
|
|
213
|
+
import json, sys
|
|
214
|
+
context = sys.stdin.read()
|
|
215
|
+
json.dump({
|
|
216
|
+
'hookSpecificOutput': {
|
|
217
|
+
'hookEventName': 'SessionStart',
|
|
218
|
+
'additionalContext': context
|
|
219
|
+
}
|
|
220
|
+
}, sys.stdout)
|
|
221
|
+
" <<< "$CONTEXT"
|