Terminal-based AI coding agent with interactive TUI for autonomous code generation.

Features:
  - Interactive TUI with React/Ink
  - Autonomous agent with tool calls (bash, read, write, edit, glob, grep)
  - Permission system with pattern-based rules
  - Session management with auto-compaction
  - Dual providers: GitHub Copilot and Ollama
  - MCP server integration
  - Todo panel and theme system
  - Streaming responses
  - GitHub-compatible project context
This commit is contained in:
2026-01-27 23:33:06 -05:00
commit 0062e5d9d9
521 changed files with 66418 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/**
* Chat command handler
*/
import { execute as executeChat } from "@commands/chat";
import type { CommandOptions } from "@/types/index";
export const handleChat = async (options: CommandOptions): Promise<void> => {
await executeChat(options);
};

View File

@@ -0,0 +1,123 @@
/**
* Classify command handler
*/
import chalk from "chalk";
import {
succeedSpinner,
startSpinner,
errorMessage,
failSpinner,
headerMessage,
} from "@utils/terminal";
import {
INTENT_KEYWORDS,
CLASSIFICATION_CONFIDENCE,
} from "@constants/handlers";
import type {
CommandOptions,
IntentRequest,
IntentResponse,
} from "@/types/index";
const classifyIntent = async (
request: IntentRequest,
): Promise<IntentResponse> => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const prompt = request.prompt.toLowerCase();
let intent: IntentResponse["intent"] = "ask";
let confidence: number = CLASSIFICATION_CONFIDENCE.DEFAULT;
const intentMatchers: Record<string, () => void> = {
fix: () => {
intent = "fix";
confidence = CLASSIFICATION_CONFIDENCE.HIGH;
},
test: () => {
intent = "test";
confidence = CLASSIFICATION_CONFIDENCE.MEDIUM;
},
refactor: () => {
intent = "refactor";
confidence = CLASSIFICATION_CONFIDENCE.LOW;
},
code: () => {
intent = "code";
confidence = CLASSIFICATION_CONFIDENCE.DEFAULT;
},
document: () => {
intent = "document";
confidence = CLASSIFICATION_CONFIDENCE.HIGH;
},
};
for (const [intentKey, keywords] of Object.entries(INTENT_KEYWORDS)) {
const hasMatch = keywords.some((keyword) => prompt.includes(keyword));
if (hasMatch) {
intentMatchers[intentKey]?.();
break;
}
}
return {
intent,
confidence,
reasoning: `Based on keywords in the prompt, this appears to be a ${intent} request.`,
needsClarification: confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD,
clarificationQuestions:
confidence < CLASSIFICATION_CONFIDENCE.THRESHOLD
? [
"Which specific files should I focus on?",
"What is the expected outcome?",
]
: undefined,
};
};
export const handleClassify = async (
options: CommandOptions,
): Promise<void> => {
const { prompt, context, files = [] } = options;
if (!prompt) {
errorMessage("Prompt is required");
return;
}
headerMessage("Classifying Intent");
console.log(chalk.bold("Prompt:") + ` ${prompt}`);
if (context) {
console.log(chalk.bold("Context:") + ` ${context}`);
}
if (files.length > 0) {
console.log(chalk.bold("Files:") + ` ${files.join(", ")}`);
}
console.log();
startSpinner("Analyzing prompt...");
try {
const result = await classifyIntent({ prompt, context, files });
succeedSpinner("Analysis complete");
console.log();
console.log(chalk.bold("Intent:") + ` ${chalk.cyan(result.intent)}`);
console.log(
chalk.bold("Confidence:") +
` ${chalk.green((result.confidence * 100).toFixed(1) + "%")}`,
);
console.log(chalk.bold("Reasoning:") + ` ${result.reasoning}`);
if (result.needsClarification && result.clarificationQuestions) {
console.log();
console.log(chalk.yellow.bold("Clarification needed:"));
result.clarificationQuestions.forEach((q, i) => {
console.log(` ${i + 1}. ${q}`);
});
}
} catch (error) {
failSpinner("Classification failed");
throw error;
}
};

View File

@@ -0,0 +1,113 @@
/**
* Config command handler
*/
import {
errorMessage,
filePath,
successMessage,
hightLigthedJson,
headerMessage,
infoMessage,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import {
VALID_CONFIG_KEYS,
VALID_PROVIDERS,
CONFIG_VALIDATION,
} from "@constants/handlers";
import type { CommandOptions, Provider } from "@/types/index";
import type { ConfigAction, ConfigKey } from "@/types/handlers";
type ConfigActionHandler = (key?: string, value?: string) => Promise<void>;
const showConfig = async (): Promise<void> => {
const config = await getConfig();
headerMessage("Configuration");
const allConfig = config.getAll();
hightLigthedJson(allConfig);
};
const showPath = async (): Promise<void> => {
const config = await getConfig();
const configPath = config.getConfigPath();
console.log(filePath(configPath));
};
const setConfigValue = async (key?: string, value?: string): Promise<void> => {
if (!key || value === undefined) {
errorMessage("Key and value are required");
return;
}
if (!VALID_CONFIG_KEYS.includes(key as ConfigKey)) {
errorMessage(`Invalid config key: ${key}`);
infoMessage(`Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`);
return;
}
const config = await getConfig();
const keySetters: Record<ConfigKey, () => boolean> = {
provider: () => {
if (!VALID_PROVIDERS.includes(value as Provider)) {
errorMessage(`Invalid provider: ${value}`);
infoMessage(`Valid providers: ${VALID_PROVIDERS.join(", ")}`);
return false;
}
config.set("provider", value as Provider);
return true;
},
model: () => {
config.set("model", value);
return true;
},
maxIterations: () => {
const num = parseInt(value, 10);
if (isNaN(num) || num < CONFIG_VALIDATION.MIN_ITERATIONS) {
errorMessage("maxIterations must be a positive number");
return false;
}
config.set("maxIterations", num);
return true;
},
timeout: () => {
const num = parseInt(value, 10);
if (isNaN(num) || num < CONFIG_VALIDATION.MIN_TIMEOUT_MS) {
errorMessage(
`timeout must be at least ${CONFIG_VALIDATION.MIN_TIMEOUT_MS}ms`,
);
return false;
}
config.set("timeout", num);
return true;
},
};
const setter = keySetters[key as ConfigKey];
const success = setter();
if (success) {
await config.save();
successMessage(`Set ${key} = ${value}`);
}
};
const CONFIG_ACTION_HANDLERS: Record<ConfigAction, ConfigActionHandler> = {
show: showConfig,
path: showPath,
set: setConfigValue,
};
export const handleConfig = async (options: CommandOptions): Promise<void> => {
const { action, key, value } = options;
const handler = CONFIG_ACTION_HANDLERS[action as ConfigAction];
if (!handler) {
errorMessage(`Unknown config action: ${action}`);
return;
}
await handler(key, value);
};

View File

@@ -0,0 +1,62 @@
/**
* Plan command handler
*/
import chalk from "chalk";
import {
hightLigthedJson,
filePath,
errorMessage,
failSpinner,
headerMessage,
startSpinner,
succeedSpinner,
successMessage,
} from "@utils/terminal";
import type { CommandOptions } from "@/types/index";
export const handlePlan = async (options: CommandOptions): Promise<void> => {
const { intent, task, files = [], output } = options;
if (!task) {
errorMessage("Task description is required");
return;
}
headerMessage("Generating Plan");
console.log(chalk.bold("Intent:") + ` ${chalk.cyan(intent || "unknown")}`);
console.log(chalk.bold("Task:") + ` ${task}`);
if (files.length > 0) {
console.log(chalk.bold("Files:") + ` ${files.join(", ")}`);
}
console.log();
startSpinner("Generating execution plan...");
try {
await new Promise((resolve) => setTimeout(resolve, 1500));
succeedSpinner("Plan generated");
const plan = {
intent,
task,
files,
steps: [
{ id: "step_1", type: "read", description: "Analyze existing code" },
{ id: "step_2", type: "edit", description: "Apply changes" },
{ id: "step_3", type: "execute", description: "Run tests" },
],
};
if (output) {
const fs = await import("fs/promises");
await fs.writeFile(output, JSON.stringify(plan, null, 2));
successMessage(`Plan saved to ${filePath(output)}`);
} else {
hightLigthedJson(plan);
}
} catch (error) {
failSpinner("Plan generation failed");
throw error;
}
};

View File

@@ -0,0 +1,28 @@
/**
* Command handler registry - object-based routing
*/
import { handleChat } from "@commands/handlers/chat";
import { handleRun } from "@commands/handlers/run";
import { handleClassify } from "@commands/handlers/classify";
import { handlePlan } from "@commands/handlers/plan";
import { handleValidate } from "@commands/handlers/validate";
import { handleConfig } from "@commands/handlers/config";
import { handleServe } from "@commands/handlers/serve";
import type { CommandRegistry } from "@/types/handlers";
export const COMMAND_REGISTRY: CommandRegistry = {
chat: handleChat,
run: handleRun,
classify: handleClassify,
plan: handlePlan,
validate: handleValidate,
config: handleConfig,
serve: handleServe,
};
export const isValidCommand = (
command: string,
): command is keyof CommandRegistry => {
return command in COMMAND_REGISTRY;
};

View File

@@ -0,0 +1,10 @@
/**
* Run command handler
*/
import { execute } from "@commands/runner";
import type { CommandOptions } from "@/types/index";
export const handleRun = async (options: CommandOptions): Promise<void> => {
await execute(options);
};

View File

@@ -0,0 +1,15 @@
/**
* Serve command handler
*/
import { boxMessage, warningMessage, infoMessage } from "@utils/terminal";
import type { CommandOptions } from "@/types/index";
import { SERVER_INFO } from "@constants/serve";
export const handleServe = async (_options: CommandOptions): Promise<void> => {
boxMessage(SERVER_INFO, "Server Mode");
warningMessage("Server mode not yet implemented");
infoMessage(
"This will integrate with the existing agent/main.py JSON-RPC server",
);
};

View File

@@ -0,0 +1,78 @@
/**
* Validate command handler
*/
import chalk from "chalk";
import {
failSpinner,
warningMessage,
successMessage,
succeedSpinner,
startSpinner,
errorMessage,
headerMessage,
filePath,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import type { CommandOptions } from "@/types/index";
export const handleValidate = async (
options: CommandOptions,
): Promise<void> => {
const { planFile } = options;
if (!planFile) {
errorMessage("Plan file is required");
return;
}
headerMessage("Validating Plan");
console.log(chalk.bold("Plan file:") + ` ${filePath(planFile)}`);
console.log();
startSpinner("Validating plan...");
try {
const fs = await import("fs/promises");
const planData = await fs.readFile(planFile, "utf-8");
const plan = JSON.parse(planData);
await new Promise((resolve) => setTimeout(resolve, 1000));
const config = await getConfig();
const warnings: string[] = [];
const errors: string[] = [];
plan.files?.forEach((file: string) => {
if (config.isProtectedPath(file)) {
warnings.push(`Protected path: ${file}`);
}
});
succeedSpinner("Validation complete");
console.log();
if (errors.length > 0) {
console.log(chalk.red.bold("Errors:"));
errors.forEach((err) => console.log(` - ${err}`));
}
if (warnings.length > 0) {
console.log(chalk.yellow.bold("Warnings:"));
warnings.forEach((warn) => console.log(` - ${warn}`));
}
if (errors.length === 0 && warnings.length === 0) {
successMessage("Plan is valid and safe to execute");
} else if (errors.length > 0) {
errorMessage("Plan has errors and cannot be executed");
process.exit(1);
} else {
warningMessage("Plan has warnings - proceed with caution");
}
} catch (error) {
failSpinner("Validation failed");
throw error;
}
};