@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
904 lines (816 loc) • 23.9 kB
text/typescript
/**
* 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.5";
/**
* 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);
});
}