kontext-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2422 @@
1
+ // src/indexer/discovery.ts
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import ignore from "ignore";
5
+ var LANGUAGE_MAP = {
6
+ ".ts": "typescript",
7
+ ".tsx": "typescript",
8
+ ".js": "javascript",
9
+ ".jsx": "javascript",
10
+ ".mjs": "javascript",
11
+ ".cjs": "javascript",
12
+ ".py": "python",
13
+ ".go": "go",
14
+ ".rs": "rust",
15
+ ".java": "java",
16
+ ".rb": "ruby",
17
+ ".php": "php",
18
+ ".swift": "swift",
19
+ ".kt": "kotlin",
20
+ ".c": "c",
21
+ ".h": "c",
22
+ ".cpp": "cpp",
23
+ ".hpp": "cpp",
24
+ ".cc": "cpp",
25
+ ".cxx": "cpp",
26
+ ".json": "json",
27
+ ".yaml": "yaml",
28
+ ".yml": "yaml",
29
+ ".toml": "toml",
30
+ ".md": "markdown",
31
+ ".mdx": "markdown",
32
+ ".env": "env"
33
+ };
34
+ var BUILTIN_IGNORE = [
35
+ "node_modules",
36
+ ".git",
37
+ "dist",
38
+ "build",
39
+ "*.lock",
40
+ "package-lock.json",
41
+ "*.png",
42
+ "*.jpg",
43
+ "*.jpeg",
44
+ "*.gif",
45
+ "*.webp",
46
+ "*.ico",
47
+ "*.bmp",
48
+ "*.svg",
49
+ "*.woff",
50
+ "*.woff2",
51
+ "*.ttf",
52
+ "*.eot",
53
+ "*.mp3",
54
+ "*.mp4",
55
+ "*.wav",
56
+ "*.avi",
57
+ "*.mov",
58
+ "*.zip",
59
+ "*.tar",
60
+ "*.gz",
61
+ "*.rar",
62
+ "*.7z",
63
+ "*.pdf",
64
+ "*.exe",
65
+ "*.dll",
66
+ "*.so",
67
+ "*.dylib",
68
+ "*.o",
69
+ "*.a",
70
+ "*.wasm",
71
+ "*.pyc",
72
+ "*.class"
73
+ ];
74
+ function getLanguage(filePath) {
75
+ const basename = path.basename(filePath);
76
+ if (basename.startsWith(".") && !basename.includes(".", 1)) {
77
+ const dotExt = basename;
78
+ return LANGUAGE_MAP[dotExt] ?? null;
79
+ }
80
+ const ext = path.extname(filePath).toLowerCase();
81
+ return LANGUAGE_MAP[ext] ?? null;
82
+ }
83
+ async function readIgnoreFile(filePath) {
84
+ try {
85
+ const content = await fs.readFile(filePath, "utf-8");
86
+ return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+ async function statSafe(filePath, followSymlinks) {
92
+ try {
93
+ return followSymlinks ? await fs.stat(filePath) : await fs.lstat(filePath);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ async function discoverFiles(options) {
99
+ const { root, extraIgnore = [], followSymlinks = true } = options;
100
+ const absoluteRoot = path.resolve(root);
101
+ const ig = ignore();
102
+ ig.add(BUILTIN_IGNORE);
103
+ const gitignoreRules = await readIgnoreFile(
104
+ path.join(absoluteRoot, ".gitignore")
105
+ );
106
+ ig.add(gitignoreRules);
107
+ const ctxignoreRules = await readIgnoreFile(
108
+ path.join(absoluteRoot, ".ctxignore")
109
+ );
110
+ ig.add(ctxignoreRules);
111
+ ig.add(extraIgnore);
112
+ const results = [];
113
+ await walkDirectory(absoluteRoot, absoluteRoot, ig, followSymlinks, results);
114
+ return results.sort((a, b) => a.path.localeCompare(b.path));
115
+ }
116
+ async function walkDirectory(dir, root, ig, followSymlinks, results) {
117
+ let entries;
118
+ try {
119
+ entries = await fs.readdir(dir, { withFileTypes: true });
120
+ } catch {
121
+ return;
122
+ }
123
+ for (const entry of entries) {
124
+ const absolutePath = path.join(dir, entry.name);
125
+ const relativePath = path.relative(root, absolutePath);
126
+ const normalizedRelative = relativePath.split(path.sep).join("/");
127
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
128
+ const stat2 = await statSafe(absolutePath, followSymlinks);
129
+ if (!stat2) continue;
130
+ if (stat2.isDirectory()) {
131
+ if (ig.ignores(normalizedRelative + "/") || ig.ignores(normalizedRelative)) {
132
+ continue;
133
+ }
134
+ await walkDirectory(absolutePath, root, ig, followSymlinks, results);
135
+ continue;
136
+ }
137
+ if (!stat2.isFile()) continue;
138
+ }
139
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
140
+ if (ig.ignores(normalizedRelative)) continue;
141
+ const language = getLanguage(relativePath);
142
+ if (language === null) continue;
143
+ const stat = await statSafe(absolutePath, followSymlinks);
144
+ if (!stat || !stat.isFile()) continue;
145
+ results.push({
146
+ path: normalizedRelative,
147
+ absolutePath,
148
+ language,
149
+ size: stat.size,
150
+ lastModified: stat.mtimeMs
151
+ });
152
+ }
153
+ }
154
+
155
+ // src/indexer/parser.ts
156
+ import fs2 from "fs/promises";
157
+ import path2 from "path";
158
+ import { createRequire } from "module";
159
+ import Parser from "web-tree-sitter";
160
+ var GRAMMAR_FILES = {
161
+ typescript: "tree-sitter-typescript.wasm",
162
+ javascript: "tree-sitter-javascript.wasm",
163
+ python: "tree-sitter-python.wasm"
164
+ };
165
+ var require2 = createRequire(import.meta.url);
166
+ var initialized = false;
167
+ var languageCache = /* @__PURE__ */ new Map();
168
+ function resolveWasmPath(filename) {
169
+ if (filename === "tree-sitter.wasm") {
170
+ return path2.join(path2.dirname(require2.resolve("web-tree-sitter")), filename);
171
+ }
172
+ return path2.join(path2.dirname(require2.resolve("tree-sitter-wasms/package.json")), "out", filename);
173
+ }
174
+ async function initParser() {
175
+ if (initialized) return;
176
+ await Parser.init({
177
+ locateFile: (scriptName) => resolveWasmPath(scriptName)
178
+ });
179
+ initialized = true;
180
+ }
181
+ async function getLanguage2(language) {
182
+ const grammarFile = GRAMMAR_FILES[language];
183
+ if (!grammarFile) return null;
184
+ const cached = languageCache.get(language);
185
+ if (cached) return cached;
186
+ const wasmPath = resolveWasmPath(grammarFile);
187
+ const lang = await Parser.Language.load(wasmPath);
188
+ languageCache.set(language, lang);
189
+ return lang;
190
+ }
191
+ function extractDocstring(node, language) {
192
+ if (language === "python") {
193
+ const body = node.childForFieldName("body");
194
+ if (body) {
195
+ const firstStmt = body.namedChildren[0];
196
+ if (firstStmt?.type === "expression_statement") {
197
+ const strNode = firstStmt.namedChildren[0];
198
+ if (strNode?.type === "string") {
199
+ const raw = strNode.text;
200
+ return raw.replace(/^["']{1,3}|["']{1,3}$/g, "").trim();
201
+ }
202
+ }
203
+ }
204
+ return void 0;
205
+ }
206
+ const prev = findPrecedingComment(node);
207
+ if (prev) return cleanJSDocComment(prev.text);
208
+ return void 0;
209
+ }
210
+ function findPrecedingComment(node) {
211
+ let candidate = node.previousNamedSibling;
212
+ if (node.parent?.type === "export_statement") {
213
+ candidate = node.parent.previousNamedSibling;
214
+ }
215
+ if (candidate?.type === "comment") return candidate;
216
+ return null;
217
+ }
218
+ function cleanJSDocComment(text) {
219
+ return text.replace(/^\/\*\*?\s*/, "").replace(/\s*\*\/$/, "").replace(/^\s*\* ?/gm, "").trim();
220
+ }
221
+ function extractParams(node, language) {
222
+ const paramsNode = node.childForFieldName("parameters") ?? node.childForFieldName("formal_parameters");
223
+ if (!paramsNode) return void 0;
224
+ if (language === "python") {
225
+ return paramsNode.namedChildren.filter((c) => c.type !== "comment").map((c) => c.text).filter((t) => t !== "self" && t !== "cls");
226
+ }
227
+ return paramsNode.namedChildren.filter((c) => c.type !== "comment").map((c) => c.text);
228
+ }
229
+ function extractReturnType(node, language) {
230
+ if (language === "python") {
231
+ const retType2 = node.childForFieldName("return_type");
232
+ return retType2?.text;
233
+ }
234
+ const retType = node.childForFieldName("return_type");
235
+ if (retType) {
236
+ const text = retType.text;
237
+ return text.startsWith(":") ? text.slice(1).trim() : text;
238
+ }
239
+ return void 0;
240
+ }
241
+ function isExported(node) {
242
+ return node.parent?.type === "export_statement";
243
+ }
244
+ function extractTopLevelNode(node) {
245
+ if (node.parent?.type === "export_statement") return node.parent;
246
+ return node;
247
+ }
248
+ function extractTypeScript(rootNode, source, language) {
249
+ const nodes = [];
250
+ function walk(node, parentClassName) {
251
+ for (const child of node.namedChildren) {
252
+ const inner = child.type === "export_statement" ? child.namedChildren.find(
253
+ (c) => c.type === "function_declaration" || c.type === "class_declaration" || c.type === "lexical_declaration" || c.type === "interface_declaration" || c.type === "type_alias_declaration" || c.type === "abstract_class_declaration"
254
+ ) ?? child : child;
255
+ switch (inner.type) {
256
+ case "import_statement": {
257
+ nodes.push({
258
+ type: "import",
259
+ name: null,
260
+ lineStart: inner.startPosition.row + 1,
261
+ lineEnd: inner.endPosition.row + 1,
262
+ language,
263
+ parent: null,
264
+ text: inner.text
265
+ });
266
+ break;
267
+ }
268
+ case "function_declaration": {
269
+ const topNode = extractTopLevelNode(inner);
270
+ const name = inner.childForFieldName("name")?.text ?? null;
271
+ nodes.push({
272
+ type: parentClassName ? "method" : "function",
273
+ name,
274
+ lineStart: topNode.startPosition.row + 1,
275
+ lineEnd: topNode.endPosition.row + 1,
276
+ language,
277
+ parent: parentClassName,
278
+ params: extractParams(inner, language),
279
+ returnType: extractReturnType(inner, language),
280
+ docstring: extractDocstring(inner, language),
281
+ exports: isExported(inner),
282
+ text: topNode.text
283
+ });
284
+ break;
285
+ }
286
+ case "class_declaration":
287
+ case "abstract_class_declaration": {
288
+ const topNode = extractTopLevelNode(inner);
289
+ const className = inner.childForFieldName("name")?.text ?? null;
290
+ nodes.push({
291
+ type: "class",
292
+ name: className,
293
+ lineStart: topNode.startPosition.row + 1,
294
+ lineEnd: topNode.endPosition.row + 1,
295
+ language,
296
+ parent: null,
297
+ docstring: extractDocstring(inner, language),
298
+ exports: isExported(inner),
299
+ text: topNode.text
300
+ });
301
+ const classBody = inner.childForFieldName("body");
302
+ if (classBody) {
303
+ for (const member of classBody.namedChildren) {
304
+ if (member.type === "method_definition") {
305
+ const methodName = member.childForFieldName("name")?.text ?? null;
306
+ nodes.push({
307
+ type: "method",
308
+ name: methodName,
309
+ lineStart: member.startPosition.row + 1,
310
+ lineEnd: member.endPosition.row + 1,
311
+ language,
312
+ parent: className,
313
+ params: extractParams(member, language),
314
+ returnType: extractReturnType(member, language),
315
+ docstring: extractDocstring(member, language),
316
+ exports: isExported(inner),
317
+ text: member.text
318
+ });
319
+ }
320
+ }
321
+ }
322
+ break;
323
+ }
324
+ case "interface_declaration": {
325
+ const topNode = extractTopLevelNode(inner);
326
+ const name = inner.childForFieldName("name")?.text ?? null;
327
+ nodes.push({
328
+ type: "type",
329
+ name,
330
+ lineStart: topNode.startPosition.row + 1,
331
+ lineEnd: topNode.endPosition.row + 1,
332
+ language,
333
+ parent: null,
334
+ docstring: extractDocstring(inner, language),
335
+ exports: isExported(inner),
336
+ text: topNode.text
337
+ });
338
+ break;
339
+ }
340
+ case "type_alias_declaration": {
341
+ const topNode = extractTopLevelNode(inner);
342
+ const name = inner.childForFieldName("name")?.text ?? null;
343
+ nodes.push({
344
+ type: "type",
345
+ name,
346
+ lineStart: topNode.startPosition.row + 1,
347
+ lineEnd: topNode.endPosition.row + 1,
348
+ language,
349
+ parent: null,
350
+ docstring: extractDocstring(inner, language),
351
+ exports: isExported(inner),
352
+ text: topNode.text
353
+ });
354
+ break;
355
+ }
356
+ case "lexical_declaration": {
357
+ const topNode = extractTopLevelNode(inner);
358
+ const declarator = inner.namedChildren.find(
359
+ (c) => c.type === "variable_declarator"
360
+ );
361
+ const name = declarator?.childForFieldName("name")?.text ?? null;
362
+ nodes.push({
363
+ type: "constant",
364
+ name,
365
+ lineStart: topNode.startPosition.row + 1,
366
+ lineEnd: topNode.endPosition.row + 1,
367
+ language,
368
+ parent: parentClassName,
369
+ docstring: extractDocstring(inner, language),
370
+ exports: isExported(inner),
371
+ text: topNode.text
372
+ });
373
+ break;
374
+ }
375
+ default:
376
+ if (child.type !== "export_statement") {
377
+ }
378
+ break;
379
+ }
380
+ }
381
+ }
382
+ walk(rootNode, null);
383
+ return nodes;
384
+ }
385
+ function extractPython(rootNode, _source, language) {
386
+ const nodes = [];
387
+ function walk(node, parentClassName) {
388
+ for (const child of node.namedChildren) {
389
+ switch (child.type) {
390
+ case "import_statement":
391
+ case "import_from_statement": {
392
+ nodes.push({
393
+ type: "import",
394
+ name: null,
395
+ lineStart: child.startPosition.row + 1,
396
+ lineEnd: child.endPosition.row + 1,
397
+ language,
398
+ parent: null,
399
+ text: child.text
400
+ });
401
+ break;
402
+ }
403
+ case "function_definition": {
404
+ const name = child.childForFieldName("name")?.text ?? null;
405
+ nodes.push({
406
+ type: parentClassName ? "method" : "function",
407
+ name,
408
+ lineStart: child.startPosition.row + 1,
409
+ lineEnd: child.endPosition.row + 1,
410
+ language,
411
+ parent: parentClassName,
412
+ params: extractParams(child, language),
413
+ returnType: extractReturnType(child, language),
414
+ docstring: extractDocstring(child, language),
415
+ text: child.text
416
+ });
417
+ break;
418
+ }
419
+ case "decorated_definition": {
420
+ const innerDef = child.namedChildren.find(
421
+ (c) => c.type === "function_definition" || c.type === "class_definition"
422
+ );
423
+ if (innerDef) {
424
+ const name = innerDef.childForFieldName("name")?.text ?? null;
425
+ if (innerDef.type === "function_definition") {
426
+ nodes.push({
427
+ type: parentClassName ? "method" : "function",
428
+ name,
429
+ lineStart: child.startPosition.row + 1,
430
+ lineEnd: child.endPosition.row + 1,
431
+ language,
432
+ parent: parentClassName,
433
+ params: extractParams(innerDef, language),
434
+ returnType: extractReturnType(innerDef, language),
435
+ docstring: extractDocstring(innerDef, language),
436
+ text: child.text
437
+ });
438
+ } else if (innerDef.type === "class_definition") {
439
+ nodes.push({
440
+ type: "class",
441
+ name,
442
+ lineStart: child.startPosition.row + 1,
443
+ lineEnd: child.endPosition.row + 1,
444
+ language,
445
+ parent: null,
446
+ docstring: extractDocstring(innerDef, language),
447
+ text: child.text
448
+ });
449
+ const body = innerDef.childForFieldName("body");
450
+ if (body) walk(body, name);
451
+ }
452
+ }
453
+ break;
454
+ }
455
+ case "class_definition": {
456
+ const name = child.childForFieldName("name")?.text ?? null;
457
+ nodes.push({
458
+ type: "class",
459
+ name,
460
+ lineStart: child.startPosition.row + 1,
461
+ lineEnd: child.endPosition.row + 1,
462
+ language,
463
+ parent: null,
464
+ docstring: extractDocstring(child, language),
465
+ text: child.text
466
+ });
467
+ const body = child.childForFieldName("body");
468
+ if (body) walk(body, name);
469
+ break;
470
+ }
471
+ case "expression_statement": {
472
+ const assignment = child.namedChildren.find(
473
+ (c) => c.type === "assignment"
474
+ );
475
+ if (assignment && parentClassName === null) {
476
+ const left = assignment.childForFieldName("left");
477
+ if (left?.type === "identifier") {
478
+ nodes.push({
479
+ type: "constant",
480
+ name: left.text,
481
+ lineStart: child.startPosition.row + 1,
482
+ lineEnd: child.endPosition.row + 1,
483
+ language,
484
+ parent: null,
485
+ text: child.text
486
+ });
487
+ }
488
+ }
489
+ break;
490
+ }
491
+ default:
492
+ break;
493
+ }
494
+ }
495
+ }
496
+ walk(rootNode, null);
497
+ return nodes;
498
+ }
499
+ async function parseFile(filePath, language) {
500
+ await initParser();
501
+ const lang = await getLanguage2(language);
502
+ if (!lang) return [];
503
+ let source;
504
+ try {
505
+ source = await fs2.readFile(filePath, "utf-8");
506
+ } catch {
507
+ return [];
508
+ }
509
+ const parser = new Parser();
510
+ parser.setLanguage(lang);
511
+ const tree = parser.parse(source);
512
+ if (!tree) return [];
513
+ try {
514
+ if (language === "python") {
515
+ return extractPython(tree.rootNode, source, language);
516
+ }
517
+ return extractTypeScript(tree.rootNode, source, language);
518
+ } finally {
519
+ tree.delete();
520
+ parser.delete();
521
+ }
522
+ }
523
+
524
+ // src/indexer/chunker.ts
525
+ import { createHash } from "crypto";
526
+ var DEFAULT_MAX_TOKENS = 500;
527
+ var MERGE_THRESHOLD = 50;
528
+ var TOKEN_MULTIPLIER = 1.3;
529
+ function estimateTokens(text) {
530
+ const wordCount = text.split(/\s+/).filter((w) => w.length > 0).length;
531
+ return Math.ceil(wordCount * TOKEN_MULTIPLIER);
532
+ }
533
+ function makeChunkId(filePath, lineStart, lineEnd) {
534
+ const input = `${filePath}:${lineStart}:${lineEnd}`;
535
+ return createHash("sha256").update(input).digest("hex").slice(0, 16);
536
+ }
537
+ function makeContentHash(text) {
538
+ return createHash("sha256").update(text).digest("hex").slice(0, 16);
539
+ }
540
+ function splitLargeNode(node, maxTokens) {
541
+ const lines = node.text.split("\n");
542
+ const chunks = [];
543
+ let currentLines = [];
544
+ let currentStartOffset = 0;
545
+ for (let i = 0; i < lines.length; i++) {
546
+ currentLines.push(lines[i]);
547
+ const currentText = currentLines.join("\n");
548
+ const tokens = estimateTokens(currentText);
549
+ if (tokens >= maxTokens && currentLines.length > 1) {
550
+ currentLines.pop();
551
+ chunks.push({
552
+ lineStart: node.lineStart + currentStartOffset,
553
+ lineEnd: node.lineStart + currentStartOffset + currentLines.length - 1,
554
+ text: currentLines.join("\n")
555
+ });
556
+ currentStartOffset = i;
557
+ currentLines = [lines[i]];
558
+ }
559
+ }
560
+ if (currentLines.length > 0) {
561
+ chunks.push({
562
+ lineStart: node.lineStart + currentStartOffset,
563
+ lineEnd: node.lineStart + currentStartOffset + currentLines.length - 1,
564
+ text: currentLines.join("\n")
565
+ });
566
+ }
567
+ return chunks;
568
+ }
569
+ function groupImports(imports) {
570
+ if (imports.length === 0) return null;
571
+ const sorted = [...imports].sort((a, b) => a.lineStart - b.lineStart);
572
+ return {
573
+ type: "import",
574
+ name: null,
575
+ lineStart: sorted[0].lineStart,
576
+ lineEnd: sorted[sorted.length - 1].lineEnd,
577
+ language: sorted[0].language,
578
+ parent: null,
579
+ text: sorted.map((n) => n.text).join("\n")
580
+ };
581
+ }
582
+ var UNMERGEABLE_TYPES = /* @__PURE__ */ new Set([
583
+ "function",
584
+ "method",
585
+ "class",
586
+ "type",
587
+ "import"
588
+ ]);
589
+ function canMerge(a, b) {
590
+ if (UNMERGEABLE_TYPES.has(a.type) || UNMERGEABLE_TYPES.has(b.type)) return false;
591
+ if (a.type !== b.type) return false;
592
+ return true;
593
+ }
594
+ function mergeSmallChunks(chunks, maxTokens) {
595
+ if (chunks.length <= 1) return chunks;
596
+ const merged = [];
597
+ let accumulator = null;
598
+ for (const chunk of chunks) {
599
+ const chunkTokens = estimateTokens(chunk.text);
600
+ if (accumulator === null) {
601
+ if (chunkTokens < MERGE_THRESHOLD && !UNMERGEABLE_TYPES.has(chunk.type)) {
602
+ accumulator = { ...chunk };
603
+ } else {
604
+ merged.push(chunk);
605
+ }
606
+ continue;
607
+ }
608
+ const accTokens = estimateTokens(accumulator.text);
609
+ const combinedTokens = accTokens + chunkTokens;
610
+ if (chunkTokens < MERGE_THRESHOLD && combinedTokens <= maxTokens && canMerge(accumulator, chunk)) {
611
+ const combinedText = accumulator.text + "\n" + chunk.text;
612
+ accumulator = {
613
+ ...accumulator,
614
+ lineEnd: chunk.lineEnd,
615
+ text: combinedText,
616
+ name: accumulator.name ?? chunk.name,
617
+ id: makeChunkId(accumulator.filePath, accumulator.lineStart, chunk.lineEnd),
618
+ hash: makeContentHash(combinedText)
619
+ };
620
+ } else {
621
+ merged.push(accumulator);
622
+ accumulator = chunkTokens < MERGE_THRESHOLD && !UNMERGEABLE_TYPES.has(chunk.type) ? { ...chunk } : null;
623
+ if (accumulator === null) {
624
+ merged.push(chunk);
625
+ }
626
+ }
627
+ }
628
+ if (accumulator) {
629
+ merged.push(accumulator);
630
+ }
631
+ return merged;
632
+ }
633
+ function collectImportTexts(nodes) {
634
+ return nodes.filter((n) => n.type === "import").map((n) => n.text);
635
+ }
636
+ function chunkFile(nodes, filePath, options) {
637
+ if (nodes.length === 0) return [];
638
+ const maxTokens = options?.maxTokens ?? DEFAULT_MAX_TOKENS;
639
+ const language = nodes[0].language;
640
+ const importTexts = collectImportTexts(nodes);
641
+ const sorted = [...nodes].sort((a, b) => a.lineStart - b.lineStart);
642
+ const importNodes = sorted.filter((n) => n.type === "import");
643
+ const nonImportNodes = sorted.filter((n) => n.type !== "import");
644
+ const classesWithMethods = /* @__PURE__ */ new Set();
645
+ for (const node of nonImportNodes) {
646
+ if (node.type === "method" && node.parent) {
647
+ classesWithMethods.add(node.parent);
648
+ }
649
+ }
650
+ const rawChunks = [];
651
+ const groupedImport = groupImports(importNodes);
652
+ if (groupedImport) {
653
+ rawChunks.push({
654
+ id: makeChunkId(filePath, groupedImport.lineStart, groupedImport.lineEnd),
655
+ filePath,
656
+ lineStart: groupedImport.lineStart,
657
+ lineEnd: groupedImport.lineEnd,
658
+ language,
659
+ type: "import",
660
+ name: null,
661
+ parent: null,
662
+ text: groupedImport.text,
663
+ imports: [],
664
+ exports: false,
665
+ hash: makeContentHash(groupedImport.text)
666
+ });
667
+ }
668
+ for (const node of nonImportNodes) {
669
+ if (node.type === "class" && node.name && classesWithMethods.has(node.name)) {
670
+ continue;
671
+ }
672
+ const tokenCount = estimateTokens(node.text);
673
+ const nodeExports = node.exports ?? false;
674
+ if (tokenCount <= maxTokens) {
675
+ rawChunks.push({
676
+ id: makeChunkId(filePath, node.lineStart, node.lineEnd),
677
+ filePath,
678
+ lineStart: node.lineStart,
679
+ lineEnd: node.lineEnd,
680
+ language,
681
+ type: node.type === "export" ? "constant" : node.type,
682
+ name: node.name,
683
+ parent: node.parent,
684
+ text: node.text,
685
+ imports: node.type !== "import" ? importTexts : [],
686
+ exports: nodeExports,
687
+ hash: makeContentHash(node.text)
688
+ });
689
+ } else {
690
+ const subChunks = splitLargeNode(node, maxTokens);
691
+ for (const sub of subChunks) {
692
+ rawChunks.push({
693
+ id: makeChunkId(filePath, sub.lineStart, sub.lineEnd),
694
+ filePath,
695
+ lineStart: sub.lineStart,
696
+ lineEnd: sub.lineEnd,
697
+ language,
698
+ type: node.type === "export" ? "constant" : node.type,
699
+ name: node.name,
700
+ parent: node.parent,
701
+ text: sub.text,
702
+ imports: importTexts,
703
+ exports: nodeExports,
704
+ hash: makeContentHash(sub.text)
705
+ });
706
+ }
707
+ }
708
+ }
709
+ rawChunks.sort((a, b) => a.lineStart - b.lineStart);
710
+ return mergeSmallChunks(rawChunks, maxTokens);
711
+ }
712
+
713
+ // src/indexer/embedder.ts
714
+ function normalizeVector(vec) {
715
+ let sumSq = 0;
716
+ for (const v of vec) sumSq += v * v;
717
+ const norm = Math.sqrt(sumSq);
718
+ if (norm === 0) return vec;
719
+ return vec.map((v) => v / norm);
720
+ }
721
+ function prepareChunkText(filePath, parent, text) {
722
+ const parts = [filePath];
723
+ if (parent) parts.push(parent);
724
+ parts.push(text);
725
+ return parts.join("\n");
726
+ }
727
+ var MAX_RETRIES = 3;
728
+ var BASE_DELAY_MS = 500;
729
+ async function fetchWithRetry(url, init) {
730
+ let lastError = null;
731
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
732
+ const response = await fetch(url, init);
733
+ if (response.ok) return response;
734
+ if (response.status === 429 && attempt < MAX_RETRIES) {
735
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt);
736
+ await new Promise((resolve) => setTimeout(resolve, delay));
737
+ lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
738
+ continue;
739
+ }
740
+ throw new Error(
741
+ `Embedding API error: HTTP ${response.status} ${response.statusText}`
742
+ );
743
+ }
744
+ throw lastError ?? new Error("Embedding API request failed after retries");
745
+ }
746
+ var LOCAL_MODEL_ID = "Xenova/all-MiniLM-L6-v2";
747
+ var LOCAL_DIMENSIONS = 384;
748
+ var LOCAL_BATCH_SIZE = 32;
749
+ var pipelineInstance = null;
750
+ async function getLocalPipeline() {
751
+ if (pipelineInstance) return pipelineInstance;
752
+ const { pipeline, env } = await import("@huggingface/transformers");
753
+ env.cacheDir = getCacheDir();
754
+ pipelineInstance = await pipeline("feature-extraction", LOCAL_MODEL_ID, {
755
+ dtype: "fp32"
756
+ });
757
+ return pipelineInstance;
758
+ }
759
+ function getCacheDir() {
760
+ const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? "/tmp";
761
+ return `${home}/.cache/kontext/models`;
762
+ }
763
+ async function createLocalEmbedder() {
764
+ const pipe = await getLocalPipeline();
765
+ return {
766
+ name: "all-MiniLM-L6-v2",
767
+ dimensions: LOCAL_DIMENSIONS,
768
+ async embed(texts, onProgress) {
769
+ const results = [];
770
+ for (let i = 0; i < texts.length; i += LOCAL_BATCH_SIZE) {
771
+ const batch = texts.slice(i, i + LOCAL_BATCH_SIZE);
772
+ const output = await pipe(batch, {
773
+ pooling: "mean",
774
+ normalize: true
775
+ });
776
+ for (let j = 0; j < batch.length; j++) {
777
+ const offset = j * LOCAL_DIMENSIONS;
778
+ const vec = new Float32Array(
779
+ output.data.buffer,
780
+ output.data.byteOffset + offset * 4,
781
+ LOCAL_DIMENSIONS
782
+ );
783
+ results.push(normalizeVector(vec));
784
+ }
785
+ onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
786
+ }
787
+ return results;
788
+ },
789
+ async embedSingle(text) {
790
+ const output = await pipe(text, {
791
+ pooling: "mean",
792
+ normalize: true
793
+ });
794
+ const vec = new Float32Array(
795
+ output.data.buffer,
796
+ output.data.byteOffset,
797
+ LOCAL_DIMENSIONS
798
+ );
799
+ return normalizeVector(vec);
800
+ }
801
+ };
802
+ }
803
+ var VOYAGE_API_URL = "https://api.voyageai.com/v1/embeddings";
804
+ var VOYAGE_MODEL = "voyage-code-3";
805
+ var VOYAGE_DIMENSIONS = 1024;
806
+ var VOYAGE_BATCH_SIZE = 128;
807
+ function createVoyageEmbedder(apiKey) {
808
+ return {
809
+ name: VOYAGE_MODEL,
810
+ dimensions: VOYAGE_DIMENSIONS,
811
+ async embed(texts, onProgress) {
812
+ const results = [];
813
+ for (let i = 0; i < texts.length; i += VOYAGE_BATCH_SIZE) {
814
+ const batch = texts.slice(i, i + VOYAGE_BATCH_SIZE);
815
+ const vectors = await callVoyageAPI(apiKey, batch);
816
+ results.push(...vectors);
817
+ onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
818
+ }
819
+ return results;
820
+ },
821
+ async embedSingle(text) {
822
+ const vectors = await callVoyageAPI(apiKey, [text]);
823
+ return vectors[0];
824
+ }
825
+ };
826
+ }
827
+ async function callVoyageAPI(apiKey, texts) {
828
+ const response = await fetchWithRetry(VOYAGE_API_URL, {
829
+ method: "POST",
830
+ headers: {
831
+ "Content-Type": "application/json",
832
+ Authorization: `Bearer ${apiKey}`
833
+ },
834
+ body: JSON.stringify({
835
+ model: VOYAGE_MODEL,
836
+ input: texts,
837
+ input_type: "document"
838
+ })
839
+ });
840
+ const json = await response.json();
841
+ return json.data.map((d) => normalizeVector(new Float32Array(d.embedding)));
842
+ }
843
+ var OPENAI_API_URL = "https://api.openai.com/v1/embeddings";
844
+ var OPENAI_MODEL = "text-embedding-3-large";
845
+ var OPENAI_DIMENSIONS = 1024;
846
+ var OPENAI_BATCH_SIZE = 128;
847
+ function createOpenAIEmbedder(apiKey) {
848
+ return {
849
+ name: OPENAI_MODEL,
850
+ dimensions: OPENAI_DIMENSIONS,
851
+ async embed(texts, onProgress) {
852
+ const results = [];
853
+ for (let i = 0; i < texts.length; i += OPENAI_BATCH_SIZE) {
854
+ const batch = texts.slice(i, i + OPENAI_BATCH_SIZE);
855
+ const vectors = await callOpenAIAPI(apiKey, batch);
856
+ results.push(...vectors);
857
+ onProgress?.(Math.min(i + batch.length, texts.length), texts.length);
858
+ }
859
+ return results;
860
+ },
861
+ async embedSingle(text) {
862
+ const vectors = await callOpenAIAPI(apiKey, [text]);
863
+ return vectors[0];
864
+ }
865
+ };
866
+ }
867
+ async function callOpenAIAPI(apiKey, texts) {
868
+ const response = await fetchWithRetry(OPENAI_API_URL, {
869
+ method: "POST",
870
+ headers: {
871
+ "Content-Type": "application/json",
872
+ Authorization: `Bearer ${apiKey}`
873
+ },
874
+ body: JSON.stringify({
875
+ model: OPENAI_MODEL,
876
+ input: texts,
877
+ dimensions: OPENAI_DIMENSIONS
878
+ })
879
+ });
880
+ const json = await response.json();
881
+ return json.data.map((d) => normalizeVector(new Float32Array(d.embedding)));
882
+ }
883
+
884
+ // src/indexer/incremental.ts
885
+ import { createHash as createHash2 } from "crypto";
886
+ import fs3 from "fs/promises";
887
+ async function hashFileContent(absolutePath) {
888
+ const content = await fs3.readFile(absolutePath);
889
+ return createHash2("sha256").update(content).digest("hex");
890
+ }
891
+ async function computeChanges(discovered, db) {
892
+ const start = performance.now();
893
+ const added = [];
894
+ const modified = [];
895
+ const unchanged = [];
896
+ const hashes = /* @__PURE__ */ new Map();
897
+ const discoveredPaths = new Set(discovered.map((f) => f.path));
898
+ await Promise.all(
899
+ discovered.map(async (file) => {
900
+ const contentHash = await hashFileContent(file.absolutePath);
901
+ const existing = db.getFile(file.path);
902
+ if (!existing) {
903
+ added.push(file.path);
904
+ hashes.set(file.path, contentHash);
905
+ } else if (existing.hash !== contentHash) {
906
+ modified.push(file.path);
907
+ hashes.set(file.path, contentHash);
908
+ } else {
909
+ unchanged.push(file.path);
910
+ }
911
+ })
912
+ );
913
+ const dbPaths = db.getAllFilePaths();
914
+ const deleted = dbPaths.filter((p) => !discoveredPaths.has(p));
915
+ added.sort();
916
+ modified.sort();
917
+ deleted.sort();
918
+ unchanged.sort();
919
+ return {
920
+ added,
921
+ modified,
922
+ deleted,
923
+ unchanged,
924
+ hashes,
925
+ duration: performance.now() - start
926
+ };
927
+ }
928
+
929
+ // src/storage/db.ts
930
+ import path3 from "path";
931
+ import fs4 from "fs";
932
+ import BetterSqlite3 from "better-sqlite3";
933
+ import * as sqliteVec from "sqlite-vec";
934
+
935
+ // src/storage/schema.ts
936
+ var SCHEMA_VERSION = 1;
937
+ var SCHEMA_SQL = `
938
+ CREATE TABLE IF NOT EXISTS meta (
939
+ key TEXT PRIMARY KEY,
940
+ value TEXT
941
+ );
942
+
943
+ CREATE TABLE IF NOT EXISTS files (
944
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
945
+ path TEXT UNIQUE NOT NULL,
946
+ language TEXT NOT NULL,
947
+ hash TEXT NOT NULL,
948
+ last_indexed INTEGER NOT NULL,
949
+ size INTEGER NOT NULL
950
+ );
951
+
952
+ CREATE TABLE IF NOT EXISTS chunks (
953
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
954
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
955
+ line_start INTEGER NOT NULL,
956
+ line_end INTEGER NOT NULL,
957
+ type TEXT NOT NULL,
958
+ name TEXT,
959
+ parent TEXT,
960
+ text TEXT NOT NULL,
961
+ imports JSON,
962
+ exports INTEGER DEFAULT 0,
963
+ hash TEXT NOT NULL
964
+ );
965
+
966
+ CREATE TABLE IF NOT EXISTS dependencies (
967
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
968
+ source_chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
969
+ target_chunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,
970
+ type TEXT NOT NULL
971
+ );
972
+
973
+ CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id);
974
+ CREATE INDEX IF NOT EXISTS idx_chunks_name ON chunks(name);
975
+ CREATE INDEX IF NOT EXISTS idx_deps_source ON dependencies(source_chunk_id);
976
+ CREATE INDEX IF NOT EXISTS idx_deps_target ON dependencies(target_chunk_id);
977
+ `;
978
+ var FTS_SQL = `
979
+ CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(
980
+ name, text, parent,
981
+ content=chunks,
982
+ content_rowid=id
983
+ );
984
+ `;
985
+ var FTS_TRIGGERS_SQL = `
986
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_ai AFTER INSERT ON chunks BEGIN
987
+ INSERT INTO chunks_fts(rowid, name, text, parent)
988
+ VALUES (new.id, new.name, new.text, new.parent);
989
+ END;
990
+
991
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_ad AFTER DELETE ON chunks BEGIN
992
+ INSERT INTO chunks_fts(chunks_fts, rowid, name, text, parent)
993
+ VALUES ('delete', old.id, old.name, old.text, old.parent);
994
+ END;
995
+
996
+ CREATE TRIGGER IF NOT EXISTS chunks_fts_au AFTER UPDATE ON chunks BEGIN
997
+ INSERT INTO chunks_fts(chunks_fts, rowid, name, text, parent)
998
+ VALUES ('delete', old.id, old.name, old.text, old.parent);
999
+ INSERT INTO chunks_fts(rowid, name, text, parent)
1000
+ VALUES (new.id, new.name, new.text, new.parent);
1001
+ END;
1002
+ `;
1003
+ var VECTOR_TABLE_SQL = (dimensions) => `CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vectors USING vec0(
1004
+ embedding float[${dimensions}]
1005
+ );`;
1006
+
1007
+ // src/storage/vectors.ts
1008
+ function vecToBuffer(vec) {
1009
+ return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
1010
+ }
1011
+ function insertVector(db, chunkId, vector) {
1012
+ db.prepare(
1013
+ `INSERT INTO chunk_vectors(rowid, embedding) VALUES (${chunkId}, ?)`
1014
+ ).run(vecToBuffer(vector));
1015
+ }
1016
+ function deleteVectorsByChunkIds(db, chunkIds) {
1017
+ if (chunkIds.length === 0) return;
1018
+ const placeholders = chunkIds.map(() => "?").join(",");
1019
+ db.prepare(
1020
+ `DELETE FROM chunk_vectors WHERE rowid IN (${placeholders})`
1021
+ ).run(...chunkIds);
1022
+ }
1023
+ function getVectorCount(db) {
1024
+ const row = db.prepare("SELECT COUNT(*) as count FROM chunk_vectors").get();
1025
+ return row.count;
1026
+ }
1027
+ function searchVectors(db, query, limit) {
1028
+ const rows = db.prepare(
1029
+ `SELECT rowid, distance
1030
+ FROM chunk_vectors
1031
+ WHERE embedding MATCH ?
1032
+ AND k = ${limit}
1033
+ ORDER BY distance`
1034
+ ).all(vecToBuffer(query));
1035
+ return rows.map((r) => ({
1036
+ chunkId: r.rowid,
1037
+ distance: r.distance
1038
+ }));
1039
+ }
1040
+
1041
+ // src/storage/db.ts
1042
+ var DEFAULT_DIMENSIONS = 384;
1043
+ function createDatabase(dbPath, dimensions = DEFAULT_DIMENSIONS) {
1044
+ const dir = path3.dirname(dbPath);
1045
+ if (!fs4.existsSync(dir)) {
1046
+ fs4.mkdirSync(dir, { recursive: true });
1047
+ }
1048
+ const db = new BetterSqlite3(dbPath);
1049
+ db.pragma("journal_mode = WAL");
1050
+ db.pragma("foreign_keys = ON");
1051
+ sqliteVec.load(db);
1052
+ initializeSchema(db, dimensions);
1053
+ const stmtUpsertFile = db.prepare(`
1054
+ INSERT INTO files (path, language, hash, last_indexed, size)
1055
+ VALUES (@path, @language, @hash, @lastIndexed, @size)
1056
+ ON CONFLICT(path) DO UPDATE SET
1057
+ language = excluded.language,
1058
+ hash = excluded.hash,
1059
+ last_indexed = excluded.last_indexed,
1060
+ size = excluded.size
1061
+ `);
1062
+ const stmtGetFile = db.prepare(
1063
+ "SELECT id, path, language, hash, last_indexed as lastIndexed, size FROM files WHERE path = ?"
1064
+ );
1065
+ const stmtDeleteFile = db.prepare("DELETE FROM files WHERE path = ?");
1066
+ const stmtInsertChunk = db.prepare(`
1067
+ INSERT INTO chunks (file_id, line_start, line_end, type, name, parent, text, imports, exports, hash)
1068
+ VALUES (@fileId, @lineStart, @lineEnd, @type, @name, @parent, @text, @imports, @exports, @hash)
1069
+ `);
1070
+ const stmtGetChunksByFile = db.prepare(
1071
+ "SELECT id, file_id as fileId, line_start as lineStart, line_end as lineEnd, type, name, parent, text, imports, exports, hash FROM chunks WHERE file_id = ? ORDER BY line_start"
1072
+ );
1073
+ const stmtGetChunkIdsByFile = db.prepare(
1074
+ "SELECT id FROM chunks WHERE file_id = ?"
1075
+ );
1076
+ const stmtDeleteChunksByFile = db.prepare(
1077
+ "DELETE FROM chunks WHERE file_id = ?"
1078
+ );
1079
+ const stmtSearchFTS = db.prepare(
1080
+ "SELECT rowid as chunkId, name, rank FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY rank LIMIT ?"
1081
+ );
1082
+ const stmtGetAllFiles = db.prepare(
1083
+ "SELECT id, path, language, hash, last_indexed as lastIndexed, size FROM files"
1084
+ );
1085
+ const stmtInsertDep = db.prepare(
1086
+ "INSERT INTO dependencies (source_chunk_id, target_chunk_id, type) VALUES (?, ?, ?)"
1087
+ );
1088
+ const stmtGetDeps = db.prepare(
1089
+ "SELECT target_chunk_id as targetChunkId, type FROM dependencies WHERE source_chunk_id = ?"
1090
+ );
1091
+ const stmtGetReverseDeps = db.prepare(
1092
+ "SELECT source_chunk_id as sourceChunkId, type FROM dependencies WHERE target_chunk_id = ?"
1093
+ );
1094
+ const stmtFileCount = db.prepare("SELECT COUNT(*) as count FROM files");
1095
+ const stmtChunkCount = db.prepare("SELECT COUNT(*) as count FROM chunks");
1096
+ const stmtLanguageBreakdown = db.prepare(
1097
+ "SELECT language, COUNT(*) as count FROM files GROUP BY language ORDER BY count DESC"
1098
+ );
1099
+ const stmtLastIndexed = db.prepare(
1100
+ "SELECT MAX(last_indexed) as lastIndexed FROM files"
1101
+ );
1102
+ return {
1103
+ upsertFile(file) {
1104
+ const result = stmtUpsertFile.run({
1105
+ path: file.path,
1106
+ language: file.language,
1107
+ hash: file.hash,
1108
+ lastIndexed: Date.now(),
1109
+ size: file.size
1110
+ });
1111
+ if (result.changes > 0 && result.lastInsertRowid) {
1112
+ return Number(result.lastInsertRowid);
1113
+ }
1114
+ const existing = stmtGetFile.get(file.path);
1115
+ return existing?.id ?? 0;
1116
+ },
1117
+ getFile(filePath) {
1118
+ const row = stmtGetFile.get(filePath);
1119
+ return row ?? null;
1120
+ },
1121
+ getFilesByHash(hashes) {
1122
+ const result = /* @__PURE__ */ new Map();
1123
+ const allFiles = stmtGetAllFiles.all();
1124
+ for (const file of allFiles) {
1125
+ const expectedHash = hashes.get(file.path);
1126
+ if (expectedHash !== void 0 && expectedHash === file.hash) {
1127
+ result.set(file.path, file);
1128
+ }
1129
+ }
1130
+ return result;
1131
+ },
1132
+ getAllFilePaths() {
1133
+ const rows = stmtGetAllFiles.all();
1134
+ return rows.map((r) => r.path);
1135
+ },
1136
+ getFileCount() {
1137
+ return stmtFileCount.get().count;
1138
+ },
1139
+ getChunkCount() {
1140
+ return stmtChunkCount.get().count;
1141
+ },
1142
+ getVectorCount() {
1143
+ return getVectorCount(db);
1144
+ },
1145
+ getLanguageBreakdown() {
1146
+ const rows = stmtLanguageBreakdown.all();
1147
+ const map = /* @__PURE__ */ new Map();
1148
+ for (const row of rows) {
1149
+ map.set(row.language, row.count);
1150
+ }
1151
+ return map;
1152
+ },
1153
+ getLastIndexed() {
1154
+ const row = stmtLastIndexed.get();
1155
+ return row.lastIndexed;
1156
+ },
1157
+ deleteFile(filePath) {
1158
+ const file = stmtGetFile.get(filePath);
1159
+ if (file) {
1160
+ const chunkRows = stmtGetChunkIdsByFile.all(file.id);
1161
+ const chunkIds = chunkRows.map((r) => r.id);
1162
+ if (chunkIds.length > 0) {
1163
+ deleteVectorsByChunkIds(db, chunkIds);
1164
+ }
1165
+ }
1166
+ stmtDeleteFile.run(filePath);
1167
+ },
1168
+ insertChunks(fileId, chunks) {
1169
+ const ids = [];
1170
+ for (const chunk of chunks) {
1171
+ const result = stmtInsertChunk.run({
1172
+ fileId,
1173
+ lineStart: chunk.lineStart,
1174
+ lineEnd: chunk.lineEnd,
1175
+ type: chunk.type,
1176
+ name: chunk.name,
1177
+ parent: chunk.parent,
1178
+ text: chunk.text,
1179
+ imports: JSON.stringify(chunk.imports),
1180
+ exports: chunk.exports ? 1 : 0,
1181
+ hash: chunk.hash
1182
+ });
1183
+ ids.push(Number(result.lastInsertRowid));
1184
+ }
1185
+ return ids;
1186
+ },
1187
+ getChunksByFile(fileId) {
1188
+ const rows = stmtGetChunksByFile.all(fileId);
1189
+ return rows.map((r) => ({
1190
+ ...r,
1191
+ imports: JSON.parse(r.imports),
1192
+ exports: r.exports === 1
1193
+ }));
1194
+ },
1195
+ getChunksByIds(ids) {
1196
+ if (ids.length === 0) return [];
1197
+ const placeholders = ids.map(() => "?").join(",");
1198
+ const rows = db.prepare(
1199
+ `SELECT c.id, c.file_id as fileId, f.path as filePath, f.language,
1200
+ c.line_start as lineStart, c.line_end as lineEnd,
1201
+ c.type, c.name, c.parent, c.text
1202
+ FROM chunks c
1203
+ JOIN files f ON f.id = c.file_id
1204
+ WHERE c.id IN (${placeholders})`
1205
+ ).all(...ids);
1206
+ return rows;
1207
+ },
1208
+ searchChunks(filters, limit) {
1209
+ const conditions = [];
1210
+ const params = [];
1211
+ if (filters.name) {
1212
+ switch (filters.nameMode ?? "contains") {
1213
+ case "exact":
1214
+ conditions.push("c.name = ?");
1215
+ params.push(filters.name);
1216
+ break;
1217
+ case "prefix":
1218
+ conditions.push("c.name LIKE ? || '%'");
1219
+ params.push(filters.name);
1220
+ break;
1221
+ case "contains":
1222
+ conditions.push("c.name LIKE '%' || ? || '%'");
1223
+ params.push(filters.name);
1224
+ break;
1225
+ }
1226
+ }
1227
+ if (filters.type) {
1228
+ conditions.push("c.type = ?");
1229
+ params.push(filters.type);
1230
+ }
1231
+ if (filters.parent) {
1232
+ conditions.push("c.parent = ?");
1233
+ params.push(filters.parent);
1234
+ }
1235
+ if (filters.language) {
1236
+ conditions.push("f.language = ?");
1237
+ params.push(filters.language);
1238
+ }
1239
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1240
+ const sql = `
1241
+ SELECT c.id, c.file_id as fileId, f.path as filePath, f.language,
1242
+ c.line_start as lineStart, c.line_end as lineEnd,
1243
+ c.type, c.name, c.parent, c.text
1244
+ FROM chunks c
1245
+ JOIN files f ON f.id = c.file_id
1246
+ ${where}
1247
+ ORDER BY c.name, c.line_start
1248
+ LIMIT ?
1249
+ `;
1250
+ params.push(limit);
1251
+ return db.prepare(sql).all(...params);
1252
+ },
1253
+ deleteChunksByFile(fileId) {
1254
+ const chunkRows = stmtGetChunkIdsByFile.all(fileId);
1255
+ const chunkIds = chunkRows.map((r) => r.id);
1256
+ if (chunkIds.length > 0) {
1257
+ deleteVectorsByChunkIds(db, chunkIds);
1258
+ }
1259
+ stmtDeleteChunksByFile.run(fileId);
1260
+ },
1261
+ insertDependency(sourceChunkId, targetChunkId, type) {
1262
+ stmtInsertDep.run(sourceChunkId, targetChunkId, type);
1263
+ },
1264
+ getDependencies(chunkId) {
1265
+ return stmtGetDeps.all(chunkId);
1266
+ },
1267
+ getReverseDependencies(chunkId) {
1268
+ return stmtGetReverseDeps.all(chunkId);
1269
+ },
1270
+ insertVector(chunkId, vector) {
1271
+ insertVector(db, chunkId, vector);
1272
+ },
1273
+ searchVectors(query, limit) {
1274
+ return searchVectors(db, query, limit);
1275
+ },
1276
+ searchFTS(query, limit) {
1277
+ const rows = stmtSearchFTS.all(query, limit);
1278
+ return rows;
1279
+ },
1280
+ transaction(fn) {
1281
+ return db.transaction(fn)();
1282
+ },
1283
+ vacuum() {
1284
+ db.exec("VACUUM");
1285
+ },
1286
+ close() {
1287
+ db.close();
1288
+ },
1289
+ getSchemaVersion() {
1290
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
1291
+ return row ? parseInt(row.value, 10) : 0;
1292
+ },
1293
+ pragma(key) {
1294
+ const result = db.pragma(key);
1295
+ if (Array.isArray(result) && result.length > 0) {
1296
+ return Object.values(result[0])[0];
1297
+ }
1298
+ return String(result);
1299
+ }
1300
+ };
1301
+ }
1302
+ function initializeSchema(db, dimensions) {
1303
+ const currentVersion = getMetaVersion(db);
1304
+ if (currentVersion >= SCHEMA_VERSION) return;
1305
+ db.exec(SCHEMA_SQL);
1306
+ db.exec(VECTOR_TABLE_SQL(dimensions));
1307
+ db.exec(FTS_SQL);
1308
+ db.exec(FTS_TRIGGERS_SQL);
1309
+ db.prepare(
1310
+ "INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)"
1311
+ ).run(String(SCHEMA_VERSION));
1312
+ }
1313
+ function getMetaVersion(db) {
1314
+ try {
1315
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
1316
+ return row ? parseInt(row.value, 10) : 0;
1317
+ } catch {
1318
+ return 0;
1319
+ }
1320
+ }
1321
+
1322
+ // src/search/vector.ts
1323
+ function distanceToScore(distance) {
1324
+ return 1 / (1 + distance);
1325
+ }
1326
+ async function vectorSearch(db, embedder, query, limit, filters) {
1327
+ const queryVec = await embedder.embedSingle(query);
1328
+ const fetchLimit = filters?.language ? limit * 3 : limit;
1329
+ const vectorResults = db.searchVectors(queryVec, fetchLimit);
1330
+ if (vectorResults.length === 0) return [];
1331
+ const chunkIds = vectorResults.map((r) => r.chunkId);
1332
+ const chunks = db.getChunksByIds(chunkIds);
1333
+ const chunkMap = /* @__PURE__ */ new Map();
1334
+ for (const chunk of chunks) {
1335
+ chunkMap.set(chunk.id, chunk);
1336
+ }
1337
+ const results = [];
1338
+ for (const vr of vectorResults) {
1339
+ const chunk = chunkMap.get(vr.chunkId);
1340
+ if (!chunk) continue;
1341
+ if (filters?.language && chunk.language !== filters.language) continue;
1342
+ results.push({
1343
+ chunkId: vr.chunkId,
1344
+ filePath: chunk.filePath,
1345
+ lineStart: chunk.lineStart,
1346
+ lineEnd: chunk.lineEnd,
1347
+ name: chunk.name,
1348
+ type: chunk.type,
1349
+ text: chunk.text,
1350
+ score: distanceToScore(vr.distance),
1351
+ language: chunk.language
1352
+ });
1353
+ }
1354
+ results.sort((a, b) => b.score - a.score);
1355
+ return results.slice(0, limit);
1356
+ }
1357
+
1358
+ // src/search/fts.ts
1359
+ function bm25ToScore(rank) {
1360
+ return 1 / (1 + Math.abs(rank));
1361
+ }
1362
+ function ftsSearch(db, query, limit, filters) {
1363
+ const fetchLimit = filters?.language ? limit * 3 : limit;
1364
+ const ftsResults = db.searchFTS(query, fetchLimit);
1365
+ if (ftsResults.length === 0) return [];
1366
+ const chunkIds = ftsResults.map((r) => r.chunkId);
1367
+ const chunks = db.getChunksByIds(chunkIds);
1368
+ const chunkMap = /* @__PURE__ */ new Map();
1369
+ for (const chunk of chunks) {
1370
+ chunkMap.set(chunk.id, chunk);
1371
+ }
1372
+ const results = [];
1373
+ for (const fts of ftsResults) {
1374
+ const chunk = chunkMap.get(fts.chunkId);
1375
+ if (!chunk) continue;
1376
+ if (filters?.language && chunk.language !== filters.language) continue;
1377
+ results.push({
1378
+ chunkId: fts.chunkId,
1379
+ filePath: chunk.filePath,
1380
+ lineStart: chunk.lineStart,
1381
+ lineEnd: chunk.lineEnd,
1382
+ name: chunk.name,
1383
+ type: chunk.type,
1384
+ text: chunk.text,
1385
+ score: bm25ToScore(fts.rank),
1386
+ language: chunk.language
1387
+ });
1388
+ }
1389
+ results.sort((a, b) => b.score - a.score);
1390
+ return results.slice(0, limit);
1391
+ }
1392
+
1393
+ // src/search/ast.ts
1394
+ var SCORE_EXACT = 1;
1395
+ var SCORE_PREFIX = 0.8;
1396
+ var SCORE_FUZZY = 0.5;
1397
+ function astSearch(db, filters, limit) {
1398
+ const matchMode = filters.matchMode ?? "fuzzy";
1399
+ const nameMode = matchMode === "exact" ? "exact" : matchMode === "prefix" ? "prefix" : "contains";
1400
+ const score = matchMode === "exact" ? SCORE_EXACT : matchMode === "prefix" ? SCORE_PREFIX : SCORE_FUZZY;
1401
+ const chunks = db.searchChunks(
1402
+ {
1403
+ name: filters.name,
1404
+ nameMode,
1405
+ type: filters.type,
1406
+ parent: filters.parent,
1407
+ language: filters.language
1408
+ },
1409
+ limit
1410
+ );
1411
+ return chunks.map((chunk) => ({
1412
+ chunkId: chunk.id,
1413
+ filePath: chunk.filePath,
1414
+ lineStart: chunk.lineStart,
1415
+ lineEnd: chunk.lineEnd,
1416
+ name: chunk.name,
1417
+ type: chunk.type,
1418
+ text: chunk.text,
1419
+ score,
1420
+ language: chunk.language
1421
+ }));
1422
+ }
1423
+
1424
+ // src/search/path.ts
1425
+ function globToRegExp(pattern) {
1426
+ let re = "";
1427
+ let i = 0;
1428
+ while (i < pattern.length) {
1429
+ const ch = pattern[i];
1430
+ if (ch === "*" && pattern[i + 1] === "*") {
1431
+ re += ".*";
1432
+ i += 2;
1433
+ if (pattern[i] === "/") i++;
1434
+ } else if (ch === "*") {
1435
+ re += "[^/]*";
1436
+ i++;
1437
+ } else if (ch === "?") {
1438
+ re += "[^/]";
1439
+ i++;
1440
+ } else if (".+^${}()|[]\\".includes(ch)) {
1441
+ re += "\\" + ch;
1442
+ i++;
1443
+ } else {
1444
+ re += ch;
1445
+ i++;
1446
+ }
1447
+ }
1448
+ return new RegExp(`^${re}$`);
1449
+ }
1450
+ function pathSearch(db, pattern, limit) {
1451
+ const allPaths = db.getAllFilePaths();
1452
+ const regex = globToRegExp(pattern);
1453
+ const matchingPaths = allPaths.filter((p) => regex.test(p));
1454
+ if (matchingPaths.length === 0) return [];
1455
+ const results = [];
1456
+ for (const filePath of matchingPaths) {
1457
+ if (results.length >= limit) break;
1458
+ const file = db.getFile(filePath);
1459
+ if (!file) continue;
1460
+ const chunks = db.getChunksByFile(file.id);
1461
+ for (const chunk of chunks) {
1462
+ if (results.length >= limit) break;
1463
+ results.push({
1464
+ chunkId: chunk.id,
1465
+ filePath: file.path,
1466
+ lineStart: chunk.lineStart,
1467
+ lineEnd: chunk.lineEnd,
1468
+ name: chunk.name,
1469
+ type: chunk.type,
1470
+ text: chunk.text,
1471
+ score: 1,
1472
+ language: file.language
1473
+ });
1474
+ }
1475
+ }
1476
+ return results;
1477
+ }
1478
+ var DEPTH_SCORE_BASE = 1;
1479
+ var DEPTH_SCORE_DECAY = 0.2;
1480
+ function dependencyTrace(db, chunkId, direction, depth) {
1481
+ const visited = /* @__PURE__ */ new Set();
1482
+ visited.add(chunkId);
1483
+ const results = [];
1484
+ let frontier = [chunkId];
1485
+ for (let d = 0; d < depth; d++) {
1486
+ const nextFrontier = [];
1487
+ for (const currentId of frontier) {
1488
+ const neighbors = getNeighbors(db, currentId, direction);
1489
+ for (const neighborId of neighbors) {
1490
+ if (visited.has(neighborId)) continue;
1491
+ visited.add(neighborId);
1492
+ nextFrontier.push(neighborId);
1493
+ }
1494
+ }
1495
+ if (nextFrontier.length === 0) break;
1496
+ const chunks = db.getChunksByIds(nextFrontier);
1497
+ const score = DEPTH_SCORE_BASE - d * DEPTH_SCORE_DECAY;
1498
+ for (const chunk of chunks) {
1499
+ results.push({
1500
+ chunkId: chunk.id,
1501
+ filePath: chunk.filePath,
1502
+ lineStart: chunk.lineStart,
1503
+ lineEnd: chunk.lineEnd,
1504
+ name: chunk.name,
1505
+ type: chunk.type,
1506
+ text: chunk.text,
1507
+ score,
1508
+ language: chunk.language
1509
+ });
1510
+ }
1511
+ frontier = nextFrontier;
1512
+ }
1513
+ return results;
1514
+ }
1515
+ function getNeighbors(db, chunkId, direction) {
1516
+ if (direction === "imports") {
1517
+ return db.getDependencies(chunkId).map((d) => d.targetChunkId);
1518
+ }
1519
+ return db.getReverseDependencies(chunkId).map((d) => d.sourceChunkId);
1520
+ }
1521
+
1522
+ // src/search/fusion.ts
1523
+ var K = 60;
1524
+ function fusionMerge(strategyResults, limit) {
1525
+ const scoreMap = /* @__PURE__ */ new Map();
1526
+ const resultMap = /* @__PURE__ */ new Map();
1527
+ for (const { weight, results: results2 } of strategyResults) {
1528
+ for (let rank = 0; rank < results2.length; rank++) {
1529
+ const result = results2[rank];
1530
+ const rrfScore = weight * (1 / (K + rank + 1));
1531
+ const existing = scoreMap.get(result.chunkId) ?? 0;
1532
+ scoreMap.set(result.chunkId, existing + rrfScore);
1533
+ if (!resultMap.has(result.chunkId)) {
1534
+ resultMap.set(result.chunkId, result);
1535
+ }
1536
+ }
1537
+ }
1538
+ if (scoreMap.size === 0) return [];
1539
+ const entries = [...scoreMap.entries()].sort((a, b) => b[1] - a[1]);
1540
+ const maxScore = entries[0][1];
1541
+ const results = [];
1542
+ for (const [chunkId, rawScore] of entries.slice(0, limit)) {
1543
+ const base = resultMap.get(chunkId);
1544
+ if (!base) continue;
1545
+ results.push({
1546
+ ...base,
1547
+ score: maxScore > 0 ? rawScore / maxScore : 0
1548
+ });
1549
+ }
1550
+ return results;
1551
+ }
1552
+
1553
+ // src/steering/llm.ts
1554
+ var PLAN_SYSTEM_PROMPT = `You are a code search strategy planner. Given a user query about code, output a JSON object with:
1555
+ - "interpretation": a one-line summary of what the user is looking for
1556
+ - "strategies": an array of search strategy objects, each with:
1557
+ - "strategy": one of "vector", "fts", "ast", "path", "dependency"
1558
+ - "query": the optimized query string for that strategy
1559
+ - "weight": a number 0-1 indicating importance
1560
+ - "reason": brief explanation of why this strategy is used
1561
+
1562
+ Choose strategies based on query type:
1563
+ - Conceptual/natural language \u2192 vector (semantic search)
1564
+ - Keywords/identifiers \u2192 fts (full-text search)
1565
+ - Symbol names (functions, classes) \u2192 ast (structural search)
1566
+ - File paths or patterns \u2192 path (path glob search)
1567
+ - Import/dependency chains \u2192 dependency
1568
+
1569
+ Output ONLY valid JSON, no markdown.`;
1570
+ var SYNTHESIZE_SYSTEM_PROMPT = `You are a code search assistant. Given search results, write a brief, helpful explanation of what was found. Be concise (2-4 sentences). Reference specific files and function names. Do not use markdown.`;
1571
+ var VALID_STRATEGIES = /* @__PURE__ */ new Set([
1572
+ "vector",
1573
+ "fts",
1574
+ "ast",
1575
+ "path",
1576
+ "dependency"
1577
+ ]);
1578
+ function buildFallbackPlan(query) {
1579
+ const strategies = [
1580
+ { strategy: "fts", query, weight: 0.8, reason: "Full-text keyword search" },
1581
+ { strategy: "ast", query, weight: 0.9, reason: "Structural symbol search" }
1582
+ ];
1583
+ return {
1584
+ interpretation: `Searching for: ${query}`,
1585
+ strategies
1586
+ };
1587
+ }
1588
+ function parseSearchPlan(raw, query) {
1589
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
1590
+ if (!jsonMatch) return buildFallbackPlan(query);
1591
+ const parsed = JSON.parse(jsonMatch[0]);
1592
+ if (!parsed.interpretation || !Array.isArray(parsed.strategies) || parsed.strategies.length === 0) {
1593
+ return buildFallbackPlan(query);
1594
+ }
1595
+ const validStrategies = parsed.strategies.filter(
1596
+ (s) => VALID_STRATEGIES.has(s.strategy)
1597
+ );
1598
+ if (validStrategies.length === 0) return buildFallbackPlan(query);
1599
+ return {
1600
+ interpretation: parsed.interpretation,
1601
+ strategies: validStrategies
1602
+ };
1603
+ }
1604
+ async function planSearch(provider, query) {
1605
+ try {
1606
+ const response = await provider.chat([
1607
+ { role: "system", content: PLAN_SYSTEM_PROMPT },
1608
+ { role: "user", content: query }
1609
+ ]);
1610
+ return parseSearchPlan(response, query);
1611
+ } catch {
1612
+ return buildFallbackPlan(query);
1613
+ }
1614
+ }
1615
+ function formatResultsForLLM(results) {
1616
+ return results.slice(0, 10).map(
1617
+ (r, i) => `${i + 1}. ${r.filePath}:${r.lineStart}-${r.lineEnd} ${r.name ?? "(unnamed)"} [${r.type}] (score: ${r.score.toFixed(2)})
1618
+ ${r.text.slice(0, 150)}`
1619
+ ).join("\n\n");
1620
+ }
1621
+ async function synthesizeExplanation(provider, query, results) {
1622
+ if (results.length === 0) {
1623
+ return `No results found for "${query}".`;
1624
+ }
1625
+ const formattedResults = formatResultsForLLM(results);
1626
+ const response = await provider.chat([
1627
+ { role: "system", content: SYNTHESIZE_SYSTEM_PROMPT },
1628
+ {
1629
+ role: "user",
1630
+ content: `Query: "${query}"
1631
+
1632
+ Search results:
1633
+ ${formattedResults}`
1634
+ }
1635
+ ]);
1636
+ return response;
1637
+ }
1638
+ function estimateTokens2(text) {
1639
+ return Math.ceil(text.length / 4);
1640
+ }
1641
+ async function steer(provider, query, limit, searchExecutor) {
1642
+ let totalTokens = 0;
1643
+ const plan = await planSearch(provider, query);
1644
+ totalTokens += estimateTokens2(PLAN_SYSTEM_PROMPT + query);
1645
+ totalTokens += estimateTokens2(JSON.stringify(plan));
1646
+ const results = await searchExecutor(plan.strategies, limit);
1647
+ let explanation;
1648
+ try {
1649
+ explanation = await synthesizeExplanation(provider, query, results);
1650
+ totalTokens += estimateTokens2(SYNTHESIZE_SYSTEM_PROMPT + query);
1651
+ totalTokens += estimateTokens2(explanation);
1652
+ } catch {
1653
+ explanation = results.length > 0 ? `Found ${results.length} result(s) for "${query}".` : `No results found for "${query}".`;
1654
+ }
1655
+ const costEstimate = totalTokens / 1e6 * 0.15;
1656
+ return {
1657
+ interpretation: plan.interpretation,
1658
+ strategies: plan.strategies,
1659
+ results,
1660
+ explanation,
1661
+ tokensUsed: totalTokens,
1662
+ costEstimate
1663
+ };
1664
+ }
1665
+
1666
+ // src/cli/commands/init.ts
1667
+ import fs5 from "fs";
1668
+ import path4 from "path";
1669
+
1670
+ // src/utils/errors.ts
1671
+ var ErrorCode = {
1672
+ NOT_INITIALIZED: "NOT_INITIALIZED",
1673
+ INDEX_FAILED: "INDEX_FAILED",
1674
+ PARSE_FAILED: "PARSE_FAILED",
1675
+ CHUNK_FAILED: "CHUNK_FAILED",
1676
+ EMBEDDER_FAILED: "EMBEDDER_FAILED",
1677
+ SEARCH_FAILED: "SEARCH_FAILED",
1678
+ CONFIG_INVALID: "CONFIG_INVALID",
1679
+ DB_CORRUPTED: "DB_CORRUPTED",
1680
+ DB_WRITE_FAILED: "DB_WRITE_FAILED",
1681
+ WATCHER_FAILED: "WATCHER_FAILED",
1682
+ LLM_FAILED: "LLM_FAILED"
1683
+ };
1684
+ var KontextError = class extends Error {
1685
+ code;
1686
+ constructor(message, code, cause) {
1687
+ super(message, { cause });
1688
+ this.name = "KontextError";
1689
+ this.code = code;
1690
+ }
1691
+ };
1692
+ var IndexError = class extends KontextError {
1693
+ constructor(message, code, cause) {
1694
+ super(message, code, cause);
1695
+ this.name = "IndexError";
1696
+ }
1697
+ };
1698
+ var SearchError = class extends KontextError {
1699
+ constructor(message, code, cause) {
1700
+ super(message, code, cause);
1701
+ this.name = "SearchError";
1702
+ }
1703
+ };
1704
+ var ConfigError = class extends KontextError {
1705
+ constructor(message, code, cause) {
1706
+ super(message, code, cause);
1707
+ this.name = "ConfigError";
1708
+ }
1709
+ };
1710
+ var DatabaseError = class extends KontextError {
1711
+ constructor(message, code, cause) {
1712
+ super(message, code, cause);
1713
+ this.name = "DatabaseError";
1714
+ }
1715
+ };
1716
+
1717
+ // src/utils/logger.ts
1718
+ var LogLevel = {
1719
+ DEBUG: 0,
1720
+ INFO: 1,
1721
+ WARN: 2,
1722
+ ERROR: 3,
1723
+ SILENT: 4
1724
+ };
1725
+ function resolveLevel(options) {
1726
+ if (options?.level !== void 0) return options.level;
1727
+ if (process.env["CTX_DEBUG"] === "1") return LogLevel.DEBUG;
1728
+ return LogLevel.INFO;
1729
+ }
1730
+ function formatArgs(args) {
1731
+ return args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
1732
+ }
1733
+ function write(level, msg, args) {
1734
+ const extra = args.length > 0 ? ` ${formatArgs(args)}` : "";
1735
+ process.stderr.write(`[${level}] ${msg}${extra}
1736
+ `);
1737
+ }
1738
+ function createLogger(options) {
1739
+ const minLevel = resolveLevel(options);
1740
+ return {
1741
+ debug(msg, ...args) {
1742
+ if (minLevel <= LogLevel.DEBUG) write("debug", msg, args);
1743
+ },
1744
+ info(msg, ...args) {
1745
+ if (minLevel <= LogLevel.INFO) write("info", msg, args);
1746
+ },
1747
+ warn(msg, ...args) {
1748
+ if (minLevel <= LogLevel.WARN) write("warn", msg, args);
1749
+ },
1750
+ error(msg, ...args) {
1751
+ if (minLevel <= LogLevel.ERROR) write("error", msg, args);
1752
+ }
1753
+ };
1754
+ }
1755
+
1756
+ // src/cli/commands/init.ts
1757
+ var CTX_DIR = ".ctx";
1758
+ var DB_FILENAME = "index.db";
1759
+ var CONFIG_FILENAME = "config.json";
1760
+ var GITIGNORE_ENTRY = ".ctx/";
1761
+ function ensureGitignore(projectRoot) {
1762
+ const gitignorePath = path4.join(projectRoot, ".gitignore");
1763
+ if (fs5.existsSync(gitignorePath)) {
1764
+ const content = fs5.readFileSync(gitignorePath, "utf-8");
1765
+ if (content.includes(GITIGNORE_ENTRY)) return;
1766
+ const suffix = content.endsWith("\n") ? "" : "\n";
1767
+ fs5.writeFileSync(gitignorePath, `${content}${suffix}${GITIGNORE_ENTRY}
1768
+ `);
1769
+ } else {
1770
+ fs5.writeFileSync(gitignorePath, `${GITIGNORE_ENTRY}
1771
+ `);
1772
+ }
1773
+ }
1774
+ function ensureConfig(ctxDir) {
1775
+ const configPath = path4.join(ctxDir, CONFIG_FILENAME);
1776
+ if (fs5.existsSync(configPath)) return;
1777
+ const config = {
1778
+ version: 1,
1779
+ dimensions: 384,
1780
+ model: "all-MiniLM-L6-v2"
1781
+ };
1782
+ fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1783
+ }
1784
+ function formatDuration(ms) {
1785
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
1786
+ return `${(ms / 1e3).toFixed(1)}s`;
1787
+ }
1788
+ function formatBytes(bytes) {
1789
+ if (bytes < 1024) return `${bytes} B`;
1790
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1791
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1792
+ }
1793
+ function formatLanguageSummary(counts) {
1794
+ const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([lang, count]) => `${lang}: ${count}`);
1795
+ return entries.join(", ");
1796
+ }
1797
+ async function runInit(projectPath, options = {}) {
1798
+ const log = options.log ?? console.log;
1799
+ const absoluteRoot = path4.resolve(projectPath);
1800
+ const start = performance.now();
1801
+ log(`Indexing ${absoluteRoot}...`);
1802
+ const ctxDir = path4.join(absoluteRoot, CTX_DIR);
1803
+ if (!fs5.existsSync(ctxDir)) fs5.mkdirSync(ctxDir, { recursive: true });
1804
+ ensureGitignore(absoluteRoot);
1805
+ ensureConfig(ctxDir);
1806
+ const dbPath = path4.join(ctxDir, DB_FILENAME);
1807
+ const db = createDatabase(dbPath);
1808
+ try {
1809
+ const discovered = await discoverFiles({
1810
+ root: absoluteRoot,
1811
+ extraIgnore: [".ctx/"]
1812
+ });
1813
+ const languageCounts = /* @__PURE__ */ new Map();
1814
+ for (const file of discovered) {
1815
+ languageCounts.set(
1816
+ file.language,
1817
+ (languageCounts.get(file.language) ?? 0) + 1
1818
+ );
1819
+ }
1820
+ log(
1821
+ ` Discovered ${discovered.length} files` + (discovered.length > 0 ? ` (${formatLanguageSummary(languageCounts)})` : "")
1822
+ );
1823
+ const changes = await computeChanges(discovered, db);
1824
+ const filesToProcess = [
1825
+ ...changes.added.map((p) => ({ path: p, reason: "added" })),
1826
+ ...changes.modified.map((p) => ({ path: p, reason: "modified" }))
1827
+ ];
1828
+ if (changes.unchanged.length > 0) {
1829
+ log(` ${changes.unchanged.length} unchanged files skipped`);
1830
+ }
1831
+ if (changes.deleted.length > 0) {
1832
+ log(` ${changes.deleted.length} deleted files removed`);
1833
+ }
1834
+ if (changes.added.length > 0) {
1835
+ log(` ${changes.added.length} new files to index`);
1836
+ }
1837
+ if (changes.modified.length > 0) {
1838
+ log(` ${changes.modified.length} modified files to re-index`);
1839
+ }
1840
+ for (const deletedPath of changes.deleted) {
1841
+ db.deleteFile(deletedPath);
1842
+ }
1843
+ await initParser();
1844
+ const allChunksWithMeta = [];
1845
+ let filesProcessed = 0;
1846
+ for (const { path: relPath } of filesToProcess) {
1847
+ const discovered_file = discovered.find((f) => f.path === relPath);
1848
+ if (!discovered_file) continue;
1849
+ const existingFile = db.getFile(relPath);
1850
+ if (existingFile) {
1851
+ db.deleteChunksByFile(existingFile.id);
1852
+ }
1853
+ let nodes;
1854
+ try {
1855
+ nodes = await parseFile(discovered_file.absolutePath, discovered_file.language);
1856
+ } catch {
1857
+ log(` \u26A0 Skipping ${relPath} (parse error)`);
1858
+ continue;
1859
+ }
1860
+ const chunks = chunkFile(nodes, relPath);
1861
+ const fileId = db.upsertFile({
1862
+ path: relPath,
1863
+ language: discovered_file.language,
1864
+ hash: changes.hashes.get(relPath) ?? "",
1865
+ size: discovered_file.size
1866
+ });
1867
+ const chunkIds = db.insertChunks(
1868
+ fileId,
1869
+ chunks.map((c) => ({
1870
+ lineStart: c.lineStart,
1871
+ lineEnd: c.lineEnd,
1872
+ type: c.type,
1873
+ name: c.name,
1874
+ parent: c.parent,
1875
+ text: c.text,
1876
+ imports: c.imports,
1877
+ exports: c.exports,
1878
+ hash: c.hash
1879
+ }))
1880
+ );
1881
+ for (let i = 0; i < chunks.length; i++) {
1882
+ allChunksWithMeta.push({
1883
+ fileRelPath: relPath,
1884
+ chunk: { ...chunks[i], id: String(chunkIds[i]) }
1885
+ });
1886
+ }
1887
+ filesProcessed++;
1888
+ if (filesProcessed % 50 === 0 || filesProcessed === filesToProcess.length) {
1889
+ log(` Parsing... ${filesProcessed}/${filesToProcess.length}`);
1890
+ }
1891
+ }
1892
+ log(` ${allChunksWithMeta.length} chunks created`);
1893
+ let vectorsCreated = 0;
1894
+ if (!options.skipEmbedding && allChunksWithMeta.length > 0) {
1895
+ const embedder = await createEmbedder();
1896
+ const texts = allChunksWithMeta.map(
1897
+ (cm) => prepareChunkText(cm.fileRelPath, cm.chunk.parent, cm.chunk.text)
1898
+ );
1899
+ const vectors = await embedder.embed(texts, (done, total) => {
1900
+ log(` Embedding... ${done}/${total}`);
1901
+ });
1902
+ db.transaction(() => {
1903
+ for (let i = 0; i < allChunksWithMeta.length; i++) {
1904
+ const chunkDbId = parseInt(allChunksWithMeta[i].chunk.id, 10);
1905
+ db.insertVector(chunkDbId, vectors[i]);
1906
+ }
1907
+ });
1908
+ vectorsCreated = vectors.length;
1909
+ }
1910
+ const durationMs = performance.now() - start;
1911
+ const dbSize = fs5.existsSync(dbPath) ? fs5.statSync(dbPath).size : 0;
1912
+ log("");
1913
+ log(`\u2713 Indexed in ${formatDuration(durationMs)}`);
1914
+ log(
1915
+ ` ${discovered.length} files \u2192 ${allChunksWithMeta.length} chunks` + (vectorsCreated > 0 ? ` \u2192 ${vectorsCreated} vectors` : "")
1916
+ );
1917
+ log(` Database: ${CTX_DIR}/${DB_FILENAME} (${formatBytes(dbSize)})`);
1918
+ return {
1919
+ filesDiscovered: discovered.length,
1920
+ filesAdded: changes.added.length,
1921
+ filesModified: changes.modified.length,
1922
+ filesDeleted: changes.deleted.length,
1923
+ filesUnchanged: changes.unchanged.length,
1924
+ chunksCreated: allChunksWithMeta.length,
1925
+ vectorsCreated,
1926
+ durationMs,
1927
+ languageCounts
1928
+ };
1929
+ } finally {
1930
+ db.close();
1931
+ }
1932
+ }
1933
+ async function createEmbedder() {
1934
+ return createLocalEmbedder();
1935
+ }
1936
+
1937
+ // src/cli/commands/query.ts
1938
+ import fs6 from "fs";
1939
+ import path5 from "path";
1940
+ var CTX_DIR2 = ".ctx";
1941
+ var DB_FILENAME2 = "index.db";
1942
+ var SNIPPET_MAX_LENGTH = 200;
1943
+ var STRATEGY_WEIGHTS = {
1944
+ vector: 1,
1945
+ fts: 0.8,
1946
+ ast: 0.9,
1947
+ path: 0.7,
1948
+ dependency: 0.6
1949
+ };
1950
+ function truncateSnippet(text) {
1951
+ const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
1952
+ if (oneLine.length <= SNIPPET_MAX_LENGTH) return oneLine;
1953
+ return oneLine.slice(0, SNIPPET_MAX_LENGTH) + "...";
1954
+ }
1955
+ function toOutputResult(r) {
1956
+ return {
1957
+ file: r.filePath,
1958
+ lines: [r.lineStart, r.lineEnd],
1959
+ name: r.name,
1960
+ type: r.type,
1961
+ score: Math.round(r.score * 100) / 100,
1962
+ snippet: truncateSnippet(r.text),
1963
+ language: r.language
1964
+ };
1965
+ }
1966
+ function formatTextOutput(query, results) {
1967
+ if (results.length === 0) {
1968
+ return `No results for "${query}"`;
1969
+ }
1970
+ const lines = [`Results for "${query}":
1971
+ `];
1972
+ for (let i = 0; i < results.length; i++) {
1973
+ const r = results[i];
1974
+ const nameLabel = r.name ? `${r.name} [${r.type}]` : `[${r.type}]`;
1975
+ lines.push(`${i + 1}. ${r.file}:${r.lines[0]}-${r.lines[1]} (score: ${r.score})`);
1976
+ lines.push(` ${nameLabel}`);
1977
+ lines.push(` ${r.snippet}`);
1978
+ lines.push("");
1979
+ }
1980
+ return lines.join("\n");
1981
+ }
1982
+ function extractSymbolNames(query) {
1983
+ const matches = query.match(/[A-Z]?[a-z]+(?:[A-Z][a-z]+)*|[a-z]+(?:_[a-z]+)+|[A-Z][a-zA-Z]+/g);
1984
+ return matches ?? [];
1985
+ }
1986
+ function isPathLike(query) {
1987
+ return query.includes("/") || query.includes("*") || query.includes(".");
1988
+ }
1989
+ async function runQuery(projectPath, query, options) {
1990
+ const absoluteRoot = path5.resolve(projectPath);
1991
+ const dbPath = path5.join(absoluteRoot, CTX_DIR2, DB_FILENAME2);
1992
+ if (!fs6.existsSync(dbPath)) {
1993
+ throw new KontextError(
1994
+ `Project not initialized. Run "ctx init" first. (${CTX_DIR2}/${DB_FILENAME2} not found)`,
1995
+ ErrorCode.NOT_INITIALIZED
1996
+ );
1997
+ }
1998
+ const start = performance.now();
1999
+ const db = createDatabase(dbPath);
2000
+ try {
2001
+ const strategyResults = await runStrategies(db, query, options);
2002
+ const fused = fusionMerge(strategyResults, options.limit);
2003
+ const outputResults = fused.map(toOutputResult);
2004
+ const searchTimeMs = Math.round(performance.now() - start);
2005
+ const text = options.format === "text" ? formatTextOutput(query, outputResults) : void 0;
2006
+ return {
2007
+ query,
2008
+ results: outputResults,
2009
+ stats: {
2010
+ strategies: strategyResults.map((s) => s.strategy),
2011
+ totalResults: outputResults.length,
2012
+ searchTimeMs
2013
+ },
2014
+ text
2015
+ };
2016
+ } finally {
2017
+ db.close();
2018
+ }
2019
+ }
2020
+ async function runStrategies(db, query, options) {
2021
+ const results = [];
2022
+ const filters = options.language ? { language: options.language } : void 0;
2023
+ const limit = options.limit * 3;
2024
+ for (const strategy of options.strategies) {
2025
+ const weight = STRATEGY_WEIGHTS[strategy];
2026
+ const searchResults = await executeStrategy(
2027
+ db,
2028
+ strategy,
2029
+ query,
2030
+ limit,
2031
+ filters
2032
+ );
2033
+ if (searchResults.length > 0) {
2034
+ results.push({ strategy, weight, results: searchResults });
2035
+ }
2036
+ }
2037
+ return results;
2038
+ }
2039
+ async function executeStrategy(db, strategy, query, limit, filters) {
2040
+ switch (strategy) {
2041
+ case "vector": {
2042
+ const embedder = await loadEmbedder();
2043
+ return vectorSearch(db, embedder, query, limit, filters);
2044
+ }
2045
+ case "fts":
2046
+ return ftsSearch(db, query, limit, filters);
2047
+ case "ast": {
2048
+ const symbols = extractSymbolNames(query);
2049
+ if (symbols.length === 0) return [];
2050
+ const allResults = [];
2051
+ for (const name of symbols) {
2052
+ const results = astSearch(
2053
+ db,
2054
+ { name, language: filters?.language },
2055
+ limit
2056
+ );
2057
+ allResults.push(...results);
2058
+ }
2059
+ const seen = /* @__PURE__ */ new Set();
2060
+ return allResults.filter((r) => {
2061
+ if (seen.has(r.chunkId)) return false;
2062
+ seen.add(r.chunkId);
2063
+ return true;
2064
+ });
2065
+ }
2066
+ case "path": {
2067
+ if (!isPathLike(query)) return [];
2068
+ return pathSearch(db, query, limit);
2069
+ }
2070
+ case "dependency":
2071
+ return [];
2072
+ }
2073
+ }
2074
+ var embedderInstance = null;
2075
+ async function loadEmbedder() {
2076
+ if (embedderInstance) return embedderInstance;
2077
+ embedderInstance = await createLocalEmbedder();
2078
+ return embedderInstance;
2079
+ }
2080
+
2081
+ // src/cli/commands/ask.ts
2082
+ import fs7 from "fs";
2083
+ import path6 from "path";
2084
+ var CTX_DIR3 = ".ctx";
2085
+ var DB_FILENAME3 = "index.db";
2086
+ var SNIPPET_MAX_LENGTH2 = 200;
2087
+ var FALLBACK_NOTICE = "No LLM provider configured. Set CTX_GEMINI_KEY, CTX_OPENAI_KEY, or CTX_ANTHROPIC_KEY. Running basic search instead.";
2088
+ function truncateSnippet2(text) {
2089
+ const oneLine = text.replace(/\n/g, " ").replace(/\s+/g, " ").trim();
2090
+ if (oneLine.length <= SNIPPET_MAX_LENGTH2) return oneLine;
2091
+ return oneLine.slice(0, SNIPPET_MAX_LENGTH2) + "...";
2092
+ }
2093
+ function toOutputResult2(r) {
2094
+ return {
2095
+ file: r.filePath,
2096
+ lines: [r.lineStart, r.lineEnd],
2097
+ name: r.name,
2098
+ type: r.type,
2099
+ score: Math.round(r.score * 100) / 100,
2100
+ snippet: truncateSnippet2(r.text),
2101
+ language: r.language
2102
+ };
2103
+ }
2104
+ function formatTextOutput2(output) {
2105
+ const lines = [];
2106
+ if (output.fallback) {
2107
+ lines.push(FALLBACK_NOTICE);
2108
+ lines.push("");
2109
+ }
2110
+ if (output.interpretation) {
2111
+ lines.push(`Understanding: ${output.interpretation}`);
2112
+ lines.push("");
2113
+ }
2114
+ if (output.results.length === 0) {
2115
+ lines.push(`No results found for "${output.query}"`);
2116
+ } else {
2117
+ lines.push(`Found ${output.results.length} relevant location(s):`);
2118
+ lines.push("");
2119
+ for (let i = 0; i < output.results.length; i++) {
2120
+ const r = output.results[i];
2121
+ const nameLabel = r.name ? `${r.name} [${r.type}]` : `[${r.type}]`;
2122
+ lines.push(`${i + 1}. ${r.file}:${r.lines[0]}-${r.lines[1]} (score: ${r.score})`);
2123
+ lines.push(` ${nameLabel}`);
2124
+ lines.push(` ${r.snippet}`);
2125
+ lines.push("");
2126
+ }
2127
+ }
2128
+ if (output.explanation) {
2129
+ lines.push("Explanation:");
2130
+ lines.push(output.explanation);
2131
+ lines.push("");
2132
+ }
2133
+ lines.push("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2134
+ const cost = output.stats.costEstimate.toFixed(4);
2135
+ lines.push(
2136
+ `Tokens: ${output.stats.tokensUsed.toLocaleString()} | Cost: ~$${cost} | Strategies: ${output.stats.strategies.join(", ")}`
2137
+ );
2138
+ return lines.join("\n");
2139
+ }
2140
+ function createSearchExecutor(db) {
2141
+ return async (strategies, limit) => {
2142
+ const strategyResults = [];
2143
+ const fetchLimit = limit * 3;
2144
+ for (const plan of strategies) {
2145
+ const results = await executeStrategy2(db, plan, fetchLimit);
2146
+ if (results.length > 0) {
2147
+ strategyResults.push({
2148
+ strategy: plan.strategy,
2149
+ weight: plan.weight,
2150
+ results
2151
+ });
2152
+ }
2153
+ }
2154
+ return fusionMerge(strategyResults, limit);
2155
+ };
2156
+ }
2157
+ async function executeStrategy2(db, plan, limit) {
2158
+ switch (plan.strategy) {
2159
+ case "vector": {
2160
+ const embedder = await loadEmbedder2();
2161
+ return vectorSearch(db, embedder, plan.query, limit);
2162
+ }
2163
+ case "fts":
2164
+ return ftsSearch(db, plan.query, limit);
2165
+ case "ast":
2166
+ return astSearch(db, { name: plan.query }, limit);
2167
+ case "path":
2168
+ return pathSearch(db, plan.query, limit);
2169
+ case "dependency":
2170
+ return [];
2171
+ }
2172
+ }
2173
+ var embedderInstance2 = null;
2174
+ async function loadEmbedder2() {
2175
+ if (embedderInstance2) return embedderInstance2;
2176
+ embedderInstance2 = await createLocalEmbedder();
2177
+ return embedderInstance2;
2178
+ }
2179
+ async function fallbackSearch(db, query, limit) {
2180
+ const executor = createSearchExecutor(db);
2181
+ const fallbackStrategies = [
2182
+ { strategy: "fts", query, weight: 0.8, reason: "fallback keyword search" },
2183
+ { strategy: "ast", query, weight: 0.9, reason: "fallback structural search" }
2184
+ ];
2185
+ const results = await executor(fallbackStrategies, limit);
2186
+ return {
2187
+ query,
2188
+ interpretation: "",
2189
+ results: results.map(toOutputResult2),
2190
+ explanation: "",
2191
+ stats: {
2192
+ strategies: fallbackStrategies.map((s) => s.strategy),
2193
+ tokensUsed: 0,
2194
+ costEstimate: 0,
2195
+ totalResults: results.length
2196
+ },
2197
+ fallback: true
2198
+ };
2199
+ }
2200
+ async function runAsk(projectPath, query, options) {
2201
+ const absoluteRoot = path6.resolve(projectPath);
2202
+ const dbPath = path6.join(absoluteRoot, CTX_DIR3, DB_FILENAME3);
2203
+ if (!fs7.existsSync(dbPath)) {
2204
+ throw new KontextError(
2205
+ `Project not initialized. Run "ctx init" first. (${CTX_DIR3}/${DB_FILENAME3} not found)`,
2206
+ ErrorCode.NOT_INITIALIZED
2207
+ );
2208
+ }
2209
+ const db = createDatabase(dbPath);
2210
+ try {
2211
+ const provider = options.provider ?? null;
2212
+ if (!provider) {
2213
+ const output = await fallbackSearch(db, query, options.limit);
2214
+ if (options.format === "text") {
2215
+ output.text = formatTextOutput2(output);
2216
+ }
2217
+ return output;
2218
+ }
2219
+ const executor = createSearchExecutor(db);
2220
+ if (options.noExplain) {
2221
+ return await runNoExplain(provider, query, options, executor);
2222
+ }
2223
+ return await runWithSteering(provider, query, options, executor);
2224
+ } finally {
2225
+ db.close();
2226
+ }
2227
+ }
2228
+ async function runNoExplain(provider, query, options, executor) {
2229
+ const plan = await planSearch(provider, query);
2230
+ const results = await executor(plan.strategies, options.limit);
2231
+ const output = {
2232
+ query,
2233
+ interpretation: plan.interpretation,
2234
+ results: results.map(toOutputResult2),
2235
+ explanation: "",
2236
+ stats: {
2237
+ strategies: plan.strategies.map((s) => s.strategy),
2238
+ tokensUsed: 0,
2239
+ costEstimate: 0,
2240
+ totalResults: results.length
2241
+ }
2242
+ };
2243
+ if (options.format === "text") {
2244
+ output.text = formatTextOutput2(output);
2245
+ }
2246
+ return output;
2247
+ }
2248
+ async function runWithSteering(provider, query, options, executor) {
2249
+ const result = await steer(provider, query, options.limit, executor);
2250
+ const output = {
2251
+ query,
2252
+ interpretation: result.interpretation,
2253
+ results: result.results.map(toOutputResult2),
2254
+ explanation: result.explanation,
2255
+ stats: {
2256
+ strategies: result.strategies.map((s) => s.strategy),
2257
+ tokensUsed: result.tokensUsed,
2258
+ costEstimate: result.costEstimate,
2259
+ totalResults: result.results.length
2260
+ }
2261
+ };
2262
+ if (options.format === "text") {
2263
+ output.text = formatTextOutput2(output);
2264
+ }
2265
+ return output;
2266
+ }
2267
+
2268
+ // src/cli/commands/status.ts
2269
+ import fs8 from "fs";
2270
+ import path7 from "path";
2271
+ var CTX_DIR4 = ".ctx";
2272
+ var DB_FILENAME4 = "index.db";
2273
+ var CONFIG_FILENAME2 = "config.json";
2274
+ function formatBytes2(bytes) {
2275
+ if (bytes < 1024) return `${bytes} B`;
2276
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2277
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2278
+ }
2279
+ function formatTimestamp(raw) {
2280
+ const num = Number(raw);
2281
+ if (Number.isNaN(num)) return raw;
2282
+ const date = new Date(num);
2283
+ return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
2284
+ }
2285
+ function capitalize(s) {
2286
+ return s.charAt(0).toUpperCase() + s.slice(1);
2287
+ }
2288
+ function readConfig(ctxDir) {
2289
+ const configPath = path7.join(ctxDir, CONFIG_FILENAME2);
2290
+ if (!fs8.existsSync(configPath)) return null;
2291
+ try {
2292
+ const raw = fs8.readFileSync(configPath, "utf-8");
2293
+ const parsed = JSON.parse(raw);
2294
+ return {
2295
+ model: parsed.model ?? "unknown",
2296
+ dimensions: parsed.dimensions ?? 0
2297
+ };
2298
+ } catch {
2299
+ return null;
2300
+ }
2301
+ }
2302
+ function formatNotInitialized(projectPath) {
2303
+ return [
2304
+ `Kontext Status \u2014 ${projectPath}`,
2305
+ "",
2306
+ ' Not initialized. Run "ctx init" first.',
2307
+ ""
2308
+ ].join("\n");
2309
+ }
2310
+ function formatStatus(projectPath, output) {
2311
+ const lines = [
2312
+ `Kontext Status \u2014 ${projectPath}`,
2313
+ "",
2314
+ ` Initialized: Yes`,
2315
+ ` Database: ${CTX_DIR4}/${DB_FILENAME4} (${formatBytes2(output.dbSizeBytes)})`
2316
+ ];
2317
+ if (output.lastIndexed) {
2318
+ lines.push(` Last indexed: ${formatTimestamp(output.lastIndexed)}`);
2319
+ }
2320
+ lines.push("");
2321
+ lines.push(` Files: ${output.fileCount.toLocaleString()}`);
2322
+ lines.push(` Chunks: ${output.chunkCount.toLocaleString()}`);
2323
+ lines.push(` Vectors: ${output.vectorCount.toLocaleString()}`);
2324
+ if (output.languages.size > 0) {
2325
+ lines.push("");
2326
+ lines.push(" Languages:");
2327
+ const maxLangLen = Math.max(
2328
+ ...[...output.languages.keys()].map((k) => capitalize(k).length)
2329
+ );
2330
+ for (const [lang, count] of output.languages) {
2331
+ const label = capitalize(lang).padEnd(maxLangLen + 2);
2332
+ lines.push(` ${label}${count} file${count !== 1 ? "s" : ""}`);
2333
+ }
2334
+ }
2335
+ if (output.config) {
2336
+ lines.push("");
2337
+ lines.push(
2338
+ ` Embedder: local (${output.config.model}, ${output.config.dimensions} dims)`
2339
+ );
2340
+ }
2341
+ lines.push("");
2342
+ return lines.join("\n");
2343
+ }
2344
+ async function runStatus(projectPath) {
2345
+ const absoluteRoot = path7.resolve(projectPath);
2346
+ const ctxDir = path7.join(absoluteRoot, CTX_DIR4);
2347
+ const dbPath = path7.join(ctxDir, DB_FILENAME4);
2348
+ if (!fs8.existsSync(dbPath)) {
2349
+ const output = {
2350
+ initialized: false,
2351
+ fileCount: 0,
2352
+ chunkCount: 0,
2353
+ vectorCount: 0,
2354
+ dbSizeBytes: 0,
2355
+ lastIndexed: null,
2356
+ languages: /* @__PURE__ */ new Map(),
2357
+ config: null,
2358
+ text: formatNotInitialized(absoluteRoot)
2359
+ };
2360
+ return output;
2361
+ }
2362
+ const db = createDatabase(dbPath);
2363
+ try {
2364
+ const fileCount = db.getFileCount();
2365
+ const chunkCount = db.getChunkCount();
2366
+ const vectorCount = db.getVectorCount();
2367
+ const languages = db.getLanguageBreakdown();
2368
+ const lastIndexed = db.getLastIndexed();
2369
+ const config = readConfig(ctxDir);
2370
+ const dbSizeBytes = fs8.statSync(dbPath).size;
2371
+ const output = {
2372
+ initialized: true,
2373
+ fileCount,
2374
+ chunkCount,
2375
+ vectorCount,
2376
+ dbSizeBytes,
2377
+ lastIndexed,
2378
+ languages,
2379
+ config,
2380
+ text: ""
2381
+ };
2382
+ output.text = formatStatus(absoluteRoot, output);
2383
+ return output;
2384
+ } finally {
2385
+ db.close();
2386
+ }
2387
+ }
2388
+ export {
2389
+ ConfigError,
2390
+ DatabaseError,
2391
+ ErrorCode,
2392
+ IndexError,
2393
+ KontextError,
2394
+ LANGUAGE_MAP,
2395
+ LogLevel,
2396
+ SearchError,
2397
+ astSearch,
2398
+ chunkFile,
2399
+ computeChanges,
2400
+ createDatabase,
2401
+ createLocalEmbedder,
2402
+ createLogger,
2403
+ createOpenAIEmbedder,
2404
+ createVoyageEmbedder,
2405
+ dependencyTrace,
2406
+ discoverFiles,
2407
+ estimateTokens,
2408
+ ftsSearch,
2409
+ fusionMerge,
2410
+ initParser,
2411
+ parseFile,
2412
+ pathSearch,
2413
+ planSearch,
2414
+ prepareChunkText,
2415
+ runAsk,
2416
+ runInit,
2417
+ runQuery,
2418
+ runStatus,
2419
+ steer,
2420
+ vectorSearch
2421
+ };
2422
+ //# sourceMappingURL=index.js.map