@theglitchking/semantic-memory 1.5.0 → 1.5.2

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.
@@ -3,10 +3,10 @@ import {
3
3
  GraphBuilder,
4
4
  TextSearch,
5
5
  VectorIndex
6
- } from "./chunk-25EIQWXL.js";
6
+ } from "./chunk-44BQDRXD.js";
7
7
  import {
8
8
  Indexer
9
- } from "./chunk-V45NJIHI.js";
9
+ } from "./chunk-IGHCVAAL.js";
10
10
  import "./chunk-PZ5AY32C.js";
11
11
 
12
12
  // src/cli/search.ts
@@ -199,4 +199,4 @@ function extractKeywords(query) {
199
199
  export {
200
200
  runSearch
201
201
  };
202
- //# sourceMappingURL=search-AHKEFI2B.js.map
202
+ //# sourceMappingURL=search-ZS6OW74K.js.map
@@ -10,12 +10,14 @@
10
10
  //
11
11
  // Fails open: any error logs to stderr and emits an empty additionalContext.
12
12
 
13
- import { spawnSync } from "node:child_process";
13
+ import { spawnSync, execFile } from "node:child_process";
14
+ import { promisify } from "node:util";
14
15
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
15
16
  import { join, resolve, basename, dirname } from "node:path";
16
17
  import { createHash } from "node:crypto";
17
18
  import { fileURLToPath } from "node:url";
18
19
 
20
+ const execFileAsync = promisify(execFile);
19
21
  const DEBUG = process.env.SIDEKICK_DEBUG === "1";
20
22
  function debug(msg) {
21
23
  if (DEBUG) process.stderr.write(`[vault-context] ${msg}\n`);
@@ -39,13 +41,16 @@ function emit(eventName, additionalContext) {
39
41
  process.stdout.write(JSON.stringify(out) + "\n");
40
42
  }
41
43
 
42
- function findVaultPath(projectRoot) {
43
- // 1. Explicit env override (useful for tests)
44
+ // Every notes path the hook searches: [{ name, path, readOnly }], in .mcp.json order.
45
+ // The standard setup has two (semantic-vault → .claude/.vault, semantic-sidekick → .documentation --read-only).
46
+ function findNotesSources(projectRoot) {
47
+ // 1. Explicit env override (useful for tests) — a single path, replaces the lookup
44
48
  if (process.env.SIDEKICK_VAULT_PATH) {
45
49
  const p = resolve(process.env.SIDEKICK_VAULT_PATH);
46
- return existsSync(p) ? p : null;
50
+ return existsSync(p) ? [{ name: "vault", path: p, readOnly: false }] : [];
47
51
  }
48
- // 2. Read .mcp.json → mcpServers.semantic-vault.args[--notes ...]
52
+ // 2. Read .mcp.json → mcpServers.semantic-{vault,sidekick,memory}.args[--notes ...]
53
+ const sources = [];
49
54
  const mcpPath = join(projectRoot, ".mcp.json");
50
55
  if (existsSync(mcpPath)) {
51
56
  try {
@@ -58,16 +63,24 @@ function findVaultPath(projectRoot) {
58
63
  const i = args.indexOf("--notes");
59
64
  if (i >= 0 && i + 1 < args.length) {
60
65
  const p = resolve(projectRoot, args[i + 1]);
61
- if (existsSync(p)) return p;
66
+ if (existsSync(p) && !sources.some((s) => s.path === p)) {
67
+ sources.push({ name, path: p, readOnly: args.includes("--read-only") });
68
+ }
62
69
  }
63
70
  }
64
71
  } catch (e) {
65
72
  debug(`.mcp.json parse failed: ${e.message}`);
66
73
  }
67
74
  }
75
+ if (sources.length > 0) return sources;
68
76
  // 3. Default
69
77
  const def = join(projectRoot, ".claude", ".vault");
70
- return existsSync(def) ? def : null;
78
+ return existsSync(def) ? [{ name: "vault", path: def, readOnly: false }] : [];
79
+ }
80
+
81
+ // The one path the hook writes to (log events, dossiers, digests, profile): never a read-only docs server
82
+ function pickVaultPath(sources) {
83
+ return (sources.find((s) => !s.readOnly) ?? sources[0])?.path ?? null;
71
84
  }
72
85
 
73
86
  function findCliBin(projectRoot) {
@@ -86,23 +99,31 @@ function findCliBin(projectRoot) {
86
99
  return null;
87
100
  }
88
101
 
89
- function runSearchCli(cliBin, args, timeoutMs) {
90
- const r = spawnSync("node", [cliBin, "search", ...args], {
91
- stdio: ["ignore", "pipe", "pipe"],
92
- timeout: timeoutMs,
93
- });
94
- if (r.status !== 0) {
95
- debug(`search exited ${r.status}: ${r.stderr?.toString().trim()}`);
96
- return null;
97
- }
102
+ async function runSearchCli(cliBin, args, timeoutMs) {
98
103
  try {
99
- return JSON.parse(r.stdout.toString());
104
+ const { stdout } = await execFileAsync("node", [cliBin, "search", ...args], {
105
+ timeout: timeoutMs,
106
+ maxBuffer: 16 * 1024 * 1024,
107
+ });
108
+ return JSON.parse(stdout);
100
109
  } catch (e) {
101
- debug(`search output parse failed: ${e.message}`);
110
+ debug(`search failed: ${e.stderr?.toString().trim() || e.message}`);
102
111
  return null;
103
112
  }
104
113
  }
105
114
 
115
+ // Search every source in parallel (one CLI spawn each) and merge by score. Hits carry
116
+ // `source` only when more than one corpus was searched, so single-corpus output is unchanged.
117
+ async function searchSources(cliBin, sources, limit, query) {
118
+ const results = await Promise.all(
119
+ sources.map((s) => runSearchCli(cliBin, ["--notes", s.path, "--limit", String(limit), query], 30_000))
120
+ );
121
+ const multi = sources.length > 1;
122
+ return results
123
+ .flatMap((hits, i) => (Array.isArray(hits) ? hits : []).map((h) => (multi ? { ...h, source: sources[i].name } : h)))
124
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
125
+ }
126
+
106
127
  // Fire-and-forget structured log event via CLI. Hooks don't import core modules;
107
128
  // the CLI is the shared log-writing surface so both MCP and hook code land in
108
129
  // the same log.md with the same format.
@@ -148,7 +169,8 @@ function dedupeByBasename(hits) {
148
169
  const seen = new Set();
149
170
  const out = [];
150
171
  for (const h of hits) {
151
- const base = String(h.path || "").split("/").pop();
172
+ // per corpus: the same basename in the vault and in the docs is two different notes
173
+ const base = `${h.source ?? ""}:${String(h.path || "").split("/").pop()}`;
152
174
  if (seen.has(base)) continue;
153
175
  seen.add(base);
154
176
  out.push(h);
@@ -169,7 +191,8 @@ export function formatContextBlock(source, query, hits) {
169
191
  lines.push("");
170
192
  for (const h of shown) {
171
193
  const score = typeof h.score === "number" ? h.score.toFixed(2) : "";
172
- lines.push(`- \`${h.path}\`${score ? ` (score: ${score})` : ""}`);
194
+ const meta = [h.source, score ? `score: ${score}` : ""].filter(Boolean).join(", ");
195
+ lines.push(`- \`${h.path}\`${meta ? ` (${meta})` : ""}`);
173
196
  if (h.snippet) {
174
197
  const snip = h.snippet.replace(/\s+/g, " ").slice(0, 240);
175
198
  lines.push(` > ${snip}`);
@@ -532,40 +555,55 @@ function detectCaptureCue(prompt) {
532
555
  return null;
533
556
  }
534
557
 
535
- function addCapturePending(projectRoot, prompt, cueSource) {
558
+ // The pending file is per project, but items are keyed by session_id so concurrent sessions
559
+ // never see (or clear) each other's cues. An item without one (pre-1.5.2 file) belongs to
560
+ // whichever session reads it first; a hook input without session_id sees everything, as before.
561
+ const PENDING_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
562
+ const ownsPendingItem = (item, sessionId) => !sessionId || !item.session_id || item.session_id === sessionId;
563
+
564
+ function readAllCapturePending(projectRoot) {
565
+ try {
566
+ const path = statePath(projectRoot, "capture-pending").resolvedRead;
567
+ const state = JSON.parse(readFileSync(path, "utf8"));
568
+ return Array.isArray(state.items) ? state.items : [];
569
+ } catch {
570
+ return [];
571
+ }
572
+ }
573
+
574
+ function addCapturePending(projectRoot, prompt, cueSource, sessionId) {
536
575
  try {
537
- const { resolvedRead, writePath } = statePath(projectRoot, "capture-pending");
576
+ const { writePath } = statePath(projectRoot, "capture-pending");
538
577
  ensureStateDir(writePath);
539
- let state = { items: [] };
540
- try { state = JSON.parse(readFileSync(resolvedRead, "utf8")); } catch {}
541
- if (!Array.isArray(state.items)) state.items = [];
542
- state.items.push({
578
+ const items = readAllCapturePending(projectRoot);
579
+ items.push({
543
580
  ts: new Date().toISOString(),
544
581
  cue: cueSource,
545
582
  excerpt: prompt.slice(0, 200),
583
+ ...(sessionId ? { session_id: sessionId } : {}),
546
584
  });
547
- writeFileSync(writePath, JSON.stringify(state));
585
+ writeFileSync(writePath, JSON.stringify({ items }));
548
586
  } catch (e) {
549
587
  debug(`capture pending write failed: ${e.message}`);
550
588
  }
551
589
  }
552
590
 
553
- function resetCapturePending(projectRoot) {
591
+ // Clears this session's items; other sessions' items stay unless older than a week (a session
592
+ // that died without a Stop never clears its own).
593
+ function resetCapturePending(projectRoot, sessionId) {
554
594
  try {
555
595
  const { writePath } = statePath(projectRoot, "capture-pending");
556
596
  ensureStateDir(writePath);
557
- writeFileSync(writePath, JSON.stringify({ items: [] }));
597
+ const now = Date.now();
598
+ const items = readAllCapturePending(projectRoot).filter(
599
+ (i) => !ownsPendingItem(i, sessionId) && now - Date.parse(i.ts) < PENDING_MAX_AGE_MS
600
+ );
601
+ writeFileSync(writePath, JSON.stringify({ items }));
558
602
  } catch {}
559
603
  }
560
604
 
561
- function readCapturePending(projectRoot) {
562
- try {
563
- const path = statePath(projectRoot, "capture-pending").resolvedRead;
564
- const state = JSON.parse(readFileSync(path, "utf8"));
565
- return Array.isArray(state.items) ? state.items : [];
566
- } catch {
567
- return [];
568
- }
605
+ function readCapturePending(projectRoot, sessionId) {
606
+ return readAllCapturePending(projectRoot).filter((i) => ownsPendingItem(i, sessionId));
569
607
  }
570
608
 
571
609
  function normalizeFingerprint(prompt) {
@@ -594,12 +632,12 @@ function saveFingerprints(projectRoot, state) {
594
632
  }
595
633
  }
596
634
 
597
- async function handleSessionStart(projectRoot, vaultPath, cliBin) {
635
+ async function handleSessionStart(projectRoot, vaultPath, cliBin, sources, sessionId) {
598
636
  // Capture the previous mode (if any) before we reset — so future-Claude
599
637
  // can tell, from log.md, that we rolled back from research/outage without
600
638
  // being shown the postmortem prompt (which is the lossy case).
601
639
  const priorMode = readMode(projectRoot);
602
- resetCapturePending(projectRoot);
640
+ resetCapturePending(projectRoot, sessionId);
603
641
  writeMode(projectRoot, "vault-first");
604
642
  if (priorMode !== "vault-first") {
605
643
  logMaybeModeChange(cliBin, vaultPath, priorMode, "vault-first");
@@ -613,8 +651,8 @@ async function handleSessionStart(projectRoot, vaultPath, cliBin) {
613
651
  if (!digestBlock) {
614
652
  const query = sessionSeedQuery(projectRoot);
615
653
  debug(`sessionstart seed query: "${query}"`);
616
- const hits = runSearchCli(cliBin, ["--notes", vaultPath, "--limit", "6", query], 30_000);
617
- searchBlock = hits && hits.length > 0 ? formatContextBlock("sessionstart", query, hits) : "";
654
+ const hits = await searchSources(cliBin, sources, 6, query);
655
+ searchBlock = hits.length > 0 ? formatContextBlock("sessionstart", query, hits) : "";
618
656
  } else {
619
657
  debug("sessionstart: paging in curated digest (skipping broad search)");
620
658
  }
@@ -790,11 +828,11 @@ function readActiveSession(projectRoot) {
790
828
  }
791
829
  }
792
830
 
793
- async function handleStop(projectRoot) {
831
+ async function handleStop(projectRoot, sessionId) {
794
832
  const stopHookActive = process.env.CLAUDE_STOP_HOOK_ACTIVE === "1";
795
833
  if (stopHookActive) {
796
834
  // Already blocking — Claude is mid-response to our nudge. Clear and yield.
797
- resetCapturePending(projectRoot);
835
+ resetCapturePending(projectRoot, sessionId);
798
836
  emitStop("", false);
799
837
  return;
800
838
  }
@@ -823,12 +861,12 @@ async function handleStop(projectRoot) {
823
861
  }
824
862
  lines.push(formatSessionDigestPrompt(session));
825
863
  lines.push(`</vault-session-close>`);
826
- resetCapturePending(projectRoot);
864
+ resetCapturePending(projectRoot, sessionId);
827
865
  emitStop(lines.join("\n"), true);
828
866
  return;
829
867
  }
830
868
 
831
- const pending = readCapturePending(projectRoot);
869
+ const pending = readCapturePending(projectRoot, sessionId);
832
870
  const mode = readMode(projectRoot);
833
871
 
834
872
  // Mode-specific transition-capture. research-mode and outage-silence each
@@ -846,7 +884,7 @@ async function handleStop(projectRoot) {
846
884
  lines.push("");
847
885
  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.`);
848
886
  lines.push(`</vault-transition-capture>`);
849
- resetCapturePending(projectRoot);
887
+ resetCapturePending(projectRoot, sessionId);
850
888
  emitStop(lines.join("\n"), true);
851
889
  return;
852
890
  }
@@ -857,7 +895,7 @@ async function handleStop(projectRoot) {
857
895
  `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.`,
858
896
  `</vault-transition-capture>`,
859
897
  ];
860
- resetCapturePending(projectRoot);
898
+ resetCapturePending(projectRoot, sessionId);
861
899
  emitStop(lines.join("\n"), true);
862
900
  return;
863
901
  }
@@ -879,11 +917,11 @@ async function handleStop(projectRoot) {
879
917
  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.`);
880
918
  lines.push(`If a cue is a speaker correction (how the user talks — "when I say X I mean Y"), update the speaker profile via \`mcp__semantic-vault__manage_profile\` (action:update_section) instead of synthesize_note.`);
881
919
  lines.push(`</vault-capture-prompt>`);
882
- resetCapturePending(projectRoot);
920
+ resetCapturePending(projectRoot, sessionId);
883
921
  emitStop(lines.join("\n"), true);
884
922
  }
885
923
 
886
- async function handlePrompt(projectRoot, vaultPath, cliBin, input) {
924
+ async function handlePrompt(projectRoot, sources, cliBin, input) {
887
925
  const prompt = typeof input.prompt === "string" ? input.prompt : "";
888
926
  if (!prompt || prompt.length < 8) {
889
927
  emit("UserPromptSubmit", "");
@@ -908,17 +946,17 @@ async function handlePrompt(projectRoot, vaultPath, cliBin, input) {
908
946
  // Tier-1 lexicon expansion: search with the alias-expanded query, but keep the
909
947
  // original prompt for display in the block.
910
948
  const searchQuery = expandQueryViaLexicon(projectRoot, prompt);
911
- const hits = runSearchCli(cliBin, ["--notes", vaultPath, "--limit", "8", searchQuery], 30_000);
949
+ const hits = await searchSources(cliBin, sources, 8, searchQuery);
912
950
  state.recent = [fp, ...recent].slice(0, 10);
913
951
  saveFingerprints(projectRoot, state);
914
952
  const cue = detectCaptureCue(prompt);
915
953
  if (cue) {
916
- addCapturePending(projectRoot, prompt, cue);
954
+ addCapturePending(projectRoot, prompt, cue, typeof input.session_id === "string" ? input.session_id : undefined);
917
955
  }
918
956
  // Two-hop: if the utterance names a tracked entity, lead with its dossier head.
919
957
  const dossier = resolveDossierForPrompt(projectRoot, prompt);
920
958
  const dossierHead = dossier ? formatDossierHead(dossier) : "";
921
- if (!hits || hits.length === 0) {
959
+ if (hits.length === 0) {
922
960
  // Even with no semantic hits, a resolved dossier is worth injecting.
923
961
  emit("UserPromptSubmit", dossierHead);
924
962
  return;
@@ -932,9 +970,11 @@ async function main() {
932
970
  const input = readStdinJson();
933
971
  const eventName = input.hook_event_name || process.env.CLAUDE_HOOK_EVENT || "SessionStart";
934
972
  const projectRoot = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
973
+ const sessionId = typeof input.session_id === "string" ? input.session_id : undefined;
935
974
  debug(`event=${eventName} projectRoot=${projectRoot}`);
936
975
 
937
- const vaultPath = findVaultPath(projectRoot);
976
+ const sources = findNotesSources(projectRoot);
977
+ const vaultPath = pickVaultPath(sources);
938
978
  if (!vaultPath) {
939
979
  debug(`no vault found under ${projectRoot}`);
940
980
  emitNoop(eventName);
@@ -946,15 +986,15 @@ async function main() {
946
986
  emitNoop(eventName);
947
987
  return;
948
988
  }
949
- debug(`vault=${vaultPath} bin=${cliBin}`);
989
+ debug(`vault=${vaultPath} search=${sources.map((s) => s.path).join(",")} bin=${cliBin}`);
950
990
 
951
991
  try {
952
992
  if (eventName === "UserPromptSubmit") {
953
- await handlePrompt(projectRoot, vaultPath, cliBin, input);
993
+ await handlePrompt(projectRoot, sources, cliBin, input);
954
994
  } else if (eventName === "Stop") {
955
- await handleStop(projectRoot);
995
+ await handleStop(projectRoot, sessionId);
956
996
  } else {
957
- await handleSessionStart(projectRoot, vaultPath, cliBin);
997
+ await handleSessionStart(projectRoot, vaultPath, cliBin, sources, sessionId);
958
998
  }
959
999
  } catch (e) {
960
1000
  debug(`handler failed: ${e.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theglitchking/semantic-memory",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
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
5
  "type": "module",
6
6
  "main": "./dist/core/index.js",
@@ -95,5 +95,19 @@
95
95
  "tsup": "^8.5.1",
96
96
  "typescript": "^6.0.2",
97
97
  "vitest": "^4.1.2"
98
+ },
99
+ "comments": {
100
+ "overrides": "Pin patched versions of transitive deps to keep this package's supply chain clean (v1.5.1). All are same-MAJOR bumps — no breaking changes. NOTE: npm ignores a dependency's overrides in downstream installs, so consuming projects that gate CI on `npm audit` should mirror this block or run `npm audit fix`. Every advisory here is unreachable in practice: this is a local stdio MCP server + hook scripts processing the user's own files — the hono/express-rate-limit/ip-address/qs cluster is the MCP SDK's HTTP transport, which this plugin never instantiates (stdio only). js-yaml (gray-matter) and fast-uri (ajv) are NOT overridden: they resolve to patched same-major versions (3.15.2 / 3.1.9 as of v1.5.2). v1.5.2 re-pinned the block to current patches. Known open: zod (all 3.x vulnerable, fix is zod 4: breaking for the MCP SDK tool schemas in src/mcp/tools/*) and esbuild (dev-only, via tsup)."
101
+ },
102
+ "overrides": {
103
+ "protobufjs": "7.6.6",
104
+ "@protobufjs/utf8": "1.1.2",
105
+ "@xmldom/xmldom": "0.9.12",
106
+ "brace-expansion": "5.0.12",
107
+ "qs": "6.16.1",
108
+ "ip-address": "10.7.4",
109
+ "express-rate-limit": "8.7.1",
110
+ "hono": "4.13.13",
111
+ "@hono/node-server": "1.19.17"
98
112
  }
99
113
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/embedder.ts","../src/core/graph.ts","../src/core/vector.ts","../src/core/search-text.ts"],"sourcesContent":["import { AutoTokenizer } from \"@huggingface/transformers\";\nimport { readFile, writeFile, mkdir, rename, unlink } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport { existsSync, createWriteStream } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { Readable } from \"node:stream\";\nimport { pipeline as streamPipeline } from \"node:stream/promises\";\nimport { Worker } from \"node:worker_threads\";\nimport { fileURLToPath } from \"node:url\";\n\n// MiniLM-L6-v2 is the default: ~3 min to index 2,853 chunks on CPU vs ~16 min for nomic.\n// nomic-embed-text-v1.5 gives higher quality embeddings but is much slower on CPU.\nconst DEFAULT_MODEL = \"sentence-transformers/all-MiniLM-L6-v2\";\nconst CACHE_DIR = join(homedir(), \".semantic-sidekick\", \"models\");\n// Default to 1 worker (serial). Worker threads only help on memory-rich machines\n// (each worker loads its own ONNX session). On typical dev machines\n// with <4 GB free RAM, parallel workers cause swap thrashing and are slower.\n// Enable with --workers N when you have sufficient RAM.\nconst DEFAULT_WORKERS = 1;\n// batch=16 is optimal for MiniLM on CPU (short sequences, low padding waste).\n// For larger models (nomic 768d), batch=1 is faster due to padding overhead.\nconst DEFAULT_BATCH_SIZE = 16;\n// Quantized ONNX: faster on CPU but not all models have a quantized variant.\n// MiniLM fp32 is already fast enough; nomic benefits from quantized.\n// Falls back to fp32 automatically if quantized file is not available.\nconst DEFAULT_QUANTIZED = false;\n\n// Full-precision ONNX model subpaths\nconst ONNX_MODEL_PATHS: Record<string, string> = {\n \"nomic-ai/nomic-embed-text-v1.5\": \"onnx/model.onnx\",\n \"sentence-transformers/all-MiniLM-L6-v2\": \"onnx/model.onnx\",\n};\n\n// Quantized (int8) ONNX model subpaths — faster on CPU, ~same quality\nconst ONNX_QUANTIZED_MODEL_PATHS: Record<string, string> = {\n \"nomic-ai/nomic-embed-text-v1.5\": \"onnx/model_quantized.onnx\",\n \"sentence-transformers/all-MiniLM-L6-v2\": \"onnx/model_quantized.onnx\",\n};\n\ninterface OrtSession {\n run(feeds: Record<string, unknown>): Promise<Record<string, { data: Float32Array; dims: number[] }>>;\n inputNames: string[];\n outputNames: string[];\n}\n\ninterface OrtModule {\n InferenceSession: {\n create(path: string, options?: Record<string, unknown>): Promise<OrtSession>;\n };\n Tensor: new (type: string, data: ArrayLike<number | bigint>, dims: number[]) => unknown;\n}\n\ntype RuntimeLabel = \"native\" | \"wasm\";\n\nasync function resolveOnnxRuntime(): Promise<{ ort: OrtModule; label: RuntimeLabel }> {\n try {\n const ort = await import(\"onnxruntime-node\");\n return { ort: ort as unknown as OrtModule, label: \"native\" };\n } catch {\n const ort = await import(\"onnxruntime-web\");\n return { ort: ort as unknown as OrtModule, label: \"wasm\" };\n }\n}\n\n// Download to a process-unique temp file, then atomically rename onto the\n// final path. Two concurrent downloaders won't corrupt each other's partial\n// writes — the worst case is one wasted download (rename is last-writer-wins\n// but each rename installs a complete, valid file). Without this, multiple\n// processes calling Embedder.init() against the same cache (e.g. parallel\n// vitest workers, or multiple semantic-sidekick servers on a fresh install) race\n// on a single shared writeStream and produce a corrupt ONNX file that fails\n// Protobuf parsing on load.\nasync function downloadFile(url: string, destPath: string): Promise<void> {\n const response = await fetch(url);\n if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`);\n if (!response.body) throw new Error(`No response body: ${url}`);\n const tempPath = `${destPath}.${process.pid}.${Date.now()}.tmp`;\n try {\n const fileStream = createWriteStream(tempPath);\n await streamPipeline(Readable.fromWeb(response.body as never), fileStream);\n await rename(tempPath, destPath);\n } catch (err) {\n try { await unlink(tempPath); } catch { /* temp may not exist */ }\n throw err;\n }\n}\n\nexport class Embedder {\n private model: string;\n private session: OrtSession | null = null;\n private tokenizer: Awaited<ReturnType<typeof AutoTokenizer.from_pretrained>> | null = null;\n private ort: OrtModule | null = null;\n private dimensions = 0;\n private runtimeLabel: RuntimeLabel = \"wasm\";\n private initialized = false;\n private numWorkers: number;\n private batchSize: number;\n private quantized: boolean;\n private modelPath = \"\";\n\n constructor(\n model: string = DEFAULT_MODEL,\n numWorkers: number = DEFAULT_WORKERS,\n batchSize: number = DEFAULT_BATCH_SIZE,\n quantized: boolean = DEFAULT_QUANTIZED\n ) {\n this.model = model;\n this.numWorkers = numWorkers;\n this.batchSize = batchSize;\n this.quantized = quantized;\n }\n\n async init(): Promise<void> {\n if (this.initialized) return;\n\n const modelDir = join(CACHE_DIR, this.model.replace(/\\//g, \"--\"));\n await mkdir(modelDir, { recursive: true });\n\n // Resolve ONNX runtime (native C++ or WASM fallback)\n const { ort, label } = await resolveOnnxRuntime();\n this.ort = ort;\n this.runtimeLabel = label;\n\n // Download ONNX model if not cached. Tries quantized first if requested,\n // falls back to fp32 if the quantized file is not available for this model.\n let useQuantized = this.quantized;\n const modelFileName = useQuantized ? \"model_quantized.onnx\" : \"model.onnx\";\n this.modelPath = join(modelDir, modelFileName);\n const modelPath = this.modelPath;\n if (!existsSync(modelPath)) {\n const pathMap = useQuantized ? ONNX_QUANTIZED_MODEL_PATHS : ONNX_MODEL_PATHS;\n const onnxSubpath = pathMap[this.model] ?? (useQuantized ? \"onnx/model_quantized.onnx\" : \"onnx/model.onnx\");\n const url = `https://huggingface.co/${this.model}/resolve/main/${onnxSubpath}`;\n process.stderr.write(`Downloading ONNX model: ${this.model} (${useQuantized ? \"quantized\" : \"fp32\"})...\\n`);\n try {\n await downloadFile(url, modelPath);\n } catch (err: any) {\n if (useQuantized && err?.message?.includes(\"404\")) {\n // Quantized not available for this model — fall back to fp32\n process.stderr.write(`Quantized model not available, falling back to fp32\\n`);\n useQuantized = false;\n this.modelPath = join(modelDir, \"model.onnx\");\n const fp32Subpath = ONNX_MODEL_PATHS[this.model] ?? \"onnx/model.onnx\";\n const fp32Url = `https://huggingface.co/${this.model}/resolve/main/${fp32Subpath}`;\n process.stderr.write(`Downloading ONNX model: ${this.model} (fp32)...\\n`);\n await downloadFile(fp32Url, this.modelPath);\n } else {\n throw err;\n }\n }\n process.stderr.write(`Model downloaded to ${modelDir}\\n`);\n }\n // Load tokenizer (uses HF transformers tokenizer infrastructure)\n this.tokenizer = await AutoTokenizer.from_pretrained(this.model, {\n cache_dir: CACHE_DIR,\n });\n\n // Create ONNX inference session (use this.modelPath — may have been updated by fallback)\n this.session = await ort.InferenceSession.create(this.modelPath, {\n executionProviders: [label === \"native\" ? \"cpu\" : \"wasm\"],\n });\n\n // Determine dimensions from a test embedding\n const test = await this.embed(\"test\");\n this.dimensions = test.length;\n this.initialized = true;\n\n const modelShort = this.model.split(\"/\").pop() ?? this.model;\n process.stderr.write(`Embedder ready (${modelShort}, ${label}, ${this.dimensions}d, batch=${this.batchSize})\\n`);\n }\n\n async embed(text: string): Promise<Float32Array> {\n if (!this.session || !this.tokenizer || !this.ort)\n throw new Error(\"Embedder not initialized. Call init() first.\");\n\n // Tokenize\n const encoded = await this.tokenizer(text, {\n padding: true,\n truncation: true,\n max_length: 512,\n return_tensor: false,\n });\n\n const inputIdsRaw: number[] = Array.from(encoded.input_ids.data ?? encoded.input_ids);\n const attentionMaskRaw: number[] = Array.from(encoded.attention_mask.data ?? encoded.attention_mask);\n const seqLen = inputIdsRaw.length;\n\n // Build ONNX input tensors (most models expect int64)\n const inputIds = new this.ort.Tensor(\n \"int64\",\n BigInt64Array.from(inputIdsRaw.map(BigInt)),\n [1, seqLen]\n );\n const attentionMask = new this.ort.Tensor(\n \"int64\",\n BigInt64Array.from(attentionMaskRaw.map(BigInt)),\n [1, seqLen]\n );\n\n const feeds: Record<string, unknown> = { input_ids: inputIds, attention_mask: attentionMask };\n\n // Some models need token_type_ids\n if (this.session.inputNames.includes(\"token_type_ids\")) {\n feeds.token_type_ids = new this.ort.Tensor(\n \"int64\",\n new BigInt64Array(seqLen),\n [1, seqLen]\n );\n }\n\n // Run inference\n const output = await this.session.run(feeds);\n const outputTensor = output[this.session.outputNames[0]];\n const hiddenSize = outputTensor.dims[outputTensor.dims.length - 1];\n\n // Mean pooling with attention mask + L2 normalization\n return this.meanPoolAndNormalize(outputTensor.data, attentionMaskRaw, seqLen, hiddenSize);\n }\n\n private meanPoolAndNormalize(\n embeddings: Float32Array,\n attentionMask: number[],\n seqLen: number,\n hiddenSize: number\n ): Float32Array {\n const result = new Float32Array(hiddenSize);\n let maskSum = 0;\n\n for (let t = 0; t < seqLen; t++) {\n const mask = attentionMask[t];\n maskSum += mask;\n const offset = t * hiddenSize;\n for (let d = 0; d < hiddenSize; d++) {\n result[d] += embeddings[offset + d] * mask;\n }\n }\n\n if (maskSum > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= maskSum;\n }\n\n // L2 normalize\n let norm = 0;\n for (let d = 0; d < hiddenSize; d++) norm += result[d] * result[d];\n norm = Math.sqrt(norm);\n if (norm > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= norm;\n }\n\n return result;\n }\n\n // Mean pool + L2 normalize the output of a batched ONNX forward pass.\n // outputData: flat Float32Array of shape [batchSize, seqLen, hiddenSize]\n // maskData: flat number[] of shape [batchSize, seqLen]\n private meanPoolAndNormalizeMany(\n outputData: Float32Array,\n maskData: number[],\n batchSize: number,\n seqLen: number,\n hiddenSize: number\n ): Float32Array[] {\n const results: Float32Array[] = [];\n for (let b = 0; b < batchSize; b++) {\n const result = new Float32Array(hiddenSize);\n let maskSum = 0;\n for (let t = 0; t < seqLen; t++) {\n const mask = maskData[b * seqLen + t];\n maskSum += mask;\n const offset = (b * seqLen + t) * hiddenSize;\n for (let d = 0; d < hiddenSize; d++) {\n result[d] += outputData[offset + d] * mask;\n }\n }\n if (maskSum > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= maskSum;\n }\n // L2 normalize\n let norm = 0;\n for (let d = 0; d < hiddenSize; d++) norm += result[d] * result[d];\n norm = Math.sqrt(norm);\n if (norm > 0) {\n for (let d = 0; d < hiddenSize; d++) result[d] /= norm;\n }\n results.push(result);\n }\n return results;\n }\n\n // Embed a sub-batch of texts (length <= batchSize) in a single ONNX forward pass.\n //\n // Tokenizes each text individually (well-defined output format for all HF versions),\n // then manually pads to the longest sequence in the batch and builds a [n, seqLen]\n // tensor for one ONNX call. The speedup is from batching the ONNX inference —\n // individual tokenization is negligible (<1ms each).\n private async embedSubBatch(texts: string[]): Promise<Float32Array[]> {\n if (!this.session || !this.tokenizer || !this.ort)\n throw new Error(\"Embedder not initialized. Call init() first.\");\n\n const n = texts.length;\n\n // Tokenize each text individually — avoids ambiguous batch tokenizer output format\n const encodings = await Promise.all(\n texts.map((text) =>\n this.tokenizer!(text, {\n padding: false,\n truncation: true,\n max_length: 512,\n return_tensor: false,\n })\n )\n );\n\n // Extract flat token arrays and find max sequence length for this batch\n const tokenized = encodings.map((enc) => ({\n ids: Array.from(enc.input_ids.data ?? enc.input_ids) as number[],\n mask: Array.from(enc.attention_mask.data ?? enc.attention_mask) as number[],\n }));\n const seqLen = Math.max(...tokenized.map((t) => t.ids.length));\n\n // Build flat padded tensors [n * seqLen] — pad with 0 (PAD token, zero attention)\n const flatIds = new BigInt64Array(n * seqLen);\n const flatMask = new BigInt64Array(n * seqLen);\n const flatMaskNums = new Array<number>(n * seqLen).fill(0);\n\n for (let i = 0; i < n; i++) {\n const { ids, mask } = tokenized[i];\n for (let j = 0; j < ids.length; j++) {\n flatIds[i * seqLen + j] = BigInt(ids[j]);\n flatMask[i * seqLen + j] = BigInt(mask[j]);\n flatMaskNums[i * seqLen + j] = mask[j];\n }\n // Positions beyond ids.length remain 0 (padding)\n }\n\n // Build batched ONNX tensors [n, seqLen]\n const inputIds = new this.ort.Tensor(\"int64\", flatIds, [n, seqLen]);\n const attentionMask = new this.ort.Tensor(\"int64\", flatMask, [n, seqLen]);\n\n const feeds: Record<string, unknown> = { input_ids: inputIds, attention_mask: attentionMask };\n\n if (this.session.inputNames.includes(\"token_type_ids\")) {\n feeds.token_type_ids = new this.ort.Tensor(\n \"int64\",\n new BigInt64Array(n * seqLen),\n [n, seqLen]\n );\n }\n\n // Single forward pass → output shape [n, seqLen, hiddenSize]\n const output = await this.session.run(feeds);\n const outputTensor = output[this.session.outputNames[0]];\n const hiddenSize = outputTensor.dims[outputTensor.dims.length - 1];\n\n return this.meanPoolAndNormalizeMany(\n outputTensor.data,\n flatMaskNums,\n n,\n seqLen,\n hiddenSize\n );\n }\n\n async embedBatch(\n texts: string[],\n onProgress?: (embedded: number, total: number, subBatch?: Float32Array[]) => Promise<void> | void\n ): Promise<Float32Array[]> {\n // Workers path (disabled by default; kept for --workers N users)\n if (this.numWorkers > 1 && texts.length >= this.numWorkers * 2) {\n return this.embedBatchParallel(texts, onProgress);\n }\n\n // True batched inference: slice into sub-batches and run one ONNX call each\n const results: Float32Array[] = [];\n for (let i = 0; i < texts.length; i += this.batchSize) {\n const subBatch = texts.slice(i, i + this.batchSize);\n const embeddings = await this.embedSubBatch(subBatch);\n results.push(...embeddings);\n await onProgress?.(Math.min(i + subBatch.length, texts.length), texts.length, embeddings);\n }\n return results;\n }\n\n private async embedBatchParallel(\n texts: string[],\n onProgress?: (embedded: number, total: number, subBatch?: Float32Array[]) => Promise<void> | void\n ): Promise<Float32Array[]> {\n // Resolve worker script path. With tsup splitting, embedder code may land\n // in a top-level chunk (dist/chunk-*.js) rather than dist/core/index.js,\n // so we try two locations: adjacent to this chunk, and dist/core/ relative\n // to the package root (two levels up from a dist/ chunk file).\n const thisDir = dirname(fileURLToPath(import.meta.url));\n let workerPath = join(thisDir, \"embed-worker.js\");\n if (!existsSync(workerPath)) {\n // Chunk file at dist/ level — worker is in dist/core/\n workerPath = join(thisDir, \"core\", \"embed-worker.js\");\n }\n\n if (!existsSync(workerPath)) {\n // Fallback to serial batched if worker script not found\n process.stderr.write(\"Worker script not found, falling back to batched embedding\\n\");\n return this.embedBatch(texts, onProgress);\n }\n\n // Split texts into chunks for each worker\n const chunkSize = Math.ceil(texts.length / this.numWorkers);\n const chunks: { texts: string[]; startIndex: number }[] = [];\n for (let i = 0; i < texts.length; i += chunkSize) {\n chunks.push({ texts: texts.slice(i, i + chunkSize), startIndex: i });\n }\n\n const allResults = new Array<Float32Array>(texts.length);\n let totalDone = 0;\n\n const workerPromises = chunks.map((chunk) => {\n return new Promise<void>((resolve, reject) => {\n const worker = new Worker(workerPath, {\n workerData: {\n modelPath: this.modelPath,\n modelName: this.model,\n cacheDir: CACHE_DIR,\n runtimeLabel: this.runtimeLabel,\n batchSize: this.batchSize,\n },\n });\n\n worker.on(\"message\", (msg: any) => {\n if (msg.type === \"ready\") {\n worker.postMessage({ type: \"embed\", texts: chunk.texts, startIndex: chunk.startIndex });\n } else if (msg.type === \"progress\") {\n totalDone++;\n onProgress?.(totalDone, texts.length);\n } else if (msg.type === \"result\") {\n for (let i = 0; i < msg.embeddings.length; i++) {\n allResults[chunk.startIndex + i] = new Float32Array(msg.embeddings[i]);\n }\n worker.terminate();\n resolve();\n } else if (msg.type === \"error\") {\n worker.terminate();\n reject(new Error(msg.error));\n }\n });\n\n worker.on(\"error\", (err) => {\n worker.terminate();\n reject(err);\n });\n });\n });\n\n await Promise.all(workerPromises);\n return allResults;\n }\n\n getDimensions(): number {\n return this.dimensions;\n }\n\n getModel(): string {\n return this.model;\n }\n\n getRuntime(): RuntimeLabel {\n return this.runtimeLabel;\n }\n\n async saveEmbeddings(\n embeddings: Map<string, Float32Array>,\n indexPath: string\n ): Promise<void> {\n const entries: Array<{ key: string; data: number[] }> = [];\n for (const [key, vec] of embeddings) {\n entries.push({ key, data: Array.from(vec) });\n }\n await writeFile(join(indexPath, \"embeddings.json\"), JSON.stringify(entries));\n }\n\n async loadEmbeddings(\n indexPath: string\n ): Promise<Map<string, Float32Array>> {\n const filePath = join(indexPath, \"embeddings.json\");\n if (!existsSync(filePath)) return new Map();\n\n const raw = await readFile(filePath, \"utf-8\");\n const entries: Array<{ key: string; data: number[] }> = JSON.parse(raw);\n const map = new Map<string, Float32Array>();\n for (const entry of entries) {\n map.set(entry.key, new Float32Array(entry.data));\n }\n return map;\n }\n}\n","import Graph from \"graphology\";\nimport { bfsFromNode } from \"graphology-traversal\";\nimport { bidirectional } from \"graphology-shortest-path\";\nimport type { IndexedDocument, GraphNode, GraphEdge, GraphStats } from \"./types.js\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\n/**\n * Tags shared by more than this many docs are too generic to be meaningful\n * graph edges (e.g. \"api\", \"testing\"). Only specific/rare tags create edges.\n * Docs without frontmatter are unaffected — their inline #tags still participate\n * but only if those tags are specific enough to pass this threshold.\n */\nconst TAG_EDGE_MAX_DOCS = 15;\n\nexport class GraphBuilder {\n private graph: Graph;\n\n constructor() {\n this.graph = new Graph({ type: \"directed\", multi: false });\n }\n\n buildFromDocuments(documents: IndexedDocument[]): void {\n this.graph.clear();\n\n // Add nodes — store optional metadata fields when present\n for (const doc of documents) {\n this.graph.addNode(doc.path, {\n title: doc.title,\n tags: doc.tags,\n ...(doc.status !== undefined && { status: doc.status }),\n ...(doc.loadPriority !== undefined && { loadPriority: doc.loadPriority }),\n });\n }\n\n const pathLookup = new Map<string, string>();\n for (const doc of documents) {\n const nameNoExt = doc.path.replace(/\\.md$/, \"\");\n const basename = nameNoExt.split(\"/\").pop()!;\n pathLookup.set(basename.toLowerCase(), doc.path);\n pathLookup.set(nameNoExt.toLowerCase(), doc.path);\n }\n\n // Add wikilink edges (weight 1.0)\n for (const doc of documents) {\n for (const link of doc.wikilinks) {\n const target = pathLookup.get(link.toLowerCase());\n if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {\n this.graph.addEdge(doc.path, target, { type: \"wikilink\", weight: 1.0 });\n }\n }\n }\n\n // Add related_docs edges (weight 1.0) — only present when frontmatter has related_docs\n // Falls back gracefully: docs without this field are simply skipped\n for (const doc of documents) {\n if (!doc.relatedDocs?.length) continue;\n for (const ref of doc.relatedDocs) {\n // Try full normalized path first, then basename — same strategy as wikilinks\n const normalized = ref.replace(/\\.md$/, \"\").toLowerCase();\n const base = normalized.split(\"/\").pop()!;\n const target = pathLookup.get(normalized) ?? pathLookup.get(base);\n if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {\n this.graph.addEdge(doc.path, target, { type: \"related\", weight: 1.0 });\n }\n }\n }\n\n // Add tag-based edges (weight 0.5) — capped at TAG_EDGE_MAX_DOCS per tag.\n // Generic tags shared by many docs (e.g. \"api\", \"testing\") create noise;\n // only specific tags (e.g. \"qdrant\", \"celery\") create meaningful connections.\n const tagToNotes = new Map<string, string[]>();\n for (const doc of documents) {\n for (const tag of doc.tags) {\n const notes = tagToNotes.get(tag) ?? [];\n notes.push(doc.path);\n tagToNotes.set(tag, notes);\n }\n }\n\n for (const [, notes] of tagToNotes) {\n if (notes.length >= TAG_EDGE_MAX_DOCS) continue; // too generic — skip\n for (let i = 0; i < notes.length; i++) {\n for (let j = i + 1; j < notes.length; j++) {\n if (!this.graph.hasEdge(notes[i], notes[j])) {\n this.graph.addEdge(notes[i], notes[j], { type: \"tag\", weight: 0.5 });\n }\n if (!this.graph.hasEdge(notes[j], notes[i])) {\n this.graph.addEdge(notes[j], notes[i], { type: \"tag\", weight: 0.5 });\n }\n }\n }\n }\n }\n\n backlinks(notePath: string): GraphNode[] {\n if (!this.graph.hasNode(notePath)) return [];\n return this.graph.inNeighbors(notePath).map((n) => this.nodeToGraphNode(n));\n }\n\n forwardlinks(notePath: string): GraphNode[] {\n if (!this.graph.hasNode(notePath)) return [];\n return this.graph.outNeighbors(notePath).map((n) => this.nodeToGraphNode(n));\n }\n\n findPath(from: string, to: string): string[] | null {\n if (!this.graph.hasNode(from) || !this.graph.hasNode(to)) return null;\n const path = bidirectional(this.graph, from, to);\n return path;\n }\n\n searchGraph(concept: string, maxDepth = 2, limit = 20): GraphNode[] {\n const lc = concept.toLowerCase();\n\n const directMatches = new Set<string>(\n this.graph.nodes().filter((n) => {\n const attrs = this.graph.getNodeAttributes(n);\n const title = typeof attrs.title === \"string\" ? attrs.title : String(attrs.title ?? \"\");\n return (\n n.toLowerCase().includes(lc) ||\n title.toLowerCase().includes(lc) ||\n (attrs.tags as string[] | undefined)?.some((t) => t.toLowerCase().includes(lc))\n );\n })\n );\n\n const visited = new Set<string>();\n for (const start of directMatches) {\n bfsFromNode(this.graph, start, (_node, _attr, depth) => {\n if (visited.size >= limit * 3) return true; // gather 3x limit before ranking\n visited.add(_node);\n return depth >= maxDepth;\n });\n }\n\n // Sort: direct matches first, then by loadPriority desc, then by degree desc\n // deprecated/archived nodes sort to the end\n const ranked = [...visited].sort((a, b) => {\n const aAttrs = this.graph.getNodeAttributes(a);\n const bAttrs = this.graph.getNodeAttributes(b);\n const aDeprecated = aAttrs.status === \"deprecated\" || aAttrs.status === \"archived\" ? 1 : 0;\n const bDeprecated = bAttrs.status === \"deprecated\" || bAttrs.status === \"archived\" ? 1 : 0;\n if (aDeprecated !== bDeprecated) return aDeprecated - bDeprecated;\n const aDirect = directMatches.has(a) ? 0 : 1;\n const bDirect = directMatches.has(b) ? 0 : 1;\n if (aDirect !== bDirect) return aDirect - bDirect;\n const aPriority = (aAttrs.loadPriority as number | undefined) ?? 5;\n const bPriority = (bAttrs.loadPriority as number | undefined) ?? 5;\n if (aPriority !== bPriority) return bPriority - aPriority;\n return this.graph.degree(b) - this.graph.degree(a);\n });\n\n return ranked.slice(0, limit).map((n) => this.nodeToGraphNode(n));\n }\n\n statistics(): GraphStats {\n const nodes = this.graph.order;\n const edges = this.graph.size;\n const orphans = this.graph.nodes().filter(\n (n) => this.graph.degree(n) === 0\n );\n\n const connections = this.graph.nodes().map((n) => ({\n path: n,\n connections: this.graph.degree(n),\n }));\n connections.sort((a, b) => b.connections - a.connections);\n\n const maxPossibleEdges = nodes * (nodes - 1);\n const density = maxPossibleEdges > 0 ? edges / maxPossibleEdges : 0;\n\n return {\n totalNodes: nodes,\n totalEdges: edges,\n orphanCount: orphans.length,\n mostConnected: connections.slice(0, 10),\n density,\n };\n }\n\n private nodeToGraphNode(nodePath: string): GraphNode {\n const attrs = this.graph.getNodeAttributes(nodePath);\n return {\n path: nodePath,\n title: typeof attrs.title === \"string\" ? attrs.title : nodePath,\n tags: (attrs.tags as string[]) || [],\n linkCount: this.graph.outDegree(nodePath),\n backlinkCount: this.graph.inDegree(nodePath),\n ...(attrs.loadPriority !== undefined && { loadPriority: attrs.loadPriority as number }),\n ...(attrs.status !== undefined && { status: attrs.status as string }),\n };\n }\n\n async save(indexPath: string): Promise<void> {\n const data = this.graph.export();\n await writeFile(join(indexPath, \"graph.json\"), JSON.stringify(data));\n }\n\n async load(indexPath: string): Promise<boolean> {\n const filePath = join(indexPath, \"graph.json\");\n if (!existsSync(filePath)) return false;\n\n const raw = await readFile(filePath, \"utf-8\");\n const data = JSON.parse(raw);\n this.graph.import(data);\n return true;\n }\n\n getGraph(): Graph {\n return this.graph;\n }\n}\n","import hnswlib from \"hnswlib-node\";\nconst { HierarchicalNSW } = hnswlib;\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport type { SearchResult } from \"./types.js\";\n\ninterface ChunkMeta {\n docPath: string;\n chunkIndex: number;\n text: string;\n}\n\nexport class VectorIndex {\n private index: InstanceType<typeof HierarchicalNSW> | null = null;\n private dimensions: number;\n private chunkMeta: ChunkMeta[] = [];\n\n constructor(dimensions: number) {\n this.dimensions = dimensions;\n }\n\n build(\n embeddings: Float32Array[],\n meta: ChunkMeta[]\n ): void {\n if (embeddings.length === 0) {\n this.index = null;\n this.chunkMeta = [];\n return;\n }\n\n this.index = new HierarchicalNSW(\"cosine\", this.dimensions);\n this.index.initIndex(embeddings.length);\n\n for (let i = 0; i < embeddings.length; i++) {\n this.index.addPoint(Array.from(embeddings[i]), i);\n }\n\n this.chunkMeta = meta;\n }\n\n search(queryEmbedding: Float32Array, k: number = 10): SearchResult[] {\n if (!this.index || this.chunkMeta.length === 0) return [];\n\n const numResults = Math.min(k, this.chunkMeta.length);\n const result = this.index.searchKnn(Array.from(queryEmbedding), numResults);\n\n const seen = new Set<string>();\n const results: SearchResult[] = [];\n\n for (let i = 0; i < result.neighbors.length; i++) {\n const idx = result.neighbors[i];\n const meta = this.chunkMeta[idx];\n if (!meta || seen.has(meta.docPath)) continue;\n seen.add(meta.docPath);\n\n results.push({\n path: meta.docPath,\n title: meta.docPath,\n score: 1 - result.distances[i],\n snippet: meta.text.slice(0, 200),\n matchedChunk: meta.text,\n });\n }\n\n return results;\n }\n\n async save(indexPath: string): Promise<void> {\n if (!this.index) return;\n\n this.index.writeIndexSync(join(indexPath, \"hnsw.bin\"));\n await writeFile(\n join(indexPath, \"hnsw-meta.json\"),\n JSON.stringify(this.chunkMeta)\n );\n }\n\n async load(indexPath: string): Promise<boolean> {\n const hnswPath = join(indexPath, \"hnsw.bin\");\n const metaPath = join(indexPath, \"hnsw-meta.json\");\n\n if (!existsSync(hnswPath) || !existsSync(metaPath)) return false;\n\n const raw = await readFile(metaPath, \"utf-8\");\n this.chunkMeta = JSON.parse(raw);\n\n this.index = new HierarchicalNSW(\"cosine\", this.dimensions);\n this.index.initIndex(this.chunkMeta.length);\n this.index.readIndexSync(hnswPath);\n\n return true;\n }\n\n getChunkMeta(): ChunkMeta[] {\n return this.chunkMeta;\n }\n}\n","import type { IndexedDocument, SearchResult, SearchTextOptions } from \"./types.js\";\nimport { minimatch } from \"minimatch\";\n\nexport class TextSearch {\n private documents: IndexedDocument[] = [];\n\n setDocuments(documents: IndexedDocument[]): void {\n this.documents = documents;\n }\n\n search(options: SearchTextOptions): SearchResult[] {\n const { pattern, regex, caseSensitive, pathGlob, tagFilter, limit = 20 } = options;\n\n let matcher: (text: string) => { matched: boolean; index: number };\n\n if (regex) {\n const flags = caseSensitive ? \"g\" : \"gi\";\n const re = new RegExp(pattern, flags);\n matcher = (text) => {\n re.lastIndex = 0;\n const m = re.exec(text);\n return { matched: !!m, index: m?.index ?? -1 };\n };\n } else {\n const needle = caseSensitive ? pattern : pattern.toLowerCase();\n matcher = (text) => {\n const haystack = caseSensitive ? text : text.toLowerCase();\n const idx = haystack.indexOf(needle);\n return { matched: idx >= 0, index: idx };\n };\n }\n\n const results: SearchResult[] = [];\n\n for (const doc of this.documents) {\n // Path filter\n if (pathGlob && !minimatch(doc.path, pathGlob)) continue;\n\n // Tag filter\n if (tagFilter?.length) {\n const hasTag = tagFilter.some((t) => doc.tags.includes(t));\n if (!hasTag) continue;\n }\n\n const { matched, index } = matcher(doc.content);\n if (!matched) continue;\n\n const snippetStart = Math.max(0, index - 80);\n const snippetEnd = Math.min(doc.content.length, index + 120);\n const snippet = doc.content.slice(snippetStart, snippetEnd).trim();\n\n results.push({\n path: doc.path,\n title: doc.title,\n score: 1.0,\n snippet: (snippetStart > 0 ? \"...\" : \"\") + snippet + (snippetEnd < doc.content.length ? \"...\" : \"\"),\n });\n\n if (results.length >= limit) break;\n }\n\n return results;\n }\n}\n"],"mappings":"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","names":["readFile","writeFile","join","existsSync","readFile","writeFile","join","existsSync"]}