@dreb/coding-agent 1.18.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/core/tools/search.d.ts.map +1 -1
  2. package/dist/core/tools/search.js +14 -36
  3. package/dist/core/tools/search.js.map +1 -1
  4. package/package.json +2 -1
  5. package/dist/core/search/chunker.d.ts +0 -21
  6. package/dist/core/search/chunker.d.ts.map +0 -1
  7. package/dist/core/search/chunker.js +0 -51
  8. package/dist/core/search/chunker.js.map +0 -1
  9. package/dist/core/search/db.d.ts +0 -89
  10. package/dist/core/search/db.d.ts.map +0 -1
  11. package/dist/core/search/db.js +0 -406
  12. package/dist/core/search/db.js.map +0 -1
  13. package/dist/core/search/embedder.d.ts +0 -52
  14. package/dist/core/search/embedder.d.ts.map +0 -1
  15. package/dist/core/search/embedder.js +0 -158
  16. package/dist/core/search/embedder.js.map +0 -1
  17. package/dist/core/search/index-manager.d.ts +0 -55
  18. package/dist/core/search/index-manager.d.ts.map +0 -1
  19. package/dist/core/search/index-manager.js +0 -311
  20. package/dist/core/search/index-manager.js.map +0 -1
  21. package/dist/core/search/metrics/bm25.d.ts +0 -10
  22. package/dist/core/search/metrics/bm25.d.ts.map +0 -1
  23. package/dist/core/search/metrics/bm25.js +0 -32
  24. package/dist/core/search/metrics/bm25.js.map +0 -1
  25. package/dist/core/search/metrics/git-recency.d.ts +0 -14
  26. package/dist/core/search/metrics/git-recency.d.ts.map +0 -1
  27. package/dist/core/search/metrics/git-recency.js +0 -123
  28. package/dist/core/search/metrics/git-recency.js.map +0 -1
  29. package/dist/core/search/metrics/import-graph.d.ts +0 -15
  30. package/dist/core/search/metrics/import-graph.d.ts.map +0 -1
  31. package/dist/core/search/metrics/import-graph.js +0 -115
  32. package/dist/core/search/metrics/import-graph.js.map +0 -1
  33. package/dist/core/search/metrics/path-match.d.ts +0 -13
  34. package/dist/core/search/metrics/path-match.d.ts.map +0 -1
  35. package/dist/core/search/metrics/path-match.js +0 -54
  36. package/dist/core/search/metrics/path-match.js.map +0 -1
  37. package/dist/core/search/metrics/symbol-match.d.ts +0 -12
  38. package/dist/core/search/metrics/symbol-match.d.ts.map +0 -1
  39. package/dist/core/search/metrics/symbol-match.js +0 -62
  40. package/dist/core/search/metrics/symbol-match.js.map +0 -1
  41. package/dist/core/search/metrics/tokenize.d.ts +0 -12
  42. package/dist/core/search/metrics/tokenize.d.ts.map +0 -1
  43. package/dist/core/search/metrics/tokenize.js +0 -29
  44. package/dist/core/search/metrics/tokenize.js.map +0 -1
  45. package/dist/core/search/poem.d.ts +0 -38
  46. package/dist/core/search/poem.d.ts.map +0 -1
  47. package/dist/core/search/poem.js +0 -214
  48. package/dist/core/search/poem.js.map +0 -1
  49. package/dist/core/search/query-classifier.d.ts +0 -17
  50. package/dist/core/search/query-classifier.d.ts.map +0 -1
  51. package/dist/core/search/query-classifier.js +0 -54
  52. package/dist/core/search/query-classifier.js.map +0 -1
  53. package/dist/core/search/scanner.d.ts +0 -30
  54. package/dist/core/search/scanner.d.ts.map +0 -1
  55. package/dist/core/search/scanner.js +0 -344
  56. package/dist/core/search/scanner.js.map +0 -1
  57. package/dist/core/search/search.d.ts +0 -51
  58. package/dist/core/search/search.d.ts.map +0 -1
  59. package/dist/core/search/search.js +0 -381
  60. package/dist/core/search/search.js.map +0 -1
  61. package/dist/core/search/text-chunker.d.ts +0 -15
  62. package/dist/core/search/text-chunker.d.ts.map +0 -1
  63. package/dist/core/search/text-chunker.js +0 -580
  64. package/dist/core/search/text-chunker.js.map +0 -1
  65. package/dist/core/search/tree-sitter-chunker.d.ts +0 -25
  66. package/dist/core/search/tree-sitter-chunker.d.ts.map +0 -1
  67. package/dist/core/search/tree-sitter-chunker.js +0 -357
  68. package/dist/core/search/tree-sitter-chunker.js.map +0 -1
  69. package/dist/core/search/types.d.ts +0 -96
  70. package/dist/core/search/types.d.ts.map +0 -1
  71. package/dist/core/search/types.js +0 -6
  72. package/dist/core/search/types.js.map +0 -1
  73. package/dist/core/search/vector-store.d.ts +0 -43
  74. package/dist/core/search/vector-store.d.ts.map +0 -1
  75. package/dist/core/search/vector-store.js +0 -73
  76. package/dist/core/search/vector-store.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/core/tools/search.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAUtF,QAAA,MAAM,YAAY;;;;;;EAQhB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC;AAM1D,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAMD,sCAAsC;AACtC,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EAC3G,KAAK,EAAE,cAAc,wCAAwC,EAAE,KAAK,GAClE,MAAM,CAkBR;AAED,sCAAsC;AACtC,wBAAgB,kBAAkB,CACjC,MAAM,EAAE;IACP,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC5B,EACD,OAAO,EAAE,uBAAuB,EAChC,KAAK,EAAE,cAAc,wCAAwC,EAAE,KAAK,GAClE,MAAM,CAoBR;AAMD,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAcD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC,OAAO,YAAY,EAAE,iBAAiB,CAAC,CAoJ9G;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,YAAY,CAAC,CAE5E","sourcesContent":["/**\n * Semantic codebase search tool.\n *\n * Uses embeddings + FTS5 to support natural language queries over the codebase.\n * Feature-gated on `node:sqlite` availability (Node 22+).\n */\n\nimport { existsSync, statSync } from \"node:fs\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { SearchEngine } from \"../search/search.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst searchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query (natural language, identifier, or path)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Restrict search to files under this path (relative to cwd)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results to return (default: 20)\" })),\n\tprojectDir: Type.Optional(\n\t\tType.String({ description: \"Directory to index and search instead of cwd (useful when cwd is ~/)\" }),\n\t),\n\trebuild: Type.Optional(Type.Boolean({ description: \"Force a clean rebuild of the search index (default: false)\" })),\n});\n\nexport type SearchToolInput = Static<typeof searchSchema>;\n\n// ============================================================================\n// Details\n// ============================================================================\n\nexport interface SearchToolDetails {\n\tresultCount: number;\n\tindexBuilt: boolean;\n\tindexStats?: { files: number; chunks: number };\n}\n\n// ============================================================================\n// Rendering\n// ============================================================================\n\n/** @internal Exported for testing. */\nexport function formatSearchCall(\n\targs: { query?: string; path?: string; limit?: number; projectDir?: string; rebuild?: boolean } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst searchPath = str(args?.path);\n\tconst projectDir = str(args?.projectDir);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"search\"))} ${theme.fg(\"accent\", `\"${query ?? \"\"}\"`)}`;\n\tif (projectDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` project ${shortenPath(projectDir)}`);\n\t}\n\tif (searchPath) {\n\t\ttext += theme.fg(\"toolOutput\", ` in ${shortenPath(searchPath)}`);\n\t}\n\tif (args?.rebuild) {\n\t\ttext += theme.fg(\"toolOutput\", \" [rebuild]\");\n\t}\n\tif (args?.limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` limit ${args.limit}`);\n\t}\n\treturn text;\n}\n\n/** @internal Exported for testing. */\nexport function formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: SearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst output = result.content[0]?.text?.trim() ?? \"\";\n\tif (!output) return \"\";\n\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 20;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tlet text = `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `\\n${theme.fg(\"muted\", `... (${remaining} more lines)`)}`;\n\t}\n\n\tif (result.details?.indexStats) {\n\t\tconst { files, chunks } = result.details.indexStats;\n\t\ttext += `\\n${theme.fg(\"muted\", `[Index: ${files} files, ${chunks} chunks]`)}`;\n\t}\n\n\treturn text;\n}\n\n// ============================================================================\n// Tool Definition\n// ============================================================================\n\n/** Check if the search tool is available (requires node:sqlite). */\nexport function isSearchAvailable(): boolean {\n\treturn SearchEngine.isAvailable();\n}\n\n// Cache search engines per project root to reuse index across calls within a session\nconst engineCache = new Map<string, SearchEngine>();\n\nfunction getSearchEngine(projectRoot: string): SearchEngine {\n\tlet engine = engineCache.get(projectRoot);\n\tif (!engine) {\n\t\tengine = new SearchEngine(projectRoot);\n\t\tengineCache.set(projectRoot, engine);\n\t}\n\treturn engine;\n}\n\nexport function createSearchToolDefinition(cwd: string): ToolDefinition<typeof searchSchema, SearchToolDetails> {\n\treturn {\n\t\tname: \"search\",\n\t\tlabel: \"search\",\n\t\tdescription:\n\t\t\t\"Search the codebase using natural language queries. Returns ranked code/doc results using semantic similarity and keyword matching. First query builds the index (may take a moment); subsequent queries are fast. Supports identifier queries (e.g. 'AuthMiddleware'), natural language (e.g. 'where is rate limiting handled'), and path queries (e.g. 'src/auth/').\",\n\t\tpromptSnippet: \"Semantic codebase search — natural language queries over code and docs\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use `search` as your default exploration tool — for understanding code, finding where things are, and answering questions about the codebase. Use `grep` when you already know the exact text or pattern you're looking for.\",\n\t\t\t\"The first search query builds an index (may take 10-60s). Subsequent queries are fast.\",\n\t\t],\n\t\tparameters: searchSchema,\n\n\t\tasync execute(_toolCallId, params, signal, onUpdate, _ctx) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tif (!isSearchAvailable()) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Semantic search requires Node.js 22+ (for built-in SQLite). Current Node.js version does not support node:sqlite.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst { query, path: searchPath, limit, projectDir, rebuild } = params;\n\n\t\t\tif (!query || query.trim().length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Search query cannot be empty.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst resolvedProjectDir = projectDir ? resolveToCwd(projectDir, cwd) : cwd;\n\n\t\t\tif (projectDir && (!existsSync(resolvedProjectDir) || !statSync(resolvedProjectDir).isDirectory())) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `projectDir does not exist or is not a directory: ${resolvedProjectDir}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst engine = getSearchEngine(resolvedProjectDir);\n\n\t\t\tif (rebuild) {\n\t\t\t\tengine.resetIndex();\n\t\t\t}\n\n\t\t\tlet indexBuilt = false;\n\t\t\tconst results = await engine.search(query, {\n\t\t\t\tlimit: limit ?? 20,\n\t\t\t\tpathFilter: searchPath,\n\t\t\t\tonProgress: (phase, current, total) => {\n\t\t\t\t\tif (phase === \"indexing\" || phase === \"scanning\" || phase === \"loading model\" || phase === \"embedding\") {\n\t\t\t\t\t\tindexBuilt = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tonUpdate({\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `${phase}: ${current}/${total}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: true } as SearchToolDetails,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No results found.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Format results\n\t\t\tconst lines: string[] = [];\n\t\t\tfor (let i = 0; i < results.length; i++) {\n\t\t\t\tconst r = results[i];\n\t\t\t\tconst { chunk, scores } = r;\n\n\t\t\t\t// Header line with file path and line range\n\t\t\t\tconst lineRange =\n\t\t\t\t\tchunk.startLine === chunk.endLine ? `L${chunk.startLine}` : `L${chunk.startLine}-${chunk.endLine}`;\n\t\t\t\tconst kindLabel = chunk.name ? `${chunk.kind} ${chunk.name}` : chunk.kind;\n\n\t\t\t\tlines.push(`${i + 1}. ${chunk.filePath}:${lineRange} (${kindLabel})`);\n\n\t\t\t\t// Score summary — show top contributing metrics\n\t\t\t\tconst topScores = Object.entries(scores)\n\t\t\t\t\t.filter(([, v]) => v > 0.01)\n\t\t\t\t\t.sort(([, a], [, b]) => b - a)\n\t\t\t\t\t.map(([k, v]) => `${k}=${v.toFixed(2)}`)\n\t\t\t\t\t.join(\" \");\n\t\t\t\tif (topScores) {\n\t\t\t\t\tlines.push(` scores: ${topScores}`);\n\t\t\t\t}\n\n\t\t\t\t// Content preview (first 3 lines)\n\t\t\t\tconst contentLines = chunk.content.split(\"\\n\");\n\t\t\t\tconst previewLines = contentLines.slice(0, 3);\n\t\t\t\tfor (const line of previewLines) {\n\t\t\t\t\tconst trimmed = line.length > 120 ? `${line.slice(0, 117)}...` : line;\n\t\t\t\t\tlines.push(` ${trimmed}`);\n\t\t\t\t}\n\t\t\t\tif (contentLines.length > 3) {\n\t\t\t\t\tlines.push(` ... (${contentLines.length - 3} more lines)`);\n\t\t\t\t}\n\n\t\t\t\tif (i < results.length - 1) lines.push(\"\");\n\t\t\t}\n\n\t\t\t// Get index stats from the existing engine (no new connection)\n\t\t\tconst stats = engine.getStats();\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: lines.join(\"\\n\") }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: results.length,\n\t\t\t\t\tindexBuilt,\n\t\t\t\t\tindexStats: stats ?? undefined,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createSearchTool(cwd: string): AgentTool<typeof searchSchema> {\n\treturn wrapToolDefinition(createSearchToolDefinition(cwd));\n}\n"]}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/core/tools/search.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAUtF,QAAA,MAAM,YAAY;;;;;;EAQhB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC;AAM1D,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAMD,sCAAsC;AACtC,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EAC3G,KAAK,EAAE,cAAc,wCAAwC,EAAE,KAAK,GAClE,MAAM,CAkBR;AAED,sCAAsC;AACtC,wBAAgB,kBAAkB,CACjC,MAAM,EAAE;IACP,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,OAAO,CAAC,EAAE,iBAAiB,CAAC;CAC5B,EACD,OAAO,EAAE,uBAAuB,EAChC,KAAK,EAAE,cAAc,wCAAwC,EAAE,KAAK,GAClE,MAAM,CAoBR;AAMD,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAmBD,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC,OAAO,YAAY,EAAE,iBAAiB,CAAC,CAiH9G;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC,OAAO,YAAY,CAAC,CAE5E","sourcesContent":["/**\n * Semantic codebase search tool.\n *\n * Uses embeddings + FTS5 to support natural language queries over the codebase.\n * Feature-gated on `node:sqlite` availability (Node 22+).\n */\n\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { formatResults, SearchEngine } from \"@dreb/semantic-search\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { getDrebToolVisibleDirs } from \"./dreb-paths.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst searchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query (natural language, identifier, or path)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Restrict search to files under this path (relative to cwd)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results to return (default: 20)\" })),\n\tprojectDir: Type.Optional(\n\t\tType.String({ description: \"Directory to index and search instead of cwd (useful when cwd is ~/)\" }),\n\t),\n\trebuild: Type.Optional(Type.Boolean({ description: \"Force a clean rebuild of the search index (default: false)\" })),\n});\n\nexport type SearchToolInput = Static<typeof searchSchema>;\n\n// ============================================================================\n// Details\n// ============================================================================\n\nexport interface SearchToolDetails {\n\tresultCount: number;\n\tindexBuilt: boolean;\n\tindexStats?: { files: number; chunks: number };\n}\n\n// ============================================================================\n// Rendering\n// ============================================================================\n\n/** @internal Exported for testing. */\nexport function formatSearchCall(\n\targs: { query?: string; path?: string; limit?: number; projectDir?: string; rebuild?: boolean } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst searchPath = str(args?.path);\n\tconst projectDir = str(args?.projectDir);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"search\"))} ${theme.fg(\"accent\", `\"${query ?? \"\"}\"`)}`;\n\tif (projectDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` project ${shortenPath(projectDir)}`);\n\t}\n\tif (searchPath) {\n\t\ttext += theme.fg(\"toolOutput\", ` in ${shortenPath(searchPath)}`);\n\t}\n\tif (args?.rebuild) {\n\t\ttext += theme.fg(\"toolOutput\", \" [rebuild]\");\n\t}\n\tif (args?.limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` limit ${args.limit}`);\n\t}\n\treturn text;\n}\n\n/** @internal Exported for testing. */\nexport function formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: SearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst output = result.content[0]?.text?.trim() ?? \"\";\n\tif (!output) return \"\";\n\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 20;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tlet text = `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `\\n${theme.fg(\"muted\", `... (${remaining} more lines)`)}`;\n\t}\n\n\tif (result.details?.indexStats) {\n\t\tconst { files, chunks } = result.details.indexStats;\n\t\ttext += `\\n${theme.fg(\"muted\", `[Index: ${files} files, ${chunks} chunks]`)}`;\n\t}\n\n\treturn text;\n}\n\n// ============================================================================\n// Tool Definition\n// ============================================================================\n\n/** Check if the search tool is available (requires node:sqlite). */\nexport function isSearchAvailable(): boolean {\n\treturn SearchEngine.isAvailable();\n}\n\n// Cache search engines per project root to reuse index across calls within a session\nconst engineCache = new Map<string, SearchEngine>();\n\nfunction getSearchEngine(projectRoot: string): SearchEngine {\n\tlet engine = engineCache.get(projectRoot);\n\tif (!engine) {\n\t\tengine = new SearchEngine(projectRoot, {\n\t\t\tindexDir: path.join(projectRoot, \".dreb\", \"index\"),\n\t\t\tglobalMemoryDir: path.join(homedir(), \".dreb\", \"memory\"),\n\t\t\tmodelCacheDir: path.join(homedir(), \".dreb\", \"agent\", \"models\"),\n\t\t\tvisibleDirs: getDrebToolVisibleDirs,\n\t\t});\n\t\tengineCache.set(projectRoot, engine);\n\t}\n\treturn engine;\n}\n\nexport function createSearchToolDefinition(cwd: string): ToolDefinition<typeof searchSchema, SearchToolDetails> {\n\treturn {\n\t\tname: \"search\",\n\t\tlabel: \"search\",\n\t\tdescription:\n\t\t\t\"Search the codebase using natural language queries. Returns ranked code/doc results using semantic similarity and keyword matching. First query builds the index (may take a moment); subsequent queries are fast. Supports identifier queries (e.g. 'AuthMiddleware'), natural language (e.g. 'where is rate limiting handled'), and path queries (e.g. 'src/auth/').\",\n\t\tpromptSnippet: \"Semantic codebase search — natural language queries over code and docs\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use `search` as your default exploration tool — for understanding code, finding where things are, and answering questions about the codebase. Use `grep` when you already know the exact text or pattern you're looking for.\",\n\t\t\t\"The first search query builds an index (may take 10-60s). Subsequent queries are fast.\",\n\t\t],\n\t\tparameters: searchSchema,\n\n\t\tasync execute(_toolCallId, params, signal, onUpdate, _ctx) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tif (!isSearchAvailable()) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Semantic search requires Node.js 22+ (for built-in SQLite). Current Node.js version does not support node:sqlite.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst { query, path: searchPath, limit, projectDir, rebuild } = params;\n\n\t\t\tif (!query || query.trim().length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Search query cannot be empty.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst resolvedProjectDir = projectDir ? resolveToCwd(projectDir, cwd) : cwd;\n\n\t\t\tif (projectDir && (!existsSync(resolvedProjectDir) || !statSync(resolvedProjectDir).isDirectory())) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `projectDir does not exist or is not a directory: ${resolvedProjectDir}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst engine = getSearchEngine(resolvedProjectDir);\n\n\t\t\tif (rebuild) {\n\t\t\t\tawait engine.resetIndex();\n\t\t\t}\n\n\t\t\tlet indexBuilt = false;\n\t\t\tconst results = await engine.search(query, {\n\t\t\t\tlimit: typeof limit === \"number\" && limit > 0 ? Math.floor(limit) : 20,\n\t\t\t\tpathFilter: searchPath,\n\t\t\t\tonProgress: (phase, current, total) => {\n\t\t\t\t\tif (phase === \"indexing\" || phase === \"scanning\" || phase === \"loading model\" || phase === \"embedding\") {\n\t\t\t\t\t\tindexBuilt = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tonUpdate({\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `${phase}: ${current}/${total}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: true } as SearchToolDetails,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No results found.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = formatResults(results);\n\n\t\t\t// Get index stats from the existing engine (no new connection)\n\t\t\tconst stats = engine.getStats();\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: results.length,\n\t\t\t\t\tindexBuilt,\n\t\t\t\t\tindexStats: stats ?? undefined,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createSearchTool(cwd: string): AgentTool<typeof searchSchema> {\n\treturn wrapToolDefinition(createSearchToolDefinition(cwd));\n}\n"]}
