UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

137 lines (134 loc) 5.4 kB
// File: src/modules/fileSearchModule.ts import { execSync } from "child_process"; import path from "path"; import fs from "fs"; import { searchFiles } 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"; import { generate } from "../../lib/generate.js"; import { cleanupModule } from "./cleanupModule.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 matching a generated query.", groups: ["analysis"], run: async (input) => { const query = input.query ?? ""; const ctx = input.context; if (!ctx) { throw new Error("[fileSearch] StructuredContext is required."); } // ------------------------------------------------- // STEP 0: Repo root resolution // ------------------------------------------------- let repoRoot = Config.getIndexDir() ?? process.cwd(); repoRoot = path.resolve(repoRoot); if (!fs.existsSync(repoRoot)) { return { query, data: { files: [] } }; } // ------------------------------------------------- // STEP 1: LLM query generation // ------------------------------------------------- const llmPrompt = ` You are a search query generator. Given the following user input, produce a concise, meaningful query suitable for searching code files. Input: ${typeof query === "string" ? query : JSON.stringify(query, null, 2)} Return only the improved query text. `.trim(); const llmResponse = await generate({ content: llmPrompt, query }); const cleaned = await cleanupModule.run({ query, content: llmResponse.data }); const searchQuery = typeof cleaned.data === "string" ? cleaned.data.trim() : ""; if (!searchQuery) { return { query, data: { files: [] } }; } // ------------------------------------------------- // STEP 2: Semantic search // ------------------------------------------------- let results = []; try { results = await searchFiles(searchQuery, 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} "${query}" "${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: DB 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 to initContext.relatedFiles // ------------------------------------------------- if (!ctx.initContext) { ctx.initContext = { userQuery: query }; } const existing = new Set(ctx.initContext.relatedFiles ?? []); const discoveredPaths = results.map(r => r.path); // Append + deduplicate ctx.initContext.relatedFiles = [ ...(ctx.initContext.relatedFiles ?? []), ...discoveredPaths.filter(p => !existing.has(p)) ]; // ------------------------------------------------- // STEP 6: Log and return ModuleIO output // ------------------------------------------------- const output = { query, data: { files: results } }; logInputOutput("fileSearch", "output", output); return output; } };