@dreb/coding-agent 1.16.0 → 1.18.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 (38) hide show
  1. package/README.md +19 -9
  2. package/agents/code-reviewer.md +1 -1
  3. package/agents/completeness-checker.md +1 -1
  4. package/agents/error-auditor.md +1 -1
  5. package/agents/explore.md +1 -1
  6. package/agents/feature-dev.md +1 -1
  7. package/agents/independent-assessor.md +1 -1
  8. package/agents/simplifier.md +1 -1
  9. package/agents/test-reviewer.md +1 -1
  10. package/dist/core/search/embedder.d.ts +1 -0
  11. package/dist/core/search/embedder.d.ts.map +1 -1
  12. package/dist/core/search/embedder.js +38 -23
  13. package/dist/core/search/embedder.js.map +1 -1
  14. package/dist/core/search/scanner.d.ts.map +1 -1
  15. package/dist/core/search/scanner.js +9 -0
  16. package/dist/core/search/scanner.js.map +1 -1
  17. package/dist/core/search/search.d.ts +10 -1
  18. package/dist/core/search/search.d.ts.map +1 -1
  19. package/dist/core/search/search.js +141 -97
  20. package/dist/core/search/search.js.map +1 -1
  21. package/dist/core/system-prompt.d.ts.map +1 -1
  22. package/dist/core/system-prompt.js +7 -1
  23. package/dist/core/system-prompt.js.map +1 -1
  24. package/dist/core/tools/dreb-paths.d.ts +17 -0
  25. package/dist/core/tools/dreb-paths.d.ts.map +1 -0
  26. package/dist/core/tools/dreb-paths.js +43 -0
  27. package/dist/core/tools/dreb-paths.js.map +1 -0
  28. package/dist/core/tools/find.d.ts.map +1 -1
  29. package/dist/core/tools/find.js +8 -0
  30. package/dist/core/tools/find.js.map +1 -1
  31. package/dist/core/tools/grep.d.ts.map +1 -1
  32. package/dist/core/tools/grep.js +8 -0
  33. package/dist/core/tools/grep.js.map +1 -1
  34. package/dist/core/tools/search.d.ts +19 -1
  35. package/dist/core/tools/search.d.ts.map +1 -1
  36. package/dist/core/tools/search.js +38 -10
  37. package/dist/core/tools/search.js.map +1 -1
  38. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/core/search/search.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,OAAO,KAAK,EAAe,qBAAqB,EAAgB,YAAY,EAAe,MAAM,YAAY,CAAC;AAe9G,MAAM,WAAW,aAAa;IAC7B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACnC;AAMD,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,QAAQ,CAAyB;IAEzC,YAAY,WAAW,EAAE,MAAM,EAE9B;IAED,oEAAoE;IACpE,MAAM,CAAC,WAAW,IAAI,OAAO,CAE5B;IAED;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAmH5E;IAED,wDAAwD;IACxD,QAAQ,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAGnD;IAED,yBAAyB;IACzB,KAAK,IAAI,IAAI,CAKZ;IAMD,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;YASR,mBAAmB;YAYnB,mBAAmB;CA4BjC","sourcesContent":["/**\n * Main search API.\n *\n * Orchestrates: check/build index → compute all 6 metrics → classify query\n * → duplicate columns → POEM rank → assemble results.\n */\n\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { SearchDatabase } from \"./db.js\";\nimport { Embedder } from \"./embedder.js\";\nimport { IndexManager } from \"./index-manager.js\";\nimport { computeBm25Scores } from \"./metrics/bm25.js\";\nimport { computeGitRecencyScores } from \"./metrics/git-recency.js\";\nimport { computeImportGraphScores } from \"./metrics/import-graph.js\";\nimport { computePathMatchScores } from \"./metrics/path-match.js\";\nimport { computeSymbolMatchScores } from \"./metrics/symbol-match.js\";\nimport { poemRank } from \"./poem.js\";\nimport { classifyQuery } from \"./query-classifier.js\";\nimport type { IndexConfig, IndexProgressCallback, MetricScores, SearchResult, StoredChunk } from \"./types.js\";\nimport { topKSimilar } from \"./vector-store.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MODEL_NAME = \"Xenova/all-MiniLM-L6-v2\";\nconst DEFAULT_RESULT_LIMIT = 20;\nconst METRIC_CANDIDATE_LIMIT = 1000;\n\n// ============================================================================\n// Search Options\n// ============================================================================\n\nexport interface SearchOptions {\n\t/** Maximum number of results to return. Default: 20. */\n\tlimit?: number;\n\t/** Restrict search to files under this path (relative to project root). */\n\tpathFilter?: string;\n\t/** Progress callback for indexing operations. */\n\tonProgress?: IndexProgressCallback;\n}\n\n// ============================================================================\n// Search Engine\n// ============================================================================\n\nexport class SearchEngine {\n\tprivate readonly projectRoot: string;\n\tprivate indexManager: IndexManager | null = null;\n\tprivate embedder: Embedder | null = null;\n\n\tconstructor(projectRoot: string) {\n\t\tthis.projectRoot = projectRoot;\n\t}\n\n\t/** Check if semantic search is available (requires node:sqlite). */\n\tstatic isAvailable(): boolean {\n\t\treturn IndexManager.isAvailable();\n\t}\n\n\t/**\n\t * Search the codebase with a natural language or identifier query.\n\t *\n\t * On first call, builds the index (scans, chunks, embeds). Subsequent calls\n\t * incrementally update changed files before searching.\n\t */\n\tasync search(query: string, options?: SearchOptions): Promise<SearchResult[]> {\n\t\tconst limit = options?.limit ?? DEFAULT_RESULT_LIMIT;\n\t\tconst onProgress = options?.onProgress;\n\n\t\t// Ensure index is built and up to date\n\t\tconst indexManager = this.getIndexManager();\n\t\tconst db = indexManager.getDb();\n\n\t\t// Share our embedder with IndexManager so it doesn't create a second one\n\t\tconst embedder = await this.getOrCreateEmbedder();\n\t\tindexManager.setEmbedder(embedder);\n\n\t\tawait indexManager.buildIndex(onProgress);\n\t\tawait indexManager.ensureEmbeddings(onProgress);\n\n\t\t// Get all chunks (potentially filtered by path)\n\t\tlet allChunks = db.getAllChunks();\n\t\tif (options?.pathFilter) {\n\t\t\tconst filter = options.pathFilter;\n\t\t\tallChunks = allChunks.filter((c) => c.filePath.startsWith(filter));\n\t\t}\n\n\t\tif (allChunks.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Classify query type for POEM column weighting\n\t\tconst queryType = classifyQuery(query);\n\n\t\t// Compute all 6 metrics\n\t\tonProgress?.(\"searching\", 0, 6);\n\n\t\t// 1. BM25 (FTS5)\n\t\tconst bm25Scores = computeBm25Scores(db, sanitizeFtsQuery(query), METRIC_CANDIDATE_LIMIT);\n\t\tonProgress?.(\"searching\", 1, 6);\n\n\t\t// 2. Cosine similarity (vector search)\n\t\tconst cosineScores = await this.computeVectorScores(db, query, METRIC_CANDIDATE_LIMIT, onProgress);\n\t\tonProgress?.(\"searching\", 2, 6);\n\n\t\t// 3. Path match\n\t\tconst pathScores = computePathMatchScores(query, allChunks);\n\t\tonProgress?.(\"searching\", 3, 6);\n\n\t\t// 4. Symbol match\n\t\tconst symbols = db.getAllSymbols();\n\t\tconst symbolScores = computeSymbolMatchScores(query, symbols);\n\t\tonProgress?.(\"searching\", 4, 6);\n\n\t\t// 5. Import graph (use BM25 + cosine as seed scores, aggregated per file)\n\t\t// Only use files with strong scores as seeds — low-scoring files (e.g. from\n\t\t// common OR terms matching everywhere) pollute the seed set and prevent\n\t\t// meaningful propagation.\n\t\tconst fileSeedScores = aggregateFileScores(allChunks, bm25Scores, cosineScores);\n\t\tconst seedThreshold = computeSeedThreshold(fileSeedScores);\n\t\tconst filteredSeeds = new Map<number, number>();\n\t\tfor (const [fileId, score] of fileSeedScores) {\n\t\t\tif (score >= seedThreshold) filteredSeeds.set(fileId, score);\n\t\t}\n\t\tconst fileIdToChunkIds = buildFileChunkMap(allChunks);\n\t\tconst importScores = computeImportGraphScores(db, filteredSeeds, fileIdToChunkIds);\n\t\tonProgress?.(\"searching\", 5, 6);\n\n\t\t// 6. Git recency\n\t\tconst recencyScores = await computeGitRecencyScores(this.projectRoot, allChunks);\n\t\tonProgress?.(\"searching\", 6, 6);\n\n\t\t// Build MetricScores for each candidate chunk\n\t\tconst candidateIds = collectCandidateIds(\n\t\t\tbm25Scores,\n\t\t\tcosineScores,\n\t\t\tpathScores,\n\t\t\tsymbolScores,\n\t\t\timportScores,\n\t\t\trecencyScores,\n\t\t);\n\t\tconst candidates = new Map<number, MetricScores>();\n\n\t\tfor (const id of candidateIds) {\n\t\t\tcandidates.set(id, {\n\t\t\t\tbm25: bm25Scores.get(id) ?? 0,\n\t\t\t\tcosine: cosineScores.get(id) ?? 0,\n\t\t\t\tpathMatch: pathScores.get(id) ?? 0,\n\t\t\t\tsymbolMatch: symbolScores.get(id) ?? 0,\n\t\t\t\timportGraph: importScores.get(id) ?? 0,\n\t\t\t\tgitRecency: recencyScores.get(id) ?? 0,\n\t\t\t});\n\t\t}\n\n\t\tif (candidates.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// POEM rank\n\t\tconst ranked = poemRank(candidates, queryType);\n\n\t\t// Assemble results\n\t\tconst chunkMap = new Map<number, StoredChunk>();\n\t\tfor (const chunk of allChunks) {\n\t\t\tchunkMap.set(chunk.id, chunk);\n\t\t}\n\n\t\tconst results: SearchResult[] = [];\n\t\tfor (const candidate of ranked.slice(0, limit)) {\n\t\t\tconst chunk = chunkMap.get(candidate.id);\n\t\t\tif (chunk) {\n\t\t\t\tresults.push({\n\t\t\t\t\tchunk,\n\t\t\t\t\tscores: candidate.scores,\n\t\t\t\t\trank: candidate.rank,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/** Get index stats without opening a new connection. */\n\tgetStats(): { files: number; chunks: number } | null {\n\t\tif (!this.indexManager) return null;\n\t\treturn this.indexManager.getStats();\n\t}\n\n\t/** Dispose resources. */\n\tclose(): void {\n\t\tthis.indexManager?.close();\n\t\tthis.indexManager = null;\n\t\tthis.embedder?.dispose();\n\t\tthis.embedder = null;\n\t}\n\n\t// ========================================================================\n\t// Private\n\t// ========================================================================\n\n\tprivate getIndexManager(): IndexManager {\n\t\tif (!this.indexManager) {\n\t\t\tconst config = this.getIndexConfig();\n\t\t\tthis.indexManager = new IndexManager(config);\n\t\t\tthis.indexManager.open();\n\t\t}\n\t\treturn this.indexManager;\n\t}\n\n\tprivate getIndexConfig(): IndexConfig {\n\t\treturn {\n\t\t\tprojectRoot: this.projectRoot,\n\t\t\tindexDir: path.join(this.projectRoot, \".dreb\", \"index\"),\n\t\t\tglobalMemoryDir: path.join(homedir(), \".dreb\", \"memory\"),\n\t\t\tmodelName: DEFAULT_MODEL_NAME,\n\t\t};\n\t}\n\n\tprivate async getOrCreateEmbedder(): Promise<Embedder> {\n\t\tif (!this.embedder) {\n\t\t\tconst config = this.getIndexConfig();\n\t\t\tthis.embedder = new Embedder({\n\t\t\t\tmodelCacheDir: path.join(homedir(), \".dreb\", \"agent\", \"models\"),\n\t\t\t\tmodelName: config.modelName,\n\t\t\t});\n\t\t\tawait this.embedder.initialize();\n\t\t}\n\t\treturn this.embedder;\n\t}\n\n\tprivate async computeVectorScores(\n\t\tdb: SearchDatabase,\n\t\tquery: string,\n\t\tlimit: number,\n\t\t_onProgress?: IndexProgressCallback,\n\t): Promise<Map<number, number>> {\n\t\tconst config = this.getIndexConfig();\n\t\tconst embedder = await this.getOrCreateEmbedder();\n\n\t\t// Embed the query\n\t\tconst queryVector = await embedder.embedQuery(query);\n\n\t\t// Get all stored embeddings\n\t\tconst storedVectors = db.getAllEmbeddings(config.modelName);\n\n\t\tif (storedVectors.size === 0) {\n\t\t\treturn new Map();\n\t\t}\n\n\t\tconst topK = topKSimilar(queryVector, storedVectors, limit);\n\n\t\t// Convert to Map, clamping negative similarities to 0\n\t\tconst scores = new Map<number, number>();\n\t\tfor (const { id, score } of topK) {\n\t\t\tscores.set(id, Math.max(0, score));\n\t\t}\n\t\treturn scores;\n\t}\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Collect all unique chunk IDs that appear in any metric's results. */\nfunction collectCandidateIds(...scoreMaps: Map<number, number>[]): Set<number> {\n\tconst ids = new Set<number>();\n\tfor (const map of scoreMaps) {\n\t\tfor (const id of map.keys()) {\n\t\t\tids.add(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\n/** Aggregate chunk-level scores to file-level scores (max per file). */\nfunction aggregateFileScores(chunks: StoredChunk[], ...scoreMaps: Map<number, number>[]): Map<number, number> {\n\tconst fileScores = new Map<number, number>();\n\n\tfor (const chunk of chunks) {\n\t\tlet maxScore = 0;\n\t\tfor (const map of scoreMaps) {\n\t\t\tconst s = map.get(chunk.id);\n\t\t\tif (s !== undefined && s > maxScore) maxScore = s;\n\t\t}\n\t\tif (maxScore > 0) {\n\t\t\tconst existing = fileScores.get(chunk.fileId);\n\t\t\tif (existing === undefined || maxScore > existing) {\n\t\t\t\tfileScores.set(chunk.fileId, maxScore);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fileScores;\n}\n\n/**\n * Compute a dynamic threshold for import graph seeds.\n * Uses the median score — only the top half of files are strong enough seeds.\n * Falls back to 0.1 minimum to avoid accepting near-zero scores.\n */\nfunction computeSeedThreshold(fileScores: Map<number, number>): number {\n\tif (fileScores.size === 0) return 0;\n\tconst sorted = [...fileScores.values()].sort((a, b) => b - a);\n\tconst median = sorted[Math.floor(sorted.length / 2)];\n\treturn Math.max(median, 0.1);\n}\n\n/** Build a map of fileId → chunk IDs for that file. */\nfunction buildFileChunkMap(chunks: StoredChunk[]): Map<number, number[]> {\n\tconst map = new Map<number, number[]>();\n\tfor (const chunk of chunks) {\n\t\tconst existing = map.get(chunk.fileId);\n\t\tif (existing) existing.push(chunk.id);\n\t\telse map.set(chunk.fileId, [chunk.id]);\n\t}\n\treturn map;\n}\n\n/** Common English stopwords to exclude from FTS queries. */\nconst STOPWORDS = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"but\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"had\",\n\t\"has\",\n\t\"have\",\n\t\"he\",\n\t\"her\",\n\t\"his\",\n\t\"how\",\n\t\"i\",\n\t\"if\",\n\t\"in\",\n\t\"into\",\n\t\"is\",\n\t\"it\",\n\t\"its\",\n\t\"me\",\n\t\"my\",\n\t\"no\",\n\t\"not\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"our\",\n\t\"she\",\n\t\"so\",\n\t\"than\",\n\t\"that\",\n\t\"the\",\n\t\"their\",\n\t\"them\",\n\t\"then\",\n\t\"there\",\n\t\"these\",\n\t\"they\",\n\t\"this\",\n\t\"to\",\n\t\"up\",\n\t\"us\",\n\t\"was\",\n\t\"we\",\n\t\"what\",\n\t\"when\",\n\t\"where\",\n\t\"which\",\n\t\"who\",\n\t\"will\",\n\t\"with\",\n\t\"would\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Sanitize a query string for FTS5 MATCH syntax.\n * FTS5 chokes on certain characters — strip operators and wrap terms.\n *\n * Removes stopwords and uses OR between terms so multi-word queries return\n * partial matches (FTS5's default implicit AND is too restrictive).\n */\nfunction sanitizeFtsQuery(query: string): string {\n\t// Remove FTS5 operators and special chars\n\tconst cleaned = query\n\t\t.replace(/[*\"():^{}[\\]~!@#$%&=+|<>]/g, \" \")\n\t\t.replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, \" \")\n\t\t.trim();\n\n\t// Split into tokens, remove stopwords, join with OR\n\tconst tokens = cleaned.split(/\\s+/).filter((t) => t.length > 0 && !STOPWORDS.has(t.toLowerCase()));\n\tif (tokens.length === 0) return '\"\"';\n\tif (tokens.length === 1) return tokens[0];\n\treturn tokens.join(\" OR \");\n}\n"]}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/core/search/search.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,OAAO,KAAK,EAAe,qBAAqB,EAAgB,YAAY,EAAe,MAAM,YAAY,CAAC;AAe9G,MAAM,WAAW,aAAa;IAC7B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,CAAC,EAAE,qBAAqB,CAAC;CACnC;AAMD,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,WAAW,CAAoC;IAEvD,YAAY,WAAW,EAAE,MAAM,EAE9B;IAED,oEAAoE;IACpE,MAAM,CAAC,WAAW,IAAI,OAAO,CAE5B;IAED;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAiI5E;IAED,wDAAwD;IACxD,QAAQ,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAGnD;IAED;;;;;;OAMG;IACH,UAAU,IAAI,IAAI,CAUjB;IAED,yBAAyB;IACzB,KAAK,IAAI,IAAI,CAQZ;IAMD,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,mBAAmB;YAoBb,mBAAmB;CA4BjC","sourcesContent":["/**\n * Main search API.\n *\n * Orchestrates: check/build index → compute all 6 metrics → classify query\n * → duplicate columns → POEM rank → assemble results.\n */\n\nimport { existsSync, unlinkSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { SearchDatabase } from \"./db.js\";\nimport { Embedder } from \"./embedder.js\";\nimport { IndexManager } from \"./index-manager.js\";\nimport { computeBm25Scores } from \"./metrics/bm25.js\";\nimport { computeGitRecencyScores } from \"./metrics/git-recency.js\";\nimport { computeImportGraphScores } from \"./metrics/import-graph.js\";\nimport { computePathMatchScores } from \"./metrics/path-match.js\";\nimport { computeSymbolMatchScores } from \"./metrics/symbol-match.js\";\nimport { poemRank } from \"./poem.js\";\nimport { classifyQuery } from \"./query-classifier.js\";\nimport type { IndexConfig, IndexProgressCallback, MetricScores, SearchResult, StoredChunk } from \"./types.js\";\nimport { topKSimilar } from \"./vector-store.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MODEL_NAME = \"Xenova/all-MiniLM-L6-v2\";\nconst DEFAULT_RESULT_LIMIT = 20;\nconst METRIC_CANDIDATE_LIMIT = 1000;\n\n// ============================================================================\n// Search Options\n// ============================================================================\n\nexport interface SearchOptions {\n\t/** Maximum number of results to return. Default: 20. */\n\tlimit?: number;\n\t/** Restrict search to files under this path (relative to project root). */\n\tpathFilter?: string;\n\t/** Progress callback for indexing operations. */\n\tonProgress?: IndexProgressCallback;\n}\n\n// ============================================================================\n// Search Engine\n// ============================================================================\n\nexport class SearchEngine {\n\tprivate readonly projectRoot: string;\n\tprivate indexManager: IndexManager | null = null;\n\tprivate embedderPromise: Promise<Embedder> | null = null;\n\tprivate searchQueue: Promise<void> = Promise.resolve();\n\n\tconstructor(projectRoot: string) {\n\t\tthis.projectRoot = projectRoot;\n\t}\n\n\t/** Check if semantic search is available (requires node:sqlite). */\n\tstatic isAvailable(): boolean {\n\t\treturn IndexManager.isAvailable();\n\t}\n\n\t/**\n\t * Search the codebase with a natural language or identifier query.\n\t *\n\t * On first call, builds the index (scans, chunks, embeds). Subsequent calls\n\t * incrementally update changed files before searching.\n\t */\n\tasync search(query: string, options?: SearchOptions): Promise<SearchResult[]> {\n\t\t// Chain through searchQueue so concurrent calls serialize\n\t\tlet resolve!: () => void;\n\t\tconst gate = new Promise<void>((r) => {\n\t\t\tresolve = r;\n\t\t});\n\t\tconst waitFor = this.searchQueue;\n\t\tthis.searchQueue = gate;\n\n\t\ttry {\n\t\t\tawait waitFor;\n\n\t\t\tconst limit = options?.limit ?? DEFAULT_RESULT_LIMIT;\n\t\t\tconst onProgress = options?.onProgress;\n\n\t\t\t// Ensure index is built and up to date\n\t\t\tconst indexManager = this.getIndexManager();\n\t\t\tconst db = indexManager.getDb();\n\n\t\t\t// Share our embedder with IndexManager so it doesn't create a second one\n\t\t\tconst embedder = await this.getOrCreateEmbedder();\n\t\t\tindexManager.setEmbedder(embedder);\n\n\t\t\tawait indexManager.buildIndex(onProgress);\n\t\t\tawait indexManager.ensureEmbeddings(onProgress);\n\n\t\t\t// Get all chunks (potentially filtered by path)\n\t\t\tlet allChunks = db.getAllChunks();\n\t\t\tif (options?.pathFilter) {\n\t\t\t\tconst filter = options.pathFilter;\n\t\t\t\tallChunks = allChunks.filter((c) => c.filePath.startsWith(filter));\n\t\t\t}\n\n\t\t\tif (allChunks.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// Classify query type for POEM column weighting\n\t\t\tconst queryType = classifyQuery(query);\n\n\t\t\t// Compute all 6 metrics\n\t\t\tonProgress?.(\"searching\", 0, 6);\n\n\t\t\t// 1. BM25 (FTS5)\n\t\t\tconst bm25Scores = computeBm25Scores(db, sanitizeFtsQuery(query), METRIC_CANDIDATE_LIMIT);\n\t\t\tonProgress?.(\"searching\", 1, 6);\n\n\t\t\t// 2. Cosine similarity (vector search)\n\t\t\tconst cosineScores = await this.computeVectorScores(db, query, METRIC_CANDIDATE_LIMIT, onProgress);\n\t\t\tonProgress?.(\"searching\", 2, 6);\n\n\t\t\t// 3. Path match\n\t\t\tconst pathScores = computePathMatchScores(query, allChunks);\n\t\t\tonProgress?.(\"searching\", 3, 6);\n\n\t\t\t// 4. Symbol match\n\t\t\tconst symbols = db.getAllSymbols();\n\t\t\tconst symbolScores = computeSymbolMatchScores(query, symbols);\n\t\t\tonProgress?.(\"searching\", 4, 6);\n\n\t\t\t// 5. Import graph (use BM25 + cosine as seed scores, aggregated per file)\n\t\t\t// Only use files with strong scores as seeds — low-scoring files (e.g. from\n\t\t\t// common OR terms matching everywhere) pollute the seed set and prevent\n\t\t\t// meaningful propagation.\n\t\t\tconst fileSeedScores = aggregateFileScores(allChunks, bm25Scores, cosineScores);\n\t\t\tconst seedThreshold = computeSeedThreshold(fileSeedScores);\n\t\t\tconst filteredSeeds = new Map<number, number>();\n\t\t\tfor (const [fileId, score] of fileSeedScores) {\n\t\t\t\tif (score >= seedThreshold) filteredSeeds.set(fileId, score);\n\t\t\t}\n\t\t\tconst fileIdToChunkIds = buildFileChunkMap(allChunks);\n\t\t\tconst importScores = computeImportGraphScores(db, filteredSeeds, fileIdToChunkIds);\n\t\t\tonProgress?.(\"searching\", 5, 6);\n\n\t\t\t// 6. Git recency\n\t\t\tconst recencyScores = await computeGitRecencyScores(this.projectRoot, allChunks);\n\t\t\tonProgress?.(\"searching\", 6, 6);\n\n\t\t\t// Build MetricScores for each candidate chunk\n\t\t\tconst candidateIds = collectCandidateIds(\n\t\t\t\tbm25Scores,\n\t\t\t\tcosineScores,\n\t\t\t\tpathScores,\n\t\t\t\tsymbolScores,\n\t\t\t\timportScores,\n\t\t\t\trecencyScores,\n\t\t\t);\n\t\t\tconst candidates = new Map<number, MetricScores>();\n\n\t\t\tfor (const id of candidateIds) {\n\t\t\t\tcandidates.set(id, {\n\t\t\t\t\tbm25: bm25Scores.get(id) ?? 0,\n\t\t\t\t\tcosine: cosineScores.get(id) ?? 0,\n\t\t\t\t\tpathMatch: pathScores.get(id) ?? 0,\n\t\t\t\t\tsymbolMatch: symbolScores.get(id) ?? 0,\n\t\t\t\t\timportGraph: importScores.get(id) ?? 0,\n\t\t\t\t\tgitRecency: recencyScores.get(id) ?? 0,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (candidates.size === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// POEM rank\n\t\t\tconst ranked = poemRank(candidates, queryType);\n\n\t\t\t// Assemble results\n\t\t\tconst chunkMap = new Map<number, StoredChunk>();\n\t\t\tfor (const chunk of allChunks) {\n\t\t\t\tchunkMap.set(chunk.id, chunk);\n\t\t\t}\n\n\t\t\tconst results: SearchResult[] = [];\n\t\t\tfor (const candidate of ranked.slice(0, limit)) {\n\t\t\t\tconst chunk = chunkMap.get(candidate.id);\n\t\t\t\tif (chunk) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\tchunk,\n\t\t\t\t\t\tscores: candidate.scores,\n\t\t\t\t\t\trank: candidate.rank,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn results;\n\t\t} finally {\n\t\t\tresolve();\n\t\t}\n\t}\n\n\t/** Get index stats without opening a new connection. */\n\tgetStats(): { files: number; chunks: number } | null {\n\t\tif (!this.indexManager) return null;\n\t\treturn this.indexManager.getStats();\n\t}\n\n\t/**\n\t * Reset the search index — delete the DB and close the IndexManager.\n\t *\n\t * Preserves the embedder (expensive ONNX model, unrelated to index state).\n\t * The next `search()` call will lazily re-create the IndexManager and build\n\t * a fresh index from scratch.\n\t */\n\tresetIndex(): void {\n\t\t// Close DB connection first (WAL mode may hold locks)\n\t\tthis.indexManager?.close();\n\t\tthis.indexManager = null;\n\n\t\t// Delete the DB file\n\t\tconst dbPath = path.join(this.projectRoot, \".dreb\", \"index\", \"search.db\");\n\t\tif (existsSync(dbPath)) {\n\t\t\tunlinkSync(dbPath);\n\t\t}\n\t}\n\n\t/** Dispose resources. */\n\tclose(): void {\n\t\tthis.indexManager?.close();\n\t\tthis.indexManager = null;\n\t\t// Dispose embedder if it was created\n\t\tif (this.embedderPromise) {\n\t\t\tthis.embedderPromise.then((e) => e.dispose()).catch(() => {});\n\t\t\tthis.embedderPromise = null;\n\t\t}\n\t}\n\n\t// ========================================================================\n\t// Private\n\t// ========================================================================\n\n\tprivate getIndexManager(): IndexManager {\n\t\tif (!this.indexManager) {\n\t\t\tconst config = this.getIndexConfig();\n\t\t\tthis.indexManager = new IndexManager(config);\n\t\t\tthis.indexManager.open();\n\t\t}\n\t\treturn this.indexManager;\n\t}\n\n\tprivate getIndexConfig(): IndexConfig {\n\t\treturn {\n\t\t\tprojectRoot: this.projectRoot,\n\t\t\tindexDir: path.join(this.projectRoot, \".dreb\", \"index\"),\n\t\t\tglobalMemoryDir: path.join(homedir(), \".dreb\", \"memory\"),\n\t\t\tmodelName: DEFAULT_MODEL_NAME,\n\t\t};\n\t}\n\n\tprivate getOrCreateEmbedder(): Promise<Embedder> {\n\t\tif (!this.embedderPromise) {\n\t\t\tthis.embedderPromise = (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst config = this.getIndexConfig();\n\t\t\t\t\tconst embedder = new Embedder({\n\t\t\t\t\t\tmodelCacheDir: path.join(homedir(), \".dreb\", \"agent\", \"models\"),\n\t\t\t\t\t\tmodelName: config.modelName,\n\t\t\t\t\t});\n\t\t\t\t\tawait embedder.initialize();\n\t\t\t\t\treturn embedder;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.embedderPromise = null; // reset on failure for retry\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t})();\n\t\t}\n\t\treturn this.embedderPromise;\n\t}\n\n\tprivate async computeVectorScores(\n\t\tdb: SearchDatabase,\n\t\tquery: string,\n\t\tlimit: number,\n\t\t_onProgress?: IndexProgressCallback,\n\t): Promise<Map<number, number>> {\n\t\tconst config = this.getIndexConfig();\n\t\tconst embedder = await this.getOrCreateEmbedder();\n\n\t\t// Embed the query\n\t\tconst queryVector = await embedder.embedQuery(query);\n\n\t\t// Get all stored embeddings\n\t\tconst storedVectors = db.getAllEmbeddings(config.modelName);\n\n\t\tif (storedVectors.size === 0) {\n\t\t\treturn new Map();\n\t\t}\n\n\t\tconst topK = topKSimilar(queryVector, storedVectors, limit);\n\n\t\t// Convert to Map, clamping negative similarities to 0\n\t\tconst scores = new Map<number, number>();\n\t\tfor (const { id, score } of topK) {\n\t\t\tscores.set(id, Math.max(0, score));\n\t\t}\n\t\treturn scores;\n\t}\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Collect all unique chunk IDs that appear in any metric's results. */\nfunction collectCandidateIds(...scoreMaps: Map<number, number>[]): Set<number> {\n\tconst ids = new Set<number>();\n\tfor (const map of scoreMaps) {\n\t\tfor (const id of map.keys()) {\n\t\t\tids.add(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\n/** Aggregate chunk-level scores to file-level scores (max per file). */\nfunction aggregateFileScores(chunks: StoredChunk[], ...scoreMaps: Map<number, number>[]): Map<number, number> {\n\tconst fileScores = new Map<number, number>();\n\n\tfor (const chunk of chunks) {\n\t\tlet maxScore = 0;\n\t\tfor (const map of scoreMaps) {\n\t\t\tconst s = map.get(chunk.id);\n\t\t\tif (s !== undefined && s > maxScore) maxScore = s;\n\t\t}\n\t\tif (maxScore > 0) {\n\t\t\tconst existing = fileScores.get(chunk.fileId);\n\t\t\tif (existing === undefined || maxScore > existing) {\n\t\t\t\tfileScores.set(chunk.fileId, maxScore);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fileScores;\n}\n\n/**\n * Compute a dynamic threshold for import graph seeds.\n * Uses the median score — only the top half of files are strong enough seeds.\n * Falls back to 0.1 minimum to avoid accepting near-zero scores.\n */\nfunction computeSeedThreshold(fileScores: Map<number, number>): number {\n\tif (fileScores.size === 0) return 0;\n\tconst sorted = [...fileScores.values()].sort((a, b) => b - a);\n\tconst median = sorted[Math.floor(sorted.length / 2)];\n\treturn Math.max(median, 0.1);\n}\n\n/** Build a map of fileId → chunk IDs for that file. */\nfunction buildFileChunkMap(chunks: StoredChunk[]): Map<number, number[]> {\n\tconst map = new Map<number, number[]>();\n\tfor (const chunk of chunks) {\n\t\tconst existing = map.get(chunk.fileId);\n\t\tif (existing) existing.push(chunk.id);\n\t\telse map.set(chunk.fileId, [chunk.id]);\n\t}\n\treturn map;\n}\n\n/** Common English stopwords to exclude from FTS queries. */\nconst STOPWORDS = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"but\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"had\",\n\t\"has\",\n\t\"have\",\n\t\"he\",\n\t\"her\",\n\t\"his\",\n\t\"how\",\n\t\"i\",\n\t\"if\",\n\t\"in\",\n\t\"into\",\n\t\"is\",\n\t\"it\",\n\t\"its\",\n\t\"me\",\n\t\"my\",\n\t\"no\",\n\t\"not\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"our\",\n\t\"she\",\n\t\"so\",\n\t\"than\",\n\t\"that\",\n\t\"the\",\n\t\"their\",\n\t\"them\",\n\t\"then\",\n\t\"there\",\n\t\"these\",\n\t\"they\",\n\t\"this\",\n\t\"to\",\n\t\"up\",\n\t\"us\",\n\t\"was\",\n\t\"we\",\n\t\"what\",\n\t\"when\",\n\t\"where\",\n\t\"which\",\n\t\"who\",\n\t\"will\",\n\t\"with\",\n\t\"would\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Sanitize a query string for FTS5 MATCH syntax.\n * FTS5 chokes on certain characters — strip operators and wrap terms.\n *\n * Removes stopwords and uses OR between terms so multi-word queries return\n * partial matches (FTS5's default implicit AND is too restrictive).\n */\nfunction sanitizeFtsQuery(query: string): string {\n\t// Remove FTS5 operators and special chars\n\tconst cleaned = query\n\t\t.replace(/[*\"():^{}[\\]~!@#$%&=+|<>]/g, \" \")\n\t\t.replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, \" \")\n\t\t.trim();\n\n\t// Split into tokens, remove stopwords, join with OR\n\tconst tokens = cleaned.split(/\\s+/).filter((t) => t.length > 0 && !STOPWORDS.has(t.toLowerCase()));\n\tif (tokens.length === 0) return '\"\"';\n\tif (tokens.length === 1) return tokens[0];\n\treturn tokens.join(\" OR \");\n}\n"]}
