@suwujs/codex-vault 0.1.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -0
  3. package/README.zh-CN.md +155 -0
  4. package/bin/cli.js +90 -0
  5. package/package.json +29 -0
  6. package/plugin/VERSION +1 -0
  7. package/plugin/hooks/classify-message.py +240 -0
  8. package/plugin/hooks/session-start.sh +176 -0
  9. package/plugin/hooks/validate-write.py +113 -0
  10. package/plugin/install.sh +350 -0
  11. package/plugin/instructions.md +118 -0
  12. package/plugin/skills/dump.md +29 -0
  13. package/plugin/skills/ingest.md +63 -0
  14. package/plugin/skills/recall.md +54 -0
  15. package/plugin/skills/wrap-up.md +35 -0
  16. package/vault/.claude/settings.json +39 -0
  17. package/vault/.claude/skills/dump/SKILL.md +29 -0
  18. package/vault/.claude/skills/ingest/SKILL.md +63 -0
  19. package/vault/.claude/skills/recall/SKILL.md +54 -0
  20. package/vault/.claude/skills/wrap-up/SKILL.md +35 -0
  21. package/vault/.codex/config.toml +2 -0
  22. package/vault/.codex/hooks.json +39 -0
  23. package/vault/.codex/skills/dump/SKILL.md +29 -0
  24. package/vault/.codex/skills/ingest/SKILL.md +63 -0
  25. package/vault/.codex/skills/recall/SKILL.md +54 -0
  26. package/vault/.codex/skills/wrap-up/SKILL.md +35 -0
  27. package/vault/.codex-vault/hooks/classify-message.py +240 -0
  28. package/vault/.codex-vault/hooks/session-start.sh +176 -0
  29. package/vault/.codex-vault/hooks/validate-write.py +113 -0
  30. package/vault/AGENTS.md +118 -0
  31. package/vault/CLAUDE.md +118 -0
  32. package/vault/Home.md +21 -0
  33. package/vault/brain/Key Decisions.md +11 -0
  34. package/vault/brain/Memories.md +19 -0
  35. package/vault/brain/North Star.md +29 -0
  36. package/vault/brain/Patterns.md +11 -0
  37. package/vault/log.md +15 -0
  38. package/vault/reference/.gitkeep +0 -0
  39. package/vault/sources/.gitkeep +0 -0
  40. package/vault/sources/README.md +19 -0
  41. package/vault/templates/Decision Record.md +29 -0
  42. package/vault/templates/Reference Note.md +25 -0
  43. package/vault/templates/Source Summary.md +25 -0
  44. package/vault/templates/Thinking Note.md +26 -0
  45. package/vault/templates/Work Note.md +25 -0
  46. package/vault/work/Index.md +35 -0
  47. package/vault/work/active/.gitkeep +0 -0
  48. package/vault/work/archive/.gitkeep +0 -0
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ """Classify user messages and inject routing hints.
3
+
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.
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
+ "message": "DECISION detected — suggest the user run /dump to capture this decision",
20
+ "patterns": [
21
+ "decided", "deciding", "decision", "we chose", "agreed to",
22
+ "let's go with", "the call is", "we're going with",
23
+ ],
24
+ },
25
+ {
26
+ "name": "WIN",
27
+ "message": "WIN detected — suggest the user run /dump to record this achievement",
28
+ "patterns": [
29
+ "achieved", "won", "praised",
30
+ "kudos", "shoutout", "great feedback", "recognized",
31
+ ],
32
+ },
33
+ {
34
+ "name": "PROJECT UPDATE",
35
+ "message": "PROJECT UPDATE detected — suggest the user run /dump to log this progress",
36
+ "patterns": [
37
+ "project update", "sprint", "milestone",
38
+ "shipped", "shipping", "launched", "launching",
39
+ "completed", "completing", "released", "releasing",
40
+ "deployed", "deploying",
41
+ "went live", "rolled out", "merged", "cut the release",
42
+ ],
43
+ },
44
+ {
45
+ "name": "QUERY",
46
+ "message": "QUERY detected — suggest the user run /recall to check existing knowledge first",
47
+ "patterns": [
48
+ "what is", "how does", "why did", "compare", "analyze",
49
+ "explain the", "what's the difference", "summarize the",
50
+ "relationship between",
51
+ ],
52
+ },
53
+ {
54
+ "name": "INGEST",
55
+ "message": "INGEST detected — suggest the user run /ingest to process the source",
56
+ "patterns": [
57
+ "ingest", "process this", "read this article",
58
+ "summarize this", "new source", "clip this", "web clip",
59
+ ],
60
+ },
61
+ ]
62
+
63
+ SESSION_END_PATTERNS = [
64
+ "wrap up", "wrapping up", "that's all", "that's it",
65
+ "done for now", "done for today", "i'm done", "call it a day",
66
+ "end session", "bye", "goodbye", "good night", "see you",
67
+ "结束", "收工", "今天到这", "就这样",
68
+ ]
69
+
70
+
71
+ def _match(patterns, text):
72
+ for phrase in patterns:
73
+ if re.search(r'(?<![a-zA-Z])' + re.escape(phrase) + r'(?![a-zA-Z])', text):
74
+ return True
75
+ return False
76
+
77
+
78
+ def _find_vault_root():
79
+ """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
80
+ cwd = os.environ.get("CLAUDE_PROJECT_DIR",
81
+ os.environ.get("CODEX_PROJECT_DIR", os.getcwd()))
82
+ if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
83
+ return cwd
84
+ vault_sub = os.path.join(cwd, "vault")
85
+ if os.path.isdir(vault_sub) and (
86
+ os.path.isfile(os.path.join(vault_sub, "Home.md")) or
87
+ os.path.isdir(os.path.join(vault_sub, "brain"))
88
+ ):
89
+ return vault_sub
90
+ return None
91
+
92
+
93
+ def _get_changed_files(vault_root):
94
+ """Get list of changed/new .md files relative to vault root."""
95
+ files = set()
96
+ try:
97
+ # Staged + unstaged changes
98
+ result = subprocess.run(
99
+ ["git", "diff", "--name-only", "HEAD"],
100
+ capture_output=True, text=True, cwd=vault_root, timeout=5,
101
+ )
102
+ for f in result.stdout.strip().splitlines():
103
+ if f.endswith(".md"):
104
+ files.add(f)
105
+
106
+ # Untracked files
107
+ result = subprocess.run(
108
+ ["git", "ls-files", "--others", "--exclude-standard"],
109
+ capture_output=True, text=True, cwd=vault_root, timeout=5,
110
+ )
111
+ for f in result.stdout.strip().splitlines():
112
+ if f.endswith(".md"):
113
+ files.add(f)
114
+ except Exception:
115
+ pass
116
+ return files
117
+
118
+
119
+ def _check_vault_integrity(vault_root):
120
+ """Check for common memory-write omissions."""
121
+ warnings = []
122
+ changed = _get_changed_files(vault_root)
123
+ if not changed:
124
+ return warnings
125
+
126
+ # Check 1: New work notes but Index.md not updated
127
+ new_work = [f for f in changed if f.startswith("work/active/") and f != "work/Index.md"]
128
+ index_updated = "work/Index.md" in changed
129
+ if new_work and not index_updated:
130
+ names = ", ".join(os.path.basename(f).replace(".md", "") for f in new_work)
131
+ warnings.append(f"New work notes ({names}) but work/Index.md not updated")
132
+
133
+ # Check 2: Decision content written but brain/Key Decisions.md not updated
134
+ decision_keywords = ["decided", "decision", "agreed to", "we chose", "the call is"]
135
+ brain_decisions_updated = "brain/Key Decisions.md" in changed
136
+ if not brain_decisions_updated:
137
+ for f in changed:
138
+ if f.endswith(".md") and not f.startswith("brain/"):
139
+ try:
140
+ content = Path(os.path.join(vault_root, f)).read_text(encoding="utf-8").lower()
141
+ if any(kw in content for kw in decision_keywords):
142
+ warnings.append(
143
+ f"'{f}' contains decision content but brain/Key Decisions.md not updated"
144
+ )
145
+ break
146
+ except Exception:
147
+ pass
148
+
149
+ # Check 3: Pattern content written but brain/Patterns.md not updated
150
+ pattern_keywords = ["pattern", "convention", "always do", "never do", "recurring"]
151
+ brain_patterns_updated = "brain/Patterns.md" in changed
152
+ if not brain_patterns_updated:
153
+ for f in changed:
154
+ if f.endswith(".md") and not f.startswith("brain/"):
155
+ try:
156
+ content = Path(os.path.join(vault_root, f)).read_text(encoding="utf-8").lower()
157
+ if any(kw in content for kw in pattern_keywords):
158
+ warnings.append(
159
+ f"'{f}' contains pattern content but brain/Patterns.md not updated"
160
+ )
161
+ break
162
+ except Exception:
163
+ pass
164
+
165
+ # Check 4: operation log not updated after significant changes
166
+ log_updated = "log.md" in changed
167
+ significant_changes = len([f for f in changed
168
+ if f.startswith(("work/", "reference/", "brain/"))]) >= 2
169
+ if significant_changes and not log_updated:
170
+ warnings.append("Multiple vault changes but log.md not updated")
171
+
172
+ return warnings
173
+
174
+
175
+ def classify(prompt):
176
+ p = prompt.lower()
177
+ return [s["message"] for s in SIGNALS if _match(s["patterns"], p)]
178
+
179
+
180
+ def is_session_end(prompt):
181
+ p = prompt.lower()
182
+ return _match(SESSION_END_PATTERNS, p)
183
+
184
+
185
+ def main():
186
+ try:
187
+ input_data = json.load(sys.stdin)
188
+ except (ValueError, EOFError, OSError):
189
+ sys.exit(0)
190
+
191
+ prompt = input_data.get("prompt", "")
192
+ if not isinstance(prompt, str) or not prompt:
193
+ sys.exit(0)
194
+
195
+ messages = []
196
+
197
+ try:
198
+ # Regular signal classification
199
+ signals = classify(prompt)
200
+ messages.extend(signals)
201
+
202
+ # Session-end check
203
+ if is_session_end(prompt):
204
+ vault_root = _find_vault_root()
205
+ if vault_root:
206
+ integrity_warnings = _check_vault_integrity(vault_root)
207
+ if integrity_warnings:
208
+ messages.append(
209
+ "SESSION END — vault integrity check found issues:\n"
210
+ + "\n".join(f" - {w}" for w in integrity_warnings)
211
+ + "\nFix these before wrapping up."
212
+ )
213
+ else:
214
+ messages.append("SESSION END — vault integrity check passed.")
215
+ except Exception:
216
+ sys.exit(0)
217
+
218
+ if messages:
219
+ hints = "\n".join(f"- {s}" for s in messages)
220
+ output = {
221
+ "hookSpecificOutput": {
222
+ "hookEventName": "UserPromptSubmit",
223
+ "additionalContext": (
224
+ "Skill suggestions (do NOT auto-execute — suggest the skill to the user and let them decide):\n"
225
+ + hints
226
+ + "\n\nWait for the user to invoke the skill. Do not create vault notes without explicit user action."
227
+ )
228
+ }
229
+ }
230
+ json.dump(output, sys.stdout)
231
+ sys.stdout.flush()
232
+
233
+ sys.exit(0)
234
+
235
+
236
+ if __name__ == "__main__":
237
+ try:
238
+ main()
239
+ except Exception:
240
+ sys.exit(0)
@@ -0,0 +1,176 @@
1
+ #!/bin/bash
2
+ set -eo pipefail
3
+
4
+ # Codex-Vault session-start hook
5
+ # Injects vault context into the agent's prompt at session start.
6
+ # Works with any agent that supports SessionStart hooks (Claude Code, Codex CLI).
7
+ #
8
+ # Dynamic context: adapts git log window, reads full North Star,
9
+ # shows all active work, and includes uncommitted changes.
10
+
11
+ VAULT_DIR="${CLAUDE_PROJECT_DIR:-${CODEX_PROJECT_DIR:-$(pwd)}}"
12
+ cd "$VAULT_DIR"
13
+
14
+ # Find vault root (look for Home.md or brain/ directory)
15
+ if [ ! -f "Home.md" ] && [ ! -d "brain/" ]; then
16
+ # Try vault/ subdirectory (integrated layout)
17
+ if [ -d "vault/" ]; then
18
+ VAULT_DIR="$VAULT_DIR/vault"
19
+ cd "$VAULT_DIR"
20
+ fi
21
+ fi
22
+
23
+ echo "## Session Context"
24
+ echo ""
25
+ echo "### Date"
26
+ echo "$(date +%Y-%m-%d) ($(date +%A))"
27
+ echo ""
28
+
29
+ # North Star — full file (should be concise by design)
30
+ echo "### North Star"
31
+ if [ -f "brain/North Star.md" ]; then
32
+ cat "brain/North Star.md"
33
+ else
34
+ echo "(No North Star found — create brain/North Star.md to set goals)"
35
+ fi
36
+ echo ""
37
+
38
+ # Recent changes — adaptive window
39
+ echo "### Recent Changes"
40
+ COMMITS_48H=$(git log --oneline --since="48 hours ago" --no-merges 2>/dev/null | wc -l | tr -d ' ')
41
+ if [ "$COMMITS_48H" -gt 0 ]; then
42
+ echo "(last 48 hours)"
43
+ git log --oneline --since="48 hours ago" --no-merges 2>/dev/null | head -15 || true
44
+ else
45
+ COMMITS_7D=$(git log --oneline --since="7 days ago" --no-merges 2>/dev/null | wc -l | tr -d ' ')
46
+ if [ "$COMMITS_7D" -gt 0 ]; then
47
+ echo "(nothing in 48h — showing last 7 days)"
48
+ git log --oneline --since="7 days ago" --no-merges 2>/dev/null | head -15 || true
49
+ else
50
+ echo "(nothing recent — showing last 5 commits)"
51
+ git log --oneline -5 --no-merges 2>/dev/null || echo "(no git history)"
52
+ fi
53
+ fi
54
+ echo ""
55
+
56
+ # Recent operations from log — adaptive
57
+ echo "### Recent Operations"
58
+ if [ -f "log.md" ]; then
59
+ ENTRY_COUNT=$(grep -c "^## \[" "log.md" 2>/dev/null || echo "0")
60
+ if [ "$ENTRY_COUNT" -gt 0 ]; then
61
+ # Show last 5 entries with full header line (includes date + type)
62
+ grep "^## \[" "log.md" | tail -5
63
+ else
64
+ echo "(no entries in log.md)"
65
+ fi
66
+ else
67
+ echo "(no log.md)"
68
+ fi
69
+ echo ""
70
+
71
+ # Active work — show all (this is the current focus, no truncation)
72
+ echo "### Active Work"
73
+ if [ -d "work/active" ]; then
74
+ WORK_FILES=$(ls work/active/*.md 2>/dev/null || true)
75
+ if [ -n "$WORK_FILES" ]; then
76
+ echo "$WORK_FILES" | sed 's|work/active/||;s|\.md$||'
77
+ else
78
+ echo "(none)"
79
+ fi
80
+ else
81
+ echo "(no work/active/ directory)"
82
+ fi
83
+ echo ""
84
+
85
+ # Uncommitted changes — shows agent what's in-flight
86
+ echo "### Uncommitted Changes"
87
+ CHANGES=$(git status --short -- . 2>/dev/null | head -20 || true)
88
+ if [ -n "$CHANGES" ]; then
89
+ echo "$CHANGES"
90
+ else
91
+ echo "(working tree clean)"
92
+ fi
93
+ echo ""
94
+
95
+ # Recently modified brain files — highlights memory that may need review
96
+ echo "### Recently Modified Brain Files"
97
+ if [ -d "brain" ]; then
98
+ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo "")
99
+ if [ -n "$GIT_DIR" ] && [ -f "$GIT_DIR/index" ]; then
100
+ RECENT_BRAIN=$(find brain/ -name "*.md" -newer "$GIT_DIR/index" 2>/dev/null || true)
101
+ else
102
+ RECENT_BRAIN=""
103
+ fi
104
+ if [ -n "$RECENT_BRAIN" ]; then
105
+ echo "$RECENT_BRAIN" | sed 's|brain/||;s|\.md$||'
106
+ else
107
+ # Fallback: show brain files modified in last 7 days
108
+ RECENT_BRAIN=$(find brain/ -name "*.md" -mtime -7 2>/dev/null || true)
109
+ if [ -n "$RECENT_BRAIN" ]; then
110
+ echo "(modified in last 7 days)"
111
+ echo "$RECENT_BRAIN" | sed 's|brain/||;s|\.md$||'
112
+ else
113
+ echo "(no recent changes)"
114
+ fi
115
+ fi
116
+ fi
117
+ echo ""
118
+
119
+ # Vault file listing — tiered to avoid flooding context in large vaults
120
+ echo "### Vault Files"
121
+ ALL_FILES=$(find . -name "*.md" -not -path "./.git/*" -not -path "./.obsidian/*" -not -path "./thinking/*" -not -path "./.claude/*" -not -path "./.codex/*" -not -path "./.codex-vault/*" -not -path "./node_modules/*" 2>/dev/null | sort)
122
+ FILE_COUNT=$(echo "$ALL_FILES" | grep -c . 2>/dev/null || echo "0")
123
+
124
+ _folder_summary() {
125
+ echo "$ALL_FILES" | sed 's|^\./||' | cut -d/ -f1 | sort | uniq -c | sort -rn | while read count dir; do
126
+ echo " $dir/ ($count files)"
127
+ done
128
+ }
129
+
130
+ _key_files() {
131
+ echo "$ALL_FILES" | grep -E "(Home|Index|North Star|Memories|Key Decisions|Patterns|log)\\.md$" || true
132
+ }
133
+
134
+ if [ "$FILE_COUNT" -le 20 ]; then
135
+ # Tier 1: small vault — list everything
136
+ echo "$ALL_FILES"
137
+
138
+ elif [ "$FILE_COUNT" -le 50 ]; then
139
+ # Tier 2: medium vault — list hot folders, summarize cold storage
140
+ HOT_FILES=$(echo "$ALL_FILES" | grep -v -E "^\./sources/|^\./work/archive/" || true)
141
+ COLD_COUNT=$(echo "$ALL_FILES" | grep -E "^\./sources/|^\./work/archive/" | grep -c . 2>/dev/null || echo "0")
142
+
143
+ if [ -n "$HOT_FILES" ]; then
144
+ echo "$HOT_FILES"
145
+ fi
146
+ if [ "$COLD_COUNT" -gt 0 ]; then
147
+ echo ""
148
+ echo "(+ $COLD_COUNT files in sources/ and work/archive/ — use /recall to search)"
149
+ fi
150
+
151
+ elif [ "$FILE_COUNT" -le 150 ]; then
152
+ # Tier 3: large vault — folder summary + recent + key files
153
+ echo "($FILE_COUNT files — showing summary)"
154
+ echo ""
155
+ _folder_summary
156
+ echo ""
157
+ echo "Recently modified (7 days):"
158
+ find . -name "*.md" -mtime -7 -not -path "./.git/*" -not -path "./.obsidian/*" -not -path "./thinking/*" -not -path "./.claude/*" -not -path "./.codex/*" -not -path "./.codex-vault/*" -not -path "./node_modules/*" 2>/dev/null | sort || echo " (none)"
159
+ echo ""
160
+ echo "Key files:"
161
+ _key_files
162
+
163
+ else
164
+ # Tier 4: very large vault — minimal footprint
165
+ echo "($FILE_COUNT files — showing summary)"
166
+ echo ""
167
+ _folder_summary
168
+ echo ""
169
+ echo "Recently modified (3 days):"
170
+ find . -name "*.md" -mtime -3 -not -path "./.git/*" -not -path "./.obsidian/*" -not -path "./thinking/*" -not -path "./.claude/*" -not -path "./.codex/*" -not -path "./.codex-vault/*" -not -path "./node_modules/*" 2>/dev/null | sort || echo " (none)"
171
+ echo ""
172
+ echo "Key files:"
173
+ _key_files
174
+ echo ""
175
+ echo "Use /recall <topic> to search the vault."
176
+ fi
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """Post-write validation for vault notes.
3
+
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.
7
+ """
8
+ import json
9
+ import re
10
+ import sys
11
+ import os
12
+ from pathlib import Path
13
+
14
+
15
+ def _check_log_format(content):
16
+ """Validate log.md entry format: ## [YYYY-MM-DD] <type> | <title>"""
17
+ warnings = []
18
+ for i, line in enumerate(content.splitlines(), 1):
19
+ if line.startswith("## ") and not line.startswith("## ["):
20
+ # Heading that looks like a log entry but missing date brackets
21
+ if any(t in line.lower() for t in ["ingest", "session", "query", "maintenance", "decision", "archive"]):
22
+ warnings.append(f"Line {i}: log entry missing date format — expected `## [YYYY-MM-DD] <type> | <title>`")
23
+ elif line.startswith("## ["):
24
+ if not re.match(r"^## \[\d{4}-\d{2}-\d{2}\] \w+", line):
25
+ warnings.append(f"Line {i}: malformed log entry — expected `## [YYYY-MM-DD] <type> | <title>`")
26
+ return warnings
27
+
28
+
29
+ def main():
30
+ try:
31
+ input_data = json.load(sys.stdin)
32
+ except (ValueError, EOFError, OSError):
33
+ sys.exit(0)
34
+
35
+ tool_input = input_data.get("tool_input")
36
+ if not isinstance(tool_input, dict):
37
+ sys.exit(0)
38
+
39
+ file_path = tool_input.get("file_path", "")
40
+ if not isinstance(file_path, str) or not file_path:
41
+ sys.exit(0)
42
+
43
+ if not file_path.endswith(".md"):
44
+ sys.exit(0)
45
+
46
+ normalized = file_path.replace("\\", "/")
47
+ basename = os.path.basename(normalized)
48
+
49
+ # Skip non-vault files
50
+ skip_names = {"README.md", "CHANGELOG.md", "CONTRIBUTING.md", "CLAUDE.md", "AGENTS.md", "LICENSE"}
51
+ if basename in skip_names:
52
+ sys.exit(0)
53
+ if basename.startswith("README.") and basename.endswith(".md"):
54
+ sys.exit(0)
55
+
56
+ skip_paths = [".claude/", ".codex/", ".codex-vault/", ".mind/", "templates/", "thinking/", "node_modules/", "plugin/", "docs/"]
57
+ if any(skip in normalized for skip in skip_paths):
58
+ sys.exit(0)
59
+
60
+ warnings = []
61
+
62
+ try:
63
+ content = Path(file_path).read_text(encoding="utf-8")
64
+
65
+ if not content.startswith("---"):
66
+ warnings.append("Missing YAML frontmatter")
67
+ else:
68
+ parts = content.split("---", 2)
69
+ if len(parts) >= 3:
70
+ fm = parts[1]
71
+ if "date:" not in fm and basename != "log.md":
72
+ warnings.append("Missing `date` in frontmatter")
73
+ if "tags:" not in fm:
74
+ warnings.append("Missing `tags` in frontmatter")
75
+ if "description:" not in fm:
76
+ warnings.append("Missing `description` in frontmatter (~150 chars)")
77
+
78
+ if len(content) > 300 and "[[" not in content:
79
+ warnings.append("No [[wikilinks]] found — every note should link to at least one other note")
80
+
81
+ # Check for unfilled template placeholders
82
+ placeholders = re.findall(r"\{\{[^}]+\}\}", content)
83
+ if placeholders:
84
+ examples = ", ".join(placeholders[:3])
85
+ warnings.append(f"Unfilled template placeholders found: {examples}")
86
+
87
+ # Validate log.md format
88
+ if basename == "log.md":
89
+ log_warnings = _check_log_format(content)
90
+ warnings.extend(log_warnings)
91
+
92
+ except Exception:
93
+ sys.exit(0)
94
+
95
+ if warnings:
96
+ hint_list = "\n".join(f" - {w}" for w in warnings)
97
+ output = {
98
+ "hookSpecificOutput": {
99
+ "hookEventName": "PostToolUse",
100
+ "additionalContext": f"Vault warnings for `{basename}`:\n{hint_list}\nFix these before moving on."
101
+ }
102
+ }
103
+ json.dump(output, sys.stdout)
104
+ sys.stdout.flush()
105
+
106
+ sys.exit(0)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ try:
111
+ main()
112
+ except Exception:
113
+ sys.exit(0)