Reorganize major src/ directories to follow a consistent pattern with
core/, menu/, submenu/, inputs/, logs/, layout/, feedback/ subdirectories.
Changes by module:
- stores/: Move 5 store files to stores/core/
- utils/: Create core/ (terminal, tools, etc.) and menu/ (progress-bar)
- api/: Create copilot/core/, copilot/auth/, ollama/core/
- providers/: Create core/, copilot/core/, copilot/auth/, ollama/core/, login/core/
- ui/: Create core/, banner/core/, banner/menu/, spinner/core/,
input-editor/core/, components/core/, components/menu/
- tools/: Create core/ for registry.ts and types.ts
- tui-solid/: Reorganize components/ into menu/, submenu/, inputs/,
logs/, modals/, panels/, layout/, feedback/
- commands/: Create core/ for runner.ts and handlers.ts
- services/: Create core/ for agent.ts, permissions.ts, session.ts,
executor.ts, config.ts
All imports updated to use new paths. TypeScript compilation verified.
655 lines
19 KiB
JavaScript
655 lines
19 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { Command } from "commander";
|
|
import { handleCommand } from "@commands/core/handlers";
|
|
import { execute } from "@commands/chat-tui";
|
|
import versionData from "@/version.json";
|
|
import {
|
|
initializeProviders,
|
|
loginProvider,
|
|
getProviderNames,
|
|
displayProvidersStatus,
|
|
} from "@providers/index";
|
|
import { getConfig } from "@services/core/config";
|
|
import { deleteSession, getSessionSummaries } from "@services/core/session";
|
|
import {
|
|
initializePermissions,
|
|
listPatterns,
|
|
addGlobalPattern,
|
|
addLocalPattern,
|
|
} from "@services/core/permissions";
|
|
import {
|
|
projectConfig,
|
|
initProject,
|
|
initGlobal,
|
|
getRules,
|
|
addRule,
|
|
getAgents,
|
|
getSkills,
|
|
getLearnings,
|
|
addLearning,
|
|
getSettings,
|
|
buildLearningsContext,
|
|
} from "@services/project-config";
|
|
import { createPlan, displayPlan, approvePlan } from "@services/planner";
|
|
import { ensureXdgDirectories } from "@utils/core/ensure-directories";
|
|
import chalk from "chalk";
|
|
|
|
// Read version from version.json
|
|
const { version } = versionData;
|
|
|
|
// Ensure XDG directories exist
|
|
await ensureXdgDirectories();
|
|
|
|
// Auto-initialize config folders on startup
|
|
await projectConfig.autoInitialize();
|
|
|
|
const program = new Command();
|
|
|
|
program
|
|
.name("codetyper")
|
|
.description("CodeTyper AI Agent - Autonomous code generation assistant")
|
|
.version(version)
|
|
.argument("[prompt...]", "Initial prompt to start chat with")
|
|
.option("-p, --print", "Print response and exit (non-interactive mode)")
|
|
.option(
|
|
"-c, --continue",
|
|
"Continue most recent conversation in current directory",
|
|
)
|
|
.option("-r, --resume <session>", "Resume a specific session by ID")
|
|
.option("-m, --model <model>", "Model to use")
|
|
.option("--provider <provider>", "Provider to use (copilot, ollama)")
|
|
.option("-f, --file <files...>", "Files to add to context")
|
|
.option("--system-prompt <prompt>", "Replace the system prompt")
|
|
.option("--append-system-prompt <prompt>", "Append to the system prompt")
|
|
.option("--verbose", "Enable verbose output")
|
|
.option("-y, --yes", "Auto-approve all tool executions")
|
|
.action(async (promptParts, options) => {
|
|
await initializeProviders();
|
|
|
|
const initialPrompt = promptParts.join(" ").trim();
|
|
|
|
// Check for piped input
|
|
let pipedInput = "";
|
|
if (!process.stdin.isTTY) {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of process.stdin) {
|
|
chunks.push(chunk);
|
|
}
|
|
pipedInput = Buffer.concat(chunks).toString("utf-8").trim();
|
|
}
|
|
|
|
// Combine piped input with prompt
|
|
const fullPrompt = pipedInput
|
|
? initialPrompt
|
|
? `${pipedInput}\n\n${initialPrompt}`
|
|
: pipedInput
|
|
: initialPrompt;
|
|
|
|
const chatOptions = {
|
|
provider: options.provider,
|
|
model: options.model,
|
|
files: options.file,
|
|
initialPrompt: fullPrompt || undefined,
|
|
continueSession: options.continue,
|
|
resumeSession: options.resume,
|
|
systemPrompt: options.systemPrompt,
|
|
appendSystemPrompt: options.appendSystemPrompt,
|
|
printMode: options.print,
|
|
verbose: options.verbose,
|
|
autoApprove: options.yes,
|
|
};
|
|
|
|
await execute(chatOptions);
|
|
});
|
|
|
|
// ========== LOGIN COMMAND ==========
|
|
program
|
|
.command("login [provider]")
|
|
.description("Configure API credentials for a provider")
|
|
.action(async (provider?: string) => {
|
|
await initializeProviders();
|
|
|
|
const validProviders = getProviderNames();
|
|
|
|
if (!provider) {
|
|
console.log("\n" + chalk.bold("Available providers:"));
|
|
for (const p of validProviders) {
|
|
console.log(` - ${p}`);
|
|
}
|
|
console.log("\n" + chalk.gray("Usage: codetyper login <provider>"));
|
|
return;
|
|
}
|
|
|
|
if (!validProviders.includes(provider as any)) {
|
|
console.error(chalk.red(`Invalid provider: ${provider}`));
|
|
console.log("Valid providers: " + validProviders.join(", "));
|
|
process.exit(1);
|
|
}
|
|
|
|
await loginProvider(provider as any);
|
|
});
|
|
|
|
// ========== RUN COMMAND ==========
|
|
program
|
|
.command("run <task>")
|
|
.description("Execute autonomous task with the agent")
|
|
.option("-a, --agent <type>", "Agent type to use", "coder")
|
|
.option("-f, --file <files...>", "Context files for the task")
|
|
.option("-d, --dry-run", "Generate plan only, don't execute", false)
|
|
.option("-i, --max-iterations <number>", "Maximum iterations", "20")
|
|
.option("--auto-approve", "Automatically approve all actions", false)
|
|
.action(async (task, options) => {
|
|
await initializeProviders();
|
|
await handleCommand("run", { task, files: options.file, ...options });
|
|
});
|
|
|
|
// ========== SESSION COMMAND ==========
|
|
const sessionCommand = program
|
|
.command("session")
|
|
.description("Manage chat sessions");
|
|
|
|
sessionCommand
|
|
.command("list")
|
|
.alias("ls")
|
|
.description("List all saved sessions")
|
|
.action(async () => {
|
|
const summaries = await getSessionSummaries();
|
|
|
|
if (summaries.length === 0) {
|
|
console.log(chalk.gray("No saved sessions"));
|
|
return;
|
|
}
|
|
|
|
console.log("\n" + chalk.bold("Saved Sessions:") + "\n");
|
|
|
|
for (const session of summaries) {
|
|
const date = new Date(session.updatedAt).toLocaleDateString();
|
|
const time = new Date(session.updatedAt).toLocaleTimeString();
|
|
const preview = session.lastMessage
|
|
? session.lastMessage.slice(0, 60).replace(/\n/g, " ")
|
|
: "(no messages)";
|
|
|
|
console.log(`${chalk.cyan(session.id)}`);
|
|
console.log(
|
|
` ${chalk.gray(`${date} ${time}`)} | ${session.messageCount} messages`,
|
|
);
|
|
console.log(
|
|
` ${chalk.gray(preview)}${preview.length >= 60 ? "..." : ""}`,
|
|
);
|
|
if (session.workingDirectory) {
|
|
console.log(` ${chalk.gray(`Dir: ${session.workingDirectory}`)}`);
|
|
}
|
|
console.log();
|
|
}
|
|
});
|
|
|
|
sessionCommand
|
|
.command("delete <id>")
|
|
.alias("rm")
|
|
.description("Delete a session by ID")
|
|
.action(async (id: string) => {
|
|
try {
|
|
await deleteSession(id);
|
|
console.log(chalk.green(`Deleted session: ${id}`));
|
|
} catch (error) {
|
|
console.error(chalk.red(`Failed to delete session: ${error}`));
|
|
}
|
|
});
|
|
|
|
// ========== MODELS COMMAND ==========
|
|
program
|
|
.command("models [provider]")
|
|
.description("List available models for a provider")
|
|
.action(async (provider?: string) => {
|
|
await initializeProviders();
|
|
const config = await getConfig();
|
|
const targetProvider = (provider || config.get("provider")) as any;
|
|
|
|
const { getProvider, getProviderStatus } = await import("@providers/index");
|
|
const providerInstance = getProvider(targetProvider);
|
|
const status = await getProviderStatus(targetProvider);
|
|
|
|
console.log(`\n${chalk.bold(providerInstance.displayName)} Models\n`);
|
|
|
|
if (!status.valid) {
|
|
console.log(
|
|
chalk.yellow(
|
|
`Provider not configured. Run: codetyper login ${targetProvider}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const models = await providerInstance.getModels();
|
|
const defaultModel = providerInstance.getDefaultModel();
|
|
|
|
for (const model of models) {
|
|
const isDefault = model.id === defaultModel;
|
|
const marker = isDefault ? chalk.green("*") : " ";
|
|
const tools = model.supportsTools ? chalk.gray("[tools]") : "";
|
|
const streaming = model.supportsStreaming ? chalk.gray("[stream]") : "";
|
|
|
|
console.log(
|
|
`${marker} ${chalk.cyan(model.id)} - ${model.name} ${tools} ${streaming}`,
|
|
);
|
|
}
|
|
|
|
console.log(`\n${chalk.gray("* = default model")}`);
|
|
});
|
|
|
|
// ========== PROVIDERS COMMAND ==========
|
|
program
|
|
.command("providers")
|
|
.description("Show status of all LLM providers")
|
|
.action(async () => {
|
|
await initializeProviders();
|
|
const config = await getConfig();
|
|
await displayProvidersStatus(config.get("provider"));
|
|
});
|
|
|
|
// ========== CONFIG COMMAND ==========
|
|
const configCommand = program
|
|
.command("config")
|
|
.description("View or modify CLI configuration");
|
|
|
|
configCommand
|
|
.command("show")
|
|
.description("Show current configuration")
|
|
.action(async () => {
|
|
await handleCommand("config", { action: "show" });
|
|
});
|
|
|
|
configCommand
|
|
.command("path")
|
|
.description("Show config file path")
|
|
.action(async () => {
|
|
await handleCommand("config", { action: "path" });
|
|
});
|
|
|
|
configCommand
|
|
.command("set <key> <value>")
|
|
.description("Set configuration value (provider, model)")
|
|
.action(async (key, value) => {
|
|
await handleCommand("config", { action: "set", key, value });
|
|
});
|
|
|
|
// ========== CLASSIFY COMMAND ==========
|
|
program
|
|
.command("classify <prompt>")
|
|
.description("Analyze user prompt and classify the intent")
|
|
.option("-c, --context <context>", "Additional context for classification")
|
|
.option("-f, --file <files...>", "Referenced files for context")
|
|
.action(async (prompt, options) => {
|
|
await initializeProviders();
|
|
await handleCommand("classify", {
|
|
prompt,
|
|
files: options.file,
|
|
...options,
|
|
});
|
|
});
|
|
|
|
// ========== PLAN COMMAND ==========
|
|
const planCommand = program
|
|
.command("plan")
|
|
.description("Generate a detailed execution plan for a task");
|
|
|
|
["code", "fix", "refactor", "test", "document", "explain"].forEach((intent) => {
|
|
planCommand
|
|
.command(intent)
|
|
.description(`Plan for ${intent} intent`)
|
|
.requiredOption("-t, --task <task>", "Task description")
|
|
.option("-f, --file <files...>", "Files to operate on")
|
|
.option("-o, --output <file>", "Save plan to file (JSON format)")
|
|
.action(async (options) => {
|
|
await initializeProviders();
|
|
await handleCommand("plan", { intent, files: options.file, ...options });
|
|
});
|
|
});
|
|
|
|
// ========== INIT COMMAND ==========
|
|
program
|
|
.command("init")
|
|
.description("Initialize codetyper configuration in current directory")
|
|
.option("-g, --global", "Initialize global configuration")
|
|
.action(async (options) => {
|
|
if (options.global) {
|
|
await initGlobal();
|
|
console.log(
|
|
chalk.green(
|
|
"✓ Initialized global configuration at ~/.config/codetyper/",
|
|
),
|
|
);
|
|
} else {
|
|
await initProject();
|
|
console.log(
|
|
chalk.green("✓ Initialized project configuration at .codetyper/"),
|
|
);
|
|
}
|
|
|
|
console.log("\nCreated directories:");
|
|
console.log(chalk.gray(" - rules/ (project-specific rules)"));
|
|
console.log(chalk.gray(" - agents/ (custom agent configurations)"));
|
|
console.log(chalk.gray(" - skills/ (custom skills/commands)"));
|
|
console.log(chalk.gray(" - learnings/ (saved learnings)"));
|
|
});
|
|
|
|
// ========== PERMISSIONS COMMAND ==========
|
|
const permCommand = program
|
|
.command("permissions")
|
|
.alias("perm")
|
|
.description("Manage command execution permissions");
|
|
|
|
permCommand
|
|
.command("list")
|
|
.alias("ls")
|
|
.description("List all permission patterns")
|
|
.action(async () => {
|
|
await initializePermissions();
|
|
const patterns = listPatterns();
|
|
|
|
console.log("\n" + chalk.bold("Permission Patterns") + "\n");
|
|
|
|
const hasPatterns =
|
|
patterns.global.length > 0 ||
|
|
patterns.local.length > 0 ||
|
|
patterns.session.length > 0;
|
|
if (!hasPatterns) {
|
|
console.log(chalk.gray("No permission patterns configured"));
|
|
console.log(
|
|
chalk.gray("\nPatterns are auto-created when you approve commands."),
|
|
);
|
|
console.log(
|
|
chalk.gray(
|
|
"Format: Bash(command:args), Read(*), Write(*.ts), Edit(src/*)",
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (patterns.global.length > 0) {
|
|
console.log(
|
|
chalk.magenta("Global Patterns (~/.config/codetyper/settings.json):"),
|
|
);
|
|
for (const pattern of patterns.global) {
|
|
console.log(` ${chalk.green("allow")} ${pattern}`);
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
if (patterns.local.length > 0) {
|
|
console.log(chalk.cyan("Project Patterns (.codetyper/settings.json):"));
|
|
for (const pattern of patterns.local) {
|
|
console.log(` ${chalk.green("allow")} ${pattern}`);
|
|
}
|
|
console.log();
|
|
}
|
|
|
|
if (patterns.session.length > 0) {
|
|
console.log(chalk.yellow("Session Patterns (temporary):"));
|
|
for (const pattern of patterns.session) {
|
|
console.log(` ${chalk.green("allow")} ${pattern}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
permCommand
|
|
.command("allow <pattern>")
|
|
.option("-g, --global", "Add to global patterns")
|
|
.option("-l, --local", "Add to project patterns (default)")
|
|
.description("Allow a pattern (e.g., Bash(npm install:*), Read(*))")
|
|
.action(async (pattern: string, options) => {
|
|
await initializePermissions();
|
|
if (options.global) {
|
|
await addGlobalPattern(pattern);
|
|
console.log(chalk.magenta(`✓ Added global pattern: ${pattern}`));
|
|
} else {
|
|
await addLocalPattern(pattern);
|
|
console.log(chalk.cyan(`✓ Added project pattern: ${pattern}`));
|
|
}
|
|
});
|
|
|
|
// ========== RULES COMMAND ==========
|
|
const rulesCommand = program
|
|
.command("rules")
|
|
.description("Manage project rules");
|
|
|
|
rulesCommand
|
|
.command("list")
|
|
.alias("ls")
|
|
.description("List all rules")
|
|
.action(async () => {
|
|
const rules = await getRules();
|
|
|
|
console.log("\n" + chalk.bold("Project Rules") + "\n");
|
|
|
|
if (rules.length === 0) {
|
|
console.log(chalk.gray("No rules configured"));
|
|
console.log(chalk.gray("Run: codetyper init"));
|
|
return;
|
|
}
|
|
|
|
for (const rule of rules) {
|
|
console.log(chalk.cyan(`• ${rule.name}`));
|
|
const preview = rule.content.split("\n").slice(0, 3).join("\n");
|
|
console.log(
|
|
chalk.gray(preview.slice(0, 200) + (preview.length > 200 ? "..." : "")),
|
|
);
|
|
console.log();
|
|
}
|
|
});
|
|
|
|
rulesCommand
|
|
.command("add <name>")
|
|
.description("Add a new rule")
|
|
.option("-g, --global", "Add as global rule")
|
|
.option("-c, --content <content>", "Rule content")
|
|
.action(async (name: string, options) => {
|
|
if (!options.content) {
|
|
console.log(
|
|
chalk.yellow("Rule content is required. Use --content flag."),
|
|
);
|
|
return;
|
|
}
|
|
|
|
await addRule(name, options.content, options.global);
|
|
console.log(chalk.green(`✓ Added rule: ${name}`));
|
|
});
|
|
|
|
// ========== AGENTS COMMAND ==========
|
|
const agentsCommand = program
|
|
.command("agents")
|
|
.description("Manage custom agents");
|
|
|
|
agentsCommand
|
|
.command("list")
|
|
.alias("ls")
|
|
.description("List all agents")
|
|
.action(async () => {
|
|
const agents = await getAgents();
|
|
|
|
console.log("\n" + chalk.bold("Custom Agents") + "\n");
|
|
|
|
if (agents.length === 0) {
|
|
console.log(chalk.gray("No custom agents configured"));
|
|
return;
|
|
}
|
|
|
|
for (const agent of agents) {
|
|
console.log(chalk.cyan(`• ${agent.name}`));
|
|
console.log(` ${chalk.gray(agent.description)}`);
|
|
if (agent.model) console.log(` ${chalk.gray(`Model: ${agent.model}`)}`);
|
|
console.log();
|
|
}
|
|
});
|
|
|
|
// ========== SKILLS COMMAND ==========
|
|
const skillsCommand = program
|
|
.command("skills")
|
|
.description("Manage custom skills");
|
|
|
|
skillsCommand
|
|
.command("list")
|
|
.alias("ls")
|
|
.description("List all skills")
|
|
.action(async () => {
|
|
const skills = await getSkills();
|
|
|
|
console.log("\n" + chalk.bold("Custom Skills") + "\n");
|
|
|
|
if (skills.length === 0) {
|
|
console.log(chalk.gray("No custom skills configured"));
|
|
return;
|
|
}
|
|
|
|
for (const skill of skills) {
|
|
console.log(chalk.cyan(`/${skill.command}`) + ` - ${skill.name}`);
|
|
console.log(` ${chalk.gray(skill.description)}`);
|
|
console.log();
|
|
}
|
|
});
|
|
|
|
// ========== LEARNINGS COMMAND ==========
|
|
const learningsCommand = program
|
|
.command("learnings")
|
|
.alias("learn")
|
|
.description("Manage project learnings");
|
|
|
|
learningsCommand
|
|
.command("list")
|
|
.alias("ls")
|
|
.description("List all learnings")
|
|
.action(async () => {
|
|
const learnings = await getLearnings();
|
|
|
|
console.log("\n" + chalk.bold("Project Learnings") + "\n");
|
|
|
|
if (learnings.length === 0) {
|
|
console.log(chalk.gray("No learnings saved"));
|
|
return;
|
|
}
|
|
|
|
for (const learning of learnings.slice(0, 20)) {
|
|
const date = new Date(learning.createdAt).toLocaleDateString();
|
|
console.log(`${chalk.gray(date)} - ${learning.content.slice(0, 80)}`);
|
|
}
|
|
});
|
|
|
|
learningsCommand
|
|
.command("add <content>")
|
|
.description("Add a new learning")
|
|
.option("-g, --global", "Add as global learning")
|
|
.option("-c, --context <context>", "Context for the learning")
|
|
.action(async (content: string, options) => {
|
|
await addLearning(content, options.context, options.global);
|
|
console.log(chalk.green("✓ Learning saved"));
|
|
});
|
|
|
|
// ========== UPGRADE COMMAND ==========
|
|
program
|
|
.command("upgrade")
|
|
.description("Update codetyper to the latest version")
|
|
.option("-c, --check", "Check for updates without installing")
|
|
.option("-v, --version <version>", "Install a specific version")
|
|
.action(async (options) => {
|
|
const { performUpgrade } = await import("@services/upgrade");
|
|
await performUpgrade({
|
|
check: options.check,
|
|
version: options.version,
|
|
});
|
|
});
|
|
|
|
// ========== MCP COMMAND ==========
|
|
program
|
|
.command("mcp [subcommand] [args...]")
|
|
.description("Manage MCP (Model Context Protocol) servers")
|
|
.action(async (subcommand, args) => {
|
|
const { mcpCommand } = await import("@commands/mcp");
|
|
await mcpCommand([subcommand, ...args].filter(Boolean));
|
|
});
|
|
|
|
// ========== TASK COMMAND ==========
|
|
program
|
|
.command("task <description>")
|
|
.alias("do")
|
|
.description("Execute a task with automatic planning")
|
|
.option("-f, --file <files...>", "Context files for the task")
|
|
.option("-d, --dry-run", "Show plan without executing")
|
|
.option("--auto-approve", "Automatically approve all actions")
|
|
.action(async (description: string, options) => {
|
|
await initializeProviders();
|
|
await initializePermissions();
|
|
|
|
const settings = await getSettings();
|
|
|
|
// Build context from files and learnings
|
|
let context = "";
|
|
|
|
if (options.file) {
|
|
const { readFile } = await import("fs/promises");
|
|
for (const file of options.file) {
|
|
try {
|
|
const content = await readFile(file, "utf-8");
|
|
context += `\n\n--- ${file} ---\n${content}`;
|
|
} catch {
|
|
console.log(chalk.yellow(`Warning: Could not read file ${file}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
const learningsContext = await buildLearningsContext();
|
|
if (learningsContext) {
|
|
context += "\n\n" + learningsContext;
|
|
}
|
|
|
|
// Generate plan
|
|
console.log(chalk.cyan("\nGenerating execution plan...\n"));
|
|
|
|
const plan = await createPlan(
|
|
description,
|
|
context,
|
|
settings.defaultProvider as any,
|
|
settings.defaultModel,
|
|
);
|
|
|
|
displayPlan(plan);
|
|
|
|
if (options.dryRun) {
|
|
console.log(chalk.yellow("Dry run - not executing plan"));
|
|
return;
|
|
}
|
|
|
|
// Ask for approval
|
|
console.log("");
|
|
console.log(chalk.yellow("Execute this plan? [y/n]: "));
|
|
|
|
const answer = await new Promise<string>((resolve) => {
|
|
process.stdin.setRawMode?.(true);
|
|
process.stdin.resume();
|
|
process.stdin.once("data", (data) => {
|
|
process.stdin.setRawMode?.(false);
|
|
resolve(data.toString().trim().toLowerCase());
|
|
});
|
|
});
|
|
|
|
if (answer !== "y" && answer !== "yes") {
|
|
console.log(chalk.gray("Plan cancelled"));
|
|
return;
|
|
}
|
|
|
|
approvePlan();
|
|
console.log(chalk.green("\n✓ Plan approved - executing...\n"));
|
|
|
|
// Execute steps - this is a simplified version
|
|
// In a full implementation, each step type would have specific handlers
|
|
await execute({
|
|
provider: settings.defaultProvider as any,
|
|
model: settings.defaultModel,
|
|
files: options.file,
|
|
initialPrompt: `Execute this plan step by step:\n\n${plan.steps.map((s) => `${s.id}. ${s.description}`).join("\n")}`,
|
|
});
|
|
});
|
|
|
|
// Parse arguments
|
|
program.parse(process.argv);
|