UNPKG

@agentics.org/sparc2

Version:

SPARC 2.0 - Autonomous Vector Coding Agent + MCP. SPARC 2.0, vectorized AI code analysis, is an intelligent coding agent framework built to automate and streamline software development. It combines secure execution environments, and version control into a

904 lines (816 loc) 23.9 kB
/** * CLI module for SPARC 2.0 * Provides a command-line interface for the autonomous diff-based coding bot */ import { parse, stringify } from "https://deno.land/std@0.215.0/toml/mod.ts"; import { loadConfig, SPARCConfig } from "../config.ts"; import { AgentOptions, FileToProcess, SPARC2Agent } from "../agent/agent.ts"; import { LogEntry, logMessage } from "../logger.ts"; import { executeCode } from "../sandbox/codeInterpreter.ts"; import { DiffEntry, searchDiffEntries } from "../vector/vectorStore.ts"; import { mcpCommand } from "./mcpCommand.ts"; import { apiCommand } from "./apiCommand.ts"; // Use a hardcoded version const VERSION = "2.0.24"; /** * CLI command structure */ interface Command { name: string; description: string; options: CommandOption[]; action: (args: Record<string, any>, options: Record<string, any>) => Promise<void>; } /** * CLI command option */ interface CommandOption { name: string; shortName?: string; description: string; type: "string" | "boolean" | "number"; required?: boolean; default?: any; } /** * Display help information */ function printHelp(): void { console.log(`SPARC 2.0 CLI v${VERSION}`); console.log("\nUsage: sparc2 <command> [options]"); console.log("\nCommands:"); for (const command of commands) { console.log(` ${command.name.padEnd(15)} ${command.description}`); } console.log("\nOptions:"); console.log(" --help, -h Show help"); console.log(" --version, -v Show version"); console.log("\nFor command-specific help, run: sparc2 <command> --help"); } /** * Print help for a specific command * @param command Command to print help for */ function printCommandHelp(command: Command): void { console.log(`SPARC 2.0 CLI v${VERSION}`); console.log(`\nCommand: ${command.name}`); console.log(`\n${command.description}`); console.log("\nOptions:"); for (const option of command.options) { const shortFlag = option.shortName ? `-${option.shortName}, ` : " "; const required = option.required ? " (required)" : ""; const defaultValue = option.default !== undefined ? ` (default: ${option.default})` : ""; console.log( ` ${shortFlag}--${option.name.padEnd(15)} ${option.description}${required}${defaultValue}`, ); } } /** * Analyze command action */ async function analyzeCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { // Parse file paths const filePaths = options.files.split(",").map((f: string) => f.trim()); // Read file contents const files: FileToProcess[] = []; for (const path of filePaths) { const content = await Deno.readTextFile(path); files.push({ path, originalContent: content, }); } // Initialize agent const agent = new SPARC2Agent({ model: options.model, mode: options.mode, diffMode: options["diff-mode"], processing: options.processing, }); await agent.init(); // Analyze changes const analysis = await agent.planAndExecute("Analyze code without making changes", files); // Output results if (options.output) { await Deno.writeTextFile(options.output, JSON.stringify(analysis, null, 2)); console.log(`Analysis written to ${options.output}`); } else { console.log("Analysis Results:"); console.log(JSON.stringify(analysis, null, 2)); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * Modify command action */ async function modifyCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { // Parse file paths const filePaths = options.files.split(",").map((f: string) => f.trim()); // Read file contents const files: FileToProcess[] = []; for (const path of filePaths) { const content = await Deno.readTextFile(path); files.push({ path, originalContent: content, }); } // Read suggestions let suggestions = options.suggestions; if (suggestions.endsWith(".txt") || suggestions.endsWith(".md")) { suggestions = await Deno.readTextFile(suggestions); } // Initialize agent const agent = new SPARC2Agent({ model: options.model, mode: options.mode, diffMode: options["diff-mode"], processing: options.processing, }); await agent.init(); // Apply changes const results = await agent.planAndExecute(suggestions, files); // Output results console.log(`Modification completed. Modified ${results.length} files:`); for (const result of results) { console.log(`- ${result.path} ${result.commitHash ? `(commit: ${result.commitHash})` : ""}`); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * Execute command action */ async function executeCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { // Read file content if file is provided let code: string; if (options.file) { code = await Deno.readTextFile(options.file); } else if (options.code) { code = options.code; } else { throw new Error("Either --file or --code is required"); } // Determine language from file extension if not specified let language = options.language; if (!language && options.file) { const extension = options.file.split(".").pop()?.toLowerCase(); if (extension === "py") { language = "python"; } else if (extension === "js") { language = "javascript"; } else if (extension === "ts") { language = "typescript"; } } // Execute code const result = await executeCode(code, { language, stream: options.stream, timeout: options.timeout, }); // Output results console.log("Execution Results:"); console.log(result.text); if (result.logs.stdout.length > 0) { console.log("\nStandard Output:"); for (const line of result.logs.stdout) { console.log(line); } } if (result.logs.stderr.length > 0) { console.error("\nStandard Error:"); for (const line of result.logs.stderr) { console.error(line); } } if (result.error) { console.error("\nError:", result.error.value); Deno.exit(1); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * Search command action */ async function searchCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { // Search for similar changes const results = await searchDiffEntries(options.query, options["max-results"]); // Output results console.log("Search Results:"); if (results.length === 0) { console.log("No results found."); return; } for (const result of results) { // Check if the entry is a DiffEntry if ("file" in result.entry && "diff" in result.entry) { const diffEntry = result.entry as DiffEntry; console.log(`\nFile: ${diffEntry.file} (Score: ${result.score.toFixed(2)})`); console.log("Diff:"); console.log(diffEntry.diff); } else { // Handle LogEntry case const logEntry = result.entry as LogEntry; console.log( `\nLog: ${logEntry.timestamp} [${logEntry.level}] (Score: ${result.score.toFixed(2)})`, ); console.log("Message:"); console.log(logEntry.message); } console.log("-".repeat(80)); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * Checkpoint command action */ async function checkpointCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { // Initialize agent const agent = new SPARC2Agent(); await agent.init(); // Create checkpoint // Sanitize the message to create a valid git tag (no spaces, special characters) const sanitizedMessage = options.message.replace(/[^a-zA-Z0-9_-]/g, "_"); const commitHash = await agent.createCheckpoint(sanitizedMessage); // Output results console.log(`Checkpoint created: ${commitHash}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * Rollback command action */ async function rollbackCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { // Initialize agent const agent = new SPARC2Agent(); await agent.init(); // Determine rollback mode const target = options.commit; const mode = target.match(/^\d{4}-\d{2}-\d{2}/) ? "temporal" : "checkpoint"; // Rollback to checkpoint await agent.rollback(target, mode); // Output results console.log(`Rolled back to ${mode === "temporal" ? "date" : "checkpoint"}: ${target}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * Get configuration value * @param key Configuration key * @returns Configuration value */ async function getConfigValue(key: string): Promise<any> { const configPath = Deno.env.get("SPARC2_CONFIG_PATH") || "config/sparc2-config.toml"; try { // Check if config file exists try { await Deno.stat(configPath); } catch { // Config file doesn't exist, create it await Deno.writeTextFile(configPath, "# SPARC2 Configuration\n"); return undefined; } // Read config file const configContent = await Deno.readTextFile(configPath); // Parse TOML const config = parse(configContent); // Handle nested keys (e.g., "agent.name") const keys = key.split("."); let value: any = config; for (const k of keys) { if (value === undefined || value === null) { return undefined; } value = value[k]; } return value; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error reading configuration: ${errorMessage}`); return undefined; } } /** * Set configuration value * @param key Configuration key * @param value Configuration value */ async function setConfigValue(key: string, value: any): Promise<void> { const configPath = Deno.env.get("SPARC2_CONFIG_PATH") || "config/sparc2-config.toml"; try { // Read existing config let config: Record<string, any> = {}; try { const configContent = await Deno.readTextFile(configPath); config = parse(configContent); } catch { // File doesn't exist or is empty, use empty config } // Handle nested keys (e.g., "agent.name") const keys = key.split("."); let current: any = config; for (let i = 0; i < keys.length - 1; i++) { const k = keys[i]; if (current[k] === undefined || current[k] === null || typeof current[k] !== "object") { current[k] = {}; } current = current[k]; } // Set the value current[keys[keys.length - 1]] = value; // Write config file await Deno.writeTextFile(configPath, stringify(config)); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error setting configuration: ${errorMessage}`); throw error; } } /** * Config command action */ async function configCommand( args: Record<string, any>, options: Record<string, any>, ): Promise<void> { try { const action = options.action; switch (action) { case "get": { if (!options.key) { throw new Error("Key is required for 'get' action"); } const value = await getConfigValue(options.key); console.log( `${options.key} = ${value !== undefined ? JSON.stringify(value) : "undefined"}`, ); break; } case "set": { if (!options.key || options.value === undefined) { throw new Error("Key and value are required for 'set' action"); } // Parse value if it's a JSON string let parsedValue = options.value; if (typeof parsedValue === "string") { try { if (parsedValue.startsWith("{") || parsedValue.startsWith("[")) { parsedValue = JSON.parse(parsedValue); } else if (parsedValue === "true") { parsedValue = true; } else if (parsedValue === "false") { parsedValue = false; } else if (!isNaN(Number(parsedValue))) { parsedValue = Number(parsedValue); } } catch { // If parsing fails, use the original string value } } await setConfigValue(options.key, parsedValue); console.log(`${options.key} set to ${JSON.stringify(parsedValue)}`); break; } case "list": { const configPath = Deno.env.get("SPARC2_CONFIG_PATH") || "config/sparc2-config.toml"; try { const configContent = await Deno.readTextFile(configPath); const config = parse(configContent); console.log("Configuration:"); console.log(JSON.stringify(config, null, 2)); } catch (error) { console.log("No configuration found or error reading configuration."); } break; } default: throw new Error(`Unknown action: ${action}`); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } /** * CLI commands */ const commands: Command[] = [ { name: "analyze", description: "Analyze code files for issues and improvements", options: [ { name: "files", description: "Comma-separated list of files to analyze", type: "string", required: true, }, { name: "output", shortName: "o", description: "Output file for analysis results", type: "string", }, { name: "model", description: "Model to use for analysis", type: "string", }, { name: "mode", description: "Execution mode (automatic, semi, manual, custom)", type: "string", }, { name: "diff-mode", description: "Diff mode (file, function)", type: "string", }, { name: "processing", description: "Processing mode (parallel, sequential, concurrent, swarm)", type: "string", }, ], action: analyzeCommand, }, { name: "modify", description: "Apply suggested modifications to code files", options: [ { name: "files", description: "Comma-separated list of files to modify", type: "string", required: true, }, { name: "suggestions", shortName: "s", description: "Suggestions file or string", type: "string", required: true, }, { name: "model", description: "Model to use for modifications", type: "string", }, { name: "mode", description: "Execution mode (automatic, semi, manual, custom)", type: "string", }, { name: "diff-mode", description: "Diff mode (file, function)", type: "string", }, { name: "processing", description: "Processing mode (parallel, sequential, concurrent, swarm)", type: "string", }, ], action: modifyCommand, }, { name: "execute", description: "Execute code in a sandbox", options: [ { name: "file", description: "File to execute", type: "string", }, { name: "code", description: "Code to execute", type: "string", }, { name: "language", shortName: "l", description: "Programming language (python, javascript, typescript)", type: "string", default: "javascript", }, { name: "stream", description: "Stream output", type: "boolean", default: false, }, { name: "timeout", description: "Timeout in milliseconds", type: "number", default: 30000, }, ], action: executeCommand, }, { name: "search", description: "Search for similar code changes", options: [ { name: "query", description: "Search query", type: "string", required: true, }, { name: "max-results", shortName: "n", description: "Maximum number of results", type: "number", default: 5, }, ], action: searchCommand, }, { name: "checkpoint", description: "Create a git checkpoint", options: [ { name: "message", shortName: "m", description: "Checkpoint message", type: "string", required: true, }, ], action: checkpointCommand, }, { name: "rollback", description: "Rollback to a previous checkpoint", options: [ { name: "commit", description: "Commit hash or date to rollback to", type: "string", required: true, }, ], action: rollbackCommand, }, { name: "config", description: "Manage configuration", options: [ { name: "action", description: "Configuration action (get, set, list)", type: "string", required: true, }, { name: "key", description: "Configuration key", type: "string", }, { name: "value", description: "Configuration value", type: "string", }, ], action: configCommand, }, { name: "api", description: "Start a Model Context Protocol (MCP) HTTP API server", options: [ { name: "port", shortName: "p", description: "Port to run the API server on", type: "number", default: 3001, }, { name: "model", description: "Model to use for the agent", type: "string", }, { name: "mode", description: "Execution mode (automatic, semi, manual, custom, interactive)", type: "string", }, { name: "diff-mode", description: "Diff mode (file, function)", type: "string", }, { name: "processing", description: "Processing mode (sequential, parallel, concurrent, swarm)", type: "string", }, { name: "config", shortName: "c", description: "Path to the agent configuration file", type: "string", }, ], action: apiCommand, }, { name: "mcp", description: "Start a Model Context Protocol (MCP) server using stdio transport", options: [ { name: "model", description: "Model to use for the agent", type: "string", }, { name: "mode", description: "Execution mode (automatic, semi, manual, custom, interactive)", type: "string", }, { name: "diff-mode", description: "Diff mode (file, function)", type: "string", }, { name: "processing", description: "Processing mode (sequential, parallel, concurrent, swarm)", type: "string", }, { name: "config", shortName: "c", description: "Path to the agent configuration file", type: "string", }, ], action: mcpCommand, }, ]; /** * Main CLI function * @param args Command line arguments */ export async function main(args: string[] = Deno.args): Promise<void> { try { // Parse command and options if (args.length === 0) { printHelp(); return; } const commandName = args[0]; if (commandName === "--help" || commandName === "-h") { printHelp(); return; } if (commandName === "--version" || commandName === "-v") { console.log(`SPARC2 CLI v${VERSION}`); return; } const command = commands.find((cmd) => cmd.name === commandName); if (!command) { console.error(`Unknown command: ${commandName}`); printHelp(); Deno.exit(1); } // Check if command-specific help is requested if (args.includes("--help") || args.includes("-h")) { printCommandHelp(command); return; } // Parse command options const options: Record<string, any> = {}; const commandArgs: Record<string, any> = {}; for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg.startsWith("--")) { // Long option const optionName = arg.slice(2); const option = command.options.find((opt) => opt.name === optionName); if (!option) { console.error(`Unknown option: ${arg}`); Deno.exit(1); } if (option.type === "boolean") { options[optionName] = true; } else { if (i + 1 >= args.length) { console.error(`Option ${arg} requires a value`); Deno.exit(1); } options[optionName] = args[++i]; } } else if (arg.startsWith("-")) { // Short option const shortName = arg.slice(1); const option = command.options.find((opt) => opt.shortName === shortName); if (!option) { console.error(`Unknown option: ${arg}`); Deno.exit(1); } if (option.type === "boolean") { options[option.name] = true; } else { if (i + 1 >= args.length) { console.error(`Option ${arg} requires a value`); Deno.exit(1); } options[option.name] = args[++i]; } } else { // Positional argument const positionalOptions = command.options.filter((opt) => opt.required && !(opt.name in options) ); if (positionalOptions.length > 0) { options[positionalOptions[0].name] = arg; } else { console.error(`Unexpected argument: ${arg}`); Deno.exit(1); } } } // Check required options for (const option of command.options) { if (option.required && !(option.name in options)) { console.error(`Required option missing: ${option.name}`); Deno.exit(1); } // Set default values if (option.default !== undefined && !(option.name in options)) { options[option.name] = option.default; } // Convert number options if (option.type === "number" && options[option.name] !== undefined) { options[option.name] = Number(options[option.name]); } } // Execute command await command.action(commandArgs, options); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error: ${errorMessage}`); Deno.exit(1); } } // Run main function if this is the main module if (import.meta.main) { main().catch((error) => { console.error("Unhandled error:", error); Deno.exit(1); }); }