scai
Version:
> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.
94 lines (93 loc) • 3.37 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { generateEmbedding } from '../lib/generateEmbedding.js';
import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
import * as sqlTemplates from './sqlTemplates.js';
import { CANDIDATE_LIMIT } from '../constants.js';
import { getDbForRepo } from './client.js';
import { scoreFiles } from '../fileRules/scoreFiles.js'; // 👈 NEW
export function indexFile(filePath, summary, type) {
const stats = fs.statSync(filePath);
const lastModified = stats.mtime.toISOString();
const indexedAt = new Date().toISOString();
const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
const fileName = path.basename(normalizedPath);
const db = getDbForRepo();
db.prepare(sqlTemplates.upsertFileTemplate).run({
path: normalizedPath,
filename: fileName,
summary,
type,
lastModified,
indexedAt,
embedding: null
});
db.prepare(`
INSERT OR REPLACE INTO files_fts (rowid, filename, summary, path)
VALUES ((SELECT id FROM files WHERE path = :path), :filename, :summary, :path)
`).run({
path: normalizedPath,
filename: fileName,
summary,
});
console.log(`📄 Indexed: ${normalizedPath}`);
}
export function queryFiles(safeQuery, limit = 10) {
console.log(`Executing search query: ${safeQuery}`);
const db = getDbForRepo();
return db.prepare(`
SELECT f.id, f.path, f.filename, f.summary, f.type, f.last_modified, f.indexed_at
FROM files f
JOIN files_fts fts ON f.id = fts.rowid
WHERE fts.files_fts MATCH ?
LIMIT ?
`).all(safeQuery, limit);
}
export async function searchFiles(query, topK = 5) {
console.log(`🧠 Searching for query: "${query}"`);
const embedding = await generateEmbedding(query);
if (!embedding) {
console.log('⚠️ Failed to generate embedding for query');
return [];
}
const safeQuery = sanitizeQueryForFts(query);
console.log(`Executing search query in FTS5: ${safeQuery}`);
const db = getDbForRepo();
const ftsResults = db.prepare(`
SELECT fts.rowid AS id, f.path, f.filename, f.summary, f.type, bm25(files_fts) AS bm25Score, f.embedding
FROM files f
JOIN files_fts fts ON f.id = fts.rowid
WHERE fts.files_fts MATCH ?
ORDER BY bm25Score ASC
LIMIT ?
`).all(safeQuery, CANDIDATE_LIMIT);
console.log(`FTS search returned ${ftsResults.length} results`);
if (ftsResults.length === 0)
return [];
const scored = scoreFiles(query, embedding, ftsResults);
return scored.slice(0, topK);
}
export function getFunctionsForFiles(fileIds) {
if (!fileIds.length)
return {};
const placeholders = fileIds.map(() => '?').join(',');
const db = getDbForRepo();
const stmt = db.prepare(`
SELECT f.file_id, f.name, f.start_line, f.end_line, f.content
FROM functions f
WHERE f.file_id IN (${placeholders})
`);
const rows = stmt.all(...fileIds);
const grouped = {};
for (const row of rows) {
if (!grouped[row.file_id])
grouped[row.file_id] = [];
grouped[row.file_id].push({
name: row.name,
start_line: row.start_line,
end_line: row.end_line,
content: row.content,
});
}
return grouped;
}