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 ❤️.

145 lines (144 loc) 5.7 kB
#!/usr/bin/env node import readline from 'readline'; import { spawn } from 'child_process'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); const shellQuote = require('shell-quote'); import { createProgram as cmdFactory, withContext } from './commands/factory.js'; import { runAskCommand } from './commands/AskCmd.js'; import { setRl } from './commands/ReadlineSingleton.js'; const program = cmdFactory(); // single Commander instance let inShell = false; const customCommands = {}; // ===================================================== // TEST QUERIES // ===================================================== const testQueries = [ 'please write me comprehensive comments for spatialmap.js and typescript.ts files', 'refactor spatialmap.js to improve readability and reduce nesting', 'explain the intent and architecture of the semantic analysis module', 'add validation and error handling to the context review step', 'summarize this repo architecture and identify weak coupling points', 'Where are all the database queries defined?', 'Which functions have no tests written for them yet?', 'What could cause memory leaks in this codebase?', 'How does authentication work here?', 'Is there a clear separation between frontend and backend code?', 'please refactor the buildContextualPrompt into smaller functions', 'Is error handling consistent across the codebase?', 'What’s missing from the README?', 'How do I run the test suite?', 'Are there any flaky tests in this repo?', 'Are there any security vulnerabilities in our dependencies?', 'Is there any dead code we can safely remove?' ]; // ===================================================== // HELPERS // ===================================================== function pickRandom(items) { return items[Math.floor(Math.random() * items.length)]; } async function runQuery(query) { await withContext(() => runAskCommand(query)); } // ===================================================== // BUILT-IN COMMANDS // ===================================================== customCommands.test = async () => { await runQuery(testQueries[0]); }; customCommands['test-random'] = async () => { const query = pickRandom(testQueries); console.log(`\n🎲 [test-random] Selected query:\n→ ${query}\n`); await runQuery(query); }; customCommands.exit = async () => { console.log('Exiting...'); process.exit(0); }; customCommands.q = customCommands.exit; customCommands.quit = customCommands.exit; // ===================================================== // EXTENSION API // ===================================================== export function registerCommand(name, fn) { customCommands[name] = fn; } async function startShell() { if (inShell) return; inShell = true; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: 'scai> ', historySize: 200, }); setRl(rl); rl.prompt(); rl.on('line', async (line) => { try { const trimmed = line.trim(); if (!trimmed) { rl.prompt(); return; } // --- Shell command if (trimmed.startsWith('!')) { const child = spawn(trimmed.slice(1).trim(), { shell: true, stdio: 'inherit' }); child.on('exit', () => rl.prompt()); child.on('error', (err) => { console.error('Shell command error:', err); rl.prompt(); }); return; } // --- Slash commands if (trimmed.startsWith('/')) { const argvParts = shellQuote.parse(trimmed.slice(1)) .map((tok) => typeof tok === 'object' ? tok.op ?? tok.pattern ?? '' : String(tok)) .filter(Boolean); const cmdName = argvParts[0]; if (customCommands[cmdName]) { await customCommands[cmdName](); } else { try { await program.parseAsync(argvParts, { from: 'user' }); } catch (err) { console.error('Command error:', err instanceof Error ? err.message : err); } } rl.prompt(); return; } // --- Bare input → LLM await withContext(() => runAskCommand(trimmed)); rl.prompt(); } catch (err) { console.error('REPL error:', err instanceof Error ? err.stack : err); rl.prompt(); } }); rl.on('close', () => { console.log('Bye!'); process.exit(0); }); process.on('SIGINT', () => { console.log('\nExiting REPL...'); rl.close(); }); } // ---------------- Main ----------------- async function main() { process.on('unhandledRejection', (reason) => console.error('Unhandled Rejection:', reason)); process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err.stack ?? err); process.exit(1); }); const isInteractive = process.stdin.isTTY && process.stdout.isTTY; const args = process.argv.slice(2); // Run REPL if no args OR user typed `scai shell` if (isInteractive && (args.length === 0 || (args.length === 1 && args[0] === 'shell'))) { await startShell(); return; } try { await program.parseAsync(process.argv); } catch (err) { console.error('CLI Error:', err instanceof Error ? err.message : err); process.exit(1); } } main();