brainbank 0.1.0-beta.1

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/assets/architecture.png +0 -0
  4. package/bin/brainbank +18 -0
  5. package/bin/brainbank-mcp +19 -0
  6. package/dist/chunk-3YBCD6DI.js +117 -0
  7. package/dist/chunk-3YBCD6DI.js.map +1 -0
  8. package/dist/chunk-63GBCDS5.js +3249 -0
  9. package/dist/chunk-63GBCDS5.js.map +1 -0
  10. package/dist/chunk-DMFMTOHF.js +123 -0
  11. package/dist/chunk-DMFMTOHF.js.map +1 -0
  12. package/dist/chunk-FQYKWB2Q.js +136 -0
  13. package/dist/chunk-FQYKWB2Q.js.map +1 -0
  14. package/dist/chunk-IMJJ2VEM.js +74 -0
  15. package/dist/chunk-IMJJ2VEM.js.map +1 -0
  16. package/dist/chunk-M744PCJQ.js +43 -0
  17. package/dist/chunk-M744PCJQ.js.map +1 -0
  18. package/dist/chunk-O3J6ZIXK.js +82 -0
  19. package/dist/chunk-O3J6ZIXK.js.map +1 -0
  20. package/dist/chunk-OPH7GZ7U.js +124 -0
  21. package/dist/chunk-OPH7GZ7U.js.map +1 -0
  22. package/dist/chunk-PXEWQMN7.js +89 -0
  23. package/dist/chunk-PXEWQMN7.js.map +1 -0
  24. package/dist/chunk-RDQYDLYZ.js +69 -0
  25. package/dist/chunk-RDQYDLYZ.js.map +1 -0
  26. package/dist/chunk-VIIHPCC4.js +254 -0
  27. package/dist/chunk-VIIHPCC4.js.map +1 -0
  28. package/dist/chunk-WCQVDF3K.js +14 -0
  29. package/dist/chunk-WCQVDF3K.js.map +1 -0
  30. package/dist/cli.d.ts +1 -0
  31. package/dist/cli.js +3076 -0
  32. package/dist/cli.js.map +1 -0
  33. package/dist/haiku-expander-YRSIPGKP.js +8 -0
  34. package/dist/haiku-expander-YRSIPGKP.js.map +1 -0
  35. package/dist/haiku-pruner-SHAXUPY6.js +8 -0
  36. package/dist/haiku-pruner-SHAXUPY6.js.map +1 -0
  37. package/dist/http-server-QUXHLWUM.js +9 -0
  38. package/dist/http-server-QUXHLWUM.js.map +1 -0
  39. package/dist/index.d.ts +2161 -0
  40. package/dist/index.js +357 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/local-embedding-NZQTILGV.js +8 -0
  43. package/dist/local-embedding-NZQTILGV.js.map +1 -0
  44. package/dist/mcp.d.ts +2 -0
  45. package/dist/mcp.js +334 -0
  46. package/dist/mcp.js.map +1 -0
  47. package/dist/openai-embedding-ZP5TSUJG.js +8 -0
  48. package/dist/openai-embedding-ZP5TSUJG.js.map +1 -0
  49. package/dist/perplexity-context-embedding-GI5PHE6X.js +9 -0
  50. package/dist/perplexity-context-embedding-GI5PHE6X.js.map +1 -0
  51. package/dist/perplexity-embedding-KZRYGJRC.js +10 -0
  52. package/dist/perplexity-embedding-KZRYGJRC.js.map +1 -0
  53. package/dist/plugin-IKQ6IRSJ.js +32 -0
  54. package/dist/plugin-IKQ6IRSJ.js.map +1 -0
  55. package/dist/resolve-ASGLBNUC.js +10 -0
  56. package/dist/resolve-ASGLBNUC.js.map +1 -0
  57. package/dist/stats-tui-ZY2NQSEA.js +1904 -0
  58. package/dist/stats-tui-ZY2NQSEA.js.map +1 -0
  59. package/package.json +96 -0
  60. package/src/brainbank.ts +617 -0
  61. package/src/cli/commands/collection.ts +77 -0
  62. package/src/cli/commands/context.ts +179 -0
  63. package/src/cli/commands/daemon.ts +100 -0
  64. package/src/cli/commands/docs.ts +71 -0
  65. package/src/cli/commands/files.ts +69 -0
  66. package/src/cli/commands/help.ts +77 -0
  67. package/src/cli/commands/index.ts +482 -0
  68. package/src/cli/commands/kv.ts +140 -0
  69. package/src/cli/commands/mcp-export.ts +273 -0
  70. package/src/cli/commands/mcp.ts +6 -0
  71. package/src/cli/commands/reembed.ts +30 -0
  72. package/src/cli/commands/scan.ts +336 -0
  73. package/src/cli/commands/search.ts +203 -0
  74. package/src/cli/commands/stats.ts +68 -0
  75. package/src/cli/commands/status.ts +47 -0
  76. package/src/cli/commands/watch.ts +47 -0
  77. package/src/cli/factory/brain-context.ts +43 -0
  78. package/src/cli/factory/builtin-registration.ts +87 -0
  79. package/src/cli/factory/config-loader.ts +77 -0
  80. package/src/cli/factory/index.ts +69 -0
  81. package/src/cli/factory/plugin-loader.ts +325 -0
  82. package/src/cli/index.ts +71 -0
  83. package/src/cli/server-client.ts +178 -0
  84. package/src/cli/tui/index-tui.tsx +667 -0
  85. package/src/cli/tui/stats-data.ts +523 -0
  86. package/src/cli/tui/stats-search.ts +262 -0
  87. package/src/cli/tui/stats-tui.tsx +1465 -0
  88. package/src/cli/tui/tree-scanner.ts +650 -0
  89. package/src/cli/utils.ts +137 -0
  90. package/src/config.ts +49 -0
  91. package/src/constants.ts +21 -0
  92. package/src/db/adapter.ts +112 -0
  93. package/src/db/metadata.ts +130 -0
  94. package/src/db/migrations.ts +66 -0
  95. package/src/db/sqlite-adapter.ts +218 -0
  96. package/src/db/tracker.ts +91 -0
  97. package/src/engine/index-api.ts +81 -0
  98. package/src/engine/reembed.ts +206 -0
  99. package/src/engine/search-api.ts +218 -0
  100. package/src/index.ts +154 -0
  101. package/src/lib/fts.ts +57 -0
  102. package/src/lib/languages.ts +180 -0
  103. package/src/lib/logger.ts +126 -0
  104. package/src/lib/math.ts +87 -0
  105. package/src/lib/provider-key.ts +20 -0
  106. package/src/lib/prune.ts +71 -0
  107. package/src/lib/rrf.ts +133 -0
  108. package/src/lib/write-lock.ts +108 -0
  109. package/src/mcp/mcp-server.ts +195 -0
  110. package/src/mcp/workspace-factory.ts +68 -0
  111. package/src/mcp/workspace-pool.ts +224 -0
  112. package/src/plugin.ts +381 -0
  113. package/src/providers/embeddings/embedding-worker-thread.ts +95 -0
  114. package/src/providers/embeddings/embedding-worker.ts +141 -0
  115. package/src/providers/embeddings/local-embedding.ts +115 -0
  116. package/src/providers/embeddings/openai-embedding.ts +167 -0
  117. package/src/providers/embeddings/perplexity-context-embedding.ts +195 -0
  118. package/src/providers/embeddings/perplexity-embedding.ts +165 -0
  119. package/src/providers/embeddings/resolve.ts +34 -0
  120. package/src/providers/pruners/haiku-expander.ts +166 -0
  121. package/src/providers/pruners/haiku-pruner.ts +112 -0
  122. package/src/providers/vector/hnsw-index.ts +174 -0
  123. package/src/providers/vector/hnsw-loader.ts +129 -0
  124. package/src/search/bm25-boost.ts +69 -0
  125. package/src/search/context-builder.ts +251 -0
  126. package/src/search/keyword/composite-bm25-search.ts +47 -0
  127. package/src/search/types.ts +37 -0
  128. package/src/search/vector/composite-vector-search.ts +61 -0
  129. package/src/search/vector/mmr.ts +64 -0
  130. package/src/services/collection.ts +384 -0
  131. package/src/services/daemon.ts +87 -0
  132. package/src/services/http-server.ts +336 -0
  133. package/src/services/kv-service.ts +64 -0
  134. package/src/services/plugin-registry.ts +77 -0
  135. package/src/services/watch.ts +340 -0
  136. package/src/services/webhook-server.ts +100 -0
  137. package/src/types.ts +493 -0
@@ -0,0 +1,523 @@
1
+ /**
2
+ * stats-data.ts — Pure data-fetching functions for the Stats TUI.
3
+ *
4
+ * Opens a read-only `node:sqlite` DatabaseSync connection and queries
5
+ * code_chunks, code_imports, code_call_edges, code_symbols, embedding_meta.
6
+ * Zero state, zero React — just data.
7
+ */
8
+
9
+ import { DatabaseSync } from 'node:sqlite';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+
13
+ // ── Types ─────────────────────────────────────────────
14
+
15
+ export interface StatsOverview {
16
+ files: number;
17
+ chunks: number;
18
+ symbols: number;
19
+ callEdges: number;
20
+ importEdges: number;
21
+ hnswSize: number;
22
+ dbSizeMB: number;
23
+ embeddingModel: string;
24
+ repoPath: string;
25
+ plugins: string[];
26
+ pruner: string;
27
+ expander: string;
28
+ }
29
+
30
+ export interface LanguageStat {
31
+ language: string;
32
+ chunks: number;
33
+ files: number;
34
+ percent: number;
35
+ }
36
+
37
+ export interface DirectoryStat {
38
+ dir: string;
39
+ files: number;
40
+ chunks: number;
41
+ percent: number;
42
+ }
43
+
44
+ export interface FileStat {
45
+ filePath: string;
46
+ fileName: string;
47
+ language: string;
48
+ chunks: number;
49
+ symbols: number;
50
+ startLine: number;
51
+ endLine: number;
52
+ }
53
+
54
+ export interface FileDetailInfo {
55
+ filePath: string;
56
+ language: string;
57
+ chunks: number;
58
+ symbols: SymbolInfo[];
59
+ importsOut: string[];
60
+ importsIn: string[];
61
+ callEdgesOut: number;
62
+ callEdgesIn: number;
63
+ }
64
+
65
+ export interface SymbolInfo {
66
+ name: string;
67
+ kind: string;
68
+ line: number;
69
+ }
70
+
71
+ export interface ChunkInfo {
72
+ id: number;
73
+ chunkType: string;
74
+ name: string | null;
75
+ startLine: number;
76
+ endLine: number;
77
+ content: string;
78
+ language: string;
79
+ callsOut: string[];
80
+ calledBy: string[];
81
+ }
82
+
83
+ export interface CallTreeNode {
84
+ chunkId: number;
85
+ symbol: string;
86
+ filePath: string;
87
+ children: CallTreeNode[];
88
+ }
89
+
90
+
91
+ // ── Data Access ───────────────────────────────────
92
+
93
+ /** Open a read-only node:sqlite connection. */
94
+ function openDb(dbPath: string): DatabaseSync {
95
+ return new DatabaseSync(dbPath, { readOnly: true } as ConstructorParameters<typeof DatabaseSync>[1]);
96
+ }
97
+
98
+ /** Check if a table exists. */
99
+ function tableExists(db: DatabaseSync, name: string): boolean {
100
+ const row = db.prepare(
101
+ `SELECT 1 as found FROM sqlite_master WHERE type='table' AND name=?`
102
+ ).get(name) as Record<string, unknown> | undefined;
103
+ return !!row;
104
+ }
105
+
106
+ /** Safe count query. */
107
+ function countQuery(db: DatabaseSync, sql: string, ...params: (string | number | bigint | null | Uint8Array)[]): number {
108
+ const row = db.prepare(sql).get(...params) as { c: number } | undefined;
109
+ return row?.c ?? 0;
110
+ }
111
+
112
+
113
+ // ── Public API ────────────────────────────────────
114
+
115
+ export function fetchOverview(dbPath: string, repoPath: string, configPath: string): StatsOverview {
116
+ const db = openDb(dbPath);
117
+ try {
118
+ const hasChunks = tableExists(db, 'code_chunks');
119
+
120
+ const files = hasChunks ? countQuery(db, 'SELECT COUNT(DISTINCT file_path) as c FROM code_chunks') : 0;
121
+ const chunks = hasChunks ? countQuery(db, 'SELECT COUNT(*) as c FROM code_chunks') : 0;
122
+ const symbols = hasChunks ? countQuery(db, "SELECT COUNT(*) as c FROM code_chunks WHERE name IS NOT NULL AND name != ''") : 0;
123
+ const callEdges = tableExists(db, 'code_call_edges') ? countQuery(db, 'SELECT COUNT(*) as c FROM code_call_edges') : 0;
124
+ const importEdges = tableExists(db, 'code_imports') ? countQuery(db, 'SELECT COUNT(*) as c FROM code_imports') : 0;
125
+ const hnswSize = tableExists(db, 'code_vectors') ? countQuery(db, 'SELECT COUNT(*) as c FROM code_vectors') : 0;
126
+
127
+ // DB file size
128
+ const stat = fs.statSync(dbPath);
129
+ const dbSizeMB = Math.round(stat.size / 1048576 * 10) / 10;
130
+
131
+ // Embedding model
132
+ let embeddingModel = 'unknown';
133
+ if (tableExists(db, 'embedding_meta')) {
134
+ for (const key of ['provider_key', 'provider', 'model']) {
135
+ const row = db.prepare(`SELECT value FROM embedding_meta WHERE key = ?`).get(key) as { value: string } | undefined;
136
+ if (row) { embeddingModel = row.value; break; }
137
+ }
138
+ }
139
+
140
+ // Config
141
+ let plugins: string[] = ['code'];
142
+ let pruner = 'none';
143
+ let expander = 'none';
144
+ try {
145
+ const raw = fs.readFileSync(configPath, 'utf-8');
146
+ const config = JSON.parse(raw) as Record<string, unknown>;
147
+ if (Array.isArray(config.plugins)) plugins = config.plugins as string[];
148
+ if (typeof config.pruner === 'string') pruner = config.pruner;
149
+ if (typeof config.expander === 'string') expander = config.expander;
150
+ } catch { /* no config */ }
151
+
152
+ return { files, chunks, symbols, callEdges, importEdges, hnswSize, dbSizeMB, embeddingModel, repoPath, plugins, pruner, expander };
153
+ } finally {
154
+ db.close();
155
+ }
156
+ }
157
+
158
+ export function fetchLanguageBreakdown(dbPath: string): LanguageStat[] {
159
+ const db = openDb(dbPath);
160
+ try {
161
+ if (!tableExists(db, 'code_chunks')) return [];
162
+
163
+ const rows = db.prepare(`
164
+ SELECT language, COUNT(*) as chunks, COUNT(DISTINCT file_path) as files
165
+ FROM code_chunks
166
+ GROUP BY language
167
+ ORDER BY chunks DESC
168
+ `).all() as { language: string; chunks: number; files: number }[];
169
+
170
+ const total = rows.reduce((sum, r) => sum + r.chunks, 0);
171
+ return rows.map(r => ({
172
+ language: r.language,
173
+ chunks: r.chunks,
174
+ files: r.files,
175
+ percent: total > 0 ? Math.round(r.chunks / total * 1000) / 10 : 0,
176
+ }));
177
+ } finally {
178
+ db.close();
179
+ }
180
+ }
181
+
182
+ export function fetchDirectories(dbPath: string): DirectoryStat[] {
183
+ const db = openDb(dbPath);
184
+ try {
185
+ if (!tableExists(db, 'code_chunks')) return [];
186
+
187
+ const rows = db.prepare(`
188
+ SELECT
189
+ CASE
190
+ WHEN INSTR(file_path, '/') > 0 THEN SUBSTR(file_path, 1, INSTR(file_path, '/') - 1)
191
+ ELSE file_path
192
+ END as dir,
193
+ COUNT(DISTINCT file_path) as files,
194
+ COUNT(*) as chunks
195
+ FROM code_chunks
196
+ GROUP BY dir
197
+ ORDER BY chunks DESC
198
+ `).all() as { dir: string; files: number; chunks: number }[];
199
+
200
+ const total = rows.reduce((sum, r) => sum + r.chunks, 0);
201
+ return rows.map(r => ({
202
+ dir: r.dir,
203
+ files: r.files,
204
+ chunks: r.chunks,
205
+ percent: total > 0 ? Math.round(r.chunks / total * 1000) / 10 : 0,
206
+ }));
207
+ } finally {
208
+ db.close();
209
+ }
210
+ }
211
+
212
+ export function fetchFilesInDir(dbPath: string, dir: string): FileStat[] {
213
+ const db = openDb(dbPath);
214
+ try {
215
+ if (!tableExists(db, 'code_chunks')) return [];
216
+
217
+ const rows = db.prepare(`
218
+ SELECT
219
+ file_path,
220
+ language,
221
+ COUNT(*) as chunks,
222
+ COUNT(CASE WHEN name IS NOT NULL AND name != '' THEN 1 END) as symbols,
223
+ MIN(start_line) as min_line,
224
+ MAX(end_line) as max_line
225
+ FROM code_chunks
226
+ WHERE file_path LIKE ? || '%'
227
+ GROUP BY file_path
228
+ ORDER BY chunks DESC, file_path
229
+ `).all(`${dir}/`) as { file_path: string; language: string; chunks: number; symbols: number; min_line: number; max_line: number }[];
230
+
231
+ return rows.map(r => ({
232
+ filePath: r.file_path,
233
+ fileName: path.basename(r.file_path),
234
+ language: r.language,
235
+ chunks: r.chunks,
236
+ symbols: r.symbols,
237
+ startLine: r.min_line,
238
+ endLine: r.max_line,
239
+ }));
240
+ } finally {
241
+ db.close();
242
+ }
243
+ }
244
+
245
+ export function fetchFileDetail(dbPath: string, filePath: string): FileDetailInfo {
246
+ const db = openDb(dbPath);
247
+ try {
248
+ // Language + chunk count
249
+ const meta = db.prepare(`
250
+ SELECT language, COUNT(*) as chunks
251
+ FROM code_chunks WHERE file_path = ?
252
+ `).get(filePath) as { language: string; chunks: number } | undefined;
253
+
254
+ // Symbols — extract named chunks as symbols
255
+ const symbols = db.prepare(`
256
+ SELECT name, chunk_type as kind, start_line as line
257
+ FROM code_chunks
258
+ WHERE file_path = ? AND name IS NOT NULL AND name != ''
259
+ ORDER BY start_line
260
+ `).all(filePath) as unknown as SymbolInfo[];
261
+
262
+ // Imports out (this file imports...)
263
+ let importsOut: string[] = [];
264
+ if (tableExists(db, 'code_imports')) {
265
+ importsOut = (db.prepare(`
266
+ SELECT imports_path FROM code_imports WHERE file_path = ?
267
+ `).all(filePath) as { imports_path: string }[]).map(r => r.imports_path);
268
+ }
269
+
270
+ // Imports in (who imports this file)
271
+ let importsIn: string[] = [];
272
+ if (tableExists(db, 'code_imports')) {
273
+ const base = path.basename(filePath, path.extname(filePath));
274
+ importsIn = (db.prepare(`
275
+ SELECT file_path FROM code_imports WHERE imports_path LIKE ?
276
+ `).all(`%${base}%`) as { file_path: string }[]).map(r => r.file_path);
277
+ }
278
+
279
+ // Call edges
280
+ let callEdgesOut = 0;
281
+ let callEdgesIn = 0;
282
+ if (tableExists(db, 'code_call_edges')) {
283
+ const chunkIds = (db.prepare(
284
+ `SELECT id FROM code_chunks WHERE file_path = ?`
285
+ ).all(filePath) as { id: number }[]).map(r => r.id);
286
+
287
+ if (chunkIds.length > 0) {
288
+ const ph = chunkIds.map(() => '?').join(',');
289
+ callEdgesOut = countQuery(db, `SELECT COUNT(*) as c FROM code_call_edges WHERE caller_chunk_id IN (${ph})`, ...chunkIds);
290
+ callEdgesIn = countQuery(db, `SELECT COUNT(*) as c FROM code_call_edges WHERE callee_chunk_id IN (${ph})`, ...chunkIds);
291
+ }
292
+ }
293
+
294
+ return {
295
+ filePath,
296
+ language: meta?.language ?? 'unknown',
297
+ chunks: meta?.chunks ?? 0,
298
+ symbols,
299
+ importsOut,
300
+ importsIn,
301
+ callEdgesOut,
302
+ callEdgesIn,
303
+ };
304
+ } finally {
305
+ db.close();
306
+ }
307
+ }
308
+
309
+ export function fetchChunksForFile(dbPath: string, filePath: string): ChunkInfo[] {
310
+ const db = openDb(dbPath);
311
+ try {
312
+ if (!tableExists(db, 'code_chunks')) return [];
313
+
314
+ const rows = db.prepare(`
315
+ SELECT id, chunk_type, name, start_line, end_line, content, language
316
+ FROM code_chunks
317
+ WHERE file_path = ?
318
+ ORDER BY start_line
319
+ `).all(filePath) as { id: number; chunk_type: string; name: string | null; start_line: number; end_line: number; content: string; language: string }[];
320
+
321
+ const hasCallEdges = tableExists(db, 'code_call_edges');
322
+
323
+ return rows.map(r => {
324
+ let callsOut: string[] = [];
325
+ let calledBy: string[] = [];
326
+
327
+ if (hasCallEdges) {
328
+ callsOut = (db.prepare(`
329
+ SELECT DISTINCT symbol_name FROM code_call_edges WHERE caller_chunk_id = ?
330
+ `).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
331
+
332
+ calledBy = (db.prepare(`
333
+ SELECT DISTINCT symbol_name FROM code_call_edges WHERE callee_chunk_id = ?
334
+ `).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
335
+ }
336
+
337
+ return {
338
+ id: r.id,
339
+ chunkType: r.chunk_type,
340
+ name: r.name,
341
+ startLine: r.start_line,
342
+ endLine: r.end_line,
343
+ content: r.content,
344
+ language: r.language,
345
+ callsOut,
346
+ calledBy,
347
+ };
348
+ });
349
+ } finally {
350
+ db.close();
351
+ }
352
+ }
353
+
354
+ export function fetchCallTree(dbPath: string, chunkId: number, depth: number = 3): CallTreeNode {
355
+ const db = openDb(dbPath);
356
+ try {
357
+ const chunk = db.prepare(`
358
+ SELECT id, name, file_path FROM code_chunks WHERE id = ?
359
+ `).get(chunkId) as { id: number; name: string | null; file_path: string } | undefined;
360
+
361
+ if (!chunk) return { chunkId, symbol: '?', filePath: '?', children: [] };
362
+
363
+ function expand(id: number, d: number, visited: Set<number>): CallTreeNode[] {
364
+ if (d <= 0 || !tableExists(db, 'code_call_edges')) return [];
365
+
366
+ const edges = db.prepare(`
367
+ SELECT DISTINCT ce.callee_chunk_id, ce.symbol_name, cc.file_path
368
+ FROM code_call_edges ce
369
+ JOIN code_chunks cc ON cc.id = ce.callee_chunk_id
370
+ WHERE ce.caller_chunk_id = ?
371
+ ORDER BY ce.symbol_name
372
+ `).all(id) as { callee_chunk_id: number; symbol_name: string; file_path: string }[];
373
+
374
+ return edges
375
+ .filter(e => !visited.has(e.callee_chunk_id))
376
+ .map(e => {
377
+ visited.add(e.callee_chunk_id);
378
+ return {
379
+ chunkId: e.callee_chunk_id,
380
+ symbol: e.symbol_name,
381
+ filePath: e.file_path,
382
+ children: expand(e.callee_chunk_id, d - 1, visited),
383
+ };
384
+ });
385
+ }
386
+
387
+ const visited = new Set([chunkId]);
388
+ return {
389
+ chunkId,
390
+ symbol: chunk.name ?? 'anonymous',
391
+ filePath: chunk.file_path,
392
+ children: expand(chunkId, depth, visited),
393
+ };
394
+ } finally {
395
+ db.close();
396
+ }
397
+ }
398
+
399
+ /** Search for chunks by symbol name — used by call graph view. */
400
+ export function searchSymbols(dbPath: string, query: string, limit: number = 10): { id: number; name: string; filePath: string }[] {
401
+ const db = openDb(dbPath);
402
+ try {
403
+ if (!tableExists(db, 'code_chunks')) return [];
404
+ return (db.prepare(`
405
+ SELECT id, name, file_path as filePath FROM code_chunks
406
+ WHERE name LIKE ? AND name IS NOT NULL AND name != ''
407
+ ORDER BY name LIMIT ?
408
+ `).all(`%${query}%`, limit) as { id: number; name: string; filePath: string }[]);
409
+ } finally {
410
+ db.close();
411
+ }
412
+ }
413
+
414
+ /** Search result for full-text search. */
415
+ export interface SearchResultItem {
416
+ id: number;
417
+ filePath: string;
418
+ name: string | null;
419
+ chunkType: string;
420
+ startLine: number;
421
+ endLine: number;
422
+ language: string;
423
+ matchContext: string;
424
+ }
425
+
426
+ /** Full-text search across chunk content, names, file paths. Returns matching chunks with context. */
427
+ export function searchChunks(dbPath: string, query: string, limit: number = 30): SearchResultItem[] {
428
+ const db = openDb(dbPath);
429
+ try {
430
+ if (!tableExists(db, 'code_chunks')) return [];
431
+
432
+ // Try FTS first (if fts_code exists)
433
+ if (tableExists(db, 'fts_code')) {
434
+ try {
435
+ const rows = db.prepare(`
436
+ SELECT cc.id, cc.file_path, cc.name, cc.chunk_type, cc.start_line, cc.end_line, cc.language,
437
+ snippet(fts_code, 0, '>>>', '<<<', '...', 40) as match_ctx
438
+ FROM fts_code ft
439
+ JOIN code_chunks cc ON cc.id = ft.rowid
440
+ WHERE fts_code MATCH ?
441
+ ORDER BY rank
442
+ LIMIT ?
443
+ `).all(query, limit) as unknown as { id: number; file_path: string; name: string | null; chunk_type: string; start_line: number; end_line: number; language: string; match_ctx: string }[];
444
+
445
+ return rows.map(r => ({
446
+ id: r.id,
447
+ filePath: r.file_path,
448
+ name: r.name,
449
+ chunkType: r.chunk_type,
450
+ startLine: r.start_line,
451
+ endLine: r.end_line,
452
+ language: r.language,
453
+ matchContext: r.match_ctx,
454
+ }));
455
+ } catch {
456
+ // FTS query parse error — fall through to LIKE
457
+ }
458
+ }
459
+
460
+ // Fallback: LIKE search across name, file_path, content
461
+ const rows = db.prepare(`
462
+ SELECT id, file_path, name, chunk_type, start_line, end_line, language,
463
+ SUBSTR(content, MAX(1, INSTR(LOWER(content), LOWER(?)) - 40), 100) as match_ctx
464
+ FROM code_chunks
465
+ WHERE chunk_type != 'synopsis'
466
+ AND (LOWER(name) LIKE LOWER(?) OR LOWER(file_path) LIKE LOWER(?) OR LOWER(content) LIKE LOWER(?))
467
+ ORDER BY
468
+ CASE WHEN LOWER(name) LIKE LOWER(?) THEN 0
469
+ WHEN LOWER(file_path) LIKE LOWER(?) THEN 1
470
+ ELSE 2 END,
471
+ file_path, start_line
472
+ LIMIT ?
473
+ `).all(query, `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, limit) as unknown as { id: number; file_path: string; name: string | null; chunk_type: string; start_line: number; end_line: number; language: string; match_ctx: string }[];
474
+
475
+ return rows.map(r => ({
476
+ id: r.id,
477
+ filePath: r.file_path,
478
+ name: r.name,
479
+ chunkType: r.chunk_type,
480
+ startLine: r.start_line,
481
+ endLine: r.end_line,
482
+ language: r.language,
483
+ matchContext: r.match_ctx || '',
484
+ }));
485
+ } finally {
486
+ db.close();
487
+ }
488
+ }
489
+
490
+ /** Fetch a single chunk by ID. */
491
+ export function fetchChunkById(dbPath: string, chunkId: number): ChunkInfo | null {
492
+ const db = openDb(dbPath);
493
+ try {
494
+ const r = db.prepare(`
495
+ SELECT id, chunk_type, name, start_line, end_line, content, language
496
+ FROM code_chunks WHERE id = ?
497
+ `).get(chunkId) as { id: number; chunk_type: string; name: string | null; start_line: number; end_line: number; content: string; language: string } | undefined;
498
+
499
+ if (!r) return null;
500
+
501
+ let callsOut: string[] = [];
502
+ let calledBy: string[] = [];
503
+ if (tableExists(db, 'code_call_edges')) {
504
+ callsOut = (db.prepare(`SELECT DISTINCT symbol_name FROM code_call_edges WHERE caller_chunk_id = ?`).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
505
+ calledBy = (db.prepare(`SELECT DISTINCT symbol_name FROM code_call_edges WHERE callee_chunk_id = ?`).all(r.id) as { symbol_name: string }[]).map(row => row.symbol_name);
506
+ }
507
+
508
+ return {
509
+ id: r.id,
510
+ chunkType: r.chunk_type,
511
+ name: r.name,
512
+ startLine: r.start_line,
513
+ endLine: r.end_line,
514
+ content: r.content,
515
+ language: r.language,
516
+ callsOut,
517
+ calledBy,
518
+ };
519
+ } finally {
520
+ db.close();
521
+ }
522
+ }
523
+