UNPKG

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
#!/usr/bin/env node 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();