UNPKG

scai

Version:

> AI-powered CLI tool for commit messages **and** pull request reviews — using local models.

179 lines (178 loc) • 6.65 kB
import fs from 'fs'; import path from 'path'; import readline from 'readline'; import { searchFiles, queryFiles, getFunctionsForFiles } from '../db/fileIndex.js'; import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js'; import { generate } from '../lib/generate.js'; import { buildContextualPrompt } from '../utils/buildContextualPrompt.js'; import { generateFocusedFileTree } from '../utils/fileTree.js'; import { log } from '../utils/log.js'; import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT, MAX_SUMMARY_LINES, getIndexDir, MAX_FUNCTION_LINES } from '../constants.js'; import chalk from 'chalk'; export async function runAskCommand(query) { if (!query) { query = await promptOnce('šŸ’¬ Ask your question:\n'); } query = query.trim(); if (!query) { console.error('āŒ No question provided.\nšŸ‘‰ Usage: scai ask "your question"'); return; } console.log(`šŸ“ Using index root: ${getIndexDir()}`); console.log(`šŸ” Searching for: "${query}"\n`); // 🟩 STEP 1: Semantic Search const start = Date.now(); const semanticResults = await searchFiles(query, RELATED_FILES_LIMIT); // RankedFile[] const duration = Date.now() - start; console.log(`ā±ļø searchFiles took ${duration}ms and returned ${semanticResults.length} result(s)`); semanticResults.forEach((file, i) => { console.log(` ${i + 1}. šŸ“„ Path: ${file.path} | Score: ${file.score?.toFixed(3) ?? 'n/a'}`); }); // 🟩 STEP 1.5: Fallback FTS search const safeQuery = sanitizeQueryForFts(query); const fallbackResults = queryFiles(safeQuery, 10); // FileRow[] fallbackResults.forEach((file, i) => { console.log(` ${i + 1}. šŸ”Ž Fallback Match: ${file.path}`); }); // 🟩 STEP 2: Merge results const seen = new Set(); const combinedResults = []; for (const file of semanticResults) { const resolved = path.resolve(file.path); seen.add(resolved); combinedResults.push(file); } for (const file of fallbackResults) { const resolved = path.resolve(file.path); if (!seen.has(resolved)) { seen.add(resolved); combinedResults.push({ id: file.id, path: file.path, summary: file.summary || '', score: 0.0, sim: 0, bm25: 0 }); } } // 🟩 STEP 3: Log results if (combinedResults.length) { console.log('\nšŸ“Š Final Related Files:'); combinedResults.forEach((f, i) => { console.log(` ${i + 1}. ${f.path} (${f.score?.toFixed(3) ?? 'fallback'})`); }); } else { console.log('āš ļø No similar files found. Using query only.'); } // 🟩 STEP 4: Load top file code + metadata const topFile = combinedResults[0]; const filepath = topFile?.path || ''; let code = ''; let topSummary = topFile.summary || '(No summary available)'; let topFunctions = []; const fileFunctions = {}; // Truncate summary topSummary = topSummary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n'); const allFileIds = combinedResults .map(file => file.id) .filter((id) => typeof id === 'number'); const allFunctionsMap = getFunctionsForFiles(allFileIds); // Record<number, Function[]> try { code = fs.readFileSync(filepath, 'utf-8'); const topFileId = topFile.id; topFunctions = allFunctionsMap[topFileId]?.map(fn => { const content = fn.content ? fn.content.split('\n').slice(0, MAX_FUNCTION_LINES).join('\n') : '(No content available)'; return { name: fn.name, content, }; }) || []; } catch (err) { console.warn(`āš ļø Failed to read or analyze top file (${filepath}):`, err); } // 🟩 STEP 5: Build relatedFiles with functions and fileFunctions const relatedFiles = combinedResults.slice(0, RELATED_FILES_LIMIT).map(file => { const fileId = file.id; let summary = file.summary || '(No summary available)'; if (summary) { summary = summary.split('\n').slice(0, MAX_SUMMARY_LINES).join('\n'); } const functions = allFunctionsMap[fileId]?.map(fn => { const content = fn.content ? fn.content.split('\n').slice(0, MAX_FUNCTION_LINES).join('\n') : '(No content available)'; return { name: fn.name, content, }; }) || []; return { path: file.path, summary, functions, }; }); // 🟩 STEP 6: Generate file tree let fileTree = ''; try { fileTree = generateFocusedFileTree(filepath, 2); } catch (e) { console.warn('āš ļø Could not generate file tree:', e); } // 🟩 STEP 7: Build prompt console.log(chalk.blueBright('\nšŸ“¦ Building contextual prompt...')); const promptContent = buildContextualPrompt({ baseInstruction: query, code, summary: topSummary, functions: topFunctions, relatedFiles, projectFileTree: fileTree || undefined, fileFunctions, }); console.log(chalk.greenBright('āœ… Prompt built successfully.')); console.log(chalk.cyan(`[runAskCommand] Prompt token estimate: ~${Math.round(promptContent.length / 4)} tokens`)); // 🟩 STEP 8: Save prompt try { if (!fs.existsSync(SCAI_HOME)) fs.mkdirSync(SCAI_HOME, { recursive: true }); fs.writeFileSync(PROMPT_LOG_PATH, promptContent, 'utf-8'); log(`šŸ“ Prompt saved to ${PROMPT_LOG_PATH}`); } catch (err) { log('āŒ Failed to write prompt log:', err); } // 🟩 STEP 9: Ask model try { console.log('\nšŸ¤– Asking the model...'); const input = { content: promptContent, filepath, }; const modelResponse = await generate(input, 'llama3'); console.log(`\n🧠 Model Response:\n${modelResponse.content}`); } catch (err) { console.error('āŒ Model request failed:', err); } } // 🟩 Helper: Prompt once function promptOnce(promptText) { return new Promise(resolve => { console.log(promptText); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('> ', answer => { rl.close(); resolve(answer.trim()); }); }); }