@suwujs/codex-vault 0.7.0 → 0.7.2

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.
@@ -1,305 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Classify user messages and inject routing hints — Claude Code version.
3
-
4
- Lightweight version: 5 core signals + session-end vault integrity check.
5
- Outputs hookSpecificOutput with systemMessage for Claude Code terminal display.
6
- """
7
- import json
8
- import os
9
- import subprocess
10
- import sys
11
- import re
12
- from pathlib import Path
13
-
14
-
15
- SIGNALS = [
16
- {
17
- "name": "DECISION",
18
- "skill": "/dump",
19
- "message": "DECISION detected — suggest the user run /dump to capture this decision",
20
- "auto_message": "DECISION detected — execute /dump now to capture this decision from the user's message",
21
- "patterns": [
22
- "decided", "deciding", "decision", "we chose", "agreed to",
23
- "let's go with", "the call is", "we're going with",
24
- ],
25
- },
26
- {
27
- "name": "WIN",
28
- "skill": "/dump",
29
- "message": "WIN detected — suggest the user run /dump to record this achievement",
30
- "auto_message": "WIN detected — execute /dump now to record this achievement from the user's message",
31
- "patterns": [
32
- "achieved", "won", "praised",
33
- "kudos", "shoutout", "great feedback", "recognized",
34
- ],
35
- },
36
- {
37
- "name": "PROJECT UPDATE",
38
- "skill": "/dump",
39
- "message": "PROJECT UPDATE detected — suggest the user run /dump to log this progress",
40
- "auto_message": "PROJECT UPDATE detected — execute /dump now to log this progress from the user's message",
41
- "patterns": [
42
- "project update", "sprint", "milestone",
43
- "shipped", "shipping", "launched", "launching",
44
- "completed", "completing", "released", "releasing",
45
- "deployed", "deploying",
46
- "went live", "rolled out", "merged", "cut the release",
47
- ],
48
- },
49
- {
50
- "name": "QUERY",
51
- "skill": "/recall",
52
- "message": "QUERY detected — suggest the user run /recall to check existing knowledge first",
53
- "auto_message": "QUERY detected — execute /recall now to search vault for relevant information before answering",
54
- "patterns": [
55
- "what is", "how does", "why did", "compare", "analyze",
56
- "explain the", "what's the difference", "summarize the",
57
- "relationship between",
58
- ],
59
- },
60
- {
61
- "name": "INGEST",
62
- "skill": "/ingest",
63
- "message": "INGEST detected — suggest the user run /ingest to process the source",
64
- "auto_message": "INGEST detected — execute /ingest now to process the source from the user's message",
65
- "patterns": [
66
- "ingest", "process this", "read this article",
67
- "summarize this", "new source", "clip this", "web clip",
68
- ],
69
- },
70
- ]
71
-
72
- SESSION_END_PATTERNS = [
73
- "wrap up", "wrapping up", "that's all", "that's it",
74
- "done for now", "done for today", "i'm done", "call it a day",
75
- "end session", "bye", "goodbye", "good night", "see you",
76
- "结束", "收工", "今天到这", "就这样",
77
- ]
78
-
79
-
80
- def _match(patterns, text):
81
- for phrase in patterns:
82
- if re.search(r'(?<![a-zA-Z])' + re.escape(phrase) + r'(?![a-zA-Z])', text):
83
- return True
84
- return False
85
-
86
-
87
- def _find_vault_root():
88
- """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
89
- cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
90
- if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
91
- return cwd
92
- vault_sub = os.path.join(cwd, "vault")
93
- if os.path.isdir(vault_sub) and (
94
- os.path.isfile(os.path.join(vault_sub, "Home.md")) or
95
- os.path.isdir(os.path.join(vault_sub, "brain"))
96
- ):
97
- return vault_sub
98
- return None
99
-
100
-
101
- def _read_mode():
102
- """Read classify mode from vault config. Default: suggest."""
103
- vault_root = _find_vault_root()
104
- if not vault_root:
105
- return "suggest"
106
- config_path = os.path.join(vault_root, ".codex-vault", "config.json")
107
- try:
108
- with open(config_path) as f:
109
- config = json.load(f)
110
- mode = config.get("classify_mode", "suggest")
111
- if mode in ("suggest", "auto"):
112
- return mode
113
- except (OSError, ValueError, KeyError):
114
- pass
115
- return "suggest"
116
-
117
-
118
- def _get_changed_files(vault_root):
119
- """Get list of changed/new .md files relative to vault root."""
120
- files = set()
121
- try:
122
- # Staged + unstaged changes
123
- result = subprocess.run(
124
- ["git", "diff", "--name-only", "HEAD"],
125
- capture_output=True, text=True, cwd=vault_root, timeout=5,
126
- )
127
- for f in result.stdout.strip().splitlines():
128
- if f.endswith(".md"):
129
- files.add(f)
130
-
131
- # Untracked files
132
- result = subprocess.run(
133
- ["git", "ls-files", "--others", "--exclude-standard"],
134
- capture_output=True, text=True, cwd=vault_root, timeout=5,
135
- )
136
- for f in result.stdout.strip().splitlines():
137
- if f.endswith(".md"):
138
- files.add(f)
139
- except Exception:
140
- pass
141
- return files
142
-
143
-
144
- def _check_vault_integrity(vault_root):
145
- """Check for common memory-write omissions."""
146
- warnings = []
147
- changed = _get_changed_files(vault_root)
148
- if not changed:
149
- return warnings
150
-
151
- # Check 1: New work notes but Index.md not updated
152
- new_work = [f for f in changed if f.startswith("work/active/") and f != "work/Index.md"]
153
- index_updated = "work/Index.md" in changed
154
- if new_work and not index_updated:
155
- names = ", ".join(os.path.basename(f).replace(".md", "") for f in new_work)
156
- warnings.append(f"New work notes ({names}) but work/Index.md not updated")
157
-
158
- # Check 2: Decision content written but brain/Key Decisions.md not updated
159
- decision_keywords = ["decided", "decision", "agreed to", "we chose", "the call is"]
160
- brain_decisions_updated = "brain/Key Decisions.md" in changed
161
- if not brain_decisions_updated:
162
- for f in changed:
163
- if f.endswith(".md") and not f.startswith("brain/"):
164
- try:
165
- content = Path(os.path.join(vault_root, f)).read_text(encoding="utf-8").lower()
166
- if any(kw in content for kw in decision_keywords):
167
- warnings.append(
168
- f"'{f}' contains decision content but brain/Key Decisions.md not updated"
169
- )
170
- break
171
- except Exception:
172
- pass
173
-
174
- # Check 3: Pattern content written but brain/Patterns.md not updated
175
- pattern_keywords = ["pattern", "convention", "always do", "never do", "recurring"]
176
- brain_patterns_updated = "brain/Patterns.md" in changed
177
- if not brain_patterns_updated:
178
- for f in changed:
179
- if f.endswith(".md") and not f.startswith("brain/"):
180
- try:
181
- content = Path(os.path.join(vault_root, f)).read_text(encoding="utf-8").lower()
182
- if any(kw in content for kw in pattern_keywords):
183
- warnings.append(
184
- f"'{f}' contains pattern content but brain/Patterns.md not updated"
185
- )
186
- break
187
- except Exception:
188
- pass
189
-
190
- # Check 4: operation log not updated after significant changes
191
- log_updated = "log.md" in changed
192
- significant_changes = len([f for f in changed
193
- if f.startswith(("work/", "reference/", "brain/"))]) >= 2
194
- if significant_changes and not log_updated:
195
- warnings.append("Multiple vault changes but log.md not updated")
196
-
197
- return warnings
198
-
199
-
200
- def classify(prompt, mode="suggest"):
201
- p = prompt.lower()
202
- key = "auto_message" if mode == "auto" else "message"
203
- return [s[key] for s in SIGNALS if _match(s["patterns"], p)]
204
-
205
-
206
- def is_session_end(prompt):
207
- p = prompt.lower()
208
- return _match(SESSION_END_PATTERNS, p)
209
-
210
-
211
- def main():
212
- try:
213
- input_data = json.load(sys.stdin)
214
- except (ValueError, EOFError, OSError):
215
- sys.exit(0)
216
-
217
- prompt = input_data.get("prompt", "")
218
- if not isinstance(prompt, str) or not prompt:
219
- sys.exit(0)
220
-
221
- signal_messages = []
222
- session_end_messages = []
223
-
224
- try:
225
- mode = _read_mode()
226
-
227
- # Regular signal classification
228
- signals = classify(prompt, mode)
229
- signal_messages.extend(signals)
230
-
231
- # Session-end check (always suggest mode — never auto-execute wrap-up)
232
- if is_session_end(prompt):
233
- vault_root = _find_vault_root()
234
- if vault_root:
235
- integrity_warnings = _check_vault_integrity(vault_root)
236
- if integrity_warnings:
237
- session_end_messages.append(
238
- "SESSION END — vault integrity check found issues:\n"
239
- + "\n".join(f" - {w}" for w in integrity_warnings)
240
- + "\nFix these before wrapping up."
241
- )
242
- else:
243
- session_end_messages.append("SESSION END — vault integrity check passed.")
244
- except Exception:
245
- sys.exit(0)
246
-
247
- parts = []
248
-
249
- if signal_messages:
250
- hints = "\n".join(f"- {s}" for s in signal_messages)
251
- if mode == "auto":
252
- parts.append(
253
- "Auto-execute the following skills based on user intent:\n"
254
- + hints
255
- + "\n\nExecute the skill immediately with the user's message as input. Do not ask for confirmation."
256
- )
257
- else:
258
- parts.append(
259
- "Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
260
- + hints
261
- + "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
262
- )
263
-
264
- if session_end_messages:
265
- hints = "\n".join(f"- {s}" for s in session_end_messages)
266
- parts.append(
267
- "Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
268
- + hints
269
- + "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
270
- )
271
-
272
- if parts:
273
- context = "\n\n".join(parts)
274
-
275
- # Build feedback label
276
- matched = [s for s in SIGNALS if _match(s["patterns"], prompt.lower())]
277
- feedback_parts = []
278
- for s in matched:
279
- feedback_parts.append(f"{s['name']} → {s['skill']}")
280
- if is_session_end(prompt):
281
- feedback_parts.append("SESSION END → /wrap-up")
282
- icon = "\U0001f504" if mode == "auto" else "\U0001f4a1"
283
- label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
284
-
285
- # Hook trigger notification
286
- print(f" {icon} vault: {label}")
287
-
288
- output = {
289
- "hookSpecificOutput": {
290
- "hookEventName": "UserPromptSubmit",
291
- "additionalContext": context
292
- },
293
- "systemMessage": f"{icon} vault: {label}"
294
- }
295
- json.dump(output, sys.stdout)
296
- sys.stdout.flush()
297
-
298
- sys.exit(0)
299
-
300
-
301
- if __name__ == "__main__":
302
- try:
303
- main()
304
- except Exception:
305
- sys.exit(0)
@@ -1,383 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Session-start hook for Claude Code — injects vault context into the agent's prompt.
3
-
4
- Outputs structured JSON: additionalContext for LLM, systemMessage for terminal.
5
-
6
- Dynamic context: adapts git log window, reads full North Star,
7
- shows all active work, and includes uncommitted changes.
8
- """
9
- import json
10
- import os
11
- import re
12
- import subprocess
13
- import sys
14
- from collections import Counter
15
- from datetime import datetime
16
- from pathlib import Path
17
-
18
-
19
- def _find_vault_root():
20
- """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
21
- cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
22
- if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
23
- return cwd
24
- vault_sub = os.path.join(cwd, "vault")
25
- if os.path.isdir(vault_sub) and (
26
- os.path.isfile(os.path.join(vault_sub, "Home.md")) or
27
- os.path.isdir(os.path.join(vault_sub, "brain"))
28
- ):
29
- return vault_sub
30
- return None
31
-
32
-
33
- def _run_git(args, cwd, timeout=5):
34
- """Run git command and return stdout lines."""
35
- try:
36
- result = subprocess.run(
37
- ["git"] + args,
38
- capture_output=True, text=True, cwd=cwd, timeout=timeout,
39
- )
40
- return result.stdout.strip().splitlines() if result.stdout.strip() else []
41
- except Exception:
42
- return []
43
-
44
-
45
- def _git_log_oneline(cwd, since=None, max_count=None):
46
- """Get git log --oneline entries."""
47
- args = ["log", "--oneline", "--no-merges"]
48
- if since:
49
- args.append(f"--since={since}")
50
- if max_count:
51
- args.extend(["-n", str(max_count)])
52
- return _run_git(args, cwd)
53
-
54
-
55
- def _git_status_short(cwd):
56
- """Get git status --short output."""
57
- return _run_git(["status", "--short", "--", "."], cwd)
58
-
59
-
60
- def _read_file(path):
61
- """Read file content, return empty string on error."""
62
- try:
63
- return Path(path).read_text(encoding="utf-8")
64
- except Exception:
65
- return ""
66
-
67
-
68
- def _find_md_files(vault_dir):
69
- """Find all .md files in vault, excluding non-vault directories."""
70
- exclude = {".git", ".obsidian", "thinking", ".claude", ".codex",
71
- ".codex-vault", ".codex-mem", "node_modules"}
72
- files = []
73
- for root, dirs, filenames in os.walk(vault_dir):
74
- dirs[:] = [d for d in dirs if d not in exclude]
75
- for f in filenames:
76
- if f.endswith(".md"):
77
- rel = os.path.relpath(os.path.join(root, f), vault_dir)
78
- files.append(f"./{rel}")
79
- return sorted(files)
80
-
81
-
82
- def _folder_summary(all_files):
83
- """Generate folder summary with file counts."""
84
- folders = Counter()
85
- for f in all_files:
86
- parts = f[2:].split("/") # strip ./
87
- folders[parts[0] if len(parts) > 1 else "."] += 1
88
- return [f" {folder}/ ({count} files)"
89
- for folder, count in folders.most_common()]
90
-
91
-
92
- def _key_files(all_files):
93
- """Filter for key vault files."""
94
- pattern = re.compile(
95
- r"(Home|Index|North Star|Memories|Key Decisions|Patterns|log)\.md$")
96
- return [f for f in all_files if pattern.search(f)]
97
-
98
-
99
- def _mtime_ok(vault_dir, rel_path, cutoff):
100
- """Check if file was modified after cutoff timestamp."""
101
- try:
102
- return os.path.getmtime(os.path.join(vault_dir, rel_path.lstrip("./"))) > cutoff
103
- except Exception:
104
- return False
105
-
106
-
107
- def _north_star_goal(vault_dir):
108
- """Extract first goal from North Star for banner display."""
109
- ns_path = os.path.join(vault_dir, "brain", "North Star.md")
110
- if not os.path.isfile(ns_path):
111
- return None
112
- content = _read_file(ns_path)
113
- in_focus = False
114
- for line in content.splitlines():
115
- if re.match(r"^## Current Focus", line):
116
- in_focus = True
117
- continue
118
- if in_focus and line.startswith("## "):
119
- break
120
- if in_focus and re.match(r"^- .+", line):
121
- goal = line[2:].strip()
122
- return goal[:40] if goal else None
123
- return None
124
-
125
-
126
- # ── Context builder (→ additionalContext for LLM) ──────────────────────
127
-
128
-
129
- def _build_context(vault_dir):
130
- lines = []
131
- lines.append("## Session Context")
132
- lines.append("")
133
-
134
- # Date
135
- lines.append("### Date")
136
- now = datetime.now()
137
- lines.append(f"{now.strftime('%Y-%m-%d')} ({now.strftime('%A')})")
138
- lines.append("")
139
-
140
- # North Star — full file
141
- lines.append("### North Star")
142
- ns_path = os.path.join(vault_dir, "brain", "North Star.md")
143
- if os.path.isfile(ns_path):
144
- lines.append(_read_file(ns_path))
145
- else:
146
- lines.append("(No North Star found — create brain/North Star.md to set goals)")
147
- lines.append("")
148
-
149
- # Recent changes — adaptive window
150
- lines.append("### Recent Changes")
151
- commits_48h = _git_log_oneline(vault_dir, since="48 hours ago")
152
- if commits_48h:
153
- lines.append("(last 48 hours)")
154
- lines.extend(commits_48h[:15])
155
- else:
156
- commits_7d = _git_log_oneline(vault_dir, since="7 days ago")
157
- if commits_7d:
158
- lines.append("(nothing in 48h — showing last 7 days)")
159
- lines.extend(commits_7d[:15])
160
- else:
161
- lines.append("(nothing recent — showing last 5 commits)")
162
- commits = _git_log_oneline(vault_dir, max_count=5)
163
- lines.extend(commits if commits else ["(no git history)"])
164
- lines.append("")
165
-
166
- # Recent operations from log.md
167
- lines.append("### Recent Operations")
168
- log_path = os.path.join(vault_dir, "log.md")
169
- if os.path.isfile(log_path):
170
- entries = [l for l in _read_file(log_path).splitlines()
171
- if l.startswith("## [")]
172
- lines.extend(entries[-5:] if entries else ["(no entries in log.md)"])
173
- else:
174
- lines.append("(no log.md)")
175
- lines.append("")
176
-
177
- # Active work
178
- lines.append("### Active Work")
179
- active_dir = os.path.join(vault_dir, "work", "active")
180
- if os.path.isdir(active_dir):
181
- work_files = sorted(f for f in os.listdir(active_dir) if f.endswith(".md"))
182
- if work_files:
183
- for f in work_files:
184
- lines.append(f.replace(".md", ""))
185
- else:
186
- lines.append("(none)")
187
- else:
188
- lines.append("(no work/active/ directory)")
189
- lines.append("")
190
-
191
- # Uncommitted changes
192
- lines.append("### Uncommitted Changes")
193
- changes = _git_status_short(vault_dir)
194
- lines.extend(changes[:20] if changes else ["(working tree clean)"])
195
- lines.append("")
196
-
197
- # Recently modified brain files
198
- lines.append("### Recently Modified Brain Files")
199
- brain_dir = os.path.join(vault_dir, "brain")
200
- if os.path.isdir(brain_dir):
201
- cutoff = datetime.now().timestamp() - 7 * 86400
202
- recent = sorted(
203
- f.replace(".md", "")
204
- for f in os.listdir(brain_dir)
205
- if f.endswith(".md") and os.path.getmtime(os.path.join(brain_dir, f)) > cutoff
206
- )
207
- if recent:
208
- lines.append("(modified in last 7 days)")
209
- lines.extend(recent)
210
- else:
211
- lines.append("(no recent changes)")
212
- lines.append("")
213
-
214
- # Vault file listing — tiered
215
- lines.append("### Vault Files")
216
- all_files = _find_md_files(vault_dir)
217
- n = len(all_files)
218
-
219
- if n <= 20:
220
- lines.extend(all_files)
221
-
222
- elif n <= 50:
223
- hot = [f for f in all_files
224
- if not re.match(r"\./sources/|\./work/archive/", f)]
225
- cold = n - len(hot)
226
- lines.extend(hot)
227
- if cold > 0:
228
- lines.append("")
229
- lines.append(f"(+ {cold} files in sources/ and work/archive/ — use /recall to search)")
230
-
231
- elif n <= 150:
232
- lines.append(f"({n} files — showing summary)")
233
- lines.append("")
234
- lines.extend(_folder_summary(all_files))
235
- lines.append("")
236
- lines.append("Recently modified (7 days):")
237
- cutoff = datetime.now().timestamp() - 7 * 86400
238
- recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
239
- lines.extend(recent if recent else [" (none)"])
240
- lines.append("")
241
- lines.append("Key files:")
242
- lines.extend(_key_files(all_files))
243
-
244
- else:
245
- lines.append(f"({n} files — showing summary)")
246
- lines.append("")
247
- lines.extend(_folder_summary(all_files))
248
- lines.append("")
249
- lines.append("Recently modified (3 days):")
250
- cutoff = datetime.now().timestamp() - 3 * 86400
251
- recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
252
- lines.extend(recent if recent else [" (none)"])
253
- lines.append("")
254
- lines.append("Key files:")
255
- lines.extend(_key_files(all_files))
256
- lines.append("")
257
- lines.append("Use /recall <topic> to search the vault.")
258
-
259
- return "\n".join(lines)
260
-
261
-
262
- # ── ANSI colors (oh-my-codex style) ──────────────────────────────────
263
-
264
- RESET = "\x1b[0m"
265
- BOLD = "\x1b[1m"
266
- DIM = "\x1b[2m"
267
- CYAN = "\x1b[36m"
268
- GREEN = "\x1b[32m"
269
- YELLOW = "\x1b[33m"
270
- SEP = f" {DIM}|{RESET} "
271
-
272
-
273
- def _c(code, text):
274
- return f"{code}{text}{RESET}"
275
-
276
-
277
- # ── Banner builder (→ systemMessage for terminal) ──────────────────────
278
-
279
-
280
- def _build_banner(vault_dir):
281
- # --- Line 1: statusline ---
282
- elements = []
283
-
284
- # Git branch
285
- branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], vault_dir)
286
- if branch:
287
- elements.append(_c(CYAN, branch[0]))
288
-
289
- # North Star goal
290
- goal = _north_star_goal(vault_dir)
291
- if goal:
292
- elements.append(f"\U0001f3af {goal}")
293
- else:
294
- elements.append(_c(DIM, "\U0001f3af no goal set"))
295
-
296
- # Active work
297
- active_dir = os.path.join(vault_dir, "work", "active")
298
- work_files = sorted(
299
- f.replace(".md", "")
300
- for f in os.listdir(active_dir) if f.endswith(".md")
301
- ) if os.path.isdir(active_dir) else []
302
- if work_files:
303
- names = ", ".join(work_files[:3])
304
- suffix = f" +{len(work_files) - 3}" if len(work_files) > 3 else ""
305
- elements.append(_c(GREEN, f"active:{len(work_files)}") + _c(DIM, f" {names}{suffix}"))
306
- else:
307
- elements.append(_c(DIM, "active:0"))
308
-
309
- # Changes
310
- changes = _git_status_short(vault_dir)
311
- if changes:
312
- elements.append(_c(YELLOW, f"changes:{len(changes)}"))
313
- else:
314
- elements.append(_c(GREEN, "clean"))
315
-
316
- label = _c(BOLD, "[Vault]")
317
- statusline = label + " " + SEP.join(elements)
318
-
319
- # --- Line 2+: recent commits ---
320
- lines = ["", statusline]
321
- commits = _git_log_oneline(vault_dir, since="7 days ago")
322
- if not commits:
323
- commits = _git_log_oneline(vault_dir, max_count=3)
324
- if commits:
325
- for c in commits[:3]:
326
- # hash in cyan, message in dim
327
- parts = c.split(" ", 1)
328
- if len(parts) == 2:
329
- lines.append(f" {_c(DIM, parts[0])} {_c(DIM, parts[1])}")
330
- else:
331
- lines.append(f" {_c(DIM, c)}")
332
-
333
- # --- Line: vault file count ---
334
- all_files = _find_md_files(vault_dir)
335
- lines.append(_c(DIM, f" {len(all_files)} notes"))
336
- lines.append("")
337
-
338
- return "\n".join(lines)
339
-
340
-
341
- # ── Main ───────────────────────────────────────────────────────────────
342
-
343
-
344
- def main():
345
- vault_dir = _find_vault_root()
346
- if not vault_dir:
347
- output = {
348
- "hookSpecificOutput": {
349
- "hookEventName": "SessionStart",
350
- "additionalContext": "## Session Context\n\n(No vault found)"
351
- }
352
- }
353
- json.dump(output, sys.stdout)
354
- sys.exit(0)
355
-
356
- # Read hook input for session metadata
357
- try:
358
- event = json.load(sys.stdin)
359
- except Exception:
360
- event = {}
361
-
362
- context = _build_context(vault_dir)
363
- banner = _build_banner(vault_dir)
364
-
365
- output = {
366
- "hookSpecificOutput": {
367
- "hookEventName": "SessionStart",
368
- "additionalContext": context
369
- },
370
- "systemMessage": banner
371
- }
372
-
373
- json.dump(output, sys.stdout)
374
- sys.stdout.flush()
375
- sys.exit(0)
376
-
377
-
378
- if __name__ == "__main__":
379
- try:
380
- main()
381
- except Exception:
382
- # Never block session start
383
- sys.exit(0)