@@ -4,6 +4,7 @@
4
4
  * Orchestrates: check/build index → compute all 6 metrics → classify query
5
5
  * → duplicate columns → POEM rank → assemble results.
6
6
  */
7
+ import { existsSync, unlinkSync } from "node:fs";
7
8
  import { homedir } from "node:os";
8
9
  import path from "node:path";
9
10
  import { Embedder } from "./embedder.js";
@@ -28,7 +29,8 @@ const METRIC_CANDIDATE_LIMIT = 1000;
28
29
  export class SearchEngine {
29
30
  projectRoot;
30
31
  indexManager = null;
31
- embedder = null;
32
+ embedderPromise = null;
33
+ searchQueue = Promise.resolve();
32
34
  constructor(projectRoot) {
33
35
  this.projectRoot = projectRoot;
34
36
  }
@@ -43,94 +45,107 @@ export class SearchEngine {
43
45
  * incrementally update changed files before searching.
44
46
  */
45
47
  async search(query, options) {
46
- const limit = options?.limit ?? DEFAULT_RESULT_LIMIT;
47
- const onProgress = options?.onProgress;
48
- // Ensure index is built and up to date
49
- const indexManager = this.getIndexManager();
50
- const db = indexManager.getDb();
51
- // Share our embedder with IndexManager so it doesn't create a second one
52
- const embedder = await this.getOrCreateEmbedder();
53
- indexManager.setEmbedder(embedder);
54
- await indexManager.buildIndex(onProgress);
55
- await indexManager.ensureEmbeddings(onProgress);
56
- // Get all chunks (potentially filtered by path)
57
- let allChunks = db.getAllChunks();
58
- if (options?.pathFilter) {
59
- const filter = options.pathFilter;
60
- allChunks = allChunks.filter((c) => c.filePath.startsWith(filter));
61
- }
62
- if (allChunks.length === 0) {
63
- return [];
64
- }
65
- // Classify query type for POEM column weighting
66
- const queryType = classifyQuery(query);
67
- // Compute all 6 metrics
68
- onProgress?.("searching", 0, 6);
69
- // 1. BM25 (FTS5)
70
- const bm25Scores = computeBm25Scores(db, sanitizeFtsQuery(query), METRIC_CANDIDATE_LIMIT);
71
- onProgress?.("searching", 1, 6);
72
- // 2. Cosine similarity (vector search)
73
- const cosineScores = await this.computeVectorScores(db, query, METRIC_CANDIDATE_LIMIT, onProgress);
74
- onProgress?.("searching", 2, 6);
75
- // 3. Path match
76
- const pathScores = computePathMatchScores(query, allChunks);
77
- onProgress?.("searching", 3, 6);
78
- // 4. Symbol match
79
- const symbols = db.getAllSymbols();
80
- const symbolScores = computeSymbolMatchScores(query, symbols);
81
- onProgress?.("searching", 4, 6);
82
- // 5. Import graph (use BM25 + cosine as seed scores, aggregated per file)
83
- // Only use files with strong scores as seeds — low-scoring files (e.g. from
84
- // common OR terms matching everywhere) pollute the seed set and prevent
85
- // meaningful propagation.
86
- const fileSeedScores = aggregateFileScores(allChunks, bm25Scores, cosineScores);
87
- const seedThreshold = computeSeedThreshold(fileSeedScores);
88
- const filteredSeeds = new Map();
89
- for (const [fileId, score] of fileSeedScores) {
90
- if (score >= seedThreshold)
91
- filteredSeeds.set(fileId, score);
92
- }
93
- const fileIdToChunkIds = buildFileChunkMap(allChunks);
94
- const importScores = computeImportGraphScores(db, filteredSeeds, fileIdToChunkIds);
95
- onProgress?.("searching", 5, 6);
96
- // 6. Git recency
97
- const recencyScores = await computeGitRecencyScores(this.projectRoot, allChunks);
98
- onProgress?.("searching", 6, 6);
99
- // Build MetricScores for each candidate chunk
100
- const candidateIds = collectCandidateIds(bm25Scores, cosineScores, pathScores, symbolScores, importScores, recencyScores);
101
- const candidates = new Map();
102
- for (const id of candidateIds) {
103
- candidates.set(id, {
104
- bm25: bm25Scores.get(id) ?? 0,
105
- cosine: cosineScores.get(id) ?? 0,
106
- pathMatch: pathScores.get(id) ?? 0,
107
- symbolMatch: symbolScores.get(id) ?? 0,
108
- importGraph: importScores.get(id) ?? 0,
109
- gitRecency: recencyScores.get(id) ?? 0,
110
- });
111
- }
112
- if (candidates.size === 0) {
113
- return [];
114
- }
115
- // POEM rank
116
- const ranked = poemRank(candidates, queryType);
117
- // Assemble results
118
- const chunkMap = new Map();
119
- for (const chunk of allChunks) {
120
- chunkMap.set(chunk.id, chunk);
121
- }
122
- const results = [];
123
- for (const candidate of ranked.slice(0, limit)) {
124
- const chunk = chunkMap.get(candidate.id);
125
- if (chunk) {
126
- results.push({
127
- chunk,
128
- scores: candidate.scores,
129
- rank: candidate.rank,
48
+ // Chain through searchQueue so concurrent calls serialize
49
+ let resolve;
50
+ const gate = new Promise((r) => {
51
+ resolve = r;
52
+ });
53
+ const waitFor = this.searchQueue;
54
+ this.searchQueue = gate;
55
+ try {
56
+ await waitFor;
57
+ const limit = options?.limit ?? DEFAULT_RESULT_LIMIT;
58
+ const onProgress = options?.onProgress;
59
+ // Ensure index is built and up to date
60
+ const indexManager = this.getIndexManager();
61
+ const db = indexManager.getDb();
62
+ // Share our embedder with IndexManager so it doesn't create a second one
63
+ const embedder = await this.getOrCreateEmbedder();
64
+ indexManager.setEmbedder(embedder);
65
+ await indexManager.buildIndex(onProgress);
66
+ await indexManager.ensureEmbeddings(onProgress);
67
+ // Get all chunks (potentially filtered by path)
68
+ let allChunks = db.getAllChunks();
69
+ if (options?.pathFilter) {
70
+ const filter = options.pathFilter;
71
+ allChunks = allChunks.filter((c) => c.filePath.startsWith(filter));
72
+ }
73
+ if (allChunks.length === 0) {
74
+ return [];
75
+ }
76
+ // Classify query type for POEM column weighting
77
+ const queryType = classifyQuery(query);
78
+ // Compute all 6 metrics
79
+ onProgress?.("searching", 0, 6);
80
+ // 1. BM25 (FTS5)
81
+ const bm25Scores = computeBm25Scores(db, sanitizeFtsQuery(query), METRIC_CANDIDATE_LIMIT);
82
+ onProgress?.("searching", 1, 6);
83
+ // 2. Cosine similarity (vector search)
84
+ const cosineScores = await this.computeVectorScores(db, query, METRIC_CANDIDATE_LIMIT, onProgress);
85
+ onProgress?.("searching", 2, 6);
86
+ // 3. Path match
87
+ const pathScores = computePathMatchScores(query, allChunks);
88
+ onProgress?.("searching", 3, 6);
89
+ // 4. Symbol match
90
+ const symbols = db.getAllSymbols();
91
+ const symbolScores = computeSymbolMatchScores(query, symbols);
92
+ onProgress?.("searching", 4, 6);
93
+ // 5. Import graph (use BM25 + cosine as seed scores, aggregated per file)
94
+ // Only use files with strong scores as seeds — low-scoring files (e.g. from
95
+ // common OR terms matching everywhere) pollute the seed set and prevent
96
+ // meaningful propagation.
97
+ const fileSeedScores = aggregateFileScores(allChunks, bm25Scores, cosineScores);
98
+ const seedThreshold = computeSeedThreshold(fileSeedScores);
99
+ const filteredSeeds = new Map();
100
+ for (const [fileId, score] of fileSeedScores) {
101
+ if (score >= seedThreshold)
102
+ filteredSeeds.set(fileId, score);
103
+ }
104
+ const fileIdToChunkIds = buildFileChunkMap(allChunks);
105
+ const importScores = computeImportGraphScores(db, filteredSeeds, fileIdToChunkIds);
106
+ onProgress?.("searching", 5, 6);
107
+ // 6. Git recency
108
+ const recencyScores = await computeGitRecencyScores(this.projectRoot, allChunks);
109
+ onProgress?.("searching", 6, 6);
110
+ // Build MetricScores for each candidate chunk
111
+ const candidateIds = collectCandidateIds(bm25Scores, cosineScores, pathScores, symbolScores, importScores, recencyScores);
112
+ const candidates = new Map();
113
+ for (const id of candidateIds) {
114
+ candidates.set(id, {
115
+ bm25: bm25Scores.get(id) ?? 0,
116
+ cosine: cosineScores.get(id) ?? 0,
117
+ pathMatch: pathScores.get(id) ?? 0,
118
+ symbolMatch: symbolScores.get(id) ?? 0,
119
+ importGraph: importScores.get(id) ?? 0,
120
+ gitRecency: recencyScores.get(id) ?? 0,
130
121
  });
131
122
  }
123
+ if (candidates.size === 0) {
124
+ return [];
125
+ }
126
+ // POEM rank
127
+ const ranked = poemRank(candidates, queryType);
128
+ // Assemble results
129
+ const chunkMap = new Map();
130
+ for (const chunk of allChunks) {
131
+ chunkMap.set(chunk.id, chunk);
132
+ }
133
+ const results = [];
134
+ for (const candidate of ranked.slice(0, limit)) {
135
+ const chunk = chunkMap.get(candidate.id);
136
+ if (chunk) {
137
+ results.push({
138
+ chunk,
139
+ scores: candidate.scores,
140
+ rank: candidate.rank,
141
+ });
142
+ }
143
+ }
144
+ return results;
145
+ }
146
+ finally {
147
+ resolve();
132
148
  }
133
- return results;
134
149
  }
135
150
  /** Get index stats without opening a new connection. */
136
151
  getStats() {
@@ -138,12 +153,32 @@ export class SearchEngine {
138
153
  return null;
139
154
  return this.indexManager.getStats();
140
155
  }
156
+ /**
157
+ * Reset the search index — delete the DB and close the IndexManager.
158
+ *
159
+ * Preserves the embedder (expensive ONNX model, unrelated to index state).
160
+ * The next `search()` call will lazily re-create the IndexManager and build
161
+ * a fresh index from scratch.
162
+ */
163
+ resetIndex() {
164
+ // Close DB connection first (WAL mode may hold locks)
165
+ this.indexManager?.close();
166
+ this.indexManager = null;
167
+ // Delete the DB file
168
+ const dbPath = path.join(this.projectRoot, ".dreb", "index", "search.db");
169
+ if (existsSync(dbPath)) {
170
+ unlinkSync(dbPath);
171
+ }
172
+ }
141
173
  /** Dispose resources. */
142
174
  close() {
143
175
  this.indexManager?.close();
144
176
  this.indexManager = null;
145
- this.embedder?.dispose();
146
- this.embedder = null;
177
+ // Dispose embedder if it was created
178
+ if (this.embedderPromise) {
179
+ this.embedderPromise.then((e) => e.dispose()).catch(() => { });
180
+ this.embedderPromise = null;
181
+ }
147
182
  }
148
183
  // ========================================================================
149
184
  // Private
@@ -164,16 +199,25 @@ export class SearchEngine {
164
199
  modelName: DEFAULT_MODEL_NAME,
165
200
  };
166
201
  }
167
- async getOrCreateEmbedder() {
168
- if (!this.embedder) {
169
- const config = this.getIndexConfig();
170
- this.embedder = new Embedder({
171
- modelCacheDir: path.join(homedir(), ".dreb", "agent", "models"),
172
- modelName: config.modelName,
173
- });
174
- await this.embedder.initialize();
202
+ getOrCreateEmbedder() {
203
+ if (!this.embedderPromise) {
204
+ this.embedderPromise = (async () => {
205
+ try {
206
+ const config = this.getIndexConfig();
207
+ const embedder = new Embedder({
208
+ modelCacheDir: path.join(homedir(), ".dreb", "agent", "models"),
209
+ modelName: config.modelName,
210
+ });
211
+ await embedder.initialize();
212
+ return embedder;
213
+ }
214
+ catch (err) {
215
+ this.embedderPromise = null; // reset on failure for retry
216
+ throw err;
217
+ }
218
+ })();
175
219
  }
176
- return this.embedder;
220
+ return this.embedderPromise;
177
221
  }
178
222
  async computeVectorScores(db, query, limit, _onProgress) {
179
223
  const config = this.getIndexConfig();
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/core/search/search.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Main search API.\n *\n * Orchestrates: check/build index → compute all 6 metrics → classify query\n * → duplicate columns → POEM rank → assemble results.\n */\n\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { SearchDatabase } from \"./db.js\";\nimport { Embedder } from \"./embedder.js\";\nimport { IndexManager } from \"./index-manager.js\";\nimport { computeBm25Scores } from \"./metrics/bm25.js\";\nimport { computeGitRecencyScores } from \"./metrics/git-recency.js\";\nimport { computeImportGraphScores } from \"./metrics/import-graph.js\";\nimport { computePathMatchScores } from \"./metrics/path-match.js\";\nimport { computeSymbolMatchScores } from \"./metrics/symbol-match.js\";\nimport { poemRank } from \"./poem.js\";\nimport { classifyQuery } from \"./query-classifier.js\";\nimport type { IndexConfig, IndexProgressCallback, MetricScores, SearchResult, StoredChunk } from \"./types.js\";\nimport { topKSimilar } from \"./vector-store.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MODEL_NAME = \"Xenova/all-MiniLM-L6-v2\";\nconst DEFAULT_RESULT_LIMIT = 20;\nconst METRIC_CANDIDATE_LIMIT = 1000;\n\n// ============================================================================\n// Search Options\n// ============================================================================\n\nexport interface SearchOptions {\n\t/** Maximum number of results to return. Default: 20. */\n\tlimit?: number;\n\t/** Restrict search to files under this path (relative to project root). */\n\tpathFilter?: string;\n\t/** Progress callback for indexing operations. */\n\tonProgress?: IndexProgressCallback;\n}\n\n// ============================================================================\n// Search Engine\n// ============================================================================\n\nexport class SearchEngine {\n\tprivate readonly projectRoot: string;\n\tprivate indexManager: IndexManager | null = null;\n\tprivate embedder: Embedder | null = null;\n\n\tconstructor(projectRoot: string) {\n\t\tthis.projectRoot = projectRoot;\n\t}\n\n\t/** Check if semantic search is available (requires node:sqlite). */\n\tstatic isAvailable(): boolean {\n\t\treturn IndexManager.isAvailable();\n\t}\n\n\t/**\n\t * Search the codebase with a natural language or identifier query.\n\t *\n\t * On first call, builds the index (scans, chunks, embeds). Subsequent calls\n\t * incrementally update changed files before searching.\n\t */\n\tasync search(query: string, options?: SearchOptions): Promise<SearchResult[]> {\n\t\tconst limit = options?.limit ?? DEFAULT_RESULT_LIMIT;\n\t\tconst onProgress = options?.onProgress;\n\n\t\t// Ensure index is built and up to date\n\t\tconst indexManager = this.getIndexManager();\n\t\tconst db = indexManager.getDb();\n\n\t\t// Share our embedder with IndexManager so it doesn't create a second one\n\t\tconst embedder = await this.getOrCreateEmbedder();\n\t\tindexManager.setEmbedder(embedder);\n\n\t\tawait indexManager.buildIndex(onProgress);\n\t\tawait indexManager.ensureEmbeddings(onProgress);\n\n\t\t// Get all chunks (potentially filtered by path)\n\t\tlet allChunks = db.getAllChunks();\n\t\tif (options?.pathFilter) {\n\t\t\tconst filter = options.pathFilter;\n\t\t\tallChunks = allChunks.filter((c) => c.filePath.startsWith(filter));\n\t\t}\n\n\t\tif (allChunks.length === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// Classify query type for POEM column weighting\n\t\tconst queryType = classifyQuery(query);\n\n\t\t// Compute all 6 metrics\n\t\tonProgress?.(\"searching\", 0, 6);\n\n\t\t// 1. BM25 (FTS5)\n\t\tconst bm25Scores = computeBm25Scores(db, sanitizeFtsQuery(query), METRIC_CANDIDATE_LIMIT);\n\t\tonProgress?.(\"searching\", 1, 6);\n\n\t\t// 2. Cosine similarity (vector search)\n\t\tconst cosineScores = await this.computeVectorScores(db, query, METRIC_CANDIDATE_LIMIT, onProgress);\n\t\tonProgress?.(\"searching\", 2, 6);\n\n\t\t// 3. Path match\n\t\tconst pathScores = computePathMatchScores(query, allChunks);\n\t\tonProgress?.(\"searching\", 3, 6);\n\n\t\t// 4. Symbol match\n\t\tconst symbols = db.getAllSymbols();\n\t\tconst symbolScores = computeSymbolMatchScores(query, symbols);\n\t\tonProgress?.(\"searching\", 4, 6);\n\n\t\t// 5. Import graph (use BM25 + cosine as seed scores, aggregated per file)\n\t\t// Only use files with strong scores as seeds — low-scoring files (e.g. from\n\t\t// common OR terms matching everywhere) pollute the seed set and prevent\n\t\t// meaningful propagation.\n\t\tconst fileSeedScores = aggregateFileScores(allChunks, bm25Scores, cosineScores);\n\t\tconst seedThreshold = computeSeedThreshold(fileSeedScores);\n\t\tconst filteredSeeds = new Map<number, number>();\n\t\tfor (const [fileId, score] of fileSeedScores) {\n\t\t\tif (score >= seedThreshold) filteredSeeds.set(fileId, score);\n\t\t}\n\t\tconst fileIdToChunkIds = buildFileChunkMap(allChunks);\n\t\tconst importScores = computeImportGraphScores(db, filteredSeeds, fileIdToChunkIds);\n\t\tonProgress?.(\"searching\", 5, 6);\n\n\t\t// 6. Git recency\n\t\tconst recencyScores = await computeGitRecencyScores(this.projectRoot, allChunks);\n\t\tonProgress?.(\"searching\", 6, 6);\n\n\t\t// Build MetricScores for each candidate chunk\n\t\tconst candidateIds = collectCandidateIds(\n\t\t\tbm25Scores,\n\t\t\tcosineScores,\n\t\t\tpathScores,\n\t\t\tsymbolScores,\n\t\t\timportScores,\n\t\t\trecencyScores,\n\t\t);\n\t\tconst candidates = new Map<number, MetricScores>();\n\n\t\tfor (const id of candidateIds) {\n\t\t\tcandidates.set(id, {\n\t\t\t\tbm25: bm25Scores.get(id) ?? 0,\n\t\t\t\tcosine: cosineScores.get(id) ?? 0,\n\t\t\t\tpathMatch: pathScores.get(id) ?? 0,\n\t\t\t\tsymbolMatch: symbolScores.get(id) ?? 0,\n\t\t\t\timportGraph: importScores.get(id) ?? 0,\n\t\t\t\tgitRecency: recencyScores.get(id) ?? 0,\n\t\t\t});\n\t\t}\n\n\t\tif (candidates.size === 0) {\n\t\t\treturn [];\n\t\t}\n\n\t\t// POEM rank\n\t\tconst ranked = poemRank(candidates, queryType);\n\n\t\t// Assemble results\n\t\tconst chunkMap = new Map<number, StoredChunk>();\n\t\tfor (const chunk of allChunks) {\n\t\t\tchunkMap.set(chunk.id, chunk);\n\t\t}\n\n\t\tconst results: SearchResult[] = [];\n\t\tfor (const candidate of ranked.slice(0, limit)) {\n\t\t\tconst chunk = chunkMap.get(candidate.id);\n\t\t\tif (chunk) {\n\t\t\t\tresults.push({\n\t\t\t\t\tchunk,\n\t\t\t\t\tscores: candidate.scores,\n\t\t\t\t\trank: candidate.rank,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/** Get index stats without opening a new connection. */\n\tgetStats(): { files: number; chunks: number } | null {\n\t\tif (!this.indexManager) return null;\n\t\treturn this.indexManager.getStats();\n\t}\n\n\t/** Dispose resources. */\n\tclose(): void {\n\t\tthis.indexManager?.close();\n\t\tthis.indexManager = null;\n\t\tthis.embedder?.dispose();\n\t\tthis.embedder = null;\n\t}\n\n\t// ========================================================================\n\t// Private\n\t// ========================================================================\n\n\tprivate getIndexManager(): IndexManager {\n\t\tif (!this.indexManager) {\n\t\t\tconst config = this.getIndexConfig();\n\t\t\tthis.indexManager = new IndexManager(config);\n\t\t\tthis.indexManager.open();\n\t\t}\n\t\treturn this.indexManager;\n\t}\n\n\tprivate getIndexConfig(): IndexConfig {\n\t\treturn {\n\t\t\tprojectRoot: this.projectRoot,\n\t\t\tindexDir: path.join(this.projectRoot, \".dreb\", \"index\"),\n\t\t\tglobalMemoryDir: path.join(homedir(), \".dreb\", \"memory\"),\n\t\t\tmodelName: DEFAULT_MODEL_NAME,\n\t\t};\n\t}\n\n\tprivate async getOrCreateEmbedder(): Promise<Embedder> {\n\t\tif (!this.embedder) {\n\t\t\tconst config = this.getIndexConfig();\n\t\t\tthis.embedder = new Embedder({\n\t\t\t\tmodelCacheDir: path.join(homedir(), \".dreb\", \"agent\", \"models\"),\n\t\t\t\tmodelName: config.modelName,\n\t\t\t});\n\t\t\tawait this.embedder.initialize();\n\t\t}\n\t\treturn this.embedder;\n\t}\n\n\tprivate async computeVectorScores(\n\t\tdb: SearchDatabase,\n\t\tquery: string,\n\t\tlimit: number,\n\t\t_onProgress?: IndexProgressCallback,\n\t): Promise<Map<number, number>> {\n\t\tconst config = this.getIndexConfig();\n\t\tconst embedder = await this.getOrCreateEmbedder();\n\n\t\t// Embed the query\n\t\tconst queryVector = await embedder.embedQuery(query);\n\n\t\t// Get all stored embeddings\n\t\tconst storedVectors = db.getAllEmbeddings(config.modelName);\n\n\t\tif (storedVectors.size === 0) {\n\t\t\treturn new Map();\n\t\t}\n\n\t\tconst topK = topKSimilar(queryVector, storedVectors, limit);\n\n\t\t// Convert to Map, clamping negative similarities to 0\n\t\tconst scores = new Map<number, number>();\n\t\tfor (const { id, score } of topK) {\n\t\t\tscores.set(id, Math.max(0, score));\n\t\t}\n\t\treturn scores;\n\t}\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Collect all unique chunk IDs that appear in any metric's results. */\nfunction collectCandidateIds(...scoreMaps: Map<number, number>[]): Set<number> {\n\tconst ids = new Set<number>();\n\tfor (const map of scoreMaps) {\n\t\tfor (const id of map.keys()) {\n\t\t\tids.add(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\n/** Aggregate chunk-level scores to file-level scores (max per file). */\nfunction aggregateFileScores(chunks: StoredChunk[], ...scoreMaps: Map<number, number>[]): Map<number, number> {\n\tconst fileScores = new Map<number, number>();\n\n\tfor (const chunk of chunks) {\n\t\tlet maxScore = 0;\n\t\tfor (const map of scoreMaps) {\n\t\t\tconst s = map.get(chunk.id);\n\t\t\tif (s !== undefined && s > maxScore) maxScore = s;\n\t\t}\n\t\tif (maxScore > 0) {\n\t\t\tconst existing = fileScores.get(chunk.fileId);\n\t\t\tif (existing === undefined || maxScore > existing) {\n\t\t\t\tfileScores.set(chunk.fileId, maxScore);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fileScores;\n}\n\n/**\n * Compute a dynamic threshold for import graph seeds.\n * Uses the median score — only the top half of files are strong enough seeds.\n * Falls back to 0.1 minimum to avoid accepting near-zero scores.\n */\nfunction computeSeedThreshold(fileScores: Map<number, number>): number {\n\tif (fileScores.size === 0) return 0;\n\tconst sorted = [...fileScores.values()].sort((a, b) => b - a);\n\tconst median = sorted[Math.floor(sorted.length / 2)];\n\treturn Math.max(median, 0.1);\n}\n\n/** Build a map of fileId → chunk IDs for that file. */\nfunction buildFileChunkMap(chunks: StoredChunk[]): Map<number, number[]> {\n\tconst map = new Map<number, number[]>();\n\tfor (const chunk of chunks) {\n\t\tconst existing = map.get(chunk.fileId);\n\t\tif (existing) existing.push(chunk.id);\n\t\telse map.set(chunk.fileId, [chunk.id]);\n\t}\n\treturn map;\n}\n\n/** Common English stopwords to exclude from FTS queries. */\nconst STOPWORDS = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"but\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"had\",\n\t\"has\",\n\t\"have\",\n\t\"he\",\n\t\"her\",\n\t\"his\",\n\t\"how\",\n\t\"i\",\n\t\"if\",\n\t\"in\",\n\t\"into\",\n\t\"is\",\n\t\"it\",\n\t\"its\",\n\t\"me\",\n\t\"my\",\n\t\"no\",\n\t\"not\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"our\",\n\t\"she\",\n\t\"so\",\n\t\"than\",\n\t\"that\",\n\t\"the\",\n\t\"their\",\n\t\"them\",\n\t\"then\",\n\t\"there\",\n\t\"these\",\n\t\"they\",\n\t\"this\",\n\t\"to\",\n\t\"up\",\n\t\"us\",\n\t\"was\",\n\t\"we\",\n\t\"what\",\n\t\"when\",\n\t\"where\",\n\t\"which\",\n\t\"who\",\n\t\"will\",\n\t\"with\",\n\t\"would\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Sanitize a query string for FTS5 MATCH syntax.\n * FTS5 chokes on certain characters — strip operators and wrap terms.\n *\n * Removes stopwords and uses OR between terms so multi-word queries return\n * partial matches (FTS5's default implicit AND is too restrictive).\n */\nfunction sanitizeFtsQuery(query: string): string {\n\t// Remove FTS5 operators and special chars\n\tconst cleaned = query\n\t\t.replace(/[*\"():^{}[\\]~!@#$%&=+|<>]/g, \" \")\n\t\t.replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, \" \")\n\t\t.trim();\n\n\t// Split into tokens, remove stopwords, join with OR\n\tconst tokens = cleaned.split(/\\s+/).filter((t) => t.length > 0 && !STOPWORDS.has(t.toLowerCase()));\n\tif (tokens.length === 0) return '\"\"';\n\tif (tokens.length === 1) return tokens[0];\n\treturn tokens.join(\" OR \");\n}\n"]}
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/core/search/search.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Main search API.\n *\n * Orchestrates: check/build index → compute all 6 metrics → classify query\n * → duplicate columns → POEM rank → assemble results.\n */\n\nimport { existsSync, unlinkSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { SearchDatabase } from \"./db.js\";\nimport { Embedder } from \"./embedder.js\";\nimport { IndexManager } from \"./index-manager.js\";\nimport { computeBm25Scores } from \"./metrics/bm25.js\";\nimport { computeGitRecencyScores } from \"./metrics/git-recency.js\";\nimport { computeImportGraphScores } from \"./metrics/import-graph.js\";\nimport { computePathMatchScores } from \"./metrics/path-match.js\";\nimport { computeSymbolMatchScores } from \"./metrics/symbol-match.js\";\nimport { poemRank } from \"./poem.js\";\nimport { classifyQuery } from \"./query-classifier.js\";\nimport type { IndexConfig, IndexProgressCallback, MetricScores, SearchResult, StoredChunk } from \"./types.js\";\nimport { topKSimilar } from \"./vector-store.js\";\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst DEFAULT_MODEL_NAME = \"Xenova/all-MiniLM-L6-v2\";\nconst DEFAULT_RESULT_LIMIT = 20;\nconst METRIC_CANDIDATE_LIMIT = 1000;\n\n// ============================================================================\n// Search Options\n// ============================================================================\n\nexport interface SearchOptions {\n\t/** Maximum number of results to return. Default: 20. */\n\tlimit?: number;\n\t/** Restrict search to files under this path (relative to project root). */\n\tpathFilter?: string;\n\t/** Progress callback for indexing operations. */\n\tonProgress?: IndexProgressCallback;\n}\n\n// ============================================================================\n// Search Engine\n// ============================================================================\n\nexport class SearchEngine {\n\tprivate readonly projectRoot: string;\n\tprivate indexManager: IndexManager | null = null;\n\tprivate embedderPromise: Promise<Embedder> | null = null;\n\tprivate searchQueue: Promise<void> = Promise.resolve();\n\n\tconstructor(projectRoot: string) {\n\t\tthis.projectRoot = projectRoot;\n\t}\n\n\t/** Check if semantic search is available (requires node:sqlite). */\n\tstatic isAvailable(): boolean {\n\t\treturn IndexManager.isAvailable();\n\t}\n\n\t/**\n\t * Search the codebase with a natural language or identifier query.\n\t *\n\t * On first call, builds the index (scans, chunks, embeds). Subsequent calls\n\t * incrementally update changed files before searching.\n\t */\n\tasync search(query: string, options?: SearchOptions): Promise<SearchResult[]> {\n\t\t// Chain through searchQueue so concurrent calls serialize\n\t\tlet resolve!: () => void;\n\t\tconst gate = new Promise<void>((r) => {\n\t\t\tresolve = r;\n\t\t});\n\t\tconst waitFor = this.searchQueue;\n\t\tthis.searchQueue = gate;\n\n\t\ttry {\n\t\t\tawait waitFor;\n\n\t\t\tconst limit = options?.limit ?? DEFAULT_RESULT_LIMIT;\n\t\t\tconst onProgress = options?.onProgress;\n\n\t\t\t// Ensure index is built and up to date\n\t\t\tconst indexManager = this.getIndexManager();\n\t\t\tconst db = indexManager.getDb();\n\n\t\t\t// Share our embedder with IndexManager so it doesn't create a second one\n\t\t\tconst embedder = await this.getOrCreateEmbedder();\n\t\t\tindexManager.setEmbedder(embedder);\n\n\t\t\tawait indexManager.buildIndex(onProgress);\n\t\t\tawait indexManager.ensureEmbeddings(onProgress);\n\n\t\t\t// Get all chunks (potentially filtered by path)\n\t\t\tlet allChunks = db.getAllChunks();\n\t\t\tif (options?.pathFilter) {\n\t\t\t\tconst filter = options.pathFilter;\n\t\t\t\tallChunks = allChunks.filter((c) => c.filePath.startsWith(filter));\n\t\t\t}\n\n\t\t\tif (allChunks.length === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// Classify query type for POEM column weighting\n\t\t\tconst queryType = classifyQuery(query);\n\n\t\t\t// Compute all 6 metrics\n\t\t\tonProgress?.(\"searching\", 0, 6);\n\n\t\t\t// 1. BM25 (FTS5)\n\t\t\tconst bm25Scores = computeBm25Scores(db, sanitizeFtsQuery(query), METRIC_CANDIDATE_LIMIT);\n\t\t\tonProgress?.(\"searching\", 1, 6);\n\n\t\t\t// 2. Cosine similarity (vector search)\n\t\t\tconst cosineScores = await this.computeVectorScores(db, query, METRIC_CANDIDATE_LIMIT, onProgress);\n\t\t\tonProgress?.(\"searching\", 2, 6);\n\n\t\t\t// 3. Path match\n\t\t\tconst pathScores = computePathMatchScores(query, allChunks);\n\t\t\tonProgress?.(\"searching\", 3, 6);\n\n\t\t\t// 4. Symbol match\n\t\t\tconst symbols = db.getAllSymbols();\n\t\t\tconst symbolScores = computeSymbolMatchScores(query, symbols);\n\t\t\tonProgress?.(\"searching\", 4, 6);\n\n\t\t\t// 5. Import graph (use BM25 + cosine as seed scores, aggregated per file)\n\t\t\t// Only use files with strong scores as seeds — low-scoring files (e.g. from\n\t\t\t// common OR terms matching everywhere) pollute the seed set and prevent\n\t\t\t// meaningful propagation.\n\t\t\tconst fileSeedScores = aggregateFileScores(allChunks, bm25Scores, cosineScores);\n\t\t\tconst seedThreshold = computeSeedThreshold(fileSeedScores);\n\t\t\tconst filteredSeeds = new Map<number, number>();\n\t\t\tfor (const [fileId, score] of fileSeedScores) {\n\t\t\t\tif (score >= seedThreshold) filteredSeeds.set(fileId, score);\n\t\t\t}\n\t\t\tconst fileIdToChunkIds = buildFileChunkMap(allChunks);\n\t\t\tconst importScores = computeImportGraphScores(db, filteredSeeds, fileIdToChunkIds);\n\t\t\tonProgress?.(\"searching\", 5, 6);\n\n\t\t\t// 6. Git recency\n\t\t\tconst recencyScores = await computeGitRecencyScores(this.projectRoot, allChunks);\n\t\t\tonProgress?.(\"searching\", 6, 6);\n\n\t\t\t// Build MetricScores for each candidate chunk\n\t\t\tconst candidateIds = collectCandidateIds(\n\t\t\t\tbm25Scores,\n\t\t\t\tcosineScores,\n\t\t\t\tpathScores,\n\t\t\t\tsymbolScores,\n\t\t\t\timportScores,\n\t\t\t\trecencyScores,\n\t\t\t);\n\t\t\tconst candidates = new Map<number, MetricScores>();\n\n\t\t\tfor (const id of candidateIds) {\n\t\t\t\tcandidates.set(id, {\n\t\t\t\t\tbm25: bm25Scores.get(id) ?? 0,\n\t\t\t\t\tcosine: cosineScores.get(id) ?? 0,\n\t\t\t\t\tpathMatch: pathScores.get(id) ?? 0,\n\t\t\t\t\tsymbolMatch: symbolScores.get(id) ?? 0,\n\t\t\t\t\timportGraph: importScores.get(id) ?? 0,\n\t\t\t\t\tgitRecency: recencyScores.get(id) ?? 0,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (candidates.size === 0) {\n\t\t\t\treturn [];\n\t\t\t}\n\n\t\t\t// POEM rank\n\t\t\tconst ranked = poemRank(candidates, queryType);\n\n\t\t\t// Assemble results\n\t\t\tconst chunkMap = new Map<number, StoredChunk>();\n\t\t\tfor (const chunk of allChunks) {\n\t\t\t\tchunkMap.set(chunk.id, chunk);\n\t\t\t}\n\n\t\t\tconst results: SearchResult[] = [];\n\t\t\tfor (const candidate of ranked.slice(0, limit)) {\n\t\t\t\tconst chunk = chunkMap.get(candidate.id);\n\t\t\t\tif (chunk) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\tchunk,\n\t\t\t\t\t\tscores: candidate.scores,\n\t\t\t\t\t\trank: candidate.rank,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn results;\n\t\t} finally {\n\t\t\tresolve();\n\t\t}\n\t}\n\n\t/** Get index stats without opening a new connection. */\n\tgetStats(): { files: number; chunks: number } | null {\n\t\tif (!this.indexManager) return null;\n\t\treturn this.indexManager.getStats();\n\t}\n\n\t/**\n\t * Reset the search index — delete the DB and close the IndexManager.\n\t *\n\t * Preserves the embedder (expensive ONNX model, unrelated to index state).\n\t * The next `search()` call will lazily re-create the IndexManager and build\n\t * a fresh index from scratch.\n\t */\n\tresetIndex(): void {\n\t\t// Close DB connection first (WAL mode may hold locks)\n\t\tthis.indexManager?.close();\n\t\tthis.indexManager = null;\n\n\t\t// Delete the DB file\n\t\tconst dbPath = path.join(this.projectRoot, \".dreb\", \"index\", \"search.db\");\n\t\tif (existsSync(dbPath)) {\n\t\t\tunlinkSync(dbPath);\n\t\t}\n\t}\n\n\t/** Dispose resources. */\n\tclose(): void {\n\t\tthis.indexManager?.close();\n\t\tthis.indexManager = null;\n\t\t// Dispose embedder if it was created\n\t\tif (this.embedderPromise) {\n\t\t\tthis.embedderPromise.then((e) => e.dispose()).catch(() => {});\n\t\t\tthis.embedderPromise = null;\n\t\t}\n\t}\n\n\t// ========================================================================\n\t// Private\n\t// ========================================================================\n\n\tprivate getIndexManager(): IndexManager {\n\t\tif (!this.indexManager) {\n\t\t\tconst config = this.getIndexConfig();\n\t\t\tthis.indexManager = new IndexManager(config);\n\t\t\tthis.indexManager.open();\n\t\t}\n\t\treturn this.indexManager;\n\t}\n\n\tprivate getIndexConfig(): IndexConfig {\n\t\treturn {\n\t\t\tprojectRoot: this.projectRoot,\n\t\t\tindexDir: path.join(this.projectRoot, \".dreb\", \"index\"),\n\t\t\tglobalMemoryDir: path.join(homedir(), \".dreb\", \"memory\"),\n\t\t\tmodelName: DEFAULT_MODEL_NAME,\n\t\t};\n\t}\n\n\tprivate getOrCreateEmbedder(): Promise<Embedder> {\n\t\tif (!this.embedderPromise) {\n\t\t\tthis.embedderPromise = (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst config = this.getIndexConfig();\n\t\t\t\t\tconst embedder = new Embedder({\n\t\t\t\t\t\tmodelCacheDir: path.join(homedir(), \".dreb\", \"agent\", \"models\"),\n\t\t\t\t\t\tmodelName: config.modelName,\n\t\t\t\t\t});\n\t\t\t\t\tawait embedder.initialize();\n\t\t\t\t\treturn embedder;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.embedderPromise = null; // reset on failure for retry\n\t\t\t\t\tthrow err;\n\t\t\t\t}\n\t\t\t})();\n\t\t}\n\t\treturn this.embedderPromise;\n\t}\n\n\tprivate async computeVectorScores(\n\t\tdb: SearchDatabase,\n\t\tquery: string,\n\t\tlimit: number,\n\t\t_onProgress?: IndexProgressCallback,\n\t): Promise<Map<number, number>> {\n\t\tconst config = this.getIndexConfig();\n\t\tconst embedder = await this.getOrCreateEmbedder();\n\n\t\t// Embed the query\n\t\tconst queryVector = await embedder.embedQuery(query);\n\n\t\t// Get all stored embeddings\n\t\tconst storedVectors = db.getAllEmbeddings(config.modelName);\n\n\t\tif (storedVectors.size === 0) {\n\t\t\treturn new Map();\n\t\t}\n\n\t\tconst topK = topKSimilar(queryVector, storedVectors, limit);\n\n\t\t// Convert to Map, clamping negative similarities to 0\n\t\tconst scores = new Map<number, number>();\n\t\tfor (const { id, score } of topK) {\n\t\t\tscores.set(id, Math.max(0, score));\n\t\t}\n\t\treturn scores;\n\t}\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\n/** Collect all unique chunk IDs that appear in any metric's results. */\nfunction collectCandidateIds(...scoreMaps: Map<number, number>[]): Set<number> {\n\tconst ids = new Set<number>();\n\tfor (const map of scoreMaps) {\n\t\tfor (const id of map.keys()) {\n\t\t\tids.add(id);\n\t\t}\n\t}\n\treturn ids;\n}\n\n/** Aggregate chunk-level scores to file-level scores (max per file). */\nfunction aggregateFileScores(chunks: StoredChunk[], ...scoreMaps: Map<number, number>[]): Map<number, number> {\n\tconst fileScores = new Map<number, number>();\n\n\tfor (const chunk of chunks) {\n\t\tlet maxScore = 0;\n\t\tfor (const map of scoreMaps) {\n\t\t\tconst s = map.get(chunk.id);\n\t\t\tif (s !== undefined && s > maxScore) maxScore = s;\n\t\t}\n\t\tif (maxScore > 0) {\n\t\t\tconst existing = fileScores.get(chunk.fileId);\n\t\t\tif (existing === undefined || maxScore > existing) {\n\t\t\t\tfileScores.set(chunk.fileId, maxScore);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fileScores;\n}\n\n/**\n * Compute a dynamic threshold for import graph seeds.\n * Uses the median score — only the top half of files are strong enough seeds.\n * Falls back to 0.1 minimum to avoid accepting near-zero scores.\n */\nfunction computeSeedThreshold(fileScores: Map<number, number>): number {\n\tif (fileScores.size === 0) return 0;\n\tconst sorted = [...fileScores.values()].sort((a, b) => b - a);\n\tconst median = sorted[Math.floor(sorted.length / 2)];\n\treturn Math.max(median, 0.1);\n}\n\n/** Build a map of fileId → chunk IDs for that file. */\nfunction buildFileChunkMap(chunks: StoredChunk[]): Map<number, number[]> {\n\tconst map = new Map<number, number[]>();\n\tfor (const chunk of chunks) {\n\t\tconst existing = map.get(chunk.fileId);\n\t\tif (existing) existing.push(chunk.id);\n\t\telse map.set(chunk.fileId, [chunk.id]);\n\t}\n\treturn map;\n}\n\n/** Common English stopwords to exclude from FTS queries. */\nconst STOPWORDS = new Set([\n\t\"a\",\n\t\"an\",\n\t\"and\",\n\t\"are\",\n\t\"as\",\n\t\"at\",\n\t\"be\",\n\t\"but\",\n\t\"by\",\n\t\"for\",\n\t\"from\",\n\t\"had\",\n\t\"has\",\n\t\"have\",\n\t\"he\",\n\t\"her\",\n\t\"his\",\n\t\"how\",\n\t\"i\",\n\t\"if\",\n\t\"in\",\n\t\"into\",\n\t\"is\",\n\t\"it\",\n\t\"its\",\n\t\"me\",\n\t\"my\",\n\t\"no\",\n\t\"not\",\n\t\"of\",\n\t\"on\",\n\t\"or\",\n\t\"our\",\n\t\"she\",\n\t\"so\",\n\t\"than\",\n\t\"that\",\n\t\"the\",\n\t\"their\",\n\t\"them\",\n\t\"then\",\n\t\"there\",\n\t\"these\",\n\t\"they\",\n\t\"this\",\n\t\"to\",\n\t\"up\",\n\t\"us\",\n\t\"was\",\n\t\"we\",\n\t\"what\",\n\t\"when\",\n\t\"where\",\n\t\"which\",\n\t\"who\",\n\t\"will\",\n\t\"with\",\n\t\"would\",\n\t\"you\",\n\t\"your\",\n]);\n\n/**\n * Sanitize a query string for FTS5 MATCH syntax.\n * FTS5 chokes on certain characters — strip operators and wrap terms.\n *\n * Removes stopwords and uses OR between terms so multi-word queries return\n * partial matches (FTS5's default implicit AND is too restrictive).\n */\nfunction sanitizeFtsQuery(query: string): string {\n\t// Remove FTS5 operators and special chars\n\tconst cleaned = query\n\t\t.replace(/[*\"():^{}[\\]~!@#$%&=+|<>]/g, \" \")\n\t\t.replace(/\\bAND\\b|\\bOR\\b|\\bNOT\\b|\\bNEAR\\b/gi, \" \")\n\t\t.trim();\n\n\t// Split into tokens, remove stopwords, join with OR\n\tconst tokens = cleaned.split(/\\s+/).filter((t) => t.length > 0 && !STOPWORDS.has(t.toLowerCase()));\n\tif (tokens.length === 0) return '\"\"';\n\tif (tokens.length === 1) return tokens[0];\n\treturn tokens.join(\" OR \");\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,2CAA2C;IAC3C,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;CACjB;AA2DD,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CAqKhF","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.js\";\nimport { getMemoryInstructions } from \"./memory-prompt.js\";\nimport type { MemoryIndexes } from \"./resource-loader.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** UI type the agent is communicating through (e.g. \"tui\", \"telegram\", \"rpc\"). */\n\tuiType?: string;\n\t/** Working directory. Default: process.cwd() */\n\tcwd?: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Memory indexes (global and project). */\n\tmemoryIndexes?: MemoryIndexes;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n}\n\nfunction formatMemoryScope(sources: readonly import(\"./resource-loader.js\").MemorySource[], heading: string): string {\n\tif (sources.length === 0) return \"\";\n\n\tconst drebSources = sources.filter((s) => s.source === \"dreb\");\n\tconst claudeSources = sources.filter((s) => s.source === \"claude\");\n\n\tlet out = `\\n### ${heading}\\n`;\n\n\tfor (const source of drebSources) {\n\t\tout += `\\n#### dreb memory (${source.dir}/)\\n\\n${source.content}\\n`;\n\t}\n\n\tif (claudeSources.length > 0) {\n\t\tout += `\\n#### Claude Code memory (read-only)\\n`;\n\t\tout += `> **Note:** These memories were written by Claude Code and may reference Claude Code-specific features, tools, or conventions that don't exist in dreb. Treat the content as useful context, but verify any tool names or workflow references.\\n`;\n\t\tfor (const source of claudeSources) {\n\t\t\tout += `\\nSource: ${source.dir}/\\n\\n${source.content}\\n`;\n\t\t}\n\t}\n\n\treturn out;\n}\n\nfunction buildMemorySection(memoryIndexes?: MemoryIndexes): string {\n\tif (!memoryIndexes) return \"\";\n\n\t// Always include memory instructions so the agent knows the convention\n\tlet section = `\\n\\n${getMemoryInstructions({ globalMemoryDir: memoryIndexes.globalMemoryDir, projectMemoryDir: memoryIndexes.projectMemoryDir })}`;\n\n\tconst { global: globalSources, project: projectSources } = memoryIndexes;\n\n\t// Append the actual memory indexes if any exist\n\tif (globalSources.length > 0 || projectSources.length > 0) {\n\t\tsection += \"\\n\\n## Current Memory Indexes\\n\";\n\t\tsection += formatMemoryScope(globalSources, \"Global Memory\");\n\t\tsection += formatMemoryScope(projectSources, \"Project Memory\");\n\t}\n\n\treturn section;\n}\n\n/** UI type descriptions for system prompt context */\nconst UI_DESCRIPTIONS: Record<string, string> = {\n\ttui: \"Terminal UI (interactive terminal with rich rendering)\",\n\ttelegram:\n\t\t\"Telegram (mobile messaging app — the user is on their phone so messages may be shorter or have typos, but this doesn't reflect less thought or intent. The user sees tool names and arguments but not tool output/results, so summarize key findings or changes when relevant)\",\n\trpc: \"RPC (programmatic interface — another application is consuming your output)\",\n\tcli: \"CLI (non-interactive command line — output will be printed and the process exits)\",\n\tagent: \"Subagent (running as a child agent — focus on the task, report results concisely)\",\n};\n\n/** Format the UI context section for the system prompt */\nfunction formatUiSection(uiType: string): string {\n\tconst description = UI_DESCRIPTIONS[uiType] || uiType;\n\treturn `\\nUI: ${description}`;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst date = new Date().toISOString().slice(0, 10);\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (when skill or read tool is available)\n\t\tconst customPromptHasSkillAccess =\n\t\t\t!selectedTools || selectedTools.includes(\"skill\") || selectedTools.includes(\"read\");\n\t\tif (customPromptHasSkillAccess && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append memory indexes\n\t\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\t\tif (options.uiType) {\n\t\t\tprompt += formatUiSection(options.uiType);\n\t\t}\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\taddGuideline(\"Use bash for file operations like ls, rg, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\taddGuideline(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside dreb, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}\n\nDreb documentation (read only when the user asks about dreb itself, its SDK, extensions, themes, skills, or TUI):\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), dreb packages (docs/packages.md)\n- When working on dreb topics, read the docs and examples, and follow .md cross-references before implementing\n- Always read dreb .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (when skill or read tool is available)\n\tconst hasSkillAccess = hasRead || tools.includes(\"skill\");\n\tif (hasSkillAccess && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append memory indexes\n\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\tif (options.uiType) {\n\t\tprompt += formatUiSection(options.uiType);\n\t}\n\n\treturn prompt;\n}\n"]}
1
+ {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,2CAA2C;IAC3C,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;CACjB;AA2DD,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,MAAM,CA4KhF","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { getDocsPath, getExamplesPath, getReadmePath } from \"../config.js\";\nimport { getMemoryInstructions } from \"./memory-prompt.js\";\nimport type { MemoryIndexes } from \"./resource-loader.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** UI type the agent is communicating through (e.g. \"tui\", \"telegram\", \"rpc\"). */\n\tuiType?: string;\n\t/** Working directory. Default: process.cwd() */\n\tcwd?: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Memory indexes (global and project). */\n\tmemoryIndexes?: MemoryIndexes;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n}\n\nfunction formatMemoryScope(sources: readonly import(\"./resource-loader.js\").MemorySource[], heading: string): string {\n\tif (sources.length === 0) return \"\";\n\n\tconst drebSources = sources.filter((s) => s.source === \"dreb\");\n\tconst claudeSources = sources.filter((s) => s.source === \"claude\");\n\n\tlet out = `\\n### ${heading}\\n`;\n\n\tfor (const source of drebSources) {\n\t\tout += `\\n#### dreb memory (${source.dir}/)\\n\\n${source.content}\\n`;\n\t}\n\n\tif (claudeSources.length > 0) {\n\t\tout += `\\n#### Claude Code memory (read-only)\\n`;\n\t\tout += `> **Note:** These memories were written by Claude Code and may reference Claude Code-specific features, tools, or conventions that don't exist in dreb. Treat the content as useful context, but verify any tool names or workflow references.\\n`;\n\t\tfor (const source of claudeSources) {\n\t\t\tout += `\\nSource: ${source.dir}/\\n\\n${source.content}\\n`;\n\t\t}\n\t}\n\n\treturn out;\n}\n\nfunction buildMemorySection(memoryIndexes?: MemoryIndexes): string {\n\tif (!memoryIndexes) return \"\";\n\n\t// Always include memory instructions so the agent knows the convention\n\tlet section = `\\n\\n${getMemoryInstructions({ globalMemoryDir: memoryIndexes.globalMemoryDir, projectMemoryDir: memoryIndexes.projectMemoryDir })}`;\n\n\tconst { global: globalSources, project: projectSources } = memoryIndexes;\n\n\t// Append the actual memory indexes if any exist\n\tif (globalSources.length > 0 || projectSources.length > 0) {\n\t\tsection += \"\\n\\n## Current Memory Indexes\\n\";\n\t\tsection += formatMemoryScope(globalSources, \"Global Memory\");\n\t\tsection += formatMemoryScope(projectSources, \"Project Memory\");\n\t}\n\n\treturn section;\n}\n\n/** UI type descriptions for system prompt context */\nconst UI_DESCRIPTIONS: Record<string, string> = {\n\ttui: \"Terminal UI (interactive terminal with rich rendering)\",\n\ttelegram:\n\t\t\"Telegram (mobile messaging app — the user is on their phone so messages may be shorter or have typos, but this doesn't reflect less thought or intent. The user sees tool names and arguments but not tool output/results, so summarize key findings or changes when relevant)\",\n\trpc: \"RPC (programmatic interface — another application is consuming your output)\",\n\tcli: \"CLI (non-interactive command line — output will be printed and the process exits)\",\n\tagent: \"Subagent (running as a child agent — focus on the task, report results concisely)\",\n};\n\n/** Format the UI context section for the system prompt */\nfunction formatUiSection(uiType: string): string {\n\tconst description = UI_DESCRIPTIONS[uiType] || uiType;\n\treturn `\\nUI: ${description}`;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t} = options;\n\tconst resolvedCwd = cwd ?? process.cwd();\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst date = new Date().toISOString().slice(0, 10);\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (when skill or read tool is available)\n\t\tconst customPromptHasSkillAccess =\n\t\t\t!selectedTools || selectedTools.includes(\"skill\") || selectedTools.includes(\"read\");\n\t\tif (customPromptHasSkillAccess && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append memory indexes\n\t\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\t\tif (options.uiType) {\n\t\t\tprompt += formatUiSection(options.uiType);\n\t\t}\n\n\t\treturn prompt;\n\t}\n\n\t// Get absolute paths to documentation and examples\n\tconst readmePath = getReadmePath();\n\tconst docsPath = getDocsPath();\n\tconst examplesPath = getExamplesPath();\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\tconst hasSearch = tools.includes(\"search\");\n\n\t// File exploration guidelines\n\tif (hasBash && !hasGrep && !hasFind && !hasLs) {\n\t\taddGuideline(\"Use bash for file operations like ls, rg, find\");\n\t} else if (hasBash && (hasGrep || hasFind || hasLs)) {\n\t\tif (hasSearch) {\n\t\t\taddGuideline(\n\t\t\t\t\"Start with `search` to explore and understand the codebase. Use grep/find/ls for exact text matches and specific file lookups. Prefer all of these over bash.\",\n\t\t\t);\n\t\t} else {\n\t\t\taddGuideline(\"Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)\");\n\t\t}\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside dreb, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}\n\nDreb documentation (read only when the user asks about dreb itself, its SDK, extensions, themes, skills, or TUI):\n- Main documentation: ${readmePath}\n- Additional docs: ${docsPath}\n- Examples: ${examplesPath} (extensions, custom tools, SDK)\n- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), dreb packages (docs/packages.md)\n- When working on dreb topics, read the docs and examples, and follow .md cross-references before implementing\n- Always read dreb .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (when skill or read tool is available)\n\tconst hasSkillAccess = hasRead || tools.includes(\"skill\");\n\tif (hasSkillAccess && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append memory indexes\n\tprompt += buildMemorySection(options.memoryIndexes);\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\tif (options.uiType) {\n\t\tprompt += formatUiSection(options.uiType);\n\t}\n\n\treturn prompt;\n}\n"]}
@@ -121,12 +121,18 @@ export function buildSystemPrompt(options = {}) {
121
121
  const hasFind = tools.includes("find");
122
122
  const hasLs = tools.includes("ls");
123
123
  const hasRead = tools.includes("read");
124
+ const hasSearch = tools.includes("search");
124
125
  // File exploration guidelines
125
126
  if (hasBash && !hasGrep && !hasFind && !hasLs) {
126
127
  addGuideline("Use bash for file operations like ls, rg, find");
127
128
  }
128
129
  else if (hasBash && (hasGrep || hasFind || hasLs)) {
129
- addGuideline("Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)");
130
+ if (hasSearch) {
131
+ addGuideline("Start with `search` to explore and understand the codebase. Use grep/find/ls for exact text matches and specific file lookups. Prefer all of these over bash.");
132
+ }
133
+ else {
134
+ addGuideline("Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)");
135
+ }
130
136
  }
131
137
  for (const guideline of promptGuidelines ?? []) {
132
138
  const normalized = guideline.trim();