@dreb/coding-agent 2.6.0 → 2.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -333,9 +333,9 @@ Task tracking is prompt-driven: the system prompt includes guidelines for when t
333
333
 
334
334
  The `search` tool provides natural language queries over the codebase using embeddings and full-text search. It supports identifier queries (e.g., `AuthMiddleware`), natural language (e.g., `where is rate limiting handled`), and path queries (e.g., `src/auth/`).
335
335
 
336
- **Parameters:** `query` (required), `path` (restrict to subdirectory), `limit` (max results, default 20), `projectDir` (index/search a different directory instead of cwd useful for Telegram sessions where cwd is `~/`), `rebuild` (force a clean re-index when results look stale or corrupt).
336
+ **Parameters:** `query` (required), `searchDir` (directory to index and search each unique value gets its own independent index; defaults to cwd, but should be set explicitly in Telegram sessions where cwd is `~/`), `restrictToDir` (filter results to files under this subdirectory within the already-built index — does not affect which files are indexed), `limit` (max results, default 20), `rebuild` (force a clean re-index when results look stale or corrupt).
337
337
 
338
- **How it works:** The first query builds a project index (typically 10–60s, longer for very large repos). Subsequent queries use the cached index, with incremental re-indexing for changed files (mtime-based). Each unique `projectDir` gets its own independent index.
338
+ **How it works:** The first query builds a project index (typically 10–60s, longer for very large repos). Subsequent queries use the cached index, with incremental re-indexing for changed files (mtime-based). Each unique `searchDir` gets its own independent index.
339
339
 
340
340
  **Indexing pipeline:**
341
341
  - AST-aware code chunking via tree-sitter (TypeScript, JavaScript, Python, Go, Rust, Java, C, C++, GDScript) — extracts functions, classes, methods, and exports as individual chunks
@@ -9,9 +9,9 @@ import { type Static } from "@sinclair/typebox";
9
9
  import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
10
10
  declare const searchSchema: import("@sinclair/typebox").TObject<{
11
11
  query: import("@sinclair/typebox").TString;
12
- path: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
12
+ restrictToDir: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
13
13
  limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
14
- projectDir: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
14
+ searchDir: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
15
15
  rebuild: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
16
16
  }>;
17
17
  export type SearchToolInput = Static<typeof searchSchema>;
@@ -26,9 +26,9 @@ export interface SearchToolDetails {
26
26
  /** @internal Exported for testing. */
27
27
  export declare function formatSearchCall(args: {
28
28
  query?: string;
29
- path?: string;
29
+ restrictToDir?: string;
30
30
  limit?: number;
31
- projectDir?: string;
31
+ searchDir?: string;
32
32
  rebuild?: boolean;
33
33
  } | undefined, theme: typeof import("../../modes/interactive/theme/theme.js").theme): string;
34
34
  /** @internal Exported for testing. */
@@ -1 +1 @@
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"]}
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;;;;;;EAgBhB,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,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACnH,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\trestrictToDir: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Filter results to files under this path (relative to searchDir or cwd). Does not affect indexing — the entire searchDir is still indexed.\",\n\t\t}),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results to return (default: 20)\" })),\n\tsearchDir: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Directory to index and search instead of cwd (useful when cwd is ~/). The entire contents of this directory are scanned and indexed.\",\n\t\t}),\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; restrictToDir?: string; limit?: number; searchDir?: string; rebuild?: boolean } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst restrictToDir = str(args?.restrictToDir);\n\tconst searchDir = str(args?.searchDir);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"search\"))} ${theme.fg(\"accent\", `\"${query ?? \"\"}\"`)}`;\n\tif (searchDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` project ${shortenPath(searchDir)}`);\n\t}\n\tif (restrictToDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` in ${shortenPath(restrictToDir)}`);\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, restrictToDir, limit, searchDir, 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 resolvedSearchDir = searchDir ? resolveToCwd(searchDir, cwd) : cwd;\n\n\t\t\tif (searchDir && (!existsSync(resolvedSearchDir) || !statSync(resolvedSearchDir).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: `searchDir does not exist or is not a directory: ${resolvedSearchDir}`,\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(resolvedSearchDir);\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: restrictToDir,\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"]}
@@ -19,9 +19,13 @@ import { wrapToolDefinition } from "./tool-definition-wrapper.js";
19
19
  // ============================================================================
20
20
  const searchSchema = Type.Object({
21
21
  query: Type.String({ description: "The search query (natural language, identifier, or path)" }),
22
- path: Type.Optional(Type.String({ description: "Restrict search to files under this path (relative to cwd)" })),
22
+ restrictToDir: Type.Optional(Type.String({
23
+ description: "Filter results to files under this path (relative to searchDir or cwd). Does not affect indexing — the entire searchDir is still indexed.",
24
+ })),
23
25
  limit: Type.Optional(Type.Number({ description: "Maximum number of results to return (default: 20)" })),
24
- projectDir: Type.Optional(Type.String({ description: "Directory to index and search instead of cwd (useful when cwd is ~/)" })),
26
+ searchDir: Type.Optional(Type.String({
27
+ description: "Directory to index and search instead of cwd (useful when cwd is ~/). The entire contents of this directory are scanned and indexed.",
28
+ })),
25
29
  rebuild: Type.Optional(Type.Boolean({ description: "Force a clean rebuild of the search index (default: false)" })),
26
30
  });
27
31
  // ============================================================================
