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:
10
src/commands/handlers/chat.ts
Normal file
10
src/commands/handlers/chat.ts
Normal 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);
|
||||
};
|
||||
123
src/commands/handlers/classify.ts
Normal file
123
src/commands/handlers/classify.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
113
src/commands/handlers/config.ts
Normal file
113
src/commands/handlers/config.ts
Normal 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);
|
||||
};
|
||||
62
src/commands/handlers/plan.ts
Normal file
62
src/commands/handlers/plan.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
28
src/commands/handlers/registry.ts
Normal file
28
src/commands/handlers/registry.ts
Normal 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;
|
||||
};
|
||||
10
src/commands/handlers/run.ts
Normal file
10
src/commands/handlers/run.ts
Normal 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);
|
||||
};
|
||||
15
src/commands/handlers/serve.ts
Normal file
15
src/commands/handlers/serve.ts
Normal 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",
|
||||
);
|
||||
};
|
||||
78
src/commands/handlers/validate.ts
Normal file
78
src/commands/handlers/validate.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user