@suwujs/codex-vault 0.5.3 → 0.7.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/README.md CHANGED
@@ -63,20 +63,20 @@ git commit (persistent)
63
63
  Next session → session-start hook injects context back
64
64
  ```
65
65
 
66
- Three hooks power the loop:
66
+ Hooks power the loop:
67
67
 
68
- | Hook | When | What |
69
- |------|------|------|
70
- | **session-start** | Agent starts | Injects North Star goals, recent git changes, active work, vault file listing |
71
- | **classify-message** | Every message | Detects decisions, wins, project updates — hints the agent where to file them |
72
- | **validate-write** | After writing `.md` | Checks frontmatter and wikilinks — catches mistakes before they stick |
68
+ | Hook | When | What | Claude Code | Codex CLI |
69
+ |------|------|------|-------------|-----------|
70
+ | **session-start** | Agent starts | Injects North Star goals, recent git changes, active work, vault file listing | SessionStart | SessionStart |
71
+ | **classify-message** | Every message | Detects decisions, wins, project updates — hints the agent where to file them | UserPromptSubmit | UserPromptSubmit |
72
+ | **validate-write** | After writing `.md` | Checks frontmatter and wikilinks — catches mistakes before they stick | PostToolUse (Write\|Edit) | N/A (Codex only supports Bash) |
73
73
 
74
74
  ## Supported Agents
75
75
 
76
76
  | Agent | Hooks | Skills | Status |
77
77
  |-------|-------|--------|--------|
78
78
  | Claude Code | 3 hooks via `.claude/settings.json` | `/dump` `/recall` `/ingest` `/wrap-up` | Full support |
79
- | Codex CLI | 3 hooks via `.codex/hooks.json` | `$dump` `$recall` `$ingest` `$wrap-up` | Full support |
79
+ | Codex CLI | 2 hooks via `.codex/hooks.json` | `$dump` `$recall` `$ingest` `$wrap-up` | Full support (PostToolUse limited to Bash by Codex) |
80
80
  | Other | Write an adapter ([docs/adding-an-agent.md](docs/adding-an-agent.md)) | Depends on agent | Community |
81
81
 
82
82
  ## Vault Structure
package/README.zh-CN.md CHANGED
@@ -95,20 +95,20 @@ git commit(持久化)
95
95
  下次 session → session-start hook 注入上下文
96
96
  ```
97
97
 
98
- 三个 hook 驱动整个循环:
98
+ Hook 驱动整个循环:
99
99
 
100
- | Hook | 触发时机 | 做什么 |
101
- |------|---------|--------|
102
- | **session-start** | Agent 启动 | 注入 North Star 目标、近期 git 变更、活跃项目、vault 文件清单 |
103
- | **classify-message** | 每条消息 | 检测决策、成果、项目更新 — 提示 agent 该归档到哪里 |
104
- | **validate-write** | 写 `.md` 后 | 检查 frontmatter 和 wikilinks — 在落盘前纠错 |
100
+ | Hook | 触发时机 | 做什么 | Claude Code | Codex CLI |
101
+ |------|---------|--------|-------------|-----------|
102
+ | **session-start** | Agent 启动 | 注入 North Star 目标、近期 git 变更、活跃项目、vault 文件清单 | SessionStart | SessionStart |
103
+ | **classify-message** | 每条消息 | 检测决策、成果、项目更新 — 提示 agent 该归档到哪里 | UserPromptSubmit | UserPromptSubmit |
104
+ | **validate-write** | 写 `.md` 后 | 检查 frontmatter 和 wikilinks — 在落盘前纠错 | PostToolUse (Write\|Edit) | 不支持(Codex 仅支持 Bash) |
105
105
 
106
106
  ## 支持的 Agent
107
107
 
108
108
  | Agent | Hooks | Skills | 状态 |
109
109
  |-------|-------|--------|------|
110
110
  | Claude Code | `.claude/settings.json` 3 hooks | `/dump` `/recall` `/ingest` `/wrap-up` | 完整支持 |
111
- | Codex CLI | `.codex/hooks.json` 3 hooks | `$dump` `$recall` `$ingest` `$wrap-up` | 完整支持 |
111
+ | Codex CLI | `.codex/hooks.json` 2 hooks | `$dump` `$recall` `$ingest` `$wrap-up` | 完整支持(PostToolUse 受 Codex 限制仅支持 Bash) |
112
112
  | 其他 | 写适配器([指南](docs/adding-an-agent.md)) | 取决于 agent | 社区贡献 |
113
113
 
