@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,651 @@
1
+ // src/core/embedder.ts
2
+ import { AutoTokenizer } from "@huggingface/transformers";
3
+ import { readFile, writeFile, mkdir, rename, unlink } from "fs/promises";
4
+ import { join, dirname } from "path";
5
+ import { existsSync, createWriteStream } from "fs";
6
+ import { homedir } from "os";
7
+ import { Readable } from "stream";
8
+ import { pipeline as streamPipeline } from "stream/promises";
9
+ import { Worker } from "worker_threads";
10
+ import { fileURLToPath } from "url";
11
+ var DEFAULT_MODEL = "sentence-transformers/all-MiniLM-L6-v2";
12
+ var CACHE_DIR = join(homedir(), ".semantic-sidekick", "models");
13
+ var DEFAULT_WORKERS = 1;
14
+ var DEFAULT_BATCH_SIZE = 16;
15
+ var DEFAULT_QUANTIZED = false;
16
+ var ONNX_MODEL_PATHS = {
17
+ "nomic-ai/nomic-embed-text-v1.5": "onnx/model.onnx",
18
+ "sentence-transformers/all-MiniLM-L6-v2": "onnx/model.onnx"
19
+ };
20
+ var ONNX_QUANTIZED_MODEL_PATHS = {
21
+ "nomic-ai/nomic-embed-text-v1.5": "onnx/model_quantized.onnx",
22
+ "sentence-transformers/all-MiniLM-L6-v2": "onnx/model_quantized.onnx"
23
+ };
24
+ async function resolveOnnxRuntime() {
25
+ try {
26
+ const ort = await import("onnxruntime-node");
27
+ return { ort, label: "native" };
28
+ } catch {
29
+ const ort = await import("onnxruntime-web");
30
+ return { ort, label: "wasm" };
31
+ }
32
+ }
33
+ async function downloadFile(url, destPath) {
34
+ const response = await fetch(url);
35
+ if (!response.ok) throw new Error(`Download failed (${response.status}): ${url}`);
36
+ if (!response.body) throw new Error(`No response body: ${url}`);
37
+ const tempPath = `${destPath}.${process.pid}.${Date.now()}.tmp`;
38
+ try {
39
+ const fileStream = createWriteStream(tempPath);
40
+ await streamPipeline(Readable.fromWeb(response.body), fileStream);
41
+ await rename(tempPath, destPath);
42
+ } catch (err) {
43
+ try {
44
+ await unlink(tempPath);
45
+ } catch {
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+ var Embedder = class {
51
+ model;
52
+ session = null;
53
+ tokenizer = null;
54
+ ort = null;
55
+ dimensions = 0;
56
+ runtimeLabel = "wasm";
57
+ initialized = false;
58
+ numWorkers;
59
+ batchSize;
60
+ quantized;
61
+ modelPath = "";
62
+ constructor(model = DEFAULT_MODEL, numWorkers = DEFAULT_WORKERS, batchSize = DEFAULT_BATCH_SIZE, quantized = DEFAULT_QUANTIZED) {
63
+ this.model = model;
64
+ this.numWorkers = numWorkers;
65
+ this.batchSize = batchSize;
66
+ this.quantized = quantized;
67
+ }
68
+ async init() {
69
+ if (this.initialized) return;
70
+ const modelDir = join(CACHE_DIR, this.model.replace(/\//g, "--"));
71
+ await mkdir(modelDir, { recursive: true });
72
+ const { ort, label } = await resolveOnnxRuntime();
73
+ this.ort = ort;
74
+ this.runtimeLabel = label;
75
+ let useQuantized = this.quantized;
76
+ const modelFileName = useQuantized ? "model_quantized.onnx" : "model.onnx";
77
+ this.modelPath = join(modelDir, modelFileName);
78
+ const modelPath = this.modelPath;
79
+ if (!existsSync(modelPath)) {
80
+ const pathMap = useQuantized ? ONNX_QUANTIZED_MODEL_PATHS : ONNX_MODEL_PATHS;
81
+ const onnxSubpath = pathMap[this.model] ?? (useQuantized ? "onnx/model_quantized.onnx" : "onnx/model.onnx");
82
+ const url = `https://huggingface.co/${this.model}/resolve/main/${onnxSubpath}`;
83
+ process.stderr.write(`Downloading ONNX model: ${this.model} (${useQuantized ? "quantized" : "fp32"})...
84
+ `);
85
+ try {
86
+ await downloadFile(url, modelPath);
87
+ } catch (err) {
88
+ if (useQuantized && err?.message?.includes("404")) {
89
+ process.stderr.write(`Quantized model not available, falling back to fp32
90
+ `);
91
+ useQuantized = false;
92
+ this.modelPath = join(modelDir, "model.onnx");
93
+ const fp32Subpath = ONNX_MODEL_PATHS[this.model] ?? "onnx/model.onnx";
94
+ const fp32Url = `https://huggingface.co/${this.model}/resolve/main/${fp32Subpath}`;
95
+ process.stderr.write(`Downloading ONNX model: ${this.model} (fp32)...
96
+ `);
97
+ await downloadFile(fp32Url, this.modelPath);
98
+ } else {
99
+ throw err;
100
+ }
101
+ }
102
+ process.stderr.write(`Model downloaded to ${modelDir}
103
+ `);
104
+ }
105
+ this.tokenizer = await AutoTokenizer.from_pretrained(this.model, {
106
+ cache_dir: CACHE_DIR
107
+ });
108
+ this.session = await ort.InferenceSession.create(this.modelPath, {
109
+ executionProviders: [label === "native" ? "cpu" : "wasm"]
110
+ });
111
+ const test = await this.embed("test");
112
+ this.dimensions = test.length;
113
+ this.initialized = true;
114
+ const modelShort = this.model.split("/").pop() ?? this.model;
115
+ process.stderr.write(`Embedder ready (${modelShort}, ${label}, ${this.dimensions}d, batch=${this.batchSize})
116
+ `);
117
+ }
118
+ async embed(text) {
119
+ if (!this.session || !this.tokenizer || !this.ort)
120
+ throw new Error("Embedder not initialized. Call init() first.");
121
+ const encoded = await this.tokenizer(text, {
122
+ padding: true,
123
+ truncation: true,
124
+ max_length: 512,
125
+ return_tensor: false
126
+ });
127
+ const inputIdsRaw = Array.from(encoded.input_ids.data ?? encoded.input_ids);
128
+ const attentionMaskRaw = Array.from(encoded.attention_mask.data ?? encoded.attention_mask);
129
+ const seqLen = inputIdsRaw.length;
130
+ const inputIds = new this.ort.Tensor(
131
+ "int64",
132
+ BigInt64Array.from(inputIdsRaw.map(BigInt)),
133
+ [1, seqLen]
134
+ );
135
+ const attentionMask = new this.ort.Tensor(
136
+ "int64",
137
+ BigInt64Array.from(attentionMaskRaw.map(BigInt)),
138
+ [1, seqLen]
139
+ );
140
+ const feeds = { input_ids: inputIds, attention_mask: attentionMask };
141
+ if (this.session.inputNames.includes("token_type_ids")) {
142
+ feeds.token_type_ids = new this.ort.Tensor(
143
+ "int64",
144
+ new BigInt64Array(seqLen),
145
+ [1, seqLen]
146
+ );
147
+ }
148
+ const output = await this.session.run(feeds);
149
+ const outputTensor = output[this.session.outputNames[0]];
150
+ const hiddenSize = outputTensor.dims[outputTensor.dims.length - 1];
151
+ return this.meanPoolAndNormalize(outputTensor.data, attentionMaskRaw, seqLen, hiddenSize);
152
+ }
153
+ meanPoolAndNormalize(embeddings, attentionMask, seqLen, hiddenSize) {
154
+ const result = new Float32Array(hiddenSize);
155
+ let maskSum = 0;
156
+ for (let t = 0; t < seqLen; t++) {
157
+ const mask = attentionMask[t];
158
+ maskSum += mask;
159
+ const offset = t * hiddenSize;
160
+ for (let d = 0; d < hiddenSize; d++) {
161
+ result[d] += embeddings[offset + d] * mask;
162
+ }
163
+ }
164
+ if (maskSum > 0) {
165
+ for (let d = 0; d < hiddenSize; d++) result[d] /= maskSum;
166
+ }
167
+ let norm = 0;
168
+ for (let d = 0; d < hiddenSize; d++) norm += result[d] * result[d];
169
+ norm = Math.sqrt(norm);
170
+ if (norm > 0) {
171
+ for (let d = 0; d < hiddenSize; d++) result[d] /= norm;
172
+ }
173
+ return result;
174
+ }
175
+ // Mean pool + L2 normalize the output of a batched ONNX forward pass.
176
+ // outputData: flat Float32Array of shape [batchSize, seqLen, hiddenSize]
177
+ // maskData: flat number[] of shape [batchSize, seqLen]
178
+ meanPoolAndNormalizeMany(outputData, maskData, batchSize, seqLen, hiddenSize) {
179
+ const results = [];
180
+ for (let b = 0; b < batchSize; b++) {
181
+ const result = new Float32Array(hiddenSize);
182
+ let maskSum = 0;
183
+ for (let t = 0; t < seqLen; t++) {
184
+ const mask = maskData[b * seqLen + t];
185
+ maskSum += mask;
186
+ const offset = (b * seqLen + t) * hiddenSize;
187
+ for (let d = 0; d < hiddenSize; d++) {
188
+ result[d] += outputData[offset + d] * mask;
189
+ }
190
+ }
191
+ if (maskSum > 0) {
192
+ for (let d = 0; d < hiddenSize; d++) result[d] /= maskSum;
193
+ }
194
+ let norm = 0;
195
+ for (let d = 0; d < hiddenSize; d++) norm += result[d] * result[d];
196
+ norm = Math.sqrt(norm);
197
+ if (norm > 0) {
198
+ for (let d = 0; d < hiddenSize; d++) result[d] /= norm;
199
+ }
200
+ results.push(result);
201
+ }
202
+ return results;
203
+ }
204
+ // Embed a sub-batch of texts (length <= batchSize) in a single ONNX forward pass.
205
+ //
206
+ // Tokenizes each text individually (well-defined output format for all HF versions),
207
+ // then manually pads to the longest sequence in the batch and builds a [n, seqLen]
208
+ // tensor for one ONNX call. The speedup is from batching the ONNX inference —
209
+ // individual tokenization is negligible (<1ms each).
210
+ async embedSubBatch(texts) {
211
+ if (!this.session || !this.tokenizer || !this.ort)
212
+ throw new Error("Embedder not initialized. Call init() first.");
213
+ const n = texts.length;
214
+ const encodings = await Promise.all(
215
+ texts.map(
216
+ (text) => this.tokenizer(text, {
217
+ padding: false,
218
+ truncation: true,
219
+ max_length: 512,
220
+ return_tensor: false
221
+ })
222
+ )
223
+ );
224
+ const tokenized = encodings.map((enc) => ({
225
+ ids: Array.from(enc.input_ids.data ?? enc.input_ids),
226
+ mask: Array.from(enc.attention_mask.data ?? enc.attention_mask)
227
+ }));
228
+ const seqLen = Math.max(...tokenized.map((t) => t.ids.length));
229
+ const flatIds = new BigInt64Array(n * seqLen);
230
+ const flatMask = new BigInt64Array(n * seqLen);
231
+ const flatMaskNums = new Array(n * seqLen).fill(0);
232
+ for (let i = 0; i < n; i++) {
233
+ const { ids, mask } = tokenized[i];
234
+ for (let j = 0; j < ids.length; j++) {
235
+ flatIds[i * seqLen + j] = BigInt(ids[j]);
236
+ flatMask[i * seqLen + j] = BigInt(mask[j]);
237
+ flatMaskNums[i * seqLen + j] = mask[j];
238
+ }
239
+ }
240
+ const inputIds = new this.ort.Tensor("int64", flatIds, [n, seqLen]);
241
+ const attentionMask = new this.ort.Tensor("int64", flatMask, [n, seqLen]);
242
+ const feeds = { input_ids: inputIds, attention_mask: attentionMask };
243
+ if (this.session.inputNames.includes("token_type_ids")) {
244
+ feeds.token_type_ids = new this.ort.Tensor(
245
+ "int64",
246
+ new BigInt64Array(n * seqLen),
247
+ [n, seqLen]
248
+ );
249
+ }
250
+ const output = await this.session.run(feeds);
251
+ const outputTensor = output[this.session.outputNames[0]];
252
+ const hiddenSize = outputTensor.dims[outputTensor.dims.length - 1];
253
+ return this.meanPoolAndNormalizeMany(
254
+ outputTensor.data,
255
+ flatMaskNums,
256
+ n,
257
+ seqLen,
258
+ hiddenSize
259
+ );
260
+ }
261
+ async embedBatch(texts, onProgress) {
262
+ if (this.numWorkers > 1 && texts.length >= this.numWorkers * 2) {
263
+ return this.embedBatchParallel(texts, onProgress);
264
+ }
265
+ const results = [];
266
+ for (let i = 0; i < texts.length; i += this.batchSize) {
267
+ const subBatch = texts.slice(i, i + this.batchSize);
268
+ const embeddings = await this.embedSubBatch(subBatch);
269
+ results.push(...embeddings);
270
+ await onProgress?.(Math.min(i + subBatch.length, texts.length), texts.length, embeddings);
271
+ }
272
+ return results;
273
+ }
274
+ async embedBatchParallel(texts, onProgress) {
275
+ const thisDir = dirname(fileURLToPath(import.meta.url));
276
+ let workerPath = join(thisDir, "embed-worker.js");
277
+ if (!existsSync(workerPath)) {
278
+ workerPath = join(thisDir, "core", "embed-worker.js");
279
+ }
280
+ if (!existsSync(workerPath)) {
281
+ process.stderr.write("Worker script not found, falling back to batched embedding\n");
282
+ return this.embedBatch(texts, onProgress);
283
+ }
284
+ const chunkSize = Math.ceil(texts.length / this.numWorkers);
285
+ const chunks = [];
286
+ for (let i = 0; i < texts.length; i += chunkSize) {
287
+ chunks.push({ texts: texts.slice(i, i + chunkSize), startIndex: i });
288
+ }
289
+ const allResults = new Array(texts.length);
290
+ let totalDone = 0;
291
+ const workerPromises = chunks.map((chunk) => {
292
+ return new Promise((resolve, reject) => {
293
+ const worker = new Worker(workerPath, {
294
+ workerData: {
295
+ modelPath: this.modelPath,
296
+ modelName: this.model,
297
+ cacheDir: CACHE_DIR,
298
+ runtimeLabel: this.runtimeLabel,
299
+ batchSize: this.batchSize
300
+ }
301
+ });
302
+ worker.on("message", (msg) => {
303
+ if (msg.type === "ready") {
304
+ worker.postMessage({ type: "embed", texts: chunk.texts, startIndex: chunk.startIndex });
305
+ } else if (msg.type === "progress") {
306
+ totalDone++;
307
+ onProgress?.(totalDone, texts.length);
308
+ } else if (msg.type === "result") {
309
+ for (let i = 0; i < msg.embeddings.length; i++) {
310
+ allResults[chunk.startIndex + i] = new Float32Array(msg.embeddings[i]);
311
+ }
312
+ worker.terminate();
313
+ resolve();
314
+ } else if (msg.type === "error") {
315
+ worker.terminate();
316
+ reject(new Error(msg.error));
317
+ }
318
+ });
319
+ worker.on("error", (err) => {
320
+ worker.terminate();
321
+ reject(err);
322
+ });
323
+ });
324
+ });
325
+ await Promise.all(workerPromises);
326
+ return allResults;
327
+ }
328
+ getDimensions() {
329
+ return this.dimensions;
330
+ }
331
+ getModel() {
332
+ return this.model;
333
+ }
334
+ getRuntime() {
335
+ return this.runtimeLabel;
336
+ }
337
+ async saveEmbeddings(embeddings, indexPath) {
338
+ const entries = [];
339
+ for (const [key, vec] of embeddings) {
340
+ entries.push({ key, data: Array.from(vec) });
341
+ }
342
+ await writeFile(join(indexPath, "embeddings.json"), JSON.stringify(entries));
343
+ }
344
+ async loadEmbeddings(indexPath) {
345
+ const filePath = join(indexPath, "embeddings.json");
346
+ if (!existsSync(filePath)) return /* @__PURE__ */ new Map();
347
+ const raw = await readFile(filePath, "utf-8");
348
+ const entries = JSON.parse(raw);
349
+ const map = /* @__PURE__ */ new Map();
350
+ for (const entry of entries) {
351
+ map.set(entry.key, new Float32Array(entry.data));
352
+ }
353
+ return map;
354
+ }
355
+ };
356
+
357
+ // src/core/graph.ts
358
+ import Graph from "graphology";
359
+ import { bfsFromNode } from "graphology-traversal";
360
+ import { bidirectional } from "graphology-shortest-path";
361
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
362
+ import { join as join2 } from "path";
363
+ import { existsSync as existsSync2 } from "fs";
364
+ var TAG_EDGE_MAX_DOCS = 15;
365
+ var GraphBuilder = class {
366
+ graph;
367
+ constructor() {
368
+ this.graph = new Graph({ type: "directed", multi: false });
369
+ }
370
+ buildFromDocuments(documents) {
371
+ this.graph.clear();
372
+ for (const doc of documents) {
373
+ this.graph.addNode(doc.path, {
374
+ title: doc.title,
375
+ tags: doc.tags,
376
+ ...doc.status !== void 0 && { status: doc.status },
377
+ ...doc.loadPriority !== void 0 && { loadPriority: doc.loadPriority }
378
+ });
379
+ }
380
+ const pathLookup = /* @__PURE__ */ new Map();
381
+ for (const doc of documents) {
382
+ const nameNoExt = doc.path.replace(/\.md$/, "");
383
+ const basename = nameNoExt.split("/").pop();
384
+ pathLookup.set(basename.toLowerCase(), doc.path);
385
+ pathLookup.set(nameNoExt.toLowerCase(), doc.path);
386
+ }
387
+ for (const doc of documents) {
388
+ for (const link of doc.wikilinks) {
389
+ const target = pathLookup.get(link.toLowerCase());
390
+ if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {
391
+ this.graph.addEdge(doc.path, target, { type: "wikilink", weight: 1 });
392
+ }
393
+ }
394
+ }
395
+ for (const doc of documents) {
396
+ if (!doc.relatedDocs?.length) continue;
397
+ for (const ref of doc.relatedDocs) {
398
+ const normalized = ref.replace(/\.md$/, "").toLowerCase();
399
+ const base = normalized.split("/").pop();
400
+ const target = pathLookup.get(normalized) ?? pathLookup.get(base);
401
+ if (target && target !== doc.path && !this.graph.hasEdge(doc.path, target)) {
402
+ this.graph.addEdge(doc.path, target, { type: "related", weight: 1 });
403
+ }
404
+ }
405
+ }
406
+ const tagToNotes = /* @__PURE__ */ new Map();
407
+ for (const doc of documents) {
408
+ for (const tag of doc.tags) {
409
+ const notes = tagToNotes.get(tag) ?? [];
410
+ notes.push(doc.path);
411
+ tagToNotes.set(tag, notes);
412
+ }
413
+ }
414
+ for (const [, notes] of tagToNotes) {
415
+ if (notes.length >= TAG_EDGE_MAX_DOCS) continue;
416
+ for (let i = 0; i < notes.length; i++) {
417
+ for (let j = i + 1; j < notes.length; j++) {
418
+ if (!this.graph.hasEdge(notes[i], notes[j])) {
419
+ this.graph.addEdge(notes[i], notes[j], { type: "tag", weight: 0.5 });
420
+ }
421
+ if (!this.graph.hasEdge(notes[j], notes[i])) {
422
+ this.graph.addEdge(notes[j], notes[i], { type: "tag", weight: 0.5 });
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ backlinks(notePath) {
429
+ if (!this.graph.hasNode(notePath)) return [];
430
+ return this.graph.inNeighbors(notePath).map((n) => this.nodeToGraphNode(n));
431
+ }
432
+ forwardlinks(notePath) {
433
+ if (!this.graph.hasNode(notePath)) return [];
434
+ return this.graph.outNeighbors(notePath).map((n) => this.nodeToGraphNode(n));
435
+ }
436
+ findPath(from, to) {
437
+ if (!this.graph.hasNode(from) || !this.graph.hasNode(to)) return null;
438
+ const path = bidirectional(this.graph, from, to);
439
+ return path;
440
+ }
441
+ searchGraph(concept, maxDepth = 2, limit = 20) {
442
+ const lc = concept.toLowerCase();
443
+ const directMatches = new Set(
444
+ this.graph.nodes().filter((n) => {
445
+ const attrs = this.graph.getNodeAttributes(n);
446
+ const title = typeof attrs.title === "string" ? attrs.title : String(attrs.title ?? "");
447
+ return n.toLowerCase().includes(lc) || title.toLowerCase().includes(lc) || attrs.tags?.some((t) => t.toLowerCase().includes(lc));
448
+ })
449
+ );
450
+ const visited = /* @__PURE__ */ new Set();
451
+ for (const start of directMatches) {
452
+ bfsFromNode(this.graph, start, (_node, _attr, depth) => {
453
+ if (visited.size >= limit * 3) return true;
454
+ visited.add(_node);
455
+ return depth >= maxDepth;
456
+ });
457
+ }
458
+ const ranked = [...visited].sort((a, b) => {
459
+ const aAttrs = this.graph.getNodeAttributes(a);
460
+ const bAttrs = this.graph.getNodeAttributes(b);
461
+ const aDeprecated = aAttrs.status === "deprecated" || aAttrs.status === "archived" ? 1 : 0;
462
+ const bDeprecated = bAttrs.status === "deprecated" || bAttrs.status === "archived" ? 1 : 0;
463
+ if (aDeprecated !== bDeprecated) return aDeprecated - bDeprecated;
464
+ const aDirect = directMatches.has(a) ? 0 : 1;
465
+ const bDirect = directMatches.has(b) ? 0 : 1;
466
+ if (aDirect !== bDirect) return aDirect - bDirect;
467
+ const aPriority = aAttrs.loadPriority ?? 5;
468
+ const bPriority = bAttrs.loadPriority ?? 5;
469
+ if (aPriority !== bPriority) return bPriority - aPriority;
470
+ return this.graph.degree(b) - this.graph.degree(a);
471
+ });
472
+ return ranked.slice(0, limit).map((n) => this.nodeToGraphNode(n));
473
+ }
474
+ statistics() {
475
+ const nodes = this.graph.order;
476
+ const edges = this.graph.size;
477
+ const orphans = this.graph.nodes().filter(
478
+ (n) => this.graph.degree(n) === 0
479
+ );
480
+ const connections = this.graph.nodes().map((n) => ({
481
+ path: n,
482
+ connections: this.graph.degree(n)
483
+ }));
484
+ connections.sort((a, b) => b.connections - a.connections);
485
+ const maxPossibleEdges = nodes * (nodes - 1);
486
+ const density = maxPossibleEdges > 0 ? edges / maxPossibleEdges : 0;
487
+ return {
488
+ totalNodes: nodes,
489
+ totalEdges: edges,
490
+ orphanCount: orphans.length,
491
+ mostConnected: connections.slice(0, 10),
492
+ density
493
+ };
494
+ }
495
+ nodeToGraphNode(nodePath) {
496
+ const attrs = this.graph.getNodeAttributes(nodePath);
497
+ return {
498
+ path: nodePath,
499
+ title: typeof attrs.title === "string" ? attrs.title : nodePath,
500
+ tags: attrs.tags || [],
501
+ linkCount: this.graph.outDegree(nodePath),
502
+ backlinkCount: this.graph.inDegree(nodePath),
503
+ ...attrs.loadPriority !== void 0 && { loadPriority: attrs.loadPriority },
504
+ ...attrs.status !== void 0 && { status: attrs.status }
505
+ };
506
+ }
507
+ async save(indexPath) {
508
+ const data = this.graph.export();
509
+ await writeFile2(join2(indexPath, "graph.json"), JSON.stringify(data));
510
+ }
511
+ async load(indexPath) {
512
+ const filePath = join2(indexPath, "graph.json");
513
+ if (!existsSync2(filePath)) return false;
514
+ const raw = await readFile2(filePath, "utf-8");
515
+ const data = JSON.parse(raw);
516
+ this.graph.import(data);
517
+ return true;
518
+ }
519
+ getGraph() {
520
+ return this.graph;
521
+ }
522
+ };
523
+
524
+ // src/core/vector.ts
525
+ import hnswlib from "hnswlib-node";
526
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
527
+ import { join as join3 } from "path";
528
+ import { existsSync as existsSync3 } from "fs";
529
+ var { HierarchicalNSW } = hnswlib;
530
+ var VectorIndex = class {
531
+ index = null;
532
+ dimensions;
533
+ chunkMeta = [];
534
+ constructor(dimensions) {
535
+ this.dimensions = dimensions;
536
+ }
537
+ build(embeddings, meta) {
538
+ if (embeddings.length === 0) {
539
+ this.index = null;
540
+ this.chunkMeta = [];
541
+ return;
542
+ }
543
+ this.index = new HierarchicalNSW("cosine", this.dimensions);
544
+ this.index.initIndex(embeddings.length);
545
+ for (let i = 0; i < embeddings.length; i++) {
546
+ this.index.addPoint(Array.from(embeddings[i]), i);
547
+ }
548
+ this.chunkMeta = meta;
549
+ }
550
+ search(queryEmbedding, k = 10) {
551
+ if (!this.index || this.chunkMeta.length === 0) return [];
552
+ const numResults = Math.min(k, this.chunkMeta.length);
553
+ const result = this.index.searchKnn(Array.from(queryEmbedding), numResults);
554
+ const seen = /* @__PURE__ */ new Set();
555
+ const results = [];
556
+ for (let i = 0; i < result.neighbors.length; i++) {
557
+ const idx = result.neighbors[i];
558
+ const meta = this.chunkMeta[idx];
559
+ if (!meta || seen.has(meta.docPath)) continue;
560
+ seen.add(meta.docPath);
561
+ results.push({
562
+ path: meta.docPath,
563
+ title: meta.docPath,
564
+ score: 1 - result.distances[i],
565
+ snippet: meta.text.slice(0, 200),
566
+ matchedChunk: meta.text
567
+ });
568
+ }
569
+ return results;
570
+ }
571
+ async save(indexPath) {
572
+ if (!this.index) return;
573
+ this.index.writeIndexSync(join3(indexPath, "hnsw.bin"));
574
+ await writeFile3(
575
+ join3(indexPath, "hnsw-meta.json"),
576
+ JSON.stringify(this.chunkMeta)
577
+ );
578
+ }
579
+ async load(indexPath) {
580
+ const hnswPath = join3(indexPath, "hnsw.bin");
581
+ const metaPath = join3(indexPath, "hnsw-meta.json");
582
+ if (!existsSync3(hnswPath) || !existsSync3(metaPath)) return false;
583
+ const raw = await readFile3(metaPath, "utf-8");
584
+ this.chunkMeta = JSON.parse(raw);
585
+ this.index = new HierarchicalNSW("cosine", this.dimensions);
586
+ this.index.initIndex(this.chunkMeta.length);
587
+ this.index.readIndexSync(hnswPath);
588
+ return true;
589
+ }
590
+ getChunkMeta() {
591
+ return this.chunkMeta;
592
+ }
593
+ };
594
+
595
+ // src/core/search-text.ts
596
+ import { minimatch } from "minimatch";
597
+ var TextSearch = class {
598
+ documents = [];
599
+ setDocuments(documents) {
600
+ this.documents = documents;
601
+ }
602
+ search(options) {
603
+ const { pattern, regex, caseSensitive, pathGlob, tagFilter, limit = 20 } = options;
604
+ let matcher;
605
+ if (regex) {
606
+ const flags = caseSensitive ? "g" : "gi";
607
+ const re = new RegExp(pattern, flags);
608
+ matcher = (text) => {
609
+ re.lastIndex = 0;
610
+ const m = re.exec(text);
611
+ return { matched: !!m, index: m?.index ?? -1 };
612
+ };
613
+ } else {
614
+ const needle = caseSensitive ? pattern : pattern.toLowerCase();
615
+ matcher = (text) => {
616
+ const haystack = caseSensitive ? text : text.toLowerCase();
617
+ const idx = haystack.indexOf(needle);
618
+ return { matched: idx >= 0, index: idx };
619
+ };
620
+ }
621
+ const results = [];
622
+ for (const doc of this.documents) {
623
+ if (pathGlob && !minimatch(doc.path, pathGlob)) continue;
624
+ if (tagFilter?.length) {
625
+ const hasTag = tagFilter.some((t) => doc.tags.includes(t));
626
+ if (!hasTag) continue;
627
+ }
628
+ const { matched, index } = matcher(doc.content);
629
+ if (!matched) continue;
630
+ const snippetStart = Math.max(0, index - 80);
631
+ const snippetEnd = Math.min(doc.content.length, index + 120);
632
+ const snippet = doc.content.slice(snippetStart, snippetEnd).trim();
633
+ results.push({
634
+ path: doc.path,
635
+ title: doc.title,
636
+ score: 1,
637
+ snippet: (snippetStart > 0 ? "..." : "") + snippet + (snippetEnd < doc.content.length ? "..." : "")
638
+ });
639
+ if (results.length >= limit) break;
640
+ }
641
+ return results;
642
+ }
643
+ };
644
+
645
+ export {
646
+ Embedder,
647
+ GraphBuilder,
648
+ VectorIndex,
649
+ TextSearch
650
+ };
651
+ //# sourceMappingURL=chunk-25EIQWXL.js.map