@theglitchking/semantic-memory 1.0.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +71 -0
  2. package/.claude-plugin/plugin.json +34 -0
  3. package/CHANGELOG.md +233 -0
  4. package/LICENSE +21 -0
  5. package/README.md +91 -0
  6. package/bin/semantic-memory +2 -0
  7. package/bin/semantic-sidekick +2 -0
  8. package/commands/healthcheck.md +6 -0
  9. package/commands/mode.md +65 -0
  10. package/commands/normalize-config.md +6 -0
  11. package/commands/policy.md +16 -0
  12. package/commands/relink.md +6 -0
  13. package/commands/status.md +6 -0
  14. package/commands/update.md +6 -0
  15. package/commands/vault.md +21 -0
  16. package/dist/chunk-25EIQWXL.js +651 -0
  17. package/dist/chunk-25EIQWXL.js.map +1 -0
  18. package/dist/chunk-CAN2GWUG.js +88 -0
  19. package/dist/chunk-CAN2GWUG.js.map +1 -0
  20. package/dist/chunk-CXTRTCIK.js +762 -0
  21. package/dist/chunk-CXTRTCIK.js.map +1 -0
  22. package/dist/chunk-E2Z4J4MK.js +180 -0
  23. package/dist/chunk-E2Z4J4MK.js.map +1 -0
  24. package/dist/chunk-HODDRXJK.js +169 -0
  25. package/dist/chunk-HODDRXJK.js.map +1 -0
  26. package/dist/chunk-JPQHNG3R.js +147 -0
  27. package/dist/chunk-JPQHNG3R.js.map +1 -0
  28. package/dist/chunk-PZ5AY32C.js +10 -0
  29. package/dist/chunk-PZ5AY32C.js.map +1 -0
  30. package/dist/cli/index.d.ts +1 -0
  31. package/dist/cli/index.js +505 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/core/embed-worker.d.ts +2 -0
  34. package/dist/core/embed-worker.js +121 -0
  35. package/dist/core/embed-worker.js.map +1 -0
  36. package/dist/core/index.d.ts +486 -0
  37. package/dist/core/index.js +62 -0
  38. package/dist/core/index.js.map +1 -0
  39. package/dist/indexer-KD6UTXZB.js +8 -0
  40. package/dist/indexer-KD6UTXZB.js.map +1 -0
  41. package/dist/lint-GMXQF4RW.js +11 -0
  42. package/dist/lint-GMXQF4RW.js.map +1 -0
  43. package/dist/log-LQXIJXIA.js +10 -0
  44. package/dist/log-LQXIJXIA.js.map +1 -0
  45. package/dist/mcp/server.d.ts +16 -0
  46. package/dist/mcp/server.js +4925 -0
  47. package/dist/mcp/server.js.map +1 -0
  48. package/dist/schema-F3RTY6SQ.js +12 -0
  49. package/dist/schema-F3RTY6SQ.js.map +1 -0
  50. package/dist/search-27WYAI24.js +202 -0
  51. package/dist/search-27WYAI24.js.map +1 -0
  52. package/docs/compat-matrix.md +127 -0
  53. package/docs/corpora-json.md +150 -0
  54. package/docs/regression-snapshot.md +122 -0
  55. package/docs/smart-middle-activation.md +97 -0
  56. package/hooks/hooks.json +39 -0
  57. package/hooks/reconcile.js +171 -0
  58. package/hooks/session-start.js +81 -0
  59. package/hooks/vault-context.js +503 -0
  60. package/package.json +97 -0
  61. package/scripts/link-skills.js +25 -0
  62. package/scripts/pre-commit-lint.sh +42 -0
  63. package/skills/outage-silence/SKILL.md +72 -0
  64. package/skills/research-mode/SKILL.md +84 -0
  65. package/skills/semantic-first/SKILL.md +117 -0
  66. package/skills/semantic-first/evals/evals.json +37 -0
  67. package/skills/semantic-first/references/vault-frontmatter.md +133 -0
  68. package/skills/vault-first/SKILL.md +100 -0
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+ // vault-context.js — Phase 1 activation hook for semantic-sidekick.
3
+ //
4
+ // Dual-mode hook:
5
+ // SessionStart → hybrid search seeded from cwd/branch, injected once at session boot
6
+ // UserPromptSubmit → text search seeded from prompt, fingerprinted to suppress re-firing
7
+ //
8
+ // Output contract (Claude Code hook protocol):
9
+ // { "hookSpecificOutput": { "hookEventName": "<event>", "additionalContext": "<text>" } }
10
+ //
11
+ // Fails open: any error logs to stderr and emits an empty additionalContext.
12
+
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
15
+ import { join, resolve, basename } from "node:path";
16
+ import { createHash } from "node:crypto";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ const DEBUG = process.env.SIDEKICK_DEBUG === "1";
20
+ function debug(msg) {
21
+ if (DEBUG) process.stderr.write(`[vault-context] ${msg}\n`);
22
+ }
23
+
24
+ function readStdinJson() {
25
+ try {
26
+ const raw = readFileSync(0, "utf8");
27
+ return raw.trim() ? JSON.parse(raw) : {};
28
+ } catch (e) {
29
+ debug(`stdin parse failed: ${e.message}`);
30
+ return {};
31
+ }
32
+ }
33
+
34
+ function emit(eventName, additionalContext) {
35
+ const out = { hookSpecificOutput: { hookEventName: eventName } };
36
+ if (additionalContext && additionalContext.trim()) {
37
+ out.hookSpecificOutput.additionalContext = additionalContext;
38
+ }
39
+ process.stdout.write(JSON.stringify(out) + "\n");
40
+ }
41
+
42
+ function findVaultPath(projectRoot) {
43
+ // 1. Explicit env override (useful for tests)
44
+ if (process.env.SIDEKICK_VAULT_PATH) {
45
+ const p = resolve(process.env.SIDEKICK_VAULT_PATH);
46
+ return existsSync(p) ? p : null;
47
+ }
48
+ // 2. Read .mcp.json → mcpServers.semantic-vault.args[--notes ...]
49
+ const mcpPath = join(projectRoot, ".mcp.json");
50
+ if (existsSync(mcpPath)) {
51
+ try {
52
+ const data = JSON.parse(readFileSync(mcpPath, "utf8"));
53
+ const entries = data?.mcpServers || {};
54
+ for (const [name, entry] of Object.entries(entries)) {
55
+ if (!entry || typeof entry !== "object") continue;
56
+ if (name !== "semantic-vault" && name !== "semantic-sidekick") continue;
57
+ const args = Array.isArray(entry.args) ? entry.args : [];
58
+ const i = args.indexOf("--notes");
59
+ if (i >= 0 && i + 1 < args.length) {
60
+ const p = resolve(projectRoot, args[i + 1]);
61
+ if (existsSync(p)) return p;
62
+ }
63
+ }
64
+ } catch (e) {
65
+ debug(`.mcp.json parse failed: ${e.message}`);
66
+ }
67
+ }
68
+ // 3. Default
69
+ const def = join(projectRoot, ".claude", ".vault");
70
+ return existsSync(def) ? def : null;
71
+ }
72
+
73
+ function findCliBin(projectRoot) {
74
+ // Prefer local installed bin (when sidekick is a dependency)
75
+ const installed = join(projectRoot, "node_modules", "@theglitchking", "semantic-sidekick", "bin", "semantic-sidekick");
76
+ if (existsSync(installed)) return installed;
77
+ // Dev fallback: this hook lives in <repo>/hooks/, the built CLI is at <repo>/dist/cli/index.js
78
+ const dev = resolve(new URL("../dist/cli/index.js", import.meta.url).pathname);
79
+ if (existsSync(dev)) return dev;
80
+ return null;
81
+ }
82
+
83
+ function runSearchCli(cliBin, args, timeoutMs) {
84
+ const r = spawnSync("node", [cliBin, "search", ...args], {
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ timeout: timeoutMs,
87
+ });
88
+ if (r.status !== 0) {
89
+ debug(`search exited ${r.status}: ${r.stderr?.toString().trim()}`);
90
+ return null;
91
+ }
92
+ try {
93
+ return JSON.parse(r.stdout.toString());
94
+ } catch (e) {
95
+ debug(`search output parse failed: ${e.message}`);
96
+ return null;
97
+ }
98
+ }
99
+
100
+ // Fire-and-forget structured log event via CLI. Hooks don't import core modules;
101
+ // the CLI is the shared log-writing surface so both MCP and hook code land in
102
+ // the same log.md with the same format.
103
+ function logEventViaCli(cliBin, vaultPath, kind, summary, payload) {
104
+ try {
105
+ const args = [cliBin, "log-event", "--notes", vaultPath, "--kind", kind, "--summary", summary];
106
+ if (payload) args.push("--payload", JSON.stringify(payload));
107
+ const r = spawnSync("node", args, { stdio: ["ignore", "ignore", "pipe"], timeout: 10_000 });
108
+ if (r.status !== 0) debug(`log-event exit ${r.status}: ${r.stderr?.toString().trim()}`);
109
+ } catch (e) {
110
+ debug(`log-event spawn failed: ${e.message}`);
111
+ }
112
+ }
113
+
114
+ function queryRecentLogViaCli(cliBin, vaultPath, sinceIso, limit = 20) {
115
+ try {
116
+ const args = [cliBin, "log-query", "--notes", vaultPath, "--after", sinceIso, "--limit", String(limit)];
117
+ const r = spawnSync("node", args, { stdio: ["ignore", "pipe", "pipe"], timeout: 10_000 });
118
+ if (r.status !== 0) {
119
+ debug(`log-query exit ${r.status}: ${r.stderr?.toString().trim()}`);
120
+ return [];
121
+ }
122
+ return JSON.parse(r.stdout.toString());
123
+ } catch (e) {
124
+ debug(`log-query failed: ${e.message}`);
125
+ return [];
126
+ }
127
+ }
128
+
129
+ export function formatContextBlock(source, query, hits) {
130
+ if (!hits || hits.length === 0) return "";
131
+ const lines = [];
132
+ lines.push(`<vault-context source="${source}" query="${escapeAttr(query)}">`);
133
+ lines.push(`The vault (semantic-sidekick) was proactively searched. Top hits:`);
134
+ lines.push("");
135
+ for (const h of hits.slice(0, 6)) {
136
+ const score = typeof h.score === "number" ? h.score.toFixed(2) : "";
137
+ lines.push(`- \`${h.path}\`${score ? ` (score: ${score})` : ""}`);
138
+ if (h.snippet) {
139
+ const snip = h.snippet.replace(/\s+/g, " ").slice(0, 240);
140
+ lines.push(` > ${snip}`);
141
+ }
142
+ }
143
+ lines.push("");
144
+ lines.push(`Instructions: This block is injected on every prompt. Apply cite-or-deflect ONLY when the user's prompt is a project prose lookup (how/why/where does X work here, runbook/process question, gotcha or known-issue lookup). For those, read promising hits via \`mcp__semantic-vault__read_note\` and either cite the filenames or say "not in vault" and name the nearest misses. For meta/tool questions, debugging, status checks, directives ("proceed", "merge"), or conversational turns, ignore this block silently. Do NOT narrate "X unrelated" or "not in vault for this" on non-lookup prompts — that is noise, not honest deflection.`);
145
+ lines.push(`</vault-context>`);
146
+ return lines.join("\n");
147
+ }
148
+
149
+ function escapeAttr(s) {
150
+ return String(s).replace(/["<>&]/g, (c) => ({ '"': "&quot;", "<": "&lt;", ">": "&gt;", "&": "&amp;" })[c]);
151
+ }
152
+
153
+ function sessionSeedQuery(projectRoot) {
154
+ const parts = [];
155
+ parts.push(basename(projectRoot));
156
+ try {
157
+ const r = spawnSync("git", ["-C", projectRoot, "rev-parse", "--abbrev-ref", "HEAD"], {
158
+ stdio: ["ignore", "pipe", "ignore"],
159
+ timeout: 2000,
160
+ });
161
+ if (r.status === 0) {
162
+ const branch = r.stdout.toString().trim();
163
+ if (branch && branch !== "HEAD") parts.push(branch.replace(/[-_/]/g, " "));
164
+ }
165
+ } catch {}
166
+ return parts.join(" ");
167
+ }
168
+
169
+ function fingerprintPath(projectRoot) {
170
+ const dir = join(projectRoot, ".claude");
171
+ try { mkdirSync(dir, { recursive: true }); } catch {}
172
+ return join(dir, ".sidekick-fingerprints.json");
173
+ }
174
+
175
+ function capturePendingPath(projectRoot) {
176
+ const dir = join(projectRoot, ".claude");
177
+ try { mkdirSync(dir, { recursive: true }); } catch {}
178
+ return join(dir, ".sidekick-capture-pending.json");
179
+ }
180
+
181
+ const VALID_MODES = new Set(["vault-first", "research", "outage-silence"]);
182
+
183
+ function modePath(projectRoot) {
184
+ return join(projectRoot, ".claude", ".sidekick-mode");
185
+ }
186
+
187
+ function readMode(projectRoot) {
188
+ try {
189
+ const m = readFileSync(modePath(projectRoot), "utf8").trim();
190
+ if (VALID_MODES.has(m)) return m;
191
+ } catch {}
192
+ return "vault-first";
193
+ }
194
+
195
+ function writeMode(projectRoot, mode) {
196
+ if (!VALID_MODES.has(mode)) return;
197
+ try {
198
+ const dir = join(projectRoot, ".claude");
199
+ mkdirSync(dir, { recursive: true });
200
+ writeFileSync(modePath(projectRoot), mode);
201
+ } catch (e) {
202
+ debug(`mode write failed: ${e.message}`);
203
+ }
204
+ }
205
+
206
+ function logMaybeModeChange(cliBin, vaultPath, from, to) {
207
+ if (!cliBin || !vaultPath) return;
208
+ if (from === to) return;
209
+ logEventViaCli(cliBin, vaultPath, "mode_change", `${from} → ${to}`, { from, to });
210
+ }
211
+
212
+ // Keywords that suggest the user is relaying new knowledge worth capturing as a note.
213
+ const CAPTURE_CUES = [
214
+ /\bbecause\b/i,
215
+ /\bdecided\b|\bwe chose\b|\bthe decision\b/i,
216
+ /\bturned out to be\b|\bthe bug was\b|\bthe fix was\b/i,
217
+ /\bgotcha\b|\bworkaround\b|\bhack\b/i,
218
+ /\bnew convention\b|\bfrom now on\b|\bgoing forward\b/i,
219
+ ];
220
+
221
+ function detectCaptureCue(prompt) {
222
+ for (const re of CAPTURE_CUES) {
223
+ if (re.test(prompt)) return re.source;
224
+ }
225
+ return null;
226
+ }
227
+
228
+ function addCapturePending(projectRoot, prompt, cueSource) {
229
+ try {
230
+ const p = capturePendingPath(projectRoot);
231
+ let state = { items: [] };
232
+ try { state = JSON.parse(readFileSync(p, "utf8")); } catch {}
233
+ if (!Array.isArray(state.items)) state.items = [];
234
+ state.items.push({
235
+ ts: new Date().toISOString(),
236
+ cue: cueSource,
237
+ excerpt: prompt.slice(0, 200),
238
+ });
239
+ writeFileSync(p, JSON.stringify(state));
240
+ } catch (e) {
241
+ debug(`capture pending write failed: ${e.message}`);
242
+ }
243
+ }
244
+
245
+ function resetCapturePending(projectRoot) {
246
+ try {
247
+ const p = capturePendingPath(projectRoot);
248
+ writeFileSync(p, JSON.stringify({ items: [] }));
249
+ } catch {}
250
+ }
251
+
252
+ function readCapturePending(projectRoot) {
253
+ try {
254
+ const p = capturePendingPath(projectRoot);
255
+ const state = JSON.parse(readFileSync(p, "utf8"));
256
+ return Array.isArray(state.items) ? state.items : [];
257
+ } catch {
258
+ return [];
259
+ }
260
+ }
261
+
262
+ function normalizeFingerprint(prompt) {
263
+ return createHash("sha1")
264
+ .update(prompt.toLowerCase().replace(/\s+/g, " ").trim())
265
+ .digest("hex")
266
+ .slice(0, 16);
267
+ }
268
+
269
+ function loadFingerprints(path) {
270
+ try {
271
+ return JSON.parse(readFileSync(path, "utf8"));
272
+ } catch {
273
+ return { recent: [] };
274
+ }
275
+ }
276
+
277
+ function saveFingerprints(path, state) {
278
+ try {
279
+ writeFileSync(path, JSON.stringify(state));
280
+ } catch (e) {
281
+ debug(`fingerprint save failed: ${e.message}`);
282
+ }
283
+ }
284
+
285
+ async function handleSessionStart(projectRoot, vaultPath, cliBin) {
286
+ // Capture the previous mode (if any) before we reset — so future-Claude
287
+ // can tell, from log.md, that we rolled back from research/outage without
288
+ // being shown the postmortem prompt (which is the lossy case).
289
+ const priorMode = readMode(projectRoot);
290
+ resetCapturePending(projectRoot);
291
+ writeMode(projectRoot, "vault-first");
292
+ if (priorMode !== "vault-first") {
293
+ logMaybeModeChange(cliBin, vaultPath, priorMode, "vault-first");
294
+ }
295
+
296
+ const query = sessionSeedQuery(projectRoot);
297
+ debug(`sessionstart seed query: "${query}"`);
298
+ const hits = runSearchCli(cliBin, ["--notes", vaultPath, "--limit", "6", query], 30_000);
299
+
300
+ // State-delta preload — 14-day window per Phase 4.5 design.
301
+ const sinceMs = Date.now() - 14 * 24 * 60 * 60 * 1000;
302
+ const sinceIso = new Date(sinceMs).toISOString();
303
+ const recentEntries = queryRecentLogViaCli(cliBin, vaultPath, sinceIso, 30);
304
+
305
+ const searchBlock = hits && hits.length > 0 ? formatContextBlock("sessionstart", query, hits) : "";
306
+ const stateBlock = formatStateDeltaBlock(sinceIso, recentEntries);
307
+
308
+ const parts = [stateBlock, searchBlock].filter(Boolean);
309
+ emit("SessionStart", parts.join("\n\n"));
310
+ }
311
+
312
+ function formatStateDeltaBlock(sinceIso, entries) {
313
+ if (!entries || entries.length === 0) {
314
+ return `<vault-state-since date="${sinceIso}">\nNo logged activity in the last 14 days.\n</vault-state-since>`;
315
+ }
316
+ const byKind = new Map();
317
+ for (const e of entries) byKind.set(e.kind, (byKind.get(e.kind) || 0) + 1);
318
+ const kindSummary = [...byKind.entries()].map(([k, n]) => `${k}=${n}`).join(", ");
319
+
320
+ const lines = [`<vault-state-since date="${sinceIso}">`];
321
+ lines.push(`Totals: ${kindSummary}`);
322
+ lines.push("");
323
+ lines.push(`Most recent ${Math.min(entries.length, 6)}:`);
324
+ for (const e of entries.slice(-6)) {
325
+ lines.push(`- ${e.ts} · ${e.kind}: ${e.summary}`);
326
+ }
327
+ lines.push(`</vault-state-since>`);
328
+ return lines.join("\n");
329
+ }
330
+
331
+ function emitStop(additionalContext, block) {
332
+ // Claude Code's Stop hook schema does NOT accept hookSpecificOutput — unlike
333
+ // SessionStart / UserPromptSubmit / PostToolUse. Stop-mode fields are all
334
+ // top-level: decision, reason, continue, stopReason, suppressOutput.
335
+ // (Emitting hookSpecificOutput.hookEventName="Stop" fails schema validation.)
336
+ const out = {};
337
+ if (block) {
338
+ out.decision = "block";
339
+ out.reason = additionalContext || "capture pending";
340
+ }
341
+ process.stdout.write(JSON.stringify(out) + "\n");
342
+ }
343
+
344
+ async function handleStop(projectRoot) {
345
+ const stopHookActive = process.env.CLAUDE_STOP_HOOK_ACTIVE === "1";
346
+ if (stopHookActive) {
347
+ // Already blocking — Claude is mid-response to our nudge. Clear and yield.
348
+ resetCapturePending(projectRoot);
349
+ emitStop("", false);
350
+ return;
351
+ }
352
+
353
+ const pending = readCapturePending(projectRoot);
354
+ const mode = readMode(projectRoot);
355
+
356
+ // Mode-specific transition-capture. research-mode and outage-silence each
357
+ // demand a structured synthesis on session end; vault-first reuses the
358
+ // Phase 2 generic capture prompt.
359
+ if (mode === "research" && pending.length > 0) {
360
+ const lines = [
361
+ `<vault-transition-capture mode="research" count="${pending.length}">`,
362
+ `Research session ending. ${pending.length} potential finding${pending.length === 1 ? "" : "s"} accumulated:`,
363
+ "",
364
+ ];
365
+ for (const item of pending.slice(0, 8)) {
366
+ lines.push(`- "${item.excerpt.replace(/\n/g, " ")}"`);
367
+ }
368
+ lines.push("");
369
+ lines.push(`Mandatory before Stop: call \`mcp__semantic-vault__synthesize_note\` with dry_run=true to preview a research-synthesis note (derived_from the session's filed sources). Review with the user, then apply. A research session that ends without synthesis is lost work.`);
370
+ lines.push(`</vault-transition-capture>`);
371
+ resetCapturePending(projectRoot);
372
+ emitStop(lines.join("\n"), true);
373
+ return;
374
+ }
375
+
376
+ if (mode === "outage-silence") {
377
+ const lines = [
378
+ `<vault-transition-capture mode="outage-silence">`,
379
+ `Outage session ending. Before closing: draft a postmortem via \`mcp__semantic-vault__synthesize_note\` with \`type: gotcha\` (or \`decision\` if architectural). Include timeline, root cause, fix, preventive actions. Dry-run first, confirm with the user, then apply.`,
380
+ `</vault-transition-capture>`,
381
+ ];
382
+ resetCapturePending(projectRoot);
383
+ emitStop(lines.join("\n"), true);
384
+ return;
385
+ }
386
+
387
+ // vault-first default: existing capture-pending behavior.
388
+ if (pending.length === 0) {
389
+ emitStop("", false);
390
+ return;
391
+ }
392
+ const lines = [
393
+ `<vault-capture-prompt count="${pending.length}">`,
394
+ `This session surfaced ${pending.length} capture-worthy moment${pending.length === 1 ? "" : "s"} (user prompts contained decision/gotcha/fix cues):`,
395
+ "",
396
+ ];
397
+ for (const item of pending.slice(0, 5)) {
398
+ lines.push(`- cue \`${item.cue}\`: "${item.excerpt.replace(/\n/g, " ")}"`);
399
+ }
400
+ lines.push("");
401
+ lines.push(`Before ending: for each still-unsynthesized item above, either call \`mcp__semantic-vault__synthesize_note\` to file it with provenance, or explicitly acknowledge that it was already captured / not worth capturing.`);
402
+ lines.push(`</vault-capture-prompt>`);
403
+ resetCapturePending(projectRoot);
404
+ emitStop(lines.join("\n"), true);
405
+ }
406
+
407
+ async function handlePrompt(projectRoot, vaultPath, cliBin, input) {
408
+ const prompt = typeof input.prompt === "string" ? input.prompt : "";
409
+ if (!prompt || prompt.length < 8) {
410
+ emit("UserPromptSubmit", "");
411
+ return;
412
+ }
413
+ // Outage-silence suppresses auto-vault activity. Skill describes the contract;
414
+ // hook enforces it at the injection layer so even a misrouted turn stays quiet.
415
+ const mode = readMode(projectRoot);
416
+ if (mode === "outage-silence") {
417
+ debug("outage-silence mode — suppressing auto-vault context");
418
+ emit("UserPromptSubmit", "");
419
+ return;
420
+ }
421
+ const fpPath = fingerprintPath(projectRoot);
422
+ const state = loadFingerprints(fpPath);
423
+ const fp = normalizeFingerprint(prompt);
424
+ const recent = Array.isArray(state.recent) ? state.recent : [];
425
+ if (recent.includes(fp)) {
426
+ debug(`fingerprint ${fp} recently fired — suppressing`);
427
+ emit("UserPromptSubmit", "");
428
+ return;
429
+ }
430
+ const hits = runSearchCli(cliBin, ["--notes", vaultPath, "--limit", "8", prompt], 30_000);
431
+ state.recent = [fp, ...recent].slice(0, 10);
432
+ saveFingerprints(fpPath, state);
433
+ const cue = detectCaptureCue(prompt);
434
+ if (cue) {
435
+ addCapturePending(projectRoot, prompt, cue);
436
+ }
437
+ if (!hits || hits.length === 0) {
438
+ emit("UserPromptSubmit", "");
439
+ return;
440
+ }
441
+ const block = formatContextBlock("prompt", prompt.slice(0, 120), hits);
442
+ emit("UserPromptSubmit", block);
443
+ }
444
+
445
+ async function main() {
446
+ const input = readStdinJson();
447
+ const eventName = input.hook_event_name || process.env.CLAUDE_HOOK_EVENT || "SessionStart";
448
+ const projectRoot = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
449
+ debug(`event=${eventName} projectRoot=${projectRoot}`);
450
+
451
+ const vaultPath = findVaultPath(projectRoot);
452
+ if (!vaultPath) {
453
+ debug(`no vault found under ${projectRoot}`);
454
+ emitNoop(eventName);
455
+ return;
456
+ }
457
+ const cliBin = findCliBin(projectRoot);
458
+ if (!cliBin) {
459
+ debug(`no CLI bin found`);
460
+ emitNoop(eventName);
461
+ return;
462
+ }
463
+ debug(`vault=${vaultPath} bin=${cliBin}`);
464
+
465
+ try {
466
+ if (eventName === "UserPromptSubmit") {
467
+ await handlePrompt(projectRoot, vaultPath, cliBin, input);
468
+ } else if (eventName === "Stop") {
469
+ await handleStop(projectRoot);
470
+ } else {
471
+ await handleSessionStart(projectRoot, vaultPath, cliBin);
472
+ }
473
+ } catch (e) {
474
+ debug(`handler failed: ${e.message}`);
475
+ // Record the crash so future-Claude can see that the hook failed here,
476
+ // not just that nothing happened. Swallow log errors — never mask the real
477
+ // failure surface (which is the empty emit below).
478
+ if (cliBin && vaultPath) {
479
+ logEventViaCli(cliBin, vaultPath, "error", `hook crash (${eventName}): ${e.message}`, {
480
+ tool: "vault-context-hook",
481
+ event: eventName,
482
+ stack: (e.stack || "").split("\n").slice(0, 6).join(" | "),
483
+ });
484
+ }
485
+ emitNoop(eventName);
486
+ }
487
+ }
488
+
489
+ // Shape-aware no-op emit. Stop's schema rejects hookSpecificOutput; every other
490
+ // event accepts the SessionStart-style envelope with empty additionalContext.
491
+ function emitNoop(eventName) {
492
+ if (eventName === "Stop") {
493
+ process.stdout.write("{}\n");
494
+ } else {
495
+ emit(eventName, "");
496
+ }
497
+ }
498
+
499
+ // Only run when invoked as a script — guards against test-time imports
500
+ // triggering stdin reads / spawnSync side effects.
501
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
502
+ main();
503
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@theglitchking/semantic-memory",
3
+ "version": "1.0.0",
4
+ "description": "Unified memory-layer plugin for Claude Code. One MCP server, multiple typed corpora (vault, code, plans, docs, research, project-map), unified cross-corpus knowledge graph, conditional tool registration, in-process drift detection, and human↔code translation. Rebrand of @theglitchking/semantic-sidekick at v1.0 — see CHANGELOG for migration.",
5
+ "type": "module",
6
+ "main": "./dist/core/index.js",
7
+ "types": "./dist/core/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/core/index.js",
10
+ "./mcp": "./dist/mcp/server.js"
11
+ },
12
+ "bin": {
13
+ "semantic-memory": "bin/semantic-memory",
14
+ "semantic-sidekick": "bin/semantic-sidekick"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "dist",
19
+ ".claude-plugin",
20
+ "hooks",
21
+ "commands",
22
+ "docs",
23
+ "scripts/link-skills.js",
24
+ "scripts/pre-commit-lint.sh",
25
+ "skills/semantic-first",
26
+ "skills/vault-first",
27
+ "skills/research-mode",
28
+ "skills/outage-silence",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "LICENSE"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "dev": "tsup --watch",
36
+ "test": "vitest run",
37
+ "test:watch": "vitest",
38
+ "lint": "tsc --noEmit",
39
+ "prepublishOnly": "npm run build",
40
+ "postinstall": "node scripts/link-skills.js"
41
+ },
42
+ "keywords": [
43
+ "mcp",
44
+ "model-context-protocol",
45
+ "memory-layer",
46
+ "semantic-search",
47
+ "knowledge-graph",
48
+ "multi-corpus",
49
+ "markdown",
50
+ "obsidian",
51
+ "vector-search",
52
+ "embeddings",
53
+ "wikilinks",
54
+ "claude",
55
+ "llm",
56
+ "ai",
57
+ "notes",
58
+ "vault",
59
+ "code-search",
60
+ "drift-detection",
61
+ "translation"
62
+ ],
63
+ "license": "MIT",
64
+ "engines": {
65
+ "node": ">=18.0.0"
66
+ },
67
+ "optionalDependencies": {
68
+ "onnxruntime-node": "^1.24.3"
69
+ },
70
+ "dependencies": {
71
+ "@huggingface/transformers": "^4.0.1",
72
+ "@modelcontextprotocol/sdk": "^1.29.0",
73
+ "@theglitchking/claude-plugin-runtime": "^0.1.0",
74
+ "chokidar": "^5.0.0",
75
+ "commander": "^14.0.3",
76
+ "glob": "^13.0.6",
77
+ "graphology": "^0.26.0",
78
+ "graphology-library": "^0.8.0",
79
+ "graphology-shortest-path": "^2.1.0",
80
+ "graphology-traversal": "^0.3.1",
81
+ "gray-matter": "^4.0.3",
82
+ "hnswlib-node": "^3.0.0",
83
+ "minimatch": "^10.2.5",
84
+ "onnxruntime-web": "^1.24.3",
85
+ "remark-parse": "^11.0.0",
86
+ "remark-wiki-link": "^2.0.1",
87
+ "unified": "^11.0.5",
88
+ "yaml": "^2.4.0"
89
+ },
90
+ "devDependencies": {
91
+ "@theglitchking/hit-em-with-the-docs": "latest",
92
+ "@types/node": "^25.5.2",
93
+ "tsup": "^8.5.1",
94
+ "typescript": "^6.0.2",
95
+ "vitest": "^4.1.2"
96
+ }
97
+ }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // Postinstall — delegates to @theglitchking/claude-plugin-runtime.
3
+ // The runtime handles: skill symlinking, default policy config write, and
4
+ // settings.json hook registration with plugin/npm dedup. See the runtime's
5
+ // docs/PLUGIN_AUTHORING_SCAFFOLD.md for the full contract.
6
+
7
+ import { runPostinstall } from "@theglitchking/claude-plugin-runtime";
8
+ import { dirname, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
12
+
13
+ try {
14
+ runPostinstall({
15
+ packageName: "@theglitchking/semantic-sidekick",
16
+ pluginName: "semantic-sidekick",
17
+ configFile: "semantic-sidekick.json",
18
+ skillsDir: "skills",
19
+ packageRoot,
20
+ hookCommand:
21
+ "node ./node_modules/@theglitchking/semantic-sidekick/hooks/session-start.js",
22
+ });
23
+ } catch (err) {
24
+ console.warn(`[semantic-sidekick] postinstall failed: ${err?.message || err}`);
25
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # semantic-sidekick — template pre-commit hook for vault repos.
3
+ #
4
+ # Install into your vault repo:
5
+ # cp /path/to/semantic-sidekick/scripts/pre-commit-lint.sh .git/hooks/pre-commit
6
+ # chmod +x .git/hooks/pre-commit
7
+ # # set VAULT_PATH below, or export it before commit
8
+ #
9
+ # On each commit this runs the cheap lint rules:
10
+ # - schema_violations → blocks commit on error
11
+ # - missing_provenance → warns
12
+ # - stale → warns
13
+ #
14
+ # Pass STRICT=1 to also block on warnings. Pass SKIP_VAULT_LINT=1 to bypass.
15
+
16
+ set -euo pipefail
17
+
18
+ if [[ "${SKIP_VAULT_LINT:-0}" == "1" ]]; then
19
+ echo "pre-commit-lint: skipped (SKIP_VAULT_LINT=1)"
20
+ exit 0
21
+ fi
22
+
23
+ # Default to repo root; override with VAULT_PATH=... when your vault is elsewhere.
24
+ VAULT_PATH="${VAULT_PATH:-$(git rev-parse --show-toplevel)}"
25
+
26
+ # Locate semantic-sidekick CLI: prefer local install, fall back to npx.
27
+ CLI=""
28
+ if [[ -x "$VAULT_PATH/node_modules/@theglitchking/semantic-sidekick/bin/semantic-sidekick" ]]; then
29
+ CLI="node $VAULT_PATH/node_modules/@theglitchking/semantic-sidekick/bin/semantic-sidekick"
30
+ elif command -v semantic-sidekick >/dev/null 2>&1; then
31
+ CLI="semantic-sidekick"
32
+ else
33
+ CLI="npx --no @theglitchking/semantic-sidekick"
34
+ fi
35
+
36
+ STRICT_FLAG=""
37
+ if [[ "${STRICT:-0}" == "1" ]]; then
38
+ STRICT_FLAG="--strict"
39
+ fi
40
+
41
+ echo "pre-commit-lint: running vault lint against $VAULT_PATH"
42
+ $CLI lint --notes "$VAULT_PATH" $STRICT_FLAG