114
114
  ## Vault 结构
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ # Codex-Vault wrapper — shows vault banner then launches Codex CLI
3
+ # Usage: codex-vault-run [codex args...]
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ PROJECT_DIR="$(pwd)"
7
+
8
+ # Find session-start hook relative to project or plugin
9
+ HOOK=""
10
+ for candidate in \
11
+ "$PROJECT_DIR/plugin/hooks/codex/session-start.py" \
12
+ "$PROJECT_DIR/.codex-vault/hooks/codex/session-start.py" \
13
+ "$SCRIPT_DIR/../plugin/hooks/codex/session-start.py"; do
14
+ if [ -f "$candidate" ]; then
15
+ HOOK="$candidate"
16
+ break
17
+ fi
18
+ done
19
+
20
+ if [ -n "$HOOK" ]; then
21
+ SUMMARY=$(echo '{}' | python3 "$HOOK" 2>/dev/null \
22
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('systemMessage',''))" 2>/dev/null)
23
+ if [ -n "$SUMMARY" ]; then
24
+ echo " $SUMMARY"
25
+ fi
26
+ fi
27
+
28
+ exec codex "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suwujs/codex-vault",
3
- "version": "0.5.3",
3
+ "version": "0.7.0",
4
4
  "description": "Persistent knowledge vault for LLM agents (Claude Code, Codex CLI)",
5
5
  "license": "MIT",
6
6
  "repository": {
package/plugin/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.3
1
+ 0.6.0
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """Classify user messages and inject routing hints.
2
+ """Classify user messages and inject routing hints — Claude Code version.
3
3
 
4
4
  Lightweight version: 5 core signals + session-end vault integrity check.
5
- Agent-agnostic outputs hookSpecificOutput compatible with both
6
- Claude Code and Codex CLI.
5
+ Outputs hookSpecificOutput with systemMessage for Claude Code terminal display.
7
6
  """
8
7
  import json
9
8
  import os
@@ -87,8 +86,7 @@ def _match(patterns, text):
87
86
 
88
87
  def _find_vault_root():
89
88
  """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
90
- cwd = os.environ.get("CLAUDE_PROJECT_DIR",
91
- os.environ.get("CODEX_PROJECT_DIR", os.getcwd()))
89
+ cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
92
90
  if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
93
91
  return cwd
94
92
  vault_sub = os.path.join(cwd, "vault")
@@ -281,7 +279,7 @@ def main():
281
279
  feedback_parts.append(f"{s['name']} → {s['skill']}")
282
280
  if is_session_end(prompt):
283
281
  feedback_parts.append("SESSION END → /wrap-up")
284
- icon = "🔄" if mode == "auto" else "💡"
282
+ icon = "\U0001f504" if mode == "auto" else "\U0001f4a1"
285
283
  label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
286
284
 
287
285
  # Hook trigger notification
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env python3
2
- """Session-start hook — injects vault context into the agent's prompt.
2
+ """Session-start hook for Claude Code — injects vault context into the agent's prompt.
3
3
 
4
- Works with any agent that supports SessionStart hooks (Claude Code, Codex CLI).
5
4
  Outputs structured JSON: additionalContext for LLM, systemMessage for terminal.
6
5
 
7
6
  Dynamic context: adapts git log window, reads full North Star,
@@ -19,8 +18,7 @@ from pathlib import Path
19
18
 
20
19
  def _find_vault_root():
21
20
  """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()))
21
+ cwd = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
24
22
  if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
25
23
  return cwd
26
24
  vault_sub = os.path.join(cwd, "vault")
@@ -343,23 +341,6 @@ def _build_banner(vault_dir):
343
341
  # ── Main ───────────────────────────────────────────────────────────────
344
342
 
345
343
 
346
- def _detect_platform():
347
- """Detect whether we're running under Claude Code or Codex CLI."""
348
- if os.environ.get("CLAUDE_PROJECT_DIR"):
349
- return "claude"
350
- if os.environ.get("CODEX_PROJECT_DIR") or os.environ.get("CODEX_HOME"):
351
- return "codex"
352
- # Fallback: check parent process name
353
- try:
354
- ppid = os.getppid()
355
- cmdline = Path(f"/proc/{ppid}/cmdline").read_text() if os.path.exists(f"/proc/{ppid}/cmdline") else ""
356
- if "codex" in cmdline.lower():
357
- return "codex"
358
- except Exception:
359
- pass
360
- return "claude" # default
361
-
362
-
363
344
  def main():
364
345
  vault_dir = _find_vault_root()
365
346
  if not vault_dir:
@@ -378,7 +359,6 @@ def main():
378
359
  except Exception:
379
360
  event = {}
380
361
 
381
- platform = _detect_platform()
382
362
  context = _build_context(vault_dir)
383
363
  banner = _build_banner(vault_dir)
384
364
 
@@ -387,16 +367,9 @@ def main():
387
367
  "hookEventName": "SessionStart",
388
368
  "additionalContext": context
389
369
  },