@@ -30,14 +34,14 @@ const searchSchema = Type.Object({
30
34
  /** @internal Exported for testing. */
31
35
  export function formatSearchCall(args, theme) {
32
36
  const query = str(args?.query);
33
- const searchPath = str(args?.path);
34
- const projectDir = str(args?.projectDir);
37
+ const restrictToDir = str(args?.restrictToDir);
38
+ const searchDir = str(args?.searchDir);
35
39
  let text = `${theme.fg("toolTitle", theme.bold("search"))} ${theme.fg("accent", `"${query ?? ""}"`)}`;
36
- if (projectDir) {
37
- text += theme.fg("toolOutput", ` project ${shortenPath(projectDir)}`);
40
+ if (searchDir) {
41
+ text += theme.fg("toolOutput", ` project ${shortenPath(searchDir)}`);
38
42
  }
39
- if (searchPath) {
40
- text += theme.fg("toolOutput", ` in ${shortenPath(searchPath)}`);
43
+ if (restrictToDir) {
44
+ text += theme.fg("toolOutput", ` in ${shortenPath(restrictToDir)}`);
41
45
  }
42
46
  if (args?.rebuild) {
43
47
  text += theme.fg("toolOutput", " [rebuild]");
@@ -113,33 +117,33 @@ export function createSearchToolDefinition(cwd) {
113
117
  details: { resultCount: 0, indexBuilt: false },
114
118
  };
115
119
  }
116
- const { query, path: searchPath, limit, projectDir, rebuild } = params;
120
+ const { query, restrictToDir, limit, searchDir, rebuild } = params;
117
121
  if (!query || query.trim().length === 0) {
118
122
  return {
119
123
  content: [{ type: "text", text: "Search query cannot be empty." }],
120
124
  details: { resultCount: 0, indexBuilt: false },
121
125
  };
122
126
  }
123
- const resolvedProjectDir = projectDir ? resolveToCwd(projectDir, cwd) : cwd;
124
- if (projectDir && (!existsSync(resolvedProjectDir) || !statSync(resolvedProjectDir).isDirectory())) {
127
+ const resolvedSearchDir = searchDir ? resolveToCwd(searchDir, cwd) : cwd;
128
+ if (searchDir && (!existsSync(resolvedSearchDir) || !statSync(resolvedSearchDir).isDirectory())) {
125
129
  return {
126
130
  content: [
127
131
  {
128
132
  type: "text",
129
- text: `projectDir does not exist or is not a directory: ${resolvedProjectDir}`,
133
+ text: `searchDir does not exist or is not a directory: ${resolvedSearchDir}`,
130
134
  },
131
135
  ],
132
136
  details: { resultCount: 0, indexBuilt: false },
133
137
  };
134
138
  }
135
- const engine = getSearchEngine(resolvedProjectDir);
139
+ const engine = getSearchEngine(resolvedSearchDir);
136
140
  if (rebuild) {
137
141
  await engine.resetIndex();
138
142
  }
139
143
  let indexBuilt = false;
140
144
  const results = await engine.search(query, {
141
145
  limit: typeof limit === "number" && limit > 0 ? Math.floor(limit) : 20,
142
- pathFilter: searchPath,
146
+ pathFilter: restrictToDir,
143
147
  onProgress: (phase, current, total) => {
144
148
  if (phase === "indexing" || phase === "scanning" || phase === "loading model" || phase === "embedding") {
145
149
  indexBuilt = true;
@@ -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 { 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"]}
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\trestrictToDir: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Filter results to files under this path (relative to searchDir or cwd). Does not affect indexing — the entire searchDir is still indexed.\",\n\t\t}),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results to return (default: 20)\" })),\n\tsearchDir: Type.Optional(\n\t\tType.String({\n\t\t\tdescription:\n\t\t\t\t\"Directory to index and search instead of cwd (useful when cwd is ~/). The entire contents of this directory are scanned and indexed.\",\n\t\t}),\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; restrictToDir?: string; limit?: number; searchDir?: string; rebuild?: boolean } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").theme,\n): string {\n\tconst query = str(args?.query);\n\tconst restrictToDir = str(args?.restrictToDir);\n\tconst searchDir = str(args?.searchDir);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"search\"))} ${theme.fg(\"accent\", `\"${query ?? \"\"}\"`)}`;\n\tif (searchDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` project ${shortenPath(searchDir)}`);\n\t}\n\tif (restrictToDir) {\n\t\ttext += theme.fg(\"toolOutput\", ` in ${shortenPath(restrictToDir)}`);\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, restrictToDir, limit, searchDir, 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 resolvedSearchDir = searchDir ? resolveToCwd(searchDir, cwd) : cwd;\n\n\t\t\tif (searchDir && (!existsSync(resolvedSearchDir) || !statSync(resolvedSearchDir).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: `searchDir does not exist or is not a directory: ${resolvedSearchDir}`,\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(resolvedSearchDir);\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: restrictToDir,\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": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drebConfig": {
@@ -52,10 +52,10 @@
52
52
  "prepublishOnly": "npm run clean && npm run build"
53
53
  },
54
54
  "dependencies": {
55
- "@dreb/agent-core": "^2.0.0",
56
- "@dreb/ai": "^2.0.0",
57
- "@dreb/semantic-search": "^2.0.0",
58
- "@dreb/tui": "^2.0.0",
55
+ "@dreb/agent-core": "*",
56
+ "@dreb/ai": "*",
57
+ "@dreb/semantic-search": "*",
58
+ "@dreb/tui": "*",
59
59
  "@huggingface/transformers": "^4.0.1",
60
60
  "@mariozechner/jiti": "^2.6.2",
61
61
  "@silvia-odwyer/photon-node": "^0.3.4",