@suwujs/codex-vault 0.5.0 → 0.5.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.
- package/package.json +1 -1
- package/plugin/VERSION +1 -1
- package/plugin/hooks/classify-message.py +16 -13
- package/plugin/hooks/session-start.py +389 -0
- package/plugin/hooks/validate-write.py +12 -9
- package/plugin/install.sh +1 -1
- package/vault/.claude/settings.json +1 -1
- package/vault/.codex-vault/hooks/classify-message.py +16 -13
- package/vault/.codex-vault/hooks/session-start.py +11 -1
- package/vault/.codex-vault/hooks/validate-write.py +12 -9
package/package.json
CHANGED
package/plugin/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.5.
|
|
1
|
+
0.5.2
|
|
@@ -273,27 +273,30 @@ def main():
|
|
|
273
273
|
|
|
274
274
|
if parts:
|
|
275
275
|
context = "\n\n".join(parts)
|
|
276
|
+
|
|
277
|
+
# Build feedback label
|
|
278
|
+
matched = [s for s in SIGNALS if _match(s["patterns"], prompt.lower())]
|
|
279
|
+
feedback_parts = []
|
|
280
|
+
for s in matched:
|
|
281
|
+
feedback_parts.append(f"{s['name']} → {s['skill']}")
|
|
282
|
+
if is_session_end(prompt):
|
|
283
|
+
feedback_parts.append("SESSION END → /wrap-up")
|
|
284
|
+
icon = "🔄" if mode == "auto" else "💡"
|
|
285
|
+
label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
|
|
286
|
+
|
|
287
|
+
# Hook trigger notification
|
|
288
|
+
print(f" {icon} vault: {label}")
|
|
289
|
+
|
|
276
290
|
output = {
|
|
277
291
|
"hookSpecificOutput": {
|
|
278
292
|
"hookEventName": "UserPromptSubmit",
|
|
279
293
|
"additionalContext": context
|
|
280
|
-
}
|
|
294
|
+
},
|
|
295
|
+
"systemMessage": f"{icon} vault: {label}"
|
|
281
296
|
}
|
|
282
297
|
json.dump(output, sys.stdout)
|
|
283
298
|
sys.stdout.flush()
|
|
284
299
|
|
|
285
|
-
# Visible feedback to user terminal (stderr)
|
|
286
|
-
matched = [s for s in SIGNALS if _match(s["patterns"], prompt.lower())]
|
|
287
|
-
parts = []
|
|
288
|
-
for s in matched:
|
|
289
|
-
parts.append(f"{s['name']} → {s['skill']}")
|
|
290
|
-
if is_session_end(prompt):
|
|
291
|
-
parts.append("SESSION END → /wrap-up")
|
|
292
|
-
if parts:
|
|
293
|
-
label = ", ".join(parts)
|
|
294
|
-
icon = "🔄" if mode == "auto" else "💡"
|
|
295
|
-
print(f" {icon} vault: {label}", file=sys.stderr)
|
|
296
|
-
|
|
297
300
|
sys.exit(0)
|
|
298
301
|
|
|
299
302
|
|
|
@@ -0,0 +1,389 @@
|
|
|
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
|
+
# Read hook input for session metadata
|
|
359
|
+
try:
|
|
360
|
+
event = json.load(sys.stdin)
|
|
361
|
+
except Exception:
|
|
362
|
+
event = {}
|
|
363
|
+
|
|
364
|
+
context = _build_context(vault_dir)
|
|
365
|
+
banner = _build_banner(vault_dir)
|
|
366
|
+
|
|
367
|
+
# =================== Codex hook trigger notification ===================
|
|
368
|
+
print("✅ 【Hook 通知】SessionStart 已触发 — 会话启动/恢复完成")
|
|
369
|
+
print(f" 会话 ID: {event.get('sessionId', '未知')} | Matcher: {event.get('matcher', 'N/A')}")
|
|
370
|
+
|
|
371
|
+
output = {
|
|
372
|
+
"hookSpecificOutput": {
|
|
373
|
+
"hookEventName": "SessionStart",
|
|
374
|
+
"additionalContext": context
|
|
375
|
+
},
|
|
376
|
+
"systemMessage": "【Codex Hook】SessionStart 执行完毕 ✅\n" + banner
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
json.dump(output, sys.stdout)
|
|
380
|
+
sys.stdout.flush()
|
|
381
|
+
sys.exit(0)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
if __name__ == "__main__":
|
|
385
|
+
try:
|
|
386
|
+
main()
|
|
387
|
+
except Exception:
|
|
388
|
+
# Never block session start
|
|
389
|
+
sys.exit(0)
|
|
@@ -94,23 +94,26 @@ def main():
|
|
|
94
94
|
|
|
95
95
|
if warnings:
|
|
96
96
|
hint_list = "\n".join(f" - {w}" for w in warnings)
|
|
97
|
+
count = len(warnings)
|
|
98
|
+
first = warnings[0]
|
|
99
|
+
if count == 1:
|
|
100
|
+
feedback = f"⚠️ vault: {basename} — {first}"
|
|
101
|
+
else:
|
|
102
|
+
feedback = f"⚠️ vault: {basename} — {first} (+{count - 1} more)"
|
|
103
|
+
|
|
104
|
+
# Hook trigger notification
|
|
105
|
+
print(f" {feedback}")
|
|
106
|
+
|
|
97
107
|
output = {
|
|
98
108
|
"hookSpecificOutput": {
|
|
99
109
|
"hookEventName": "PostToolUse",
|
|
100
110
|
"additionalContext": f"Vault warnings for `{basename}`:\n{hint_list}\nFix these before moving on."
|
|
101
|
-
}
|
|
111
|
+
},
|
|
112
|
+
"systemMessage": feedback
|
|
102
113
|
}
|
|
103
114
|
json.dump(output, sys.stdout)
|
|
104
115
|
sys.stdout.flush()
|
|
105
116
|
|
|
106
|
-
# Visible feedback to user terminal (stderr)
|
|
107
|
-
count = len(warnings)
|
|
108
|
-
first = warnings[0]
|
|
109
|
-
if count == 1:
|
|
110
|
-
print(f" ⚠️ vault: {basename} — {first}", file=sys.stderr)
|
|
111
|
-
else:
|
|
112
|
-
print(f" ⚠️ vault: {basename} — {first} (+{count - 1} more)", file=sys.stderr)
|
|
113
|
-
|
|
114
117
|
sys.exit(0)
|
|
115
118
|
|
|
116
119
|
|
package/plugin/install.sh
CHANGED
|
@@ -113,7 +113,7 @@ hooks_rel = os.environ["CVAULT_HOOKS_REL"]
|
|
|
113
113
|
new_hooks = {
|
|
114
114
|
"SessionStart": [{
|
|
115
115
|
"matcher": "startup|resume|compact",
|
|
116
|
-
"hooks": [{"type": "command", "command": f"
|
|
116
|
+
"hooks": [{"type": "command", "command": f"python3 {hooks_rel}/session-start.py", "timeout": 30}]
|
|
117
117
|
}],
|
|
118
118
|
"UserPromptSubmit": [{
|
|
119
119
|
"hooks": [{"type": "command", "command": f"python3 {hooks_rel}/classify-message.py", "timeout": 15}]
|
|
@@ -273,27 +273,30 @@ def main():
|
|
|
273
273
|
|
|
274
274
|
if parts:
|
|
275
275
|
context = "\n\n".join(parts)
|
|
276
|
+
|
|
277
|
+
# Build feedback label
|
|
278
|
+
matched = [s for s in SIGNALS if _match(s["patterns"], prompt.lower())]
|
|
279
|
+
feedback_parts = []
|
|
280
|
+
for s in matched:
|
|
281
|
+
feedback_parts.append(f"{s['name']} → {s['skill']}")
|
|
282
|
+
if is_session_end(prompt):
|
|
283
|
+
feedback_parts.append("SESSION END → /wrap-up")
|
|
284
|
+
icon = "🔄" if mode == "auto" else "💡"
|
|
285
|
+
label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
|
|
286
|
+
|
|
287
|
+
# Hook trigger notification
|
|
288
|
+
print(f" {icon} vault: {label}")
|
|
289
|
+
|
|
276
290
|
output = {
|
|
277
291
|
"hookSpecificOutput": {
|
|
278
292
|
"hookEventName": "UserPromptSubmit",
|
|
279
293
|
"additionalContext": context
|
|
280
|
-
}
|
|
294
|
+
},
|
|
295
|
+
"systemMessage": f"{icon} vault: {label}"
|
|
281
296
|
}
|
|
282
297
|
json.dump(output, sys.stdout)
|
|
283
298
|
sys.stdout.flush()
|
|
284
299
|
|
|
285
|
-
# Visible feedback to user terminal (stderr)
|
|
286
|
-
matched = [s for s in SIGNALS if _match(s["patterns"], prompt.lower())]
|
|
287
|
-
parts = []
|
|
288
|
-
for s in matched:
|
|
289
|
-
parts.append(f"{s['name']} → {s['skill']}")
|
|
290
|
-
if is_session_end(prompt):
|
|
291
|
-
parts.append("SESSION END → /wrap-up")
|
|
292
|
-
if parts:
|
|
293
|
-
label = ", ".join(parts)
|
|
294
|
-
icon = "🔄" if mode == "auto" else "💡"
|
|
295
|
-
print(f" {icon} vault: {label}", file=sys.stderr)
|
|
296
|
-
|
|
297
300
|
sys.exit(0)
|
|
298
301
|
|
|
299
302
|
|
|
@@ -355,15 +355,25 @@ def main():
|
|
|
355
355
|
json.dump(output, sys.stdout)
|
|
356
356
|
sys.exit(0)
|
|
357
357
|
|
|
358
|
+
# Read hook input for session metadata
|
|
359
|
+
try:
|
|
360
|
+
event = json.load(sys.stdin)
|
|
361
|
+
except Exception:
|
|
362
|
+
event = {}
|
|
363
|
+
|
|
358
364
|
context = _build_context(vault_dir)
|
|
359
365
|
banner = _build_banner(vault_dir)
|
|
360
366
|
|
|
367
|
+
# =================== Codex hook trigger notification ===================
|
|
368
|
+
print("✅ 【Hook 通知】SessionStart 已触发 — 会话启动/恢复完成")
|
|
369
|
+
print(f" 会话 ID: {event.get('sessionId', '未知')} | Matcher: {event.get('matcher', 'N/A')}")
|
|
370
|
+
|
|
361
371
|
output = {
|
|
362
372
|
"hookSpecificOutput": {
|
|
363
373
|
"hookEventName": "SessionStart",
|
|
364
374
|
"additionalContext": context
|
|
365
375
|
},
|
|
366
|
-
"systemMessage": banner
|
|
376
|
+
"systemMessage": "【Codex Hook】SessionStart 执行完毕 ✅\n" + banner
|
|
367
377
|
}
|
|
368
378
|
|
|
369
379
|
json.dump(output, sys.stdout)
|
|
@@ -94,23 +94,26 @@ def main():
|
|
|
94
94
|
|
|
95
95
|
if warnings:
|
|
96
96
|
hint_list = "\n".join(f" - {w}" for w in warnings)
|
|
97
|
+
count = len(warnings)
|
|
98
|
+
first = warnings[0]
|
|
99
|
+
if count == 1:
|
|
100
|
+
feedback = f"⚠️ vault: {basename} — {first}"
|
|
101
|
+
else:
|
|
102
|
+
feedback = f"⚠️ vault: {basename} — {first} (+{count - 1} more)"
|
|
103
|
+
|
|
104
|
+
# Hook trigger notification
|
|
105
|
+
print(f" {feedback}")
|
|
106
|
+
|
|
97
107
|
output = {
|
|
98
108
|
"hookSpecificOutput": {
|
|
99
109
|
"hookEventName": "PostToolUse",
|
|
100
110
|
"additionalContext": f"Vault warnings for `{basename}`:\n{hint_list}\nFix these before moving on."
|
|
101
|
-
}
|
|
111
|
+
},
|
|
112
|
+
"systemMessage": feedback
|
|
102
113
|
}
|
|
103
114
|
json.dump(output, sys.stdout)
|
|
104
115
|
sys.stdout.flush()
|
|
105
116
|
|
|
106
|
-
# Visible feedback to user terminal (stderr)
|
|
107
|
-
count = len(warnings)
|
|
108
|
-
first = warnings[0]
|
|
109
|
-
if count == 1:
|
|
110
|
-
print(f" ⚠️ vault: {basename} — {first}", file=sys.stderr)
|
|
111
|
-
else:
|
|
112
|
-
print(f" ⚠️ vault: {basename} — {first} (+{count - 1} more)", file=sys.stderr)
|
|
113
|
-
|
|
114
117
|
sys.exit(0)
|
|
115
118
|
|
|
116
119
|
|