UNPKG

scai

Version:

> **A local-first AI CLI for understanding, querying, and iterating on large codebases.** > **100% local • No token costs • No cloud • No prompt injection • Private by design**

129 lines (128 loc) 5.42 kB
// File: src/modules/fileSearchModule.ts import { execSync } from "child_process"; import path from "path"; import fs from "fs"; import { plannerSearchFiles } from "../../db/fileIndex.js"; import { Config } from "../../config.js"; import { getDbForRepo } from "../../db/client.js"; import { IGNORED_EXTENSIONS } from "../../fileRules/ignoredExtensions.js"; import { logInputOutput } from "../../utils/promptLogHelper.js"; async function fetchSummariesForPaths(paths) { if (paths.length === 0) return {}; const db = getDbForRepo(); const placeholders = paths.map(() => "?").join(", "); const rows = db.prepare(`SELECT path, summary FROM summaries WHERE path IN (${placeholders}) AND summary IS NOT NULL;`).all(...paths); const map = {}; for (const row of rows) map[row.path] = row.summary; return map; } export const fileSearchModule = { name: "fileSearch", description: "Searches the indexed repo for files using a planner-provided sub-query.", groups: ["analysis"], run: async (input) => { const ctx = input.context; if (!ctx) { throw new Error("[fileSearch] StructuredContext is required."); } // ------------------------------------------------- // STEP 0: Resolve sub-query from current plan step // ------------------------------------------------- const planStep = ctx.currentStep; const subQueryTerms = planStep?.subQuery ?? []; if (subQueryTerms.length === 0) { return { query: input.query, data: { files: [] } }; } const subQuery = subQueryTerms.join(" ").trim(); if (!subQuery) { return { query: input.query, data: { files: [] } }; } // ------------------------------------------------- // STEP 1: Repo root resolution // ------------------------------------------------- let repoRoot = Config.getIndexDir() ?? process.cwd(); repoRoot = path.resolve(repoRoot); if (!fs.existsSync(repoRoot)) { return { query: input.query, data: { files: [] } }; } // ------------------------------------------------- // STEP 2: Semantic search (primary) // ------------------------------------------------- let results = []; try { results = await plannerSearchFiles(input.query, subQuery, 5); } catch (err) { console.warn("❌ [fileSearch] Semantic search failed:", err); } // ------------------------------------------------- // STEP 3: Grep fallback // ------------------------------------------------- if (results.length === 0) { try { const exclude = IGNORED_EXTENSIONS .map(ext => `--exclude=*${ext}`) .join(" "); const stdout = execSync(`grep -ril ${exclude} "${subQuery}" "${repoRoot}"`, { encoding: "utf8" }); results = stdout .split("\n") .filter(Boolean) .map(f => ({ path: f })); } catch (err) { if (err?.status !== 1) { console.warn("⚠️ [fileSearch] Grep fallback failed:", err); } } } // ------------------------------------------------- // STEP 4: Summary enrichment // ------------------------------------------------- const missingPaths = results.filter(f => !f.summary).map(f => f.path); if (missingPaths.length > 0) { try { const map = await fetchSummariesForPaths(missingPaths); results.forEach(f => { if (!f.summary && map[f.path]) { f.summary = map[f.path]; } }); } catch (err) { console.warn("⚠️ [fileSearch] Summary enrichment failed:", err); } } // ------------------------------------------------- // STEP 5: Persist discovered files // ------------------------------------------------- ctx.initContext ?? (ctx.initContext = { userQuery: input.query }); const existing = new Set(ctx.initContext.relatedFiles ?? []); const discovered = results.map(r => r.path); ctx.initContext.relatedFiles = [ ...(ctx.initContext.relatedFiles ?? []), ...discovered.filter(p => !existing.has(p)) ]; const expandedTerms = results .flatMap(r => r.queryExpansionTerms ?? []) .map(term => String(term).trim().toLowerCase()) .filter(term => term.length >= 2); if (expandedTerms.length > 0) { const existingTerms = new Set(ctx.initContext.queryExpansionTerms ?? []); for (const term of expandedTerms) existingTerms.add(term); ctx.initContext.queryExpansionTerms = Array.from(existingTerms); } // ------------------------------------------------- // STEP 6: Return // ------------------------------------------------- const output = { query: input.query, data: { files: results } }; logInputOutput("fileSearch", "output", output); return output; } };