@@ -5,9 +5,12 @@
5
5
  * Feature-gated on `node:sqlite` availability (Node 22+).
6
6
  */
7
7
  import { existsSync, statSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import path from "node:path";
10
+ import { formatResults, SearchEngine } from "@dreb/semantic-search";
8
11
  import { Text } from "@dreb/tui";
9
12
  import { Type } from "@sinclair/typebox";
10
- import { SearchEngine } from "../search/search.js";
13
+ import { getDrebToolVisibleDirs } from "./dreb-paths.js";
11
14
  import { resolveToCwd } from "./path-utils.js";
12
15
  import { shortenPath, str } from "./render-utils.js";
13
16
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
@@ -75,7 +78,12 @@ const engineCache = new Map();
75
78
  function getSearchEngine(projectRoot) {
76
79
  let engine = engineCache.get(projectRoot);
77
80
  if (!engine) {
78
- engine = new SearchEngine(projectRoot);
81
+ engine = new SearchEngine(projectRoot, {
82
+ indexDir: path.join(projectRoot, ".dreb", "index"),
83
+ globalMemoryDir: path.join(homedir(), ".dreb", "memory"),
84
+ modelCacheDir: path.join(homedir(), ".dreb", "agent", "models"),
85
+ visibleDirs: getDrebToolVisibleDirs,
86
+ });
79
87
  engineCache.set(projectRoot, engine);
80
88
  }
81
89
  return engine;
@@ -126,11 +134,11 @@ export function createSearchToolDefinition(cwd) {
126
134
  }
127
135
  const engine = getSearchEngine(resolvedProjectDir);
128
136
  if (rebuild) {
129
- engine.resetIndex();
137
+ await engine.resetIndex();
130
138
  }
131
139
  let indexBuilt = false;
132
140
  const results = await engine.search(query, {
133
- limit: limit ?? 20,
141
+ limit: typeof limit === "number" && limit > 0 ? Math.floor(limit) : 20,
134
142
  pathFilter: searchPath,
135
143
  onProgress: (phase, current, total) => {
136
144
  if (phase === "indexing" || phase === "scanning" || phase === "loading model" || phase === "embedding") {
@@ -155,41 +163,11 @@ export function createSearchToolDefinition(cwd) {
155
163
  details: { resultCount: 0, indexBuilt },
156
164
  };
157
165
  }
158
- // Format results
159
- const lines = [];
160
- for (let i = 0; i < results.length; i++) {
161
- const r = results[i];
162
- const { chunk, scores } = r;
163
- // Header line with file path and line range
164
- const lineRange = chunk.startLine === chunk.endLine ? `L${chunk.startLine}` : `L${chunk.startLine}-${chunk.endLine}`;
165
- const kindLabel = chunk.name ? `${chunk.kind} ${chunk.name}` : chunk.kind;
166
- lines.push(`${i + 1}. ${chunk.filePath}:${lineRange} (${kindLabel})`);
167
- // Score summary — show top contributing metrics
168
- const topScores = Object.entries(scores)
169
- .filter(([, v]) => v > 0.01)
170
- .sort(([, a], [, b]) => b - a)
171
- .map(([k, v]) => `${k}=${v.toFixed(2)}`)
172
- .join(" ");
173
- if (topScores) {
174
- lines.push(` scores: ${topScores}`);
175
- }
176
- // Content preview (first 3 lines)
177
- const contentLines = chunk.content.split("\n");
178
- const previewLines = contentLines.slice(0, 3);
179
- for (const line of previewLines) {
180
- const trimmed = line.length > 120 ? `${line.slice(0, 117)}...` : line;
181
- lines.push(` ${trimmed}`);
182
- }
183
- if (contentLines.length > 3) {
184
- lines.push(` ... (${contentLines.length - 3} more lines)`);
185
- }
186
- if (i < results.length - 1)
187
- lines.push("");
188
- }
166
+ const text = formatResults(results);
189
167
  // Get index stats from the existing engine (no new connection)
190
168
  const stats = engine.getStats();
191
169
  return {
192
- content: [{ type: "text", text: lines.join("\n") }],
170
+ content: [{ type: "text", text }],
193
171
  details: {
194
172
  resultCount: results.length,
195
173
  indexBuilt,
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/core/tools/search.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Semantic codebase search tool.\n *\n * Uses embeddings + FTS5 to support natural language queries over the codebase.\n * Feature-gated on `node:sqlite` availability (Node 22+).\n */\n\nimport { existsSync, statSync } from \"node:fs\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { SearchEngine } from \"../search/search.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst searchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query (natural language, identifier, or path)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Restrict search to files under this path (relative to cwd)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results to return (default: 20)\" })),\n\tprojectDir: Type.Optional(\n\t\tType.String({ description: \"Directory to index and search instead of cwd (useful when cwd is ~/)\" }),\n\t),\n\trebuild: Type.Optional(Type.Boolean({ description: \"Force a clean rebuild of the search index (default: false)\" })),\n});\n\nexport type SearchToolInput = Static<typeof searchSchema>;\n\n// ============================================================================\n// Details\n// ============================================================================\n\nexport interface SearchToolDetails {\n\tresultCount: number;\n\tindexBuilt: boolean;\n\tindexStats?: { files: number; chunks: number };\n}\n\n// ============================================================================\n// Rendering\n// ============================================================================\n\n/** @internal Exported for testing. */\nexport function formatSearchCall(\n\targs: { query?: string; path?: string; limit?: number; projectDir?: string; rebuild?: boolean } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst searchPath = str(args?.path);\n\tconst projectDir = str(args?.projectDir);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"search\"))} ${theme.fg(\"accent\", `\"${query ?? \"\"}\"`)}`;\n\tif (projectDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` project ${shortenPath(projectDir)}`);\n\t}\n\tif (searchPath) {\n\t\ttext += theme.fg(\"toolOutput\", ` in ${shortenPath(searchPath)}`);\n\t}\n\tif (args?.rebuild) {\n\t\ttext += theme.fg(\"toolOutput\", \" [rebuild]\");\n\t}\n\tif (args?.limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` limit ${args.limit}`);\n\t}\n\treturn text;\n}\n\n/** @internal Exported for testing. */\nexport function formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: SearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst output = result.content[0]?.text?.trim() ?? \"\";\n\tif (!output) return \"\";\n\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 20;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tlet text = `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `\\n${theme.fg(\"muted\", `... (${remaining} more lines)`)}`;\n\t}\n\n\tif (result.details?.indexStats) {\n\t\tconst { files, chunks } = result.details.indexStats;\n\t\ttext += `\\n${theme.fg(\"muted\", `[Index: ${files} files, ${chunks} chunks]`)}`;\n\t}\n\n\treturn text;\n}\n\n// ============================================================================\n// Tool Definition\n// ============================================================================\n\n/** Check if the search tool is available (requires node:sqlite). */\nexport function isSearchAvailable(): boolean {\n\treturn SearchEngine.isAvailable();\n}\n\n// Cache search engines per project root to reuse index across calls within a session\nconst engineCache = new Map<string, SearchEngine>();\n\nfunction getSearchEngine(projectRoot: string): SearchEngine {\n\tlet engine = engineCache.get(projectRoot);\n\tif (!engine) {\n\t\tengine = new SearchEngine(projectRoot);\n\t\tengineCache.set(projectRoot, engine);\n\t}\n\treturn engine;\n}\n\nexport function createSearchToolDefinition(cwd: string): ToolDefinition<typeof searchSchema, SearchToolDetails> {\n\treturn {\n\t\tname: \"search\",\n\t\tlabel: \"search\",\n\t\tdescription:\n\t\t\t\"Search the codebase using natural language queries. Returns ranked code/doc results using semantic similarity and keyword matching. First query builds the index (may take a moment); subsequent queries are fast. Supports identifier queries (e.g. 'AuthMiddleware'), natural language (e.g. 'where is rate limiting handled'), and path queries (e.g. 'src/auth/').\",\n\t\tpromptSnippet: \"Semantic codebase search — natural language queries over code and docs\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use `search` as your default exploration tool — for understanding code, finding where things are, and answering questions about the codebase. Use `grep` when you already know the exact text or pattern you're looking for.\",\n\t\t\t\"The first search query builds an index (may take 10-60s). Subsequent queries are fast.\",\n\t\t],\n\t\tparameters: searchSchema,\n\n\t\tasync execute(_toolCallId, params, signal, onUpdate, _ctx) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tif (!isSearchAvailable()) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Semantic search requires Node.js 22+ (for built-in SQLite). Current Node.js version does not support node:sqlite.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst { query, path: searchPath, limit, projectDir, rebuild } = params;\n\n\t\t\tif (!query || query.trim().length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Search query cannot be empty.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst resolvedProjectDir = projectDir ? resolveToCwd(projectDir, cwd) : cwd;\n\n\t\t\tif (projectDir && (!existsSync(resolvedProjectDir) || !statSync(resolvedProjectDir).isDirectory())) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `projectDir does not exist or is not a directory: ${resolvedProjectDir}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst engine = getSearchEngine(resolvedProjectDir);\n\n\t\t\tif (rebuild) {\n\t\t\t\tengine.resetIndex();\n\t\t\t}\n\n\t\t\tlet indexBuilt = false;\n\t\t\tconst results = await engine.search(query, {\n\t\t\t\tlimit: limit ?? 20,\n\t\t\t\tpathFilter: searchPath,\n\t\t\t\tonProgress: (phase, current, total) => {\n\t\t\t\t\tif (phase === \"indexing\" || phase === \"scanning\" || phase === \"loading model\" || phase === \"embedding\") {\n\t\t\t\t\t\tindexBuilt = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tonUpdate({\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `${phase}: ${current}/${total}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: true } as SearchToolDetails,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No results found.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Format results\n\t\t\tconst lines: string[] = [];\n\t\t\tfor (let i = 0; i < results.length; i++) {\n\t\t\t\tconst r = results[i];\n\t\t\t\tconst { chunk, scores } = r;\n\n\t\t\t\t// Header line with file path and line range\n\t\t\t\tconst lineRange =\n\t\t\t\t\tchunk.startLine === chunk.endLine ? `L${chunk.startLine}` : `L${chunk.startLine}-${chunk.endLine}`;\n\t\t\t\tconst kindLabel = chunk.name ? `${chunk.kind} ${chunk.name}` : chunk.kind;\n\n\t\t\t\tlines.push(`${i + 1}. ${chunk.filePath}:${lineRange} (${kindLabel})`);\n\n\t\t\t\t// Score summary — show top contributing metrics\n\t\t\t\tconst topScores = Object.entries(scores)\n\t\t\t\t\t.filter(([, v]) => v > 0.01)\n\t\t\t\t\t.sort(([, a], [, b]) => b - a)\n\t\t\t\t\t.map(([k, v]) => `${k}=${v.toFixed(2)}`)\n\t\t\t\t\t.join(\" \");\n\t\t\t\tif (topScores) {\n\t\t\t\t\tlines.push(` scores: ${topScores}`);\n\t\t\t\t}\n\n\t\t\t\t// Content preview (first 3 lines)\n\t\t\t\tconst contentLines = chunk.content.split(\"\\n\");\n\t\t\t\tconst previewLines = contentLines.slice(0, 3);\n\t\t\t\tfor (const line of previewLines) {\n\t\t\t\t\tconst trimmed = line.length > 120 ? `${line.slice(0, 117)}...` : line;\n\t\t\t\t\tlines.push(` ${trimmed}`);\n\t\t\t\t}\n\t\t\t\tif (contentLines.length > 3) {\n\t\t\t\t\tlines.push(` ... (${contentLines.length - 3} more lines)`);\n\t\t\t\t}\n\n\t\t\t\tif (i < results.length - 1) lines.push(\"\");\n\t\t\t}\n\n\t\t\t// Get index stats from the existing engine (no new connection)\n\t\t\tconst stats = engine.getStats();\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: lines.join(\"\\n\") }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: results.length,\n\t\t\t\t\tindexBuilt,\n\t\t\t\t\tindexStats: stats ?? undefined,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createSearchTool(cwd: string): AgentTool<typeof searchSchema> {\n\treturn wrapToolDefinition(createSearchToolDefinition(cwd));\n}\n"]}
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/core/tools/search.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Semantic codebase search tool.\n *\n * Uses embeddings + FTS5 to support natural language queries over the codebase.\n * Feature-gated on `node:sqlite` availability (Node 22+).\n */\n\nimport { existsSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { AgentTool } from \"@dreb/agent-core\";\nimport { formatResults, SearchEngine } from \"@dreb/semantic-search\";\nimport { Text } from \"@dreb/tui\";\nimport { type Static, Type } from \"@sinclair/typebox\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { getDrebToolVisibleDirs } from \"./dreb-paths.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\n\n// ============================================================================\n// Schema\n// ============================================================================\n\nconst searchSchema = Type.Object({\n\tquery: Type.String({ description: \"The search query (natural language, identifier, or path)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Restrict search to files under this path (relative to cwd)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results to return (default: 20)\" })),\n\tprojectDir: Type.Optional(\n\t\tType.String({ description: \"Directory to index and search instead of cwd (useful when cwd is ~/)\" }),\n\t),\n\trebuild: Type.Optional(Type.Boolean({ description: \"Force a clean rebuild of the search index (default: false)\" })),\n});\n\nexport type SearchToolInput = Static<typeof searchSchema>;\n\n// ============================================================================\n// Details\n// ============================================================================\n\nexport interface SearchToolDetails {\n\tresultCount: number;\n\tindexBuilt: boolean;\n\tindexStats?: { files: number; chunks: number };\n}\n\n// ============================================================================\n// Rendering\n// ============================================================================\n\n/** @internal Exported for testing. */\nexport function formatSearchCall(\n\targs: { query?: string; path?: string; limit?: number; projectDir?: string; rebuild?: boolean } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst searchPath = str(args?.path);\n\tconst projectDir = str(args?.projectDir);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"search\"))} ${theme.fg(\"accent\", `\"${query ?? \"\"}\"`)}`;\n\tif (projectDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` project ${shortenPath(projectDir)}`);\n\t}\n\tif (searchPath) {\n\t\ttext += theme.fg(\"toolOutput\", ` in ${shortenPath(searchPath)}`);\n\t}\n\tif (args?.rebuild) {\n\t\ttext += theme.fg(\"toolOutput\", \" [rebuild]\");\n\t}\n\tif (args?.limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` limit ${args.limit}`);\n\t}\n\treturn text;\n}\n\n/** @internal Exported for testing. */\nexport function formatSearchResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string }>;\n\t\tdetails?: SearchToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst output = result.content[0]?.text?.trim() ?? \"\";\n\tif (!output) return \"\";\n\n\tconst lines = output.split(\"\\n\");\n\tconst maxLines = options.expanded ? lines.length : 20;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\n\tlet text = `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `\\n${theme.fg(\"muted\", `... (${remaining} more lines)`)}`;\n\t}\n\n\tif (result.details?.indexStats) {\n\t\tconst { files, chunks } = result.details.indexStats;\n\t\ttext += `\\n${theme.fg(\"muted\", `[Index: ${files} files, ${chunks} chunks]`)}`;\n\t}\n\n\treturn text;\n}\n\n// ============================================================================\n// Tool Definition\n// ============================================================================\n\n/** Check if the search tool is available (requires node:sqlite). */\nexport function isSearchAvailable(): boolean {\n\treturn SearchEngine.isAvailable();\n}\n\n// Cache search engines per project root to reuse index across calls within a session\nconst engineCache = new Map<string, SearchEngine>();\n\nfunction getSearchEngine(projectRoot: string): SearchEngine {\n\tlet engine = engineCache.get(projectRoot);\n\tif (!engine) {\n\t\tengine = new SearchEngine(projectRoot, {\n\t\t\tindexDir: path.join(projectRoot, \".dreb\", \"index\"),\n\t\t\tglobalMemoryDir: path.join(homedir(), \".dreb\", \"memory\"),\n\t\t\tmodelCacheDir: path.join(homedir(), \".dreb\", \"agent\", \"models\"),\n\t\t\tvisibleDirs: getDrebToolVisibleDirs,\n\t\t});\n\t\tengineCache.set(projectRoot, engine);\n\t}\n\treturn engine;\n}\n\nexport function createSearchToolDefinition(cwd: string): ToolDefinition<typeof searchSchema, SearchToolDetails> {\n\treturn {\n\t\tname: \"search\",\n\t\tlabel: \"search\",\n\t\tdescription:\n\t\t\t\"Search the codebase using natural language queries. Returns ranked code/doc results using semantic similarity and keyword matching. First query builds the index (may take a moment); subsequent queries are fast. Supports identifier queries (e.g. 'AuthMiddleware'), natural language (e.g. 'where is rate limiting handled'), and path queries (e.g. 'src/auth/').\",\n\t\tpromptSnippet: \"Semantic codebase search — natural language queries over code and docs\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use `search` as your default exploration tool — for understanding code, finding where things are, and answering questions about the codebase. Use `grep` when you already know the exact text or pattern you're looking for.\",\n\t\t\t\"The first search query builds an index (may take 10-60s). Subsequent queries are fast.\",\n\t\t],\n\t\tparameters: searchSchema,\n\n\t\tasync execute(_toolCallId, params, signal, onUpdate, _ctx) {\n\t\t\tif (signal?.aborted) throw new Error(\"Operation aborted\");\n\n\t\t\tif (!isSearchAvailable()) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: \"Semantic search requires Node.js 22+ (for built-in SQLite). Current Node.js version does not support node:sqlite.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst { query, path: searchPath, limit, projectDir, rebuild } = params;\n\n\t\t\tif (!query || query.trim().length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"Search query cannot be empty.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst resolvedProjectDir = projectDir ? resolveToCwd(projectDir, cwd) : cwd;\n\n\t\t\tif (projectDir && (!existsSync(resolvedProjectDir) || !statSync(resolvedProjectDir).isDirectory())) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `projectDir does not exist or is not a directory: ${resolvedProjectDir}`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: false },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst engine = getSearchEngine(resolvedProjectDir);\n\n\t\t\tif (rebuild) {\n\t\t\t\tawait engine.resetIndex();\n\t\t\t}\n\n\t\t\tlet indexBuilt = false;\n\t\t\tconst results = await engine.search(query, {\n\t\t\t\tlimit: typeof limit === \"number\" && limit > 0 ? Math.floor(limit) : 20,\n\t\t\t\tpathFilter: searchPath,\n\t\t\t\tonProgress: (phase, current, total) => {\n\t\t\t\t\tif (phase === \"indexing\" || phase === \"scanning\" || phase === \"loading model\" || phase === \"embedding\") {\n\t\t\t\t\t\tindexBuilt = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (onUpdate) {\n\t\t\t\t\t\tonUpdate({\n\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\ttext: `${phase}: ${current}/${total}`,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tdetails: { resultCount: 0, indexBuilt: true } as SearchToolDetails,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tif (results.length === 0) {\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [{ type: \"text\", text: \"No results found.\" }],\n\t\t\t\t\tdetails: { resultCount: 0, indexBuilt },\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst text = formatResults(results);\n\n\t\t\t// Get index stats from the existing engine (no new connection)\n\t\t\tconst stats = engine.getStats();\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text }],\n\t\t\t\tdetails: {\n\t\t\t\t\tresultCount: results.length,\n\t\t\t\t\tindexBuilt,\n\t\t\t\t\tindexStats: stats ?? undefined,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatSearchResult(result as any, options, theme));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createSearchTool(cwd: string): AgentTool<typeof searchSchema> {\n\treturn wrapToolDefinition(createSearchToolDefinition(cwd));\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreb/coding-agent",
3
- "version": "1.18.0",
3
+ "version": "2.0.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drebConfig": {
@@ -54,6 +54,7 @@
54
54
  "dependencies": {
55
55
  "@dreb/agent-core": "^1.0.0",
56
56
  "@dreb/ai": "^1.0.0",
57
+ "@dreb/semantic-search": "^1.0.0",
57
58
  "@dreb/tui": "^1.0.0",
58
59
  "@huggingface/transformers": "^4.0.1",
59
60
  "@mariozechner/jiti": "^2.6.2",
@@ -1,21 +0,0 @@
1
- /**
2
- * Chunking coordinator for the semantic search subsystem.
3
- *
4
- * Dispatches to the tree-sitter AST chunker for code files and the
5
- * text chunker for non-code files (markdown, YAML, JSON, etc.).
6
- */
7
- import type { Chunk, FileType } from "./types.js";
8
- /**
9
- * Chunk a file's content into semantically meaningful pieces.
10
- *
11
- * For code files, uses tree-sitter to parse the AST and extract functions,
12
- * classes, methods, etc. For text files, uses format-specific splitting rules.
13
- *
14
- * If tree-sitter parsing fails for a code file, falls back to plaintext chunking.
15
- *
16
- * @param content - Raw file content
17
- * @param filePath - Relative file path (stored in chunk metadata)
18
- * @param fileType - Detected file type
19
- */
20
- export declare function chunkFile(content: string, filePath: string, fileType: FileType): Promise<Chunk[]>;
21
- //# sourceMappingURL=chunker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../../src/core/search/chunker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAoC,MAAM,YAAY,CAAC;AAsBpF;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAYvG","sourcesContent":["/**\n * Chunking coordinator for the semantic search subsystem.\n *\n * Dispatches to the tree-sitter AST chunker for code files and the\n * text chunker for non-code files (markdown, YAML, JSON, etc.).\n */\n\nimport { chunkTextFile } from \"./text-chunker.js\";\nimport { chunkWithTreeSitter, initTreeSitter } from \"./tree-sitter-chunker.js\";\nimport type { Chunk, FileType, TextFileType, TreeSitterLanguage } from \"./types.js\";\n\n// ============================================================================\n// Language Sets\n// ============================================================================\n\nconst TREE_SITTER_LANGUAGES: Set<string> = new Set([\n\t\"typescript\",\n\t\"tsx\",\n\t\"javascript\",\n\t\"python\",\n\t\"go\",\n\t\"rust\",\n\t\"java\",\n\t\"c\",\n\t\"cpp\",\n]);\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Chunk a file's content into semantically meaningful pieces.\n *\n * For code files, uses tree-sitter to parse the AST and extract functions,\n * classes, methods, etc. For text files, uses format-specific splitting rules.\n *\n * If tree-sitter parsing fails for a code file, falls back to plaintext chunking.\n *\n * @param content - Raw file content\n * @param filePath - Relative file path (stored in chunk metadata)\n * @param fileType - Detected file type\n */\nexport async function chunkFile(content: string, filePath: string, fileType: FileType): Promise<Chunk[]> {\n\tif (TREE_SITTER_LANGUAGES.has(fileType)) {\n\t\ttry {\n\t\t\tawait initTreeSitter();\n\t\t\treturn await chunkWithTreeSitter(content, filePath, fileType as TreeSitterLanguage);\n\t\t} catch {\n\t\t\t// Tree-sitter failed — fall back to plaintext chunking\n\t\t\treturn chunkTextFile(content, filePath, \"plaintext\");\n\t\t}\n\t}\n\n\treturn chunkTextFile(content, filePath, fileType as TextFileType);\n}\n"]}
@@ -1,51 +0,0 @@
1
- /**
2
- * Chunking coordinator for the semantic search subsystem.
3
- *
4
- * Dispatches to the tree-sitter AST chunker for code files and the
5
- * text chunker for non-code files (markdown, YAML, JSON, etc.).
6
- */
7
- import { chunkTextFile } from "./text-chunker.js";
8
- import { chunkWithTreeSitter, initTreeSitter } from "./tree-sitter-chunker.js";
9
- // ============================================================================
10
- // Language Sets
11
- // ============================================================================
12
- const TREE_SITTER_LANGUAGES = new Set([
13
- "typescript",
14
- "tsx",
15
- "javascript",
16
- "python",
17
- "go",
18
- "rust",
19
- "java",
20
- "c",
21
- "cpp",
22
- ]);
23
- // ============================================================================
24
- // Public API
25
- // ============================================================================
26
- /**
27
- * Chunk a file's content into semantically meaningful pieces.
28
- *
29
- * For code files, uses tree-sitter to parse the AST and extract functions,
30
- * classes, methods, etc. For text files, uses format-specific splitting rules.
31
- *
32
- * If tree-sitter parsing fails for a code file, falls back to plaintext chunking.
33
- *
34
- * @param content - Raw file content
35
- * @param filePath - Relative file path (stored in chunk metadata)
36
- * @param fileType - Detected file type
37
- */
38
- export async function chunkFile(content, filePath, fileType) {
39
- if (TREE_SITTER_LANGUAGES.has(fileType)) {
40
- try {
41
- await initTreeSitter();
42
- return await chunkWithTreeSitter(content, filePath, fileType);
43
- }
44
- catch {
45
- // Tree-sitter failed — fall back to plaintext chunking
46
- return chunkTextFile(content, filePath, "plaintext");
47
- }
48
- }
49
- return chunkTextFile(content, filePath, fileType);
50
- }
51
- //# sourceMappingURL=chunker.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../../src/core/search/chunker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAG/E,+EAA+E;AAC/E,gBAAgB;AAChB,+EAA+E;AAE/E,MAAM,qBAAqB,GAAgB,IAAI,GAAG,CAAC;IAClD,YAAY;IACZ,KAAK;IACL,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ,MAAM;IACN,MAAM;IACN,GAAG;IACH,KAAK;CACL,CAAC,CAAC;AAEH,+EAA+E;AAC/E,aAAa;AACb,+EAA+E;AAE/E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAkB,EAAoB;IACxG,IAAI,qBAAqB,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC;YACJ,MAAM,cAAc,EAAE,CAAC;YACvB,OAAO,MAAM,mBAAmB,CAAC,OAAO,EAAE,QAAQ,EAAE,QAA8B,CAAC,CAAC;QACrF,CAAC;QAAC,MAAM,CAAC;YACR,yDAAuD;YACvD,OAAO,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;QACtD,CAAC;IACF,CAAC;IAED,OAAO,aAAa,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAwB,CAAC,CAAC;AAAA,CAClE","sourcesContent":["/**\n * Chunking coordinator for the semantic search subsystem.\n *\n * Dispatches to the tree-sitter AST chunker for code files and the\n * text chunker for non-code files (markdown, YAML, JSON, etc.).\n */\n\nimport { chunkTextFile } from \"./text-chunker.js\";\nimport { chunkWithTreeSitter, initTreeSitter } from \"./tree-sitter-chunker.js\";\nimport type { Chunk, FileType, TextFileType, TreeSitterLanguage } from \"./types.js\";\n\n// ============================================================================\n// Language Sets\n// ============================================================================\n\nconst TREE_SITTER_LANGUAGES: Set<string> = new Set([\n\t\"typescript\",\n\t\"tsx\",\n\t\"javascript\",\n\t\"python\",\n\t\"go\",\n\t\"rust\",\n\t\"java\",\n\t\"c\",\n\t\"cpp\",\n]);\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Chunk a file's content into semantically meaningful pieces.\n *\n * For code files, uses tree-sitter to parse the AST and extract functions,\n * classes, methods, etc. For text files, uses format-specific splitting rules.\n *\n * If tree-sitter parsing fails for a code file, falls back to plaintext chunking.\n *\n * @param content - Raw file content\n * @param filePath - Relative file path (stored in chunk metadata)\n * @param fileType - Detected file type\n */\nexport async function chunkFile(content: string, filePath: string, fileType: FileType): Promise<Chunk[]> {\n\tif (TREE_SITTER_LANGUAGES.has(fileType)) {\n\t\ttry {\n\t\t\tawait initTreeSitter();\n\t\t\treturn await chunkWithTreeSitter(content, filePath, fileType as TreeSitterLanguage);\n\t\t} catch {\n\t\t\t// Tree-sitter failed — fall back to plaintext chunking\n\t\t\treturn chunkTextFile(content, filePath, \"plaintext\");\n\t\t}\n\t}\n\n\treturn chunkTextFile(content, filePath, fileType as TextFileType);\n}\n"]}
@@ -1,89 +0,0 @@
1
- /**
2
- * SQLite database abstraction for the search index.
3
- *
4
- * Uses `node:sqlite` (built-in Node 22+). Feature-gated — callers must check
5
- * availability via `isSqliteAvailable()` before constructing a SearchDatabase.
6
- */
7
- import type { ChunkKind, FileType, IndexedFile, StoredChunk } from "./types.js";
8
- /** Check whether `node:sqlite` is available in this Node.js runtime. */
9
- export declare function isSqliteAvailable(): boolean;
10
- /** Wrapper around `node:sqlite` DatabaseSync for the search index. */
11
- export declare class SearchDatabase {
12
- private db;
13
- constructor(dbPath: string);
14
- /** Create or migrate the database schema. */
15
- private initSchema;
16
- /** Insert or update a file record. Returns the file ID. */
17
- upsertFile(filePath: string, mtime: number, fileType: FileType): number;
18
- /** Get a file by path. */
19
- getFile(filePath: string): IndexedFile | null;
20
- /** Get all indexed files. */
21
- getAllFiles(): IndexedFile[];
22
- /** Delete a file and all its chunks/embeddings/symbols (cascading). */
23
- deleteFile(fileId: number): void;
24
- /** Insert a chunk. Returns the chunk ID. */
25
- insertChunk(fileId: number, filePath: string, startLine: number, endLine: number, kind: ChunkKind, name: string | null, content: string, fileType: FileType): number;
26
- /** Delete all chunks for a file. */
27
- deleteChunksForFile(fileId: number): void;
28
- /** Get all chunks. */
29
- getAllChunks(): StoredChunk[];
30
- /** Get chunks by file ID. */
31
- getChunksByFileId(fileId: number): StoredChunk[];
32
- /** Get a chunk by ID. */
33
- getChunk(chunkId: number): StoredChunk | null;
34
- /** Get multiple chunks by IDs. Batches queries to avoid exceeding SQLite's bind variable limit. */
35
- getChunksById(chunkIds: number[]): StoredChunk[];
36
- private rowToChunk;
37
- /** Store an embedding vector for a chunk. */
38
- upsertEmbedding(chunkId: number, modelName: string, vector: Float32Array): void;
39
- /** Batch insert embeddings. Uses a transaction for performance. */
40
- batchUpsertEmbeddings(items: Array<{
41
- chunkId: number;
42
- modelName: string;
43
- vector: Float32Array;
44
- }>): void;
45
- /** Get the embedding for a chunk. */
46
- getEmbedding(chunkId: number, modelName: string): Float32Array | null;
47
- /** Get all embeddings for a model. Returns map of chunkId → vector. */
48
- getAllEmbeddings(modelName: string): Map<number, Float32Array>;
49
- /** Get chunk IDs that have no embedding for a given model. */
50
- getChunkIdsWithoutEmbedding(modelName: string): number[];
51
- /** Rebuild the FTS5 index (use after bulk operations). */
52
- rebuildFts(): void;
53
- /**
54
- * Search via FTS5 with BM25 ranking.
55
- * Returns chunk IDs with their BM25 scores (negated so higher = better).
56
- */
57
- ftsSearch(query: string, limit: number): Array<{
58
- chunkId: number;
59
- score: number;
60
- }>;
61
- /** Record an import edge. */
62
- insertImport(sourceFileId: number, targetFilePath: string): void;
63
- /** Delete all imports for a source file. */
64
- deleteImportsForFile(sourceFileId: number): void;
65
- /** Get files imported by a given source file. */
66
- getImportsFrom(sourceFileId: number): string[];
67
- /** Get file IDs that import a given target path. */
68
- getImportersOf(targetFilePath: string): number[];
69
- /** Get all import edges. */
70
- getAllImports(): Array<{
71
- sourceFileId: number;
72
- targetFilePath: string;
73
- }>;
74
- /** Insert a symbol. */
75
- insertSymbol(chunkId: number, name: string, kind: string): void;
76
- /** Delete symbols for a chunk. */
77
- deleteSymbolsForChunk(chunkId: number): void;
78
- /** Get all symbols. Returns map of chunkId → symbol names. */
79
- getAllSymbols(): Map<number, string[]>;
80
- /** Run a function inside a transaction. */
81
- transaction<T>(fn: () => T): T;
82
- /** Get the total number of chunks. */
83
- getChunkCount(): number;
84
- /** Get the total number of files. */
85
- getFileCount(): number;
86
- /** Close the database connection. */
87
- close(): void;
88
- }
89
- //# sourceMappingURL=db.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../../src/core/search/db.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SQLite database abstraction for the search index.\n *\n * Uses `node:sqlite` (built-in Node 22+). Feature-gated — callers must check\n * availability via `isSqliteAvailable()` before constructing a SearchDatabase.\n */\n\nimport { createRequire } from \"node:module\";\nimport type { ChunkKind, FileType, IndexedFile, StoredChunk } from \"./types.js\";\nimport { unpackVector } from \"./vector-store.js\";\n\n// ============================================================================\n// Availability Check\n// ============================================================================\n\nconst require = createRequire(import.meta.url);\nlet _sqliteAvailable: boolean | null = null;\n\n/** Check whether `node:sqlite` is available in this Node.js runtime. */\nexport function isSqliteAvailable(): boolean {\n\tif (_sqliteAvailable !== null) return _sqliteAvailable;\n\ttry {\n\t\trequire(\"node:sqlite\");\n\t\t_sqliteAvailable = true;\n\t} catch {\n\t\t_sqliteAvailable = false;\n\t}\n\treturn _sqliteAvailable;\n}\n\n// ============================================================================\n// Schema Version\n// ============================================================================\n\nconst SCHEMA_VERSION = 1;\n\n// ============================================================================\n// Database\n// ============================================================================\n\n/** Wrapper around `node:sqlite` DatabaseSync for the search index. */\nexport class SearchDatabase {\n\tprivate db: any; // DatabaseSync from node:sqlite\n\n\tconstructor(dbPath: string) {\n\t\t// Import synchronously — caller must have verified availability\n\t\tconst { DatabaseSync } = require(\"node:sqlite\");\n\t\tthis.db = new DatabaseSync(dbPath);\n\t\tthis.db.exec(\"PRAGMA journal_mode=WAL\");\n\t\tthis.db.exec(\"PRAGMA synchronous=NORMAL\");\n\t\tthis.db.exec(\"PRAGMA foreign_keys=ON\");\n\t\tthis.initSchema();\n\t}\n\n\t/** Create or migrate the database schema. */\n\tprivate initSchema(): void {\n\t\t// Check schema version\n\t\tthis.db.exec(\"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)\");\n\t\tconst versionRow = this.db.prepare(\"SELECT value FROM meta WHERE key = 'schema_version'\").get();\n\t\tconst currentVersion = versionRow ? Number.parseInt(versionRow.value, 10) : 0;\n\n\t\tif (currentVersion >= SCHEMA_VERSION) return;\n\n\t\t// Files table\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS files (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tfile_path TEXT NOT NULL UNIQUE,\n\t\t\t\tmtime REAL NOT NULL,\n\t\t\t\tfile_type TEXT NOT NULL\n\t\t\t)\n\t\t`);\n\n\t\t// Chunks table\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS chunks (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tfile_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n\t\t\t\tfile_path TEXT NOT NULL,\n\t\t\t\tstart_line INTEGER NOT NULL,\n\t\t\t\tend_line INTEGER NOT NULL,\n\t\t\t\tkind TEXT NOT NULL,\n\t\t\t\tname TEXT,\n\t\t\t\tcontent TEXT NOT NULL,\n\t\t\t\tfile_type TEXT NOT NULL\n\t\t\t)\n\t\t`);\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_chunks_file_id ON chunks(file_id)\");\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON chunks(file_path)\");\n\n\t\t// FTS5 virtual table (content-synced with chunks)\n\t\tthis.db.exec(`\n\t\t\tCREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(\n\t\t\t\tcontent,\n\t\t\t\tname,\n\t\t\t\tfile_path,\n\t\t\t\tcontent='chunks',\n\t\t\t\tcontent_rowid='id',\n\t\t\t\ttokenize='porter unicode61'\n\t\t\t)\n\t\t`);\n\n\t\t// FTS triggers for incremental updates\n\t\tthis.db.exec(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS chunks_ai AFTER INSERT ON chunks BEGIN\n\t\t\t\tINSERT INTO chunks_fts(rowid, content, name, file_path)\n\t\t\t\tVALUES (new.id, new.content, new.name, new.file_path);\n\t\t\tEND\n\t\t`);\n\t\tthis.db.exec(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS chunks_ad AFTER DELETE ON chunks BEGIN\n\t\t\t\tINSERT INTO chunks_fts(chunks_fts, rowid, content, name, file_path)\n\t\t\t\tVALUES ('delete', old.id, old.content, old.name, old.file_path);\n\t\t\tEND\n\t\t`);\n\t\tthis.db.exec(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS chunks_au AFTER UPDATE ON chunks BEGIN\n\t\t\t\tINSERT INTO chunks_fts(chunks_fts, rowid, content, name, file_path)\n\t\t\t\tVALUES ('delete', old.id, old.content, old.name, old.file_path);\n\t\t\t\tINSERT INTO chunks_fts(rowid, content, name, file_path)\n\t\t\t\tVALUES (new.id, new.content, new.name, new.file_path);\n\t\t\tEND\n\t\t`);\n\n\t\t// Embeddings table (keyed by model name for multi-model support)\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS embeddings (\n\t\t\t\tchunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,\n\t\t\t\tmodel_name TEXT NOT NULL,\n\t\t\t\tvector BLOB NOT NULL,\n\t\t\t\tPRIMARY KEY (chunk_id, model_name)\n\t\t\t)\n\t\t`);\n\n\t\t// Imports table (import graph edges between files)\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS imports (\n\t\t\t\tsource_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,\n\t\t\t\ttarget_file_path TEXT NOT NULL,\n\t\t\t\tPRIMARY KEY (source_file_id, target_file_path)\n\t\t\t)\n\t\t`);\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_imports_target ON imports(target_file_path)\");\n\n\t\t// Symbols table (symbol names extracted from chunks)\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS symbols (\n\t\t\t\tchunk_id INTEGER NOT NULL REFERENCES chunks(id) ON DELETE CASCADE,\n\t\t\t\tname TEXT NOT NULL,\n\t\t\t\tkind TEXT NOT NULL\n\t\t\t)\n\t\t`);\n\t\tthis.db.exec(\"CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)\");\n\n\t\t// Set schema version\n\t\tthis.db\n\t\t\t.prepare(\"INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)\")\n\t\t\t.run(String(SCHEMA_VERSION));\n\t}\n\n\t// ========================================================================\n\t// Files\n\t// ========================================================================\n\n\t/** Insert or update a file record. Returns the file ID. */\n\tupsertFile(filePath: string, mtime: number, fileType: FileType): number {\n\t\tconst existing = this.db.prepare(\"SELECT id FROM files WHERE file_path = ?\").get(filePath);\n\t\tif (existing) {\n\t\t\tthis.db.prepare(\"UPDATE files SET mtime = ?, file_type = ? WHERE id = ?\").run(mtime, fileType, existing.id);\n\t\t\treturn existing.id;\n\t\t}\n\t\tconst result = this.db\n\t\t\t.prepare(\"INSERT INTO files (file_path, mtime, file_type) VALUES (?, ?, ?)\")\n\t\t\t.run(filePath, mtime, fileType);\n\t\treturn Number(result.lastInsertRowid);\n\t}\n\n\t/** Get a file by path. */\n\tgetFile(filePath: string): IndexedFile | null {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT id, file_path, mtime, file_type FROM files WHERE file_path = ?\")\n\t\t\t.get(filePath);\n\t\tif (!row) return null;\n\t\treturn { id: row.id, filePath: row.file_path, mtime: row.mtime, fileType: row.file_type as FileType };\n\t}\n\n\t/** Get all indexed files. */\n\tgetAllFiles(): IndexedFile[] {\n\t\tconst rows = this.db.prepare(\"SELECT id, file_path, mtime, file_type FROM files\").all();\n\t\treturn rows.map((r: any) => ({\n\t\t\tid: r.id,\n\t\t\tfilePath: r.file_path,\n\t\t\tmtime: r.mtime,\n\t\t\tfileType: r.file_type as FileType,\n\t\t}));\n\t}\n\n\t/** Delete a file and all its chunks/embeddings/symbols (cascading). */\n\tdeleteFile(fileId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM files WHERE id = ?\").run(fileId);\n\t}\n\n\t// ========================================================================\n\t// Chunks\n\t// ========================================================================\n\n\t/** Insert a chunk. Returns the chunk ID. */\n\tinsertChunk(\n\t\tfileId: number,\n\t\tfilePath: string,\n\t\tstartLine: number,\n\t\tendLine: number,\n\t\tkind: ChunkKind,\n\t\tname: string | null,\n\t\tcontent: string,\n\t\tfileType: FileType,\n\t): number {\n\t\tconst result = this.db\n\t\t\t.prepare(\n\t\t\t\t\"INSERT INTO chunks (file_id, file_path, start_line, end_line, kind, name, content, file_type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\",\n\t\t\t)\n\t\t\t.run(fileId, filePath, startLine, endLine, kind, name, content, fileType);\n\t\treturn Number(result.lastInsertRowid);\n\t}\n\n\t/** Delete all chunks for a file. */\n\tdeleteChunksForFile(fileId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM chunks WHERE file_id = ?\").run(fileId);\n\t}\n\n\t/** Get all chunks. */\n\tgetAllChunks(): StoredChunk[] {\n\t\tconst rows = this.db\n\t\t\t.prepare(\"SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks\")\n\t\t\t.all();\n\t\treturn rows.map((r: any) => this.rowToChunk(r));\n\t}\n\n\t/** Get chunks by file ID. */\n\tgetChunksByFileId(fileId: number): StoredChunk[] {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE file_id = ?\",\n\t\t\t)\n\t\t\t.all(fileId);\n\t\treturn rows.map((r: any) => this.rowToChunk(r));\n\t}\n\n\t/** Get a chunk by ID. */\n\tgetChunk(chunkId: number): StoredChunk | null {\n\t\tconst row = this.db\n\t\t\t.prepare(\n\t\t\t\t\"SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE id = ?\",\n\t\t\t)\n\t\t\t.get(chunkId);\n\t\tif (!row) return null;\n\t\treturn this.rowToChunk(row);\n\t}\n\n\t/** Get multiple chunks by IDs. Batches queries to avoid exceeding SQLite's bind variable limit. */\n\tgetChunksById(chunkIds: number[]): StoredChunk[] {\n\t\tif (chunkIds.length === 0) return [];\n\n\t\tconst BATCH_SIZE = 500;\n\t\tconst results: StoredChunk[] = [];\n\n\t\tfor (let i = 0; i < chunkIds.length; i += BATCH_SIZE) {\n\t\t\tconst batch = chunkIds.slice(i, i + BATCH_SIZE);\n\t\t\tconst placeholders = batch.map(() => \"?\").join(\",\");\n\t\t\tconst rows = this.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT id, file_id, file_path, start_line, end_line, kind, name, content, file_type FROM chunks WHERE id IN (${placeholders})`,\n\t\t\t\t)\n\t\t\t\t.all(...batch);\n\t\t\tfor (const r of rows) {\n\t\t\t\tresults.push(this.rowToChunk(r));\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tprivate rowToChunk(r: any): StoredChunk {\n\t\treturn {\n\t\t\tid: r.id,\n\t\t\tfileId: r.file_id,\n\t\t\tfilePath: r.file_path,\n\t\t\tstartLine: r.start_line,\n\t\t\tendLine: r.end_line,\n\t\t\tkind: r.kind as ChunkKind,\n\t\t\tname: r.name,\n\t\t\tcontent: r.content,\n\t\t\tfileType: r.file_type as FileType,\n\t\t};\n\t}\n\n\t// ========================================================================\n\t// Embeddings\n\t// ========================================================================\n\n\t/** Store an embedding vector for a chunk. */\n\tupsertEmbedding(chunkId: number, modelName: string, vector: Float32Array): void {\n\t\tconst blob = Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);\n\t\tthis.db\n\t\t\t.prepare(\"INSERT OR REPLACE INTO embeddings (chunk_id, model_name, vector) VALUES (?, ?, ?)\")\n\t\t\t.run(chunkId, modelName, blob);\n\t}\n\n\t/** Batch insert embeddings. Uses a transaction for performance. */\n\tbatchUpsertEmbeddings(items: Array<{ chunkId: number; modelName: string; vector: Float32Array }>): void {\n\t\tconst stmt = this.db.prepare(\"INSERT OR REPLACE INTO embeddings (chunk_id, model_name, vector) VALUES (?, ?, ?)\");\n\t\tthis.transaction(() => {\n\t\t\tfor (const item of items) {\n\t\t\t\tconst blob = Buffer.from(item.vector.buffer, item.vector.byteOffset, item.vector.byteLength);\n\t\t\t\tstmt.run(item.chunkId, item.modelName, blob);\n\t\t\t}\n\t\t});\n\t}\n\n\t/** Get the embedding for a chunk. */\n\tgetEmbedding(chunkId: number, modelName: string): Float32Array | null {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT vector FROM embeddings WHERE chunk_id = ? AND model_name = ?\")\n\t\t\t.get(chunkId, modelName);\n\t\tif (!row) return null;\n\t\treturn unpackVector(row.vector);\n\t}\n\n\t/** Get all embeddings for a model. Returns map of chunkId → vector. */\n\tgetAllEmbeddings(modelName: string): Map<number, Float32Array> {\n\t\tconst rows = this.db.prepare(\"SELECT chunk_id, vector FROM embeddings WHERE model_name = ?\").all(modelName);\n\t\tconst map = new Map<number, Float32Array>();\n\t\tfor (const row of rows) {\n\t\t\tmap.set(row.chunk_id, unpackVector(row.vector));\n\t\t}\n\t\treturn map;\n\t}\n\n\t/** Get chunk IDs that have no embedding for a given model. */\n\tgetChunkIdsWithoutEmbedding(modelName: string): number[] {\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT c.id FROM chunks c\n\t\t\t\t LEFT JOIN embeddings e ON c.id = e.chunk_id AND e.model_name = ?\n\t\t\t\t WHERE e.chunk_id IS NULL`,\n\t\t\t)\n\t\t\t.all(modelName);\n\t\treturn rows.map((r: any) => r.id);\n\t}\n\n\t// ========================================================================\n\t// FTS5\n\t// ========================================================================\n\n\t/** Rebuild the FTS5 index (use after bulk operations). */\n\trebuildFts(): void {\n\t\tthis.db.exec(\"INSERT INTO chunks_fts(chunks_fts) VALUES ('rebuild')\");\n\t}\n\n\t/**\n\t * Search via FTS5 with BM25 ranking.\n\t * Returns chunk IDs with their BM25 scores (negated so higher = better).\n\t */\n\tftsSearch(query: string, limit: number): Array<{ chunkId: number; score: number }> {\n\t\ttry {\n\t\t\tconst rows = this.db\n\t\t\t\t.prepare(\n\t\t\t\t\t`SELECT chunks_fts.rowid as chunk_id, -bm25(chunks_fts, 1.0, 10.0, 5.0) as score\n\t\t\t\t\t FROM chunks_fts\n\t\t\t\t\t WHERE chunks_fts MATCH ?\n\t\t\t\t\t ORDER BY score DESC\n\t\t\t\t\t LIMIT ?`,\n\t\t\t\t)\n\t\t\t\t.all(query, limit);\n\t\t\treturn rows.map((r: any) => ({ chunkId: r.chunk_id, score: r.score }));\n\t\t} catch {\n\t\t\t// FTS5 MATCH can fail on malformed queries\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t// ========================================================================\n\t// Imports\n\t// ========================================================================\n\n\t/** Record an import edge. */\n\tinsertImport(sourceFileId: number, targetFilePath: string): void {\n\t\tthis.db\n\t\t\t.prepare(\"INSERT OR IGNORE INTO imports (source_file_id, target_file_path) VALUES (?, ?)\")\n\t\t\t.run(sourceFileId, targetFilePath);\n\t}\n\n\t/** Delete all imports for a source file. */\n\tdeleteImportsForFile(sourceFileId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM imports WHERE source_file_id = ?\").run(sourceFileId);\n\t}\n\n\t/** Get files imported by a given source file. */\n\tgetImportsFrom(sourceFileId: number): string[] {\n\t\tconst rows = this.db.prepare(\"SELECT target_file_path FROM imports WHERE source_file_id = ?\").all(sourceFileId);\n\t\treturn rows.map((r: any) => r.target_file_path);\n\t}\n\n\t/** Get file IDs that import a given target path. */\n\tgetImportersOf(targetFilePath: string): number[] {\n\t\tconst rows = this.db.prepare(\"SELECT source_file_id FROM imports WHERE target_file_path = ?\").all(targetFilePath);\n\t\treturn rows.map((r: any) => r.source_file_id);\n\t}\n\n\t/** Get all import edges. */\n\tgetAllImports(): Array<{ sourceFileId: number; targetFilePath: string }> {\n\t\tconst rows = this.db.prepare(\"SELECT source_file_id, target_file_path FROM imports\").all();\n\t\treturn rows.map((r: any) => ({ sourceFileId: r.source_file_id, targetFilePath: r.target_file_path }));\n\t}\n\n\t// ========================================================================\n\t// Symbols\n\t// ========================================================================\n\n\t/** Insert a symbol. */\n\tinsertSymbol(chunkId: number, name: string, kind: string): void {\n\t\tthis.db.prepare(\"INSERT INTO symbols (chunk_id, name, kind) VALUES (?, ?, ?)\").run(chunkId, name, kind);\n\t}\n\n\t/** Delete symbols for a chunk. */\n\tdeleteSymbolsForChunk(chunkId: number): void {\n\t\tthis.db.prepare(\"DELETE FROM symbols WHERE chunk_id = ?\").run(chunkId);\n\t}\n\n\t/** Get all symbols. Returns map of chunkId → symbol names. */\n\tgetAllSymbols(): Map<number, string[]> {\n\t\tconst rows = this.db.prepare(\"SELECT chunk_id, name FROM symbols\").all();\n\t\tconst map = new Map<number, string[]>();\n\t\tfor (const row of rows) {\n\t\t\tconst existing = map.get(row.chunk_id);\n\t\t\tif (existing) existing.push(row.name);\n\t\t\telse map.set(row.chunk_id, [row.name]);\n\t\t}\n\t\treturn map;\n\t}\n\n\t// ========================================================================\n\t// Transaction Helpers\n\t// ========================================================================\n\n\t/** Run a function inside a transaction. */\n\ttransaction<T>(fn: () => T): T {\n\t\tthis.db.exec(\"BEGIN\");\n\t\ttry {\n\t\t\tconst result = fn();\n\t\t\tthis.db.exec(\"COMMIT\");\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tthis.db.exec(\"ROLLBACK\");\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\t/** Get the total number of chunks. */\n\tgetChunkCount(): number {\n\t\tconst row = this.db.prepare(\"SELECT COUNT(*) as count FROM chunks\").get();\n\t\treturn row.count;\n\t}\n\n\t/** Get the total number of files. */\n\tgetFileCount(): number {\n\t\tconst row = this.db.prepare(\"SELECT COUNT(*) as count FROM files\").get();\n\t\treturn row.count;\n\t}\n\n\t/** Close the database connection. */\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n}\n"]}