@vkmikc/create-vkm-kit 4.2.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 (39) hide show
  1. package/LICENSE.md +44 -0
  2. package/README.md +148 -0
  3. package/package.json +57 -0
  4. package/src/asset-install.mjs +109 -0
  5. package/src/claude-native-memory.mjs +507 -0
  6. package/src/file-perms.mjs +53 -0
  7. package/src/hooks/_transcript-cache.mjs +223 -0
  8. package/src/hooks/compact-mcp-output.mjs +87 -0
  9. package/src/hooks/compact-tool-output.mjs +177 -0
  10. package/src/hooks/ensure-otel-sink.mjs +51 -0
  11. package/src/hooks/guard-effort-gate.mjs +209 -0
  12. package/src/hooks/guard-native-memory-write.mjs +129 -0
  13. package/src/hooks/session-start-vault-context.mjs +200 -0
  14. package/src/hooks/stop-vault-close-reminder.mjs +150 -0
  15. package/src/index.js +1547 -0
  16. package/src/mcp-merge.mjs +279 -0
  17. package/src/memory-rules.mjs +205 -0
  18. package/src/obscura-setup.mjs +272 -0
  19. package/src/ollama-setup.mjs +126 -0
  20. package/src/rules-merge.mjs +106 -0
  21. package/src/settings-io.mjs +193 -0
  22. package/src/settings-writers.mjs +150 -0
  23. package/src/skills-install.mjs +96 -0
  24. package/src/telemetry.mjs +154 -0
  25. package/src/token-saver.mjs +248 -0
  26. package/templates/agents/vkm-implementer.md +23 -0
  27. package/templates/output-styles/vkm-terse.md +23 -0
  28. package/templates/skills/vkm-discipline/SKILL.md +77 -0
  29. package/templates/skills/vkm-discipline/domains/coding.md +39 -0
  30. package/templates/skills/vkm-discipline/domains/data.md +37 -0
  31. package/templates/skills/vkm-discipline/domains/debugging.md +41 -0
  32. package/templates/skills/vkm-discipline/domains/design-ui.md +41 -0
  33. package/templates/skills/vkm-discipline/domains/expertise.md +26 -0
  34. package/templates/skills/vkm-discipline/domains/infra.md +35 -0
  35. package/templates/skills/vkm-discipline/domains/llm-artifacts.md +30 -0
  36. package/templates/skills/vkm-discipline/domains/security.md +37 -0
  37. package/templates/skills/vkm-discipline/domains/web-search.md +43 -0
  38. package/templates/skills/vkm-discipline/domains/writing.md +32 -0
  39. package/templates/skills/vkm-spec/SKILL.md +33 -0
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code `Stop` hook — nudges the vault close ritual, AT MOST ONCE per turn, when
4
+ * the session did substantive file work (Write/Edit/MultiEdit/NotebookEdit) but never
5
+ * touched the vault's close-ritual tools (vault_write_file/vault_edit_file/
6
+ * memory_extract_candidates, or basic-memory's write_note/edit_note).
7
+ *
8
+ * Why: the CLAUDE.md "close ritual" rule is aspirational — a model that never gets
9
+ * reminded at the right moment just stops without writing anything down, and the gap
10
+ * compounds session over session. Same idea as ADR-0029's native-memory override
11
+ * (deterministic beats a prose rule), aimed at the OTHER failure mode: not "the wrong
12
+ * system wins" but "no system wins at all". See ADR-0030.
13
+ *
14
+ * Deliberately conservative: this is a NUDGE, not a hard gate. It gives the model an
15
+ * explicit escape hatch ("nothing reusable this time → ignore and stop") so it doesn't
16
+ * write low-value noise to the vault just to satisfy the hook — that would undermine the
17
+ * "only what's reusable beyond the session" doctrine the vault rules already teach.
18
+ *
19
+ * Loop-safe: Claude Code sets `stop_hook_active:true` on the input when this turn is
20
+ * already continuing because of a prior Stop block. We check it and stand down, so this
21
+ * fires at most once per turn-chain — never a loop.
22
+ *
23
+ * Installed by create-obsidian-memory — the vkm-kit installer — into `~/.claude/hooks/` next
24
+ * to the SessionStart and PreToolUse guard hooks, registered in `~/.claude/settings.json` as:
25
+ * node "<this file>" [lang]
26
+ *
27
+ * Contract (Claude Code `Stop` hooks):
28
+ * - Read the hook's JSON payload from stdin (`transcript_path`, `stop_hook_active`, …).
29
+ * - To block the stop and keep the conversation going, print ONE JSON object to stdout:
30
+ * { "decision": "block", "reason": "<text shown to Claude as context>" }
31
+ * - To allow the stop, print nothing and exit 0.
32
+ * - Never throw: an unreadable/odd transcript must fall through to allowing the stop,
33
+ * not hang or break the session.
34
+ *
35
+ * Performance: re-parsing the WHOLE transcript on every Stop event gets slower as a session
36
+ * grows (measured ~75ms on a real 25.8MB transcript) — see `_transcript-cache.mjs` (installed
37
+ * alongside this file) for the sidecar-cache fix that makes each call only read the NEW
38
+ * suffix appended since the last one. Purely a performance change: `scanTranscript` below
39
+ * returns the identical shape/values a full rescan always did.
40
+ */
41
+ import fs from "node:fs";
42
+ import path from "node:path";
43
+ import { fileURLToPath } from "node:url";
44
+ import { getIncrementalState } from "./_transcript-cache.mjs";
45
+
46
+ const SUBSTANTIVE_TOOLS = /^(Write|Edit|MultiEdit|NotebookEdit)$/;
47
+ const VAULT_CLOSE_TOOLS =
48
+ /vault_write_file|vault_edit_file|memory_extract_candidates|write_note|edit_note/;
49
+ /** Below this many substantive edits, the nudge is more noise than signal — stay quiet. */
50
+ const MIN_SUBSTANTIVE_CALLS = 2;
51
+
52
+ function readStdin() {
53
+ try {
54
+ return fs.readFileSync(0, "utf8");
55
+ } catch {
56
+ return "";
57
+ }
58
+ }
59
+
60
+ function initialScanState() {
61
+ return { substantive: 0, vaultTouches: 0 };
62
+ }
63
+
64
+ /** Fold ONE raw JSONL line into `state` (mutate-and-return) — purely additive counters, so
65
+ * folding over just a transcript's NEW suffix gives the identical result as folding from
66
+ * byte 0, which is what makes incremental resumption (`_transcript-cache.mjs`) safe here. */
67
+ function foldTranscriptLine(state, line) {
68
+ const trimmed = line.trim();
69
+ if (!trimmed) return state;
70
+ let entry;
71
+ try {
72
+ entry = JSON.parse(trimmed);
73
+ } catch {
74
+ return state;
75
+ }
76
+ const content = entry?.message?.content;
77
+ if (!Array.isArray(content)) return state;
78
+ for (const block of content) {
79
+ if (!block || block.type !== "tool_use" || typeof block.name !== "string") continue;
80
+ if (VAULT_CLOSE_TOOLS.test(block.name)) state.vaultTouches++;
81
+ else if (SUBSTANTIVE_TOOLS.test(block.name)) state.substantive++;
82
+ }
83
+ return state;
84
+ }
85
+
86
+ /**
87
+ * Scan of the JSONL transcript for assistant tool_use blocks, computed incrementally via the
88
+ * shared sidecar cache (`_transcript-cache.mjs`) — resumes from the last-consumed byte offset
89
+ * when safe, full rescan otherwise. Never throws; returns the SAME shape a full rescan from
90
+ * byte 0 always did.
91
+ */
92
+ export function scanTranscript(transcriptPath) {
93
+ return getIncrementalState(transcriptPath, "stop-reminder", initialScanState, foldTranscriptLine);
94
+ }
95
+
96
+ function reason(lang) {
97
+ if (lang === "en") {
98
+ return (
99
+ "Before stopping: this session edited/wrote files but never touched the Obsidian " +
100
+ "vault (vault_write_file / vault_edit_file / memory_extract_candidates). If there's " +
101
+ "anything reusable beyond this session — a closed decision, an architecture choice, " +
102
+ "a lesson, a gotcha — close it now: SESSION_LOG.md (one line) + " +
103
+ "PROJECTS/<project>.md (incremental). If nothing here is worth saving, ignore this " +
104
+ "and stop normally — don't write low-value notes just to satisfy this reminder."
105
+ );
106
+ }
107
+ return (
108
+ "Antes de terminar: esta sesión editó/escribió archivos pero no tocó el vault " +
109
+ "Obsidian (vault_write_file / vault_edit_file / memory_extract_candidates). Si hay " +
110
+ "algo reutilizable más allá de esta sesión — una decisión cerrada, una elección de " +
111
+ "arquitectura, una lección, un gotcha — ciérralo ahora: SESSION_LOG.md (una línea) + " +
112
+ "PROJECTS/<proyecto>.md (incremental). Si nada de esto vale la pena guardar, ignora " +
113
+ "este aviso y termina normalmente — no escribas notas de bajo valor solo por este recordatorio."
114
+ );
115
+ }
116
+
117
+ function main() {
118
+ const lang = (process.argv[2] || "es").toLowerCase() === "en" ? "en" : "es";
119
+
120
+ let input;
121
+ try {
122
+ input = JSON.parse(readStdin() || "{}");
123
+ } catch {
124
+ return; // unparseable payload — allow the stop
125
+ }
126
+
127
+ if (input?.stop_hook_active) return; // already nudged once this turn-chain — stand down
128
+
129
+ const transcriptPath = typeof input?.transcript_path === "string" ? input.transcript_path : "";
130
+ if (!transcriptPath) return;
131
+
132
+ const { substantive, vaultTouches } = scanTranscript(transcriptPath);
133
+ if (substantive < MIN_SUBSTANTIVE_CALLS || vaultTouches > 0) return;
134
+
135
+ process.stdout.write(JSON.stringify({ decision: "block", reason: reason(lang) }));
136
+ }
137
+
138
+ // Only auto-run when executed directly (`node stop-vault-close-reminder.mjs ...`), not when
139
+ // imported — `scanTranscript` is exported above so tests can unit-test the caching behavior
140
+ // directly (call counts, resumed-vs-full-scan byte counts) without a stdin read as an import
141
+ // side effect.
142
+ const isMainModule =
143
+ process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
144
+ if (isMainModule) {
145
+ try {
146
+ main();
147
+ } catch {
148
+ // Never let a bug in this hook hang or break the session's ability to stop.
149
+ }
150
+ }