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