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
JavaScript
// 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;
}
};