370
+ "systemMessage": banner
390
371
  }
391
372
 
392
- if platform == "claude":
393
- # Claude Code renders systemMessage in terminal
394
- output["systemMessage"] = banner
395
- else:
396
- # Codex CLI: systemMessage not rendered by TUI,
397
- # use stderr for terminal visibility (best effort)
398
- sys.stderr.write(banner + "\n")
399
-
400
373
  json.dump(output, sys.stdout)
401
374
  sys.stdout.flush()
402
375
  sys.exit(0)
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """Post-write validation for vault notes.
2
+ """Post-write validation for vault notes — Claude Code version.
3
3
 
4
4
  Checks frontmatter and wikilinks on any .md file written to the vault.
5
- Agent-agnostic outputs hookSpecificOutput compatible with both
6
- Claude Code and Codex CLI.
5
+ Outputs hookSpecificOutput with systemMessage for Claude Code terminal display.
7
6
  """
8
7
  import json
9
8
  import re
@@ -97,9 +96,9 @@ def main():
97
96
  count = len(warnings)
98
97
  first = warnings[0]
99
98
  if count == 1:
100
- feedback = f"⚠️ vault: {basename} — {first}"
99
+ feedback = f"\u26a0\ufe0f vault: {basename} — {first}"
101
100
  else:
102
- feedback = f"⚠️ vault: {basename} — {first} (+{count - 1} more)"
101
+ feedback = f"\u26a0\ufe0f vault: {basename} — {first} (+{count - 1} more)"
103
102
 
104
103
  # Hook trigger notification
105
104
  print(f" {feedback}")
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ """Classify user messages and inject routing hints — Codex CLI version.
3
+
4
+ Lightweight version: 5 core signals + session-end vault integrity check.
5
+ Outputs hookSpecificOutput for Codex CLI. Feedback via stderr
6
+ (Codex CLI does not render systemMessage).
7
+ """
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import re
13
+ from pathlib import Path
14
+
15
+
16
+ SIGNALS = [
17
+ {
18
+ "name": "DECISION",
19
+ "skill": "/dump",
20
+ "message": "DECISION detected — suggest the user run /dump to capture this decision",
21
+ "auto_message": "DECISION detected — execute /dump now to capture this decision from the user's message",
22
+ "patterns": [
23
+ "decided", "deciding", "decision", "we chose", "agreed to",
24
+ "let's go with", "the call is", "we're going with",
25
+ ],
26
+ },
27
+ {
28
+ "name": "WIN",
29
+ "skill": "/dump",
30
+ "message": "WIN detected — suggest the user run /dump to record this achievement",
31
+ "auto_message": "WIN detected — execute /dump now to record this achievement from the user's message",
32
+ "patterns": [
33
+ "achieved", "won", "praised",
34
+ "kudos", "shoutout", "great feedback", "recognized",
35
+ ],
36
+ },
37
+ {
38
+ "name": "PROJECT UPDATE",
39
+ "skill": "/dump",
40
+ "message": "PROJECT UPDATE detected — suggest the user run /dump to log this progress",
41
+ "auto_message": "PROJECT UPDATE detected — execute /dump now to log this progress from the user's message",
42
+ "patterns": [
43
+ "project update", "sprint", "milestone",
44
+ "shipped", "shipping", "launched", "launching",
45
+ "completed", "completing", "released", "releasing",
46
+ "deployed", "deploying",
47
+ "went live", "rolled out", "merged", "cut the release",
48
+ ],
49
+ },
50
+ {
51
+ "name": "QUERY",
52
+ "skill": "/recall",
53
+ "message": "QUERY detected — suggest the user run /recall to check existing knowledge first",
54
+ "auto_message": "QUERY detected — execute /recall now to search vault for relevant information before answering",
55
+ "patterns": [
56
+ "what is", "how does", "why did", "compare", "analyze",
57
+ "explain the", "what's the difference", "summarize the",
58
+ "relationship between",
59
+ ],
60
+ },
61
+ {
62
+ "name": "INGEST",
63
+ "skill": "/ingest",
64
+ "message": "INGEST detected — suggest the user run /ingest to process the source",
65
+ "auto_message": "INGEST detected — execute /ingest now to process the source from the user's message",
66
+ "patterns": [
67
+ "ingest", "process this", "read this article",
68
+ "summarize this", "new source", "clip this", "web clip",
69
+ ],
70
+ },
71
+ ]
72
+
73
+ SESSION_END_PATTERNS = [
74
+ "wrap up", "wrapping up", "that's all", "that's it",
75
+ "done for now", "done for today", "i'm done", "call it a day",
76
+ "end session", "bye", "goodbye", "good night", "see you",
77
+ "结束", "收工", "今天到这", "就这样",
78
+ ]
79
+
80
+
81
+ def _match(patterns, text):
82
+ for phrase in patterns:
83
+ if re.search(r'(?<![a-zA-Z])' + re.escape(phrase) + r'(?![a-zA-Z])', text):
84
+ return True
85
+ return False
86
+
87
+
88
+ def _find_vault_root():
89
+ """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
90
+ cwd = os.environ.get("CODEX_PROJECT_DIR", os.getcwd())
91
+ if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
92
+ return cwd
93
+ vault_sub = os.path.join(cwd, "vault")
94
+ if os.path.isdir(vault_sub) and (
95
+ os.path.isfile(os.path.join(vault_sub, "Home.md")) or
96
+ os.path.isdir(os.path.join(vault_sub, "brain"))
97
+ ):
98
+ return vault_sub
99
+ return None
100
+
101
+
102
+ def _read_mode():
103
+ """Read classify mode from vault config. Default: suggest."""
104
+ vault_root = _find_vault_root()
105
+ if not vault_root:
106
+ return "suggest"
107
+ config_path = os.path.join(vault_root, ".codex-vault", "config.json")
108
+ try:
109
+ with open(config_path) as f:
110
+ config = json.load(f)
111
+ mode = config.get("classify_mode", "suggest")
112
+ if mode in ("suggest", "auto"):
113
+ return mode
114
+ except (OSError, ValueError, KeyError):
115
+ pass
116
+ return "suggest"
117
+
118
+
119
+ def _get_changed_files(vault_root):
120
+ """Get list of changed/new .md files relative to vault root."""
121
+ files = set()
122
+ try:
123
+ # Staged + unstaged changes
124
+ result = subprocess.run(
125
+ ["git", "diff", "--name-only", "HEAD"],
126
+ capture_output=True, text=True, cwd=vault_root, timeout=5,
127
+ )
128
+ for f in result.stdout.strip().splitlines():
129
+ if f.endswith(".md"):
130
+ files.add(f)
131
+
132
+ # Untracked files
133
+ result = subprocess.run(
134
+ ["git", "ls-files", "--others", "--exclude-standard"],
135
+ capture_output=True, text=True, cwd=vault_root, timeout=5,
136
+ )
137
+ for f in result.stdout.strip().splitlines():
138
+ if f.endswith(".md"):
139
+ files.add(f)
140
+ except Exception:
141
+ pass
142
+ return files
143
+
144
+
145
+ def _check_vault_integrity(vault_root):
146
+ """Check for common memory-write omissions."""
147
+ warnings = []
148
+ changed = _get_changed_files(vault_root)
149
+ if not changed:
150
+ return warnings
151
+
152
+ # Check 1: New work notes but Index.md not updated
153
+ new_work = [f for f in changed if f.startswith("work/active/") and f != "work/Index.md"]
154
+ index_updated = "work/Index.md" in changed
155
+ if new_work and not index_updated:
156
+ names = ", ".join(os.path.basename(f).replace(".md", "") for f in new_work)
157
+ warnings.append(f"New work notes ({names}) but work/Index.md not updated")
158
+
159
+ # Check 2: Decision content written but brain/Key Decisions.md not updated
160
+ decision_keywords = ["decided", "decision", "agreed to", "we chose", "the call is"]
161
+ brain_decisions_updated = "brain/Key Decisions.md" in changed
162
+ if not brain_decisions_updated:
163
+ for f in changed:
164
+ if f.endswith(".md") and not f.startswith("brain/"):
165
+ try:
166
+ content = Path(os.path.join(vault_root, f)).read_text(encoding="utf-8").lower()
167
+ if any(kw in content for kw in decision_keywords):
168
+ warnings.append(
169
+ f"'{f}' contains decision content but brain/Key Decisions.md not updated"
170
+ )
171
+ break
172
+ except Exception:
173
+ pass
174
+
175
+ # Check 3: Pattern content written but brain/Patterns.md not updated
176
+ pattern_keywords = ["pattern", "convention", "always do", "never do", "recurring"]
177
+ brain_patterns_updated = "brain/Patterns.md" in changed
178
+ if not brain_patterns_updated:
179
+ for f in changed:
180
+ if f.endswith(".md") and not f.startswith("brain/"):
181
+ try:
182
+ content = Path(os.path.join(vault_root, f)).read_text(encoding="utf-8").lower()
183
+ if any(kw in content for kw in pattern_keywords):
184
+ warnings.append(
185
+ f"'{f}' contains pattern content but brain/Patterns.md not updated"
186
+ )
187
+ break
188
+ except Exception:
189
+ pass
190
+
191
+ # Check 4: operation log not updated after significant changes
192
+ log_updated = "log.md" in changed
193
+ significant_changes = len([f for f in changed
194
+ if f.startswith(("work/", "reference/", "brain/"))]) >= 2
195
+ if significant_changes and not log_updated:
196
+ warnings.append("Multiple vault changes but log.md not updated")
197
+
198
+ return warnings
199
+
200
+
201
+ def classify(prompt, mode="suggest"):
202
+ p = prompt.lower()
203
+ key = "auto_message" if mode == "auto" else "message"
204
+ return [s[key] for s in SIGNALS if _match(s["patterns"], p)]
205
+
206
+
207
+ def is_session_end(prompt):
208
+ p = prompt.lower()
209
+ return _match(SESSION_END_PATTERNS, p)
210
+
211
+
212
+ def main():
213
+ try:
214
+ input_data = json.load(sys.stdin)
215
+ except (ValueError, EOFError, OSError):
216
+ sys.exit(0)
217
+
218
+ prompt = input_data.get("prompt", "")
219
+ if not isinstance(prompt, str) or not prompt:
220
+ sys.exit(0)
221
+
222
+ signal_messages = []
223
+ session_end_messages = []
224
+
225
+ try:
226
+ mode = _read_mode()
227
+
228
+ # Regular signal classification
229
+ signals = classify(prompt, mode)
230
+ signal_messages.extend(signals)
231
+
232
+ # Session-end check (always suggest mode — never auto-execute wrap-up)
233
+ if is_session_end(prompt):
234
+ vault_root = _find_vault_root()
235
+ if vault_root:
236
+ integrity_warnings = _check_vault_integrity(vault_root)
237
+ if integrity_warnings:
238
+ session_end_messages.append(
239
+ "SESSION END — vault integrity check found issues:\n"
240
+ + "\n".join(f" - {w}" for w in integrity_warnings)
241
+ + "\nFix these before wrapping up."
242
+ )
243
+ else:
244
+ session_end_messages.append("SESSION END — vault integrity check passed.")
245
+ except Exception:
246
+ sys.exit(0)
247
+
248
+ parts = []
249
+
250
+ if signal_messages:
251
+ hints = "\n".join(f"- {s}" for s in signal_messages)
252
+ if mode == "auto":
253
+ parts.append(
254
+ "Auto-execute the following skills based on user intent:\n"
255
+ + hints
256
+ + "\n\nExecute the skill immediately with the user's message as input. Do not ask for confirmation."
257
+ )
258
+ else:
259
+ parts.append(
260
+ "Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
261
+ + hints
262
+ + "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
263
+ )
264
+
265
+ if session_end_messages:
266
+ hints = "\n".join(f"- {s}" for s in session_end_messages)
267
+ parts.append(
268
+ "Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
269
+ + hints
270
+ + "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
271
+ )
272
+
273
+ if parts:
274
+ context = "\n\n".join(parts)
275
+
276
+ # Build feedback label
277
+ matched = [s for s in SIGNALS if _match(s["patterns"], prompt.lower())]
278
+ feedback_parts = []
279
+ for s in matched:
280
+ feedback_parts.append(f"{s['name']} → {s['skill']}")
281
+ if is_session_end(prompt):
282
+ feedback_parts.append("SESSION END → /wrap-up")
283
+ icon = "\U0001f504" if mode == "auto" else "\U0001f4a1"
284
+ label = ", ".join(feedback_parts) if feedback_parts else "intent detected"
285
+
286
+ # hookSpecificOutput + additionalContext → injected into LLM context
287
+ output = {
288
+ "hookSpecificOutput": {
289
+ "hookEventName": "UserPromptSubmit",
290
+ "additionalContext": context
291
+ }
292
+ }
293
+ sys.stdout.write(json.dumps(output) + "\n")
294
+ sys.stdout.flush()
295
+
296
+ sys.exit(0)
297
+
298
+
299
+ if __name__ == "__main__":
300
+ try:
301
+ main()
302
+ except Exception:
303
+ sys.exit(0)