@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.
- package/LICENSE.md +44 -0
- package/README.md +148 -0
- package/package.json +57 -0
- package/src/asset-install.mjs +109 -0
- package/src/claude-native-memory.mjs +507 -0
- package/src/file-perms.mjs +53 -0
- package/src/hooks/_transcript-cache.mjs +223 -0
- package/src/hooks/compact-mcp-output.mjs +87 -0
- package/src/hooks/compact-tool-output.mjs +177 -0
- package/src/hooks/ensure-otel-sink.mjs +51 -0
- package/src/hooks/guard-effort-gate.mjs +209 -0
- package/src/hooks/guard-native-memory-write.mjs +129 -0
- package/src/hooks/session-start-vault-context.mjs +200 -0
- package/src/hooks/stop-vault-close-reminder.mjs +150 -0
- package/src/index.js +1547 -0
- package/src/mcp-merge.mjs +279 -0
- package/src/memory-rules.mjs +205 -0
- package/src/obscura-setup.mjs +272 -0
- package/src/ollama-setup.mjs +126 -0
- package/src/rules-merge.mjs +106 -0
- package/src/settings-io.mjs +193 -0
- package/src/settings-writers.mjs +150 -0
- package/src/skills-install.mjs +96 -0
- package/src/telemetry.mjs +154 -0
- package/src/token-saver.mjs +248 -0
- package/templates/agents/vkm-implementer.md +23 -0
- package/templates/output-styles/vkm-terse.md +23 -0
- package/templates/skills/vkm-discipline/SKILL.md +77 -0
- package/templates/skills/vkm-discipline/domains/coding.md +39 -0
- package/templates/skills/vkm-discipline/domains/data.md +37 -0
- package/templates/skills/vkm-discipline/domains/debugging.md +41 -0
- package/templates/skills/vkm-discipline/domains/design-ui.md +41 -0
- package/templates/skills/vkm-discipline/domains/expertise.md +26 -0
- package/templates/skills/vkm-discipline/domains/infra.md +35 -0
- package/templates/skills/vkm-discipline/domains/llm-artifacts.md +30 -0
- package/templates/skills/vkm-discipline/domains/security.md +37 -0
- package/templates/skills/vkm-discipline/domains/web-search.md +43 -0
- package/templates/skills/vkm-discipline/domains/writing.md +32 -0
- 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
|
+
}
|