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**
167 lines (166 loc) • 6.52 kB
JavaScript
import { spawn } from 'child_process';
import { createRequire } from 'module';
import fs from 'fs';
import os from 'os';
import path from 'path';
import chalk from 'chalk';
const require = createRequire(import.meta.url);
const shellQuote = require('shell-quote');
import { createProgram as cmdFactory } from './commands/factory.js';
import enquirer from 'enquirer';
import { runQueryWithDaemonControl } from './utils/runQueryWithDaemonControl.js';
import { RUN_LOG_PATH } from './constants.js';
import { registerTestingCommands } from './testing/testCommands.js';
const { prompt } = enquirer;
const program = cmdFactory();
const customCommands = {};
// ---------------- Run Query with Daemon Control ----------------
async function runQuery(query) {
await runQueryWithDaemonControl(query);
}
// ---------------- Testing Commands ----------------
registerTestingCommands(customCommands, runQuery);
// ---------------- Built-in Commands ----------------
customCommands.clear = async () => {
// Clear screen and move cursor to top-left
process.stdout.write('\x1Bc');
};
customCommands.help = async () => {
console.log(chalk.green('\n✔ SCAI Help:\n'));
console.log(program.helpInformation());
};
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;
}
// ---------------- Editor Helper ----------------
function editInEditorAsync(initialContent = '') {
return new Promise((resolve, reject) => {
const tmpFile = path.join(os.tmpdir(), `scai_input_${Date.now()}.txt`);
fs.writeFileSync(tmpFile, initialContent);
const editor = process.env.EDITOR || 'vi';
const child = spawn(editor, [tmpFile], { stdio: 'inherit' });
child.on('exit', (code) => {
try {
const content = fs.readFileSync(tmpFile, 'utf-8');
fs.unlinkSync(tmpFile);
resolve(content);
}
catch (err) {
reject(err);
}
});
child.on('error', (err) => reject(err));
});
}
// ---------------- REPL / Shell ----------------
// The startShell function initializes an interactive shell for the CLI.
// It allows users to input queries directly, use built-in commands with '/',
// run terminal commands with '!', or edit input in an external editor with '/edit'.
async function startShell() {
// Clear screen first
process.stdout.write('\x1Bc');
console.log(chalk.yellow("Welcome to SCAI shell!") + "\n" +
chalk.blueBright(`- Type your query directly for short commands or questions.
- Use !command to run terminal commands.
- Use /command for built-in or custom commands.
- /help
- /clear
- /edit for pasting long queries & multi-line input
- /test, /test-random, /test-evals
- /git commit\n`));
while (true) {
try {
const { line } = (await prompt({
type: 'input',
name: 'line',
message: 'Write your query:',
validate: (input) => !!input.trim() || 'Please enter something',
}));
const trimmed = line.trim();
// --- Exit immediately
if (['exit', 'quit', 'q'].includes(trimmed)) {
await customCommands.exit();
}
// --- Shell commands (!)
if (trimmed.startsWith('!')) {
const child = spawn(trimmed.slice(1), {
shell: true,
stdio: 'inherit',
});
await new Promise((resolve) => child.on('exit', resolve));
continue;
}
// --- 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];
// Special case: open editor
if (cmdName === 'edit') {
const content = await editInEditorAsync();
const trimmedContent = content.trim();
if (trimmedContent) {
console.log(chalk.hex('#FFA500')('\n[Editor input]:\n' + trimmedContent + '\n'));
await runQuery(trimmedContent);
}
continue;
}
if (customCommands[cmdName]) {
await customCommands[cmdName]();
}
else {
await program.parseAsync(argvParts, { from: 'user' });
}
continue;
}
// --- Otherwise → treat as normal query
await runQuery(trimmed);
}
catch (err) {
console.error('REPL error:', err instanceof Error ? err.stack : err);
}
}
}
function initRunLog() {
fs.mkdirSync(path.dirname(RUN_LOG_PATH), { recursive: true });
fs.writeFileSync(RUN_LOG_PATH, '', { flag: 'w' });
}
// ---------------- Main ----------------
async function main() {
initRunLog();
// ---------------- Global Error Handlers ----------------
process.on('unhandledRejection', (reason) => console.error('Unhandled Rejection:', reason));
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err.stack ?? err);
process.exit(1);
});
// ---------------- Start Model Once ----------------
try {
const { startModelProcess } = await import('./utils/checkModel.js');
await startModelProcess(); // 🔒 ensures only one startup
}
catch (err) {
console.error('❌ Failed to start model process:', err);
process.exit(1);
}
// ---------------- Determine CLI Mode ----------------
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
const args = process.argv.slice(2);
if (isInteractive && (args.length === 0 || (args.length === 1 && args[0] === 'shell'))) {
await startShell(); // REPL mode
return;
}
// ---------------- Non-interactive / command mode ----------------
await program.parseAsync(process.argv);
}
main();