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

36
src/commands/chat-tui.ts Normal file
View File

@@ -0,0 +1,36 @@
/**
* TUI-based Chat Command for CodeTyper CLI (Presentation Layer)
*
* This file is the main entry point for the chat TUI.
* It assembles callbacks and re-exports the execute function.
* All business logic is delegated to chat-tui-service.ts
*/
import type { ChatServiceCallbacks } from "@services/chat-tui-service.ts";
import { onModeChange } from "@commands/components/callbacks/on-mode-change.ts";
import { onLog } from "@commands/components/callbacks/on-log.ts";
import { onToolCall } from "@commands/components/callbacks/on-tool-call.ts";
import { onToolResult } from "@commands/components/callbacks/on-tool-result.ts";
import { onPermissionRequest } from "@commands/components/callbacks/on-permission-request.ts";
import { onLearningDetected } from "@commands/components/callbacks/on-learning-detected.ts";
import executeCommand from "@commands/components/execute/index.ts";
export const createCallbacks = (): ChatServiceCallbacks => ({
onModeChange,
onLog,
onToolCall,
onToolResult,
onPermissionRequest,
onLearningDetected,
});
export const execute = executeCommand;
export {
onModeChange,
onLog,
onToolCall,
onToolResult,
onPermissionRequest,
onLearningDetected,
};

8
src/commands/chat.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Interactive chat mode for CodeTyper CLI
*
* This file re-exports the modular chat implementation.
*/
export { execute, createInitialState } from "@commands/components/chat/index";
export type { ChatState } from "@commands/components/chat/index";

View File

@@ -0,0 +1,23 @@
import { v4 as uuidv4 } from "uuid";
import { appStore } from "@tui/index.ts";
import type { LearningResponse } from "@tui/types.ts";
import type { LearningCandidate } from "@services/learning-service.ts";
export const onLearningDetected = async (
candidate: LearningCandidate,
): Promise<LearningResponse> => {
return new Promise((resolve) => {
appStore.setMode("learning_prompt");
appStore.setLearningPrompt({
id: uuidv4(),
content: candidate.content,
context: candidate.context,
category: candidate.category,
resolve: (response: LearningResponse) => {
appStore.setLearningPrompt(null);
appStore.setMode("idle");
resolve(response);
},
});
});
};

View File

@@ -0,0 +1,14 @@
import { appStore } from "@tui/index.ts";
import type { LogType } from "@/types/log";
export const onLog = (
type: string,
content: string,
metadata?: Record<string, unknown>,
): void => {
appStore.addLog({
type: type as LogType,
content,
metadata,
});
};

View File

@@ -0,0 +1,5 @@
import { appStore } from "@tui/index.ts";
export const onModeChange = (mode: string): void => {
appStore.setMode(mode as Parameters<typeof appStore.setMode>[0]);
};

View File

@@ -0,0 +1,7 @@
interface PermissionResponse {
allowed: boolean;
}
export const onPermissionRequest = async (): Promise<PermissionResponse> => {
return { allowed: false };
};

View File

@@ -0,0 +1,25 @@
import { appStore } from "@tui/index.ts";
import { isQuietTool } from "@utils/tools.ts";
import type { ToolCallParams } from "@interfaces/ToolCallParams.ts";
export const onToolCall = (call: ToolCallParams): void => {
appStore.setCurrentToolCall({
id: call.id,
name: call.name,
description: call.description,
status: "running",
});
const isQuiet = isQuietTool(call.name, call.args);
appStore.addLog({
type: "tool",
content: call.description,
metadata: {
toolName: call.name,
toolStatus: "running",
toolDescription: call.description,
quiet: isQuiet,
},
});
};

View File

@@ -0,0 +1,47 @@
import { appStore } from "@tui/index.ts";
import {
truncateOutput,
detectDiffContent,
} from "@services/chat-tui-service.ts";
import { getThinkingMessage } from "@constants/status-messages.ts";
export const onToolResult = (
success: boolean,
title: string,
output?: string,
error?: string,
): void => {
appStore.updateToolCall({
status: success ? "success" : "error",
result: success ? output : undefined,
error: error,
});
const state = appStore.getState();
const logEntry = state.logs.find(
(log) => log.type === "tool" && log.metadata?.toolStatus === "running",
);
if (logEntry) {
const diffData = output ? detectDiffContent(output) : undefined;
const displayContent = diffData?.isDiff
? output
: output
? truncateOutput(output)
: "";
appStore.updateLog(logEntry.id, {
content: success
? `${title}${displayContent ? "\n" + displayContent : ""}`
: `${title}: ${error}`,
metadata: {
...logEntry.metadata,
toolStatus: success ? "success" : "error",
toolDescription: title,
diffData: diffData,
},
});
}
appStore.setThinkingMessage(getThinkingMessage());
};

View File

@@ -0,0 +1,35 @@
/**
* Show available agents command
*/
import chalk from "chalk";
import { agentLoader } from "@services/agent-loader";
import type { ChatState } from "@commands/components/chat/state";
export const showAgents = async (state: ChatState): Promise<void> => {
const agents = await agentLoader.getAvailableAgents(process.cwd());
const currentAgent = state.currentAgent ?? "coder";
console.log("\n" + chalk.bold.underline("Available Agents") + "\n");
for (const agent of agents) {
const isCurrent = agent.id === currentAgent;
const marker = isCurrent ? chalk.cyan("→") : " ";
const nameStyle = isCurrent ? chalk.cyan.bold : chalk.white;
console.log(`${marker} ${nameStyle(agent.name)}`);
if (agent.description) {
console.log(` ${chalk.gray(agent.description)}`);
}
if (agent.model) {
console.log(` ${chalk.gray(`Model: ${agent.model}`)}`);
}
console.log();
}
console.log(chalk.gray("Use /agent <name> to switch agents"));
console.log();
};

View File

@@ -0,0 +1,60 @@
/**
* Switch agent command
*/
import chalk from "chalk";
import { errorMessage, infoMessage, warningMessage } from "@utils/terminal";
import { agentLoader } from "@services/agent-loader";
import type { ChatState } from "@commands/components/chat/state";
export const switchAgent = async (
agentName: string,
state: ChatState,
): Promise<void> => {
if (!agentName.trim()) {
warningMessage("Usage: /agent <name>");
infoMessage("Use /agents to see available agents");
return;
}
const normalizedName = agentName.toLowerCase().trim();
const agents = await agentLoader.getAvailableAgents(process.cwd());
// Find agent by id or partial name match
const agent = agents.find(
(a) =>
a.id === normalizedName ||
a.name.toLowerCase() === normalizedName ||
a.id.includes(normalizedName) ||
a.name.toLowerCase().includes(normalizedName),
);
if (!agent) {
errorMessage(`Agent not found: ${agentName}`);
infoMessage("Use /agents to see available agents");
return;
}
state.currentAgent = agent.id;
// Update system prompt with agent prompt
if (agent.prompt) {
// Prepend agent prompt to system prompt
const basePrompt = state.systemPrompt;
state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`;
// Update the system message in messages array
if (state.messages.length > 0 && state.messages[0].role === "system") {
state.messages[0].content = state.systemPrompt;
}
}
console.log();
console.log(chalk.green(`✓ Switched to agent: ${chalk.bold(agent.name)}`));
if (agent.description) {
console.log(chalk.gray(` ${agent.description}`));
}
console.log();
};

View File

@@ -0,0 +1,12 @@
import chalk from "chalk";
import type { ChatState } from "./state.ts";
export const createCleanup = (state: ChatState) => (): void => {
state.isRunning = false;
if (state.inputEditor) {
state.inputEditor.stop();
state.inputEditor = null;
}
console.log("\n" + chalk.cyan("Goodbye!"));
process.exit(0);
};

View File

@@ -0,0 +1,111 @@
import { saveSession } from "@services/session";
import { showHelp } from "@commands/components/chat/commands/show-help";
import { clearConversation } from "@commands/components/chat/history/clear-conversation";
import { showContextFiles } from "@commands/components/chat/context/show-context-files";
import { removeFile } from "@commands/components/chat/context/remove-file";
import { showContext } from "@commands/components/chat/history/show-context";
import { compactHistory } from "@commands/components/chat/history/compact-history";
import { showHistory } from "@commands/components/chat/history/show-history";
import { showModels } from "@commands/components/chat/models/show-models";
import { showProviders } from "@commands/components/chat/models/show-providers";
import { switchProvider } from "@commands/components/chat/models/switch-provider";
import { switchModel } from "@commands/components/chat/models/switch-model";
import { showSessionInfo } from "@commands/components/chat/session/show-session-info";
import { listSessions } from "@commands/components/chat/session/list-sessions";
import { showUsage } from "@commands/components/chat/usage/show-usage";
import { showAgents } from "@commands/components/chat/agents/show-agents";
import { switchAgent } from "@commands/components/chat/agents/switch-agent";
import { handleMCP } from "@commands/components/chat/mcp/handle-mcp";
import { CommandContext } from "@interfaces/commandContext";
import type { CommandHandler } from "@/types/commandHandler";
import { successMessage } from "@utils/terminal";
const COMMAND_REGISTRY: Map<string, CommandHandler> = new Map<
string,
CommandHandler
>([
["help", () => showHelp()],
["h", () => showHelp()],
["clear", (ctx: CommandContext) => clearConversation(ctx.state)],
["c", (ctx: CommandContext) => clearConversation(ctx.state)],
["files", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)],
["f", (ctx: CommandContext) => showContextFiles(ctx.state.contextFiles)],
["exit", (ctx: CommandContext) => ctx.cleanup()],
["quit", (ctx: CommandContext) => ctx.cleanup()],
["q", (ctx: CommandContext) => ctx.cleanup()],
[
"save",
async () => {
await saveSession();
successMessage("Session saved");
},
],
[
"s",
async () => {
await saveSession();
successMessage("Session saved");
},
],
[
"models",
async (ctx: CommandContext) =>
showModels(ctx.state.currentProvider, ctx.state.currentModel),
],
[
"m",
async (ctx: CommandContext) =>
showModels(ctx.state.currentProvider, ctx.state.currentModel),
],
["providers", async () => showProviders()],
["p", async () => showProviders()],
[
"provider",
async (ctx: CommandContext) =>
switchProvider(ctx.args.join(" "), ctx.state),
],
[
"model",
async (ctx: CommandContext) => switchModel(ctx.args.join(" "), ctx.state),
],
["context", (ctx: CommandContext) => showContext(ctx.state)],
["compact", (ctx: CommandContext) => compactHistory(ctx.state)],
["history", (ctx: CommandContext) => showHistory(ctx.state)],
[
"remove",
(ctx: CommandContext) =>
removeFile(ctx.args.join(" "), ctx.state.contextFiles),
],
[
"rm",
(ctx: CommandContext) =>
removeFile(ctx.args.join(" "), ctx.state.contextFiles),
],
["session", async () => showSessionInfo()],
["sessions", async () => listSessions()],
["usage", async (ctx: CommandContext) => showUsage(ctx.state)],
["u", async (ctx: CommandContext) => showUsage(ctx.state)],
[
"agent",
async (ctx: CommandContext) => {
if (ctx.args.length === 0) {
await showAgents(ctx.state);
} else {
await switchAgent(ctx.args.join(" "), ctx.state);
}
},
],
[
"a",
async (ctx: CommandContext) => {
if (ctx.args.length === 0) {
await showAgents(ctx.state);
} else {
await switchAgent(ctx.args.join(" "), ctx.state);
}
},
],
["mcp", async (ctx: CommandContext) => handleMCP(ctx.args)],
]);
export default COMMAND_REGISTRY;

View File

@@ -0,0 +1,28 @@
import { warningMessage, infoMessage } from "@utils/terminal";
import type { ChatState } from "@commands/components/chat/state";
import COMMAND_REGISTRY from "@commands/components/chat/commands/commandsRegistry";
const isValidCommand = (cmd: string): boolean => {
return COMMAND_REGISTRY.has(cmd);
};
export const handleCommand = async (
command: string,
state: ChatState,
cleanup: () => void,
): Promise<void> => {
const parts = command.slice(1).split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
if (!isValidCommand(cmd)) {
warningMessage(`Unknown command: /${cmd}`);
infoMessage("Type /help for available commands");
return;
}
const handler = COMMAND_REGISTRY.get(cmd);
if (handler) {
await handler({ state, args, cleanup });
}
};

View File

@@ -0,0 +1,25 @@
import chalk from "chalk";
import { HELP_COMMANDS } from "@constants/help-commands.ts";
export const showHelp = (): void => {
console.log("\n" + chalk.bold.underline("Commands") + "\n");
for (const [cmd, desc] of HELP_COMMANDS) {
console.log(` ${chalk.yellow(cmd.padEnd(20))} ${desc}`);
}
console.log("\n" + chalk.bold.underline("File References") + "\n");
console.log(` ${chalk.yellow("@<file>")} Add a file to context`);
console.log(
` ${chalk.yellow('@"file with spaces"')} Add file with spaces in name`,
);
console.log(
` ${chalk.yellow("@src/*.ts")} Add files matching glob pattern`,
);
console.log("\n" + chalk.bold.underline("Examples") + "\n");
console.log(" @src/app.ts explain this code");
console.log(" @src/utils.ts @src/types.ts refactor these files");
console.log(" /model gpt-4o");
console.log(" /provider copilot\n");
};

View File

@@ -0,0 +1,35 @@
import { resolve } from "path";
import { existsSync } from "fs";
import fg from "fast-glob";
import { errorMessage, warningMessage } from "@utils/terminal";
import { loadFile } from "@commands/components/chat/context/load-file";
import { IGNORE_FOLDERS } from "@constants/paths";
export const addContextFile = async (
pattern: string,
contextFiles: Map<string, string>,
): Promise<void> => {
try {
const paths = await fg(pattern, {
cwd: process.cwd(),
absolute: true,
ignore: IGNORE_FOLDERS,
});
if (paths.length === 0) {
const absolutePath = resolve(process.cwd(), pattern);
if (existsSync(absolutePath)) {
await loadFile(absolutePath, contextFiles);
} else {
warningMessage(`File not found: ${pattern}`);
}
return;
}
for (const filePath of paths) {
await loadFile(filePath, contextFiles);
}
} catch (error) {
errorMessage(`Failed to add file: ${error}`);
}
};

View File

@@ -0,0 +1,32 @@
import { readFile, stat } from "fs/promises";
import { basename } from "path";
import { warningMessage, successMessage, errorMessage } from "@utils/terminal";
import { addContextFile } from "@services/session";
export const loadFile = async (
filePath: string,
contextFiles: Map<string, string>,
): Promise<void> => {
try {
const stats = await stat(filePath);
if (stats.isDirectory()) {
warningMessage(`Skipping directory: ${filePath}`);
return;
}
if (stats.size > 100 * 1024) {
warningMessage(`File too large (>100KB): ${basename(filePath)}`);
return;
}
const content = await readFile(filePath, "utf-8");
contextFiles.set(filePath, content);
successMessage(
`Added: ${basename(filePath)} (${content.split("\n").length} lines)`,
);
await addContextFile(filePath);
} catch (error) {
errorMessage(`Failed to read file: ${error}`);
}
};

View File

@@ -0,0 +1,27 @@
import { FILE_REFERENCE_PATTERN } from "@constants/patterns";
import { addContextFile } from "@commands/components/chat/context/add-context-file";
export const processFileReferences = async (
input: string,
contextFiles: Map<string, string>,
): Promise<string> => {
const pattern = new RegExp(FILE_REFERENCE_PATTERN.source, "g");
let match;
const filesToAdd: string[] = [];
while ((match = pattern.exec(input)) !== null) {
const filePath = match[1] || match[2] || match[3];
filesToAdd.push(filePath);
}
for (const filePath of filesToAdd) {
await addContextFile(filePath, contextFiles);
}
const textOnly = input.replace(pattern, "").trim();
if (!textOnly && filesToAdd.length > 0) {
return `Analyze the files I've added to the context.`;
}
return input;
};

View File

@@ -0,0 +1,22 @@
import { basename } from "path";
import { warningMessage, successMessage } from "@utils/terminal";
export const removeFile = (
filename: string,
contextFiles: Map<string, string>,
): void => {
if (!filename) {
warningMessage("Please specify a file to remove");
return;
}
for (const [path] of contextFiles) {
if (path.includes(filename) || basename(path) === filename) {
contextFiles.delete(path);
successMessage(`Removed: ${basename(path)}`);
return;
}
}
warningMessage(`File not found in context: ${filename}`);
};

View File

@@ -0,0 +1,32 @@
import chalk from "chalk";
import { basename } from "path";
import { getCurrentSession } from "@services/session";
import { infoMessage, filePath } from "@utils/terminal";
export const showContextFiles = (contextFiles: Map<string, string>): void => {
const session = getCurrentSession();
const files = session?.contextFiles || [];
if (files.length === 0 && contextFiles.size === 0) {
infoMessage("No context files loaded");
return;
}
console.log("\n" + chalk.bold("Context Files:"));
if (contextFiles.size > 0) {
console.log(chalk.gray(" Pending (will be included in next message):"));
for (const [path] of contextFiles) {
console.log(` - ${filePath(basename(path))}`);
}
}
if (files.length > 0) {
console.log(chalk.gray(" In session:"));
files.forEach((file, index) => {
console.log(` ${index + 1}. ${filePath(file)}`);
});
}
console.log();
};

View File

@@ -0,0 +1,10 @@
import { clearMessages } from "@services/session";
import { successMessage } from "@utils/terminal";
import type { ChatState } from "@commands/components/chat/state";
export const clearConversation = (state: ChatState): void => {
state.messages = [{ role: "system", content: state.systemPrompt }];
state.contextFiles.clear();
clearMessages();
successMessage("Conversation cleared");
};

View File

@@ -0,0 +1,15 @@
import { successMessage, infoMessage } from "@utils/terminal";
import type { ChatState } from "@commands/components/chat/state";
export const compactHistory = (state: ChatState): void => {
if (state.messages.length <= 11) {
infoMessage("History is already compact");
return;
}
const systemPrompt = state.messages[0];
const recentMessages = state.messages.slice(-10);
state.messages = [systemPrompt, ...recentMessages];
successMessage(`Compacted to ${state.messages.length - 1} messages`);
};

View File

@@ -0,0 +1,18 @@
import chalk from "chalk";
import type { ChatState } from "@commands/components/chat/state";
export const showContext = (state: ChatState): void => {
const messageCount = state.messages.length - 1;
const totalChars = state.messages.reduce(
(acc, m) => acc + m.content.length,
0,
);
const estimatedTokens = Math.round(totalChars / 4);
console.log("\n" + chalk.bold("Context Information:"));
console.log(` Messages: ${messageCount}`);
console.log(` Characters: ${totalChars.toLocaleString()}`);
console.log(` Estimated tokens: ~${estimatedTokens.toLocaleString()}`);
console.log(` Pending files: ${state.contextFiles.size}`);
console.log();
};

View File

@@ -0,0 +1,18 @@
import chalk from "chalk";
import type { ChatState } from "@commands/components/chat/state";
export const showHistory = (state: ChatState): void => {
console.log("\n" + chalk.bold("Conversation History:") + "\n");
for (let i = 1; i < state.messages.length; i++) {
const msg = state.messages[i];
const role =
msg.role === "user" ? chalk.cyan("You") : chalk.green("Assistant");
const preview = msg.content.slice(0, 100).replace(/\n/g, " ");
console.log(
` ${i}. ${role}: ${preview}${msg.content.length > 100 ? "..." : ""}`,
);
}
console.log();
};

View File

@@ -0,0 +1,224 @@
import chalk from "chalk";
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
import {
createSession,
loadSession,
getMostRecentSession,
findSession,
setWorkingDirectory,
} from "@services/session";
import { getConfig } from "@services/config";
import type { Provider as ProviderName, ChatSession } from "@/types/index";
import { getProvider, getProviderStatus } from "@providers/index.ts";
import {
printWelcome,
formatTipLine,
Style,
Theme,
createInputEditor,
} from "@ui/index";
import {
DEFAULT_SYSTEM_PROMPT,
buildSystemPromptWithRules,
} from "@prompts/index.ts";
import type { ChatOptions } from "@interfaces/ChatOptions.ts";
import { createInitialState, type ChatState } from "./state.ts";
import { restoreMessagesFromSession } from "./session/restore-messages.ts";
import { addContextFile } from "./context/add-context-file.ts";
import { handleCommand } from "./commands/handle-command.ts";
import { handleInput } from "./messages/handle-input.ts";
import { executePrintMode } from "./print-mode.ts";
import { createCleanup } from "./cleanup.ts";
export const execute = async (options: ChatOptions): Promise<void> => {
const config = await getConfig();
const state = createInitialState(
(options.provider || config.get("provider")) as ProviderName,
);
state.verbose = options.verbose || false;
state.autoApprove = options.autoApprove || false;
state.currentModel = options.model || config.get("model") || "auto";
const status = await getProviderStatus(state.currentProvider);
if (!status.valid) {
errorMessage(`Provider ${state.currentProvider} is not configured.`);
infoMessage(`Run: codetyper login ${state.currentProvider}`);
process.exit(1);
}
if (options.systemPrompt) {
state.systemPrompt = options.systemPrompt;
} else {
const { prompt: promptWithRules, rulesPaths } =
await buildSystemPromptWithRules(DEFAULT_SYSTEM_PROMPT, process.cwd());
state.systemPrompt = promptWithRules;
if (rulesPaths.length > 0 && state.verbose) {
infoMessage(`Loaded ${rulesPaths.length} rule file(s):`);
for (const rulePath of rulesPaths) {
infoMessage(` - ${rulePath}`);
}
}
if (options.appendSystemPrompt) {
state.systemPrompt =
state.systemPrompt + "\n\n" + options.appendSystemPrompt;
}
}
let session: ChatSession;
if (options.continueSession) {
const recent = await getMostRecentSession(process.cwd());
if (recent) {
session = recent;
await loadSession(session.id);
state.messages = restoreMessagesFromSession(session, state.systemPrompt);
if (state.verbose) {
infoMessage(`Continuing session: ${session.id}`);
}
} else {
warningMessage(
"No previous session found in this directory. Starting new session.",
);
session = await createSession("coder");
}
} else if (options.resumeSession) {
const found = await findSession(options.resumeSession);
if (found) {
session = found;
await loadSession(session.id);
state.messages = restoreMessagesFromSession(session, state.systemPrompt);
if (state.verbose) {
infoMessage(`Resumed session: ${session.id}`);
}
} else {
errorMessage(`Session not found: ${options.resumeSession}`);
process.exit(1);
}
} else {
session = await createSession("coder");
await setWorkingDirectory(process.cwd());
}
if (state.messages.length === 0) {
state.messages = [{ role: "system", content: state.systemPrompt }];
}
if (options.files && options.files.length > 0) {
for (const file of options.files) {
await addContextFile(file, state.contextFiles);
}
}
if (options.printMode && options.initialPrompt) {
await executePrintMode(options.initialPrompt, state);
return;
}
const hasInitialPrompt =
options.initialPrompt && options.initialPrompt.trim().length > 0;
const provider = getProvider(state.currentProvider);
const model = state.currentModel || "auto";
printWelcome("0.1.0", provider.displayName, model);
console.log(
Theme.textMuted +
" Session: " +
Style.RESET +
chalk.gray(session.id.slice(0, 16) + "..."),
);
console.log("");
console.log(
Theme.textMuted +
" Commands: " +
Style.RESET +
chalk.cyan("@file") +
" " +
chalk.cyan("/help") +
" " +
chalk.cyan("/clear") +
" " +
chalk.cyan("/exit"),
);
console.log(
Theme.textMuted +
" Input: " +
Style.RESET +
chalk.cyan("Enter") +
" to send, " +
chalk.cyan("Alt+Enter") +
" for newline",
);
console.log("");
console.log(" " + formatTipLine());
console.log("");
state.inputEditor = createInputEditor({
prompt: "\x1b[36m> \x1b[0m",
continuationPrompt: "\x1b[90m│ \x1b[0m",
});
state.isRunning = true;
const cleanup = createCleanup(state);
const commandHandler = async (command: string, st: ChatState) => {
await handleCommand(command, st, cleanup);
};
state.inputEditor.on("submit", async (input: string) => {
if (state.isProcessing) return;
state.isProcessing = true;
state.inputEditor?.lock();
try {
await handleInput(input, state, commandHandler);
} catch (error) {
errorMessage(`Error: ${error}`);
}
state.isProcessing = false;
if (state.isRunning && state.inputEditor) {
state.inputEditor.unlock();
}
});
state.inputEditor.on("interrupt", () => {
if (state.isProcessing) {
console.log("\n" + chalk.yellow("Interrupted"));
state.isProcessing = false;
state.inputEditor?.unlock();
} else {
cleanup();
}
});
state.inputEditor.on("close", () => {
cleanup();
});
state.inputEditor.start();
if (hasInitialPrompt) {
state.isProcessing = true;
state.inputEditor.lock();
console.log(chalk.cyan("> ") + options.initialPrompt);
try {
await handleInput(options.initialPrompt!, state, commandHandler);
} catch (error) {
errorMessage(`Error: ${error}`);
}
state.isProcessing = false;
if (state.isRunning && state.inputEditor) {
state.inputEditor.unlock();
}
}
};
export { createInitialState, type ChatState } from "./state.ts";

View File

@@ -0,0 +1,143 @@
/**
* Handle MCP commands in chat
*/
import chalk from "chalk";
import {
initializeMCP,
connectServer,
disconnectServer,
connectAllServers,
disconnectAllServers,
getAllTools,
} from "@services/mcp/index";
import { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
import { appStore } from "@tui-solid/context/app";
/**
* Handle MCP subcommands
*/
export const handleMCP = async (args: string[]): Promise<void> => {
const subcommand = args[0] || "status";
const handlers: Record<string, (args: string[]) => Promise<void>> = {
status: handleStatus,
connect: handleConnect,
disconnect: handleDisconnect,
tools: handleTools,
add: handleAdd,
};
const handler = handlers[subcommand];
if (!handler) {
console.log(chalk.yellow(`Unknown MCP command: ${subcommand}`));
console.log(
chalk.gray("Available: status, connect, disconnect, tools, add"),
);
return;
}
await handler(args.slice(1));
};
/**
* Show MCP status
*/
const handleStatus = async (_args: string[]): Promise<void> => {
await showMCPStatus();
};
/**
* Connect to MCP servers
*/
const handleConnect = async (args: string[]): Promise<void> => {
await initializeMCP();
const name = args[0];
if (name) {
try {
console.log(chalk.gray(`Connecting to ${name}...`));
const instance = await connectServer(name);
console.log(chalk.green(`✓ Connected to ${name}`));
console.log(chalk.gray(` Tools: ${instance.tools.length}`));
} catch (err) {
console.log(chalk.red(`✗ Failed to connect: ${err}`));
}
} else {
console.log(chalk.gray("Connecting to all servers..."));
const results = await connectAllServers();
for (const [serverName, instance] of results) {
if (instance.state === "connected") {
console.log(
chalk.green(`${serverName}: ${instance.tools.length} tools`),
);
} else {
console.log(
chalk.red(`${serverName}: ${instance.error || "Failed"}`),
);
}
}
}
console.log();
};
/**
* Disconnect from MCP servers
*/
const handleDisconnect = async (args: string[]): Promise<void> => {
const name = args[0];
if (name) {
await disconnectServer(name);
console.log(chalk.green(`✓ Disconnected from ${name}`));
} else {
await disconnectAllServers();
console.log(chalk.green("✓ Disconnected from all servers"));
}
console.log();
};
/**
* List available MCP tools
*/
const handleTools = async (_args: string[]): Promise<void> => {
await connectAllServers();
const tools = getAllTools();
if (tools.length === 0) {
console.log(chalk.yellow("\nNo tools available."));
console.log(chalk.gray("Connect to MCP servers first with /mcp connect"));
console.log();
return;
}
console.log(chalk.bold("\nMCP Tools\n"));
// Group by server
const byServer = new Map<string, typeof tools>();
for (const item of tools) {
const existing = byServer.get(item.server) || [];
existing.push(item);
byServer.set(item.server, existing);
}
for (const [server, serverTools] of byServer) {
console.log(chalk.cyan(`${server}:`));
for (const { tool } of serverTools) {
console.log(` ${chalk.white(tool.name)}`);
if (tool.description) {
console.log(` ${chalk.gray(tool.description)}`);
}
}
console.log();
}
};
/**
* Open the MCP add form
*/
const handleAdd = async (_args: string[]): Promise<void> => {
appStore.setMode("mcp_add");
};

View File

@@ -0,0 +1,6 @@
/**
* MCP chat commands
*/
export { showMCPStatus } from "@commands/components/chat/mcp/show-mcp-status";
export { handleMCP } from "@commands/components/chat/mcp/handle-mcp";

View File

@@ -0,0 +1,76 @@
/**
* Show MCP server status in chat
*/
import chalk from "chalk";
import {
initializeMCP,
getServerInstances,
getAllTools,
isMCPAvailable,
} from "@services/mcp/index";
/**
* Display MCP server status
*/
export const showMCPStatus = async (): Promise<void> => {
await initializeMCP();
const hasServers = await isMCPAvailable();
if (!hasServers) {
console.log(chalk.yellow("\nNo MCP servers configured."));
console.log(chalk.gray("Add a server with: codetyper mcp add <name>"));
console.log();
return;
}
const instances = getServerInstances();
const tools = getAllTools();
console.log(chalk.bold("\nMCP Status\n"));
// Server status
console.log(chalk.cyan("Servers:"));
for (const [name, instance] of instances) {
const stateColors: Record<string, (s: string) => string> = {
connected: chalk.green,
connecting: chalk.yellow,
disconnected: chalk.gray,
error: chalk.red,
};
const colorFn = stateColors[instance.state] || chalk.white;
const status = colorFn(instance.state);
const toolCount =
instance.state === "connected" ? ` (${instance.tools.length} tools)` : "";
console.log(` ${chalk.white(name)}: ${status}${chalk.gray(toolCount)}`);
if (instance.error) {
console.log(` ${chalk.red(instance.error)}`);
}
}
// Tool summary
if (tools.length > 0) {
console.log();
console.log(chalk.cyan(`Available Tools: ${chalk.white(tools.length)}`));
// Group by server
const byServer = new Map<string, string[]>();
for (const { server, tool } of tools) {
const existing = byServer.get(server) || [];
existing.push(tool.name);
byServer.set(server, existing);
}
for (const [server, toolNames] of byServer) {
console.log(` ${chalk.gray(server)}: ${toolNames.join(", ")}`);
}
}
console.log();
console.log(chalk.gray("Use /mcp connect to connect servers"));
console.log(chalk.gray("Use /mcp tools for detailed tool info"));
console.log();
};

View File

@@ -0,0 +1,17 @@
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
import { sendMessage } from "@commands/components/chat/messages/send-message";
import type { ChatState } from "@commands/components/chat/state";
export const handleInput = async (
input: string,
state: ChatState,
handleCommand: (command: string, state: ChatState) => Promise<void>,
): Promise<void> => {
if (input.startsWith("/")) {
await handleCommand(input, state);
return;
}
const processedInput = await processFileReferences(input, state.contextFiles);
await sendMessage(processedInput, state);
};

View File

@@ -0,0 +1,228 @@
import chalk from "chalk";
import { basename, extname } from "path";
import { addMessage } from "@services/session";
import { initializePermissions } from "@services/permissions";
import { createAgent } from "@services/agent";
import { infoMessage, errorMessage, warningMessage } from "@utils/terminal";
import { getThinkingMessage } from "@constants/status-messages";
import {
detectDebuggingRequest,
buildDebuggingContext,
getDebuggingPrompt,
} from "@services/debugging-service";
import {
detectCodeReviewRequest,
buildCodeReviewContext,
getCodeReviewPrompt,
} from "@services/code-review-service";
import {
detectRefactoringRequest,
buildRefactoringContext,
getRefactoringPrompt,
} from "@services/refactoring-service";
import {
detectMemoryCommand,
processMemoryCommand,
buildRelevantMemoryPrompt,
} from "@services/memory-service";
import type { ChatState } from "@commands/components/chat/state";
export const sendMessage = async (
content: string,
state: ChatState,
): Promise<void> => {
let userMessage = content;
if (state.contextFiles.size > 0) {
const contextParts: string[] = [];
for (const [path, fileContent] of state.contextFiles) {
const ext = extname(path).slice(1) || "txt";
contextParts.push(
`File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``,
);
}
userMessage = contextParts.join("\n\n") + "\n\n" + content;
state.contextFiles.clear();
}
// Detect debugging requests and enhance message with context
const debugContext = detectDebuggingRequest(userMessage);
if (debugContext.isDebugging) {
const debugPrompt = getDebuggingPrompt();
const contextInfo = buildDebuggingContext(debugContext);
// Inject debugging system message before user message if not already present
const hasDebuggingPrompt = state.messages.some(
(msg) => msg.role === "system" && msg.content.includes("debugging mode"),
);
if (!hasDebuggingPrompt) {
state.messages.push({ role: "system", content: debugPrompt });
}
// Append debug context to user message if extracted
if (contextInfo) {
userMessage = userMessage + "\n\n" + contextInfo;
}
if (state.verbose) {
infoMessage(`Debugging mode activated: ${debugContext.debugType}`);
}
}
// Detect code review requests and enhance message with context
const reviewContext = detectCodeReviewRequest(userMessage);
if (reviewContext.isReview) {
const reviewPrompt = getCodeReviewPrompt();
const contextInfo = buildCodeReviewContext(reviewContext);
// Inject code review system message before user message if not already present
const hasReviewPrompt = state.messages.some(
(msg) =>
msg.role === "system" && msg.content.includes("code review mode"),
);
if (!hasReviewPrompt) {
state.messages.push({ role: "system", content: reviewPrompt });
}
// Append review context to user message if extracted
if (contextInfo) {
userMessage = userMessage + "\n\n" + contextInfo;
}
if (state.verbose) {
infoMessage(`Code review mode activated: ${reviewContext.reviewType}`);
}
}
// Detect refactoring requests and enhance message with context
const refactorContext = detectRefactoringRequest(userMessage);
if (refactorContext.isRefactoring) {
const refactorPrompt = getRefactoringPrompt();
const contextInfo = buildRefactoringContext(refactorContext);
// Inject refactoring system message before user message if not already present
const hasRefactoringPrompt = state.messages.some(
(msg) =>
msg.role === "system" && msg.content.includes("refactoring mode"),
);
if (!hasRefactoringPrompt) {
state.messages.push({ role: "system", content: refactorPrompt });
}
// Append refactoring context to user message if extracted
if (contextInfo) {
userMessage = userMessage + "\n\n" + contextInfo;
}
if (state.verbose) {
infoMessage(
`Refactoring mode activated: ${refactorContext.refactoringType}`,
);
}
}
// Detect memory commands
const memoryContext = detectMemoryCommand(userMessage);
if (memoryContext.isMemoryCommand) {
const result = await processMemoryCommand(memoryContext);
console.log(chalk.cyan("\n[Memory System]"));
console.log(
result.success
? chalk.green(result.message)
: chalk.yellow(result.message),
);
// For store/forget commands, still send to agent for confirmation response
if (
memoryContext.commandType === "list" ||
memoryContext.commandType === "query"
) {
// Just display results, don't send to agent
return;
}
if (state.verbose) {
infoMessage(`Memory command: ${memoryContext.commandType}`);
}
}
// Auto-retrieve relevant memories for context
const relevantMemoryPrompt = await buildRelevantMemoryPrompt(userMessage);
if (relevantMemoryPrompt) {
userMessage = userMessage + "\n\n" + relevantMemoryPrompt;
if (state.verbose) {
infoMessage("Relevant memories retrieved");
}
}
state.messages.push({ role: "user", content: userMessage });
await addMessage("user", content);
await initializePermissions();
const agent = createAgent(process.cwd(), {
provider: state.currentProvider,
model: state.currentModel,
verbose: state.verbose,
autoApprove: state.autoApprove,
onToolCall: (call) => {
console.log(chalk.cyan(`\n[Tool: ${call.name}]`));
if (state.verbose) {
console.log(chalk.gray(JSON.stringify(call.arguments, null, 2)));
}
},
onToolResult: (_callId, result) => {
if (result.success) {
console.log(chalk.green(`${result.title}`));
} else {
console.log(chalk.red(`${result.title}: ${result.error}`));
}
},
onText: (_text) => {},
});
process.stdout.write(chalk.gray(getThinkingMessage() + "\n"));
try {
const result = await agent.run(state.messages);
if (result.finalResponse) {
state.messages.push({
role: "assistant",
content: result.finalResponse,
});
await addMessage("assistant", result.finalResponse);
console.log(chalk.bold("\nAssistant:"));
console.log(result.finalResponse);
console.log();
if (result.toolCalls.length > 0) {
const successful = result.toolCalls.filter(
(tc) => tc.result.success,
).length;
infoMessage(
chalk.gray(
`Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)`,
),
);
}
} else if (result.toolCalls.length > 0) {
const successful = result.toolCalls.filter(
(tc) => tc.result.success,
).length;
infoMessage(
chalk.gray(
`Completed: ${successful}/${result.toolCalls.length} tools successful`,
),
);
} else {
warningMessage("No response received");
}
} catch (error) {
errorMessage(`Failed: ${error}`);
}
};

View File

@@ -0,0 +1,31 @@
import chalk from "chalk";
import type { Provider as ProviderName } from "@/types/index";
import { getProvider } from "@providers/index.ts";
export const showModels = async (
currentProvider: ProviderName,
currentModel: string | undefined,
): Promise<void> => {
const provider = getProvider(currentProvider);
const models = await provider.getModels();
const activeModel = currentModel || "auto";
const isAutoSelected = activeModel === "auto";
console.log(`\n${chalk.bold(provider.displayName + " Models")}\n`);
// Show "auto" option first
const autoMarker = isAutoSelected ? chalk.cyan("→") : " ";
console.log(
`${autoMarker} ${chalk.cyan("auto")} - Let API choose the best model`,
);
for (const model of models) {
const isCurrent = model.id === activeModel;
const marker = isCurrent ? chalk.cyan("→") : " ";
console.log(`${marker} ${chalk.cyan(model.id)} - ${model.name}`);
}
console.log("\n" + chalk.gray("Use /model <name> to switch"));
console.log();
};

View File

@@ -0,0 +1,7 @@
import { getConfig } from "@services/config";
import { displayProvidersStatus } from "@providers/index.ts";
export const showProviders = async (): Promise<void> => {
const config = await getConfig();
await displayProvidersStatus(config.get("provider"));
};

View File

@@ -0,0 +1,50 @@
import {
infoMessage,
warningMessage,
successMessage,
errorMessage,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import { getProvider } from "@providers/index.ts";
import { showModels } from "./show-models.ts";
import type { ChatState } from "../state.ts";
export const switchModel = async (
modelName: string,
state: ChatState,
): Promise<void> => {
if (!modelName) {
warningMessage("Please specify a model name");
await showModels(state.currentProvider, state.currentModel);
return;
}
// Handle "auto" as a special case
if (modelName.toLowerCase() === "auto") {
state.currentModel = "auto";
successMessage("Switched to model: auto (API will choose)");
const config = await getConfig();
config.set("model", "auto");
await config.save();
return;
}
const provider = getProvider(state.currentProvider);
const models = await provider.getModels();
const model = models.find((m) => m.id === modelName || m.name === modelName);
if (!model) {
errorMessage(`Model not found: ${modelName}`);
infoMessage("Use /models to see available models, or use 'auto'");
return;
}
state.currentModel = model.id;
successMessage(`Switched to model: ${model.name}`);
// Persist model selection to config
const config = await getConfig();
config.set("model", model.id);
await config.save();
};

View File

@@ -0,0 +1,51 @@
import type { Provider as ProviderName } from "@/types/index";
import {
errorMessage,
warningMessage,
infoMessage,
successMessage,
} from "@utils/terminal";
import { getConfig } from "@services/config";
import {
getProvider,
getProviderStatus,
getDefaultModel,
} from "@providers/index.ts";
import type { ChatState } from "../state.ts";
export const switchProvider = async (
providerName: string,
state: ChatState,
): Promise<void> => {
if (!providerName) {
warningMessage("Please specify a provider: copilot, or ollama");
return;
}
const validProviders = ["copilot", "ollama"];
if (!validProviders.includes(providerName)) {
errorMessage(`Invalid provider: ${providerName}`);
infoMessage("Valid providers: " + validProviders.join(", "));
return;
}
const status = await getProviderStatus(providerName as ProviderName);
if (!status.valid) {
errorMessage(`Provider ${providerName} is not configured`);
infoMessage(`Run: codetyper login ${providerName}`);
return;
}
state.currentProvider = providerName as ProviderName;
state.currentModel = undefined;
const config = await getConfig();
config.set("provider", providerName as ProviderName);
await config.save();
const provider = getProvider(state.currentProvider);
const model = getDefaultModel(state.currentProvider);
successMessage(`Switched to ${provider.displayName}`);
infoMessage(`Using model: ${model}`);
};

View File

@@ -0,0 +1,71 @@
import chalk from "chalk";
import { basename, extname } from "path";
import { initializePermissions } from "@services/permissions";
import { createAgent } from "@services/agent";
import type { ChatState } from "@commands/components/chat/state";
import { processFileReferences } from "@commands/components/chat/context/process-file-references";
export const executePrintMode = async (
prompt: string,
state: ChatState,
): Promise<void> => {
const processedPrompt = await processFileReferences(
prompt,
state.contextFiles,
);
let userMessage = processedPrompt;
if (state.contextFiles.size > 0) {
const contextParts: string[] = [];
for (const [path, fileContent] of state.contextFiles) {
const ext = extname(path).slice(1) || "txt";
contextParts.push(
`File: ${basename(path)}\n\`\`\`${ext}\n${fileContent}\n\`\`\``,
);
}
userMessage = contextParts.join("\n\n") + "\n\n" + processedPrompt;
}
state.messages.push({ role: "user", content: userMessage });
await initializePermissions();
const agent = createAgent(process.cwd(), {
provider: state.currentProvider,
model: state.currentModel,
verbose: state.verbose,
autoApprove: state.autoApprove,
onToolCall: (call) => {
console.error(chalk.cyan(`[Tool: ${call.name}]`));
},
onToolResult: (_callId, result) => {
if (result.success) {
console.error(chalk.green(`${result.title}`));
} else {
console.error(chalk.red(`${result.title}: ${result.error}`));
}
},
});
try {
const result = await agent.run(state.messages);
if (result.finalResponse) {
console.log(result.finalResponse);
}
if (state.verbose && result.toolCalls.length > 0) {
const successful = result.toolCalls.filter(
(tc) => tc.result.success,
).length;
console.error(
chalk.gray(
`[Tools: ${successful}/${result.toolCalls.length} successful, ${result.iterations} iteration(s)]`,
),
);
}
} catch (error) {
console.error(chalk.red(`Error: ${error}`));
process.exit(1);
}
};

View File

@@ -0,0 +1,38 @@
import chalk from "chalk";
import { getSessionSummaries } from "@services/session";
import { infoMessage } from "@utils/terminal";
export const listSessions = async (): Promise<void> => {
const summaries = await getSessionSummaries();
if (summaries.length === 0) {
infoMessage("No saved sessions");
return;
}
console.log("\n" + chalk.bold("Saved Sessions:") + "\n");
for (const session of summaries.slice(0, 10)) {
const date = new Date(session.updatedAt).toLocaleDateString();
const time = new Date(session.updatedAt).toLocaleTimeString();
const preview = session.lastMessage
? session.lastMessage.slice(0, 50).replace(/\n/g, " ")
: "(no messages)";
console.log(` ${chalk.cyan(session.id.slice(0, 20))}...`);
console.log(
` ${chalk.gray(`${date} ${time}`)} - ${session.messageCount} messages`,
);
console.log(
` ${chalk.gray(preview)}${preview.length >= 50 ? "..." : ""}`,
);
console.log();
}
if (summaries.length > 10) {
infoMessage(`... and ${summaries.length - 10} more sessions`);
}
console.log(chalk.gray("Resume with: codetyper -r <session-id>"));
console.log();
};

View File

@@ -0,0 +1,20 @@
import type { ChatSession } from "@/types/index";
import type { Message } from "@providers/index.ts";
export const restoreMessagesFromSession = (
session: ChatSession,
systemPrompt: string,
): Message[] => {
const messages: Message[] = [{ role: "system", content: systemPrompt }];
for (const msg of session.messages) {
if (msg.role !== "system") {
messages.push({
role: msg.role as "user" | "assistant",
content: msg.content,
});
}
}
return messages;
};

View File

@@ -0,0 +1,20 @@
import chalk from "chalk";
import { getCurrentSession } from "@services/session";
import { warningMessage } from "@utils/terminal";
export const showSessionInfo = async (): Promise<void> => {
const session = getCurrentSession();
if (!session) {
warningMessage("No active session");
return;
}
console.log("\n" + chalk.bold("Session Information:"));
console.log(` ID: ${chalk.cyan(session.id)}`);
console.log(` Agent: ${session.agent}`);
console.log(` Messages: ${session.messages.length}`);
console.log(` Context files: ${session.contextFiles.length}`);
console.log(` Created: ${new Date(session.createdAt).toLocaleString()}`);
console.log(` Updated: ${new Date(session.updatedAt).toLocaleString()}`);
console.log();
};

View File

@@ -0,0 +1,34 @@
import type { Provider as ProviderName } from "@/types/index";
import type { Message } from "@providers/index";
import type { InputEditorInstance } from "@ui/index";
import { DEFAULT_SYSTEM_PROMPT } from "@prompts/index";
export interface ChatState {
inputEditor: InputEditorInstance | null;
isRunning: boolean;
isProcessing: boolean;
currentProvider: ProviderName;
currentModel: string | undefined;
currentAgent: string | undefined;
messages: Message[];
contextFiles: Map<string, string>;
systemPrompt: string;
verbose: boolean;
autoApprove: boolean;
}
export const createInitialState = (
provider: ProviderName = "copilot",
): ChatState => ({
inputEditor: null,
isRunning: false,
isProcessing: false,
currentProvider: provider,
currentModel: undefined,
currentAgent: "coder",
messages: [],
contextFiles: new Map(),
systemPrompt: DEFAULT_SYSTEM_PROMPT,
verbose: false,
autoApprove: false,
});

View File

@@ -0,0 +1,129 @@
/**
* Show usage statistics command
*/
import chalk from "chalk";
import { usageStore } from "@stores/usage-store";
import { getUserInfo } from "@providers/copilot/credentials";
import { getCopilotUsage } from "@providers/copilot/usage";
import { getProvider } from "@providers/index";
import { renderUsageBar, renderUnlimitedBar } from "@utils/progress-bar";
import type { ChatState } from "@commands/components/chat/state";
import type { CopilotQuotaDetail } from "@/types/copilot-usage";
const formatNumber = (num: number): string => {
return num.toLocaleString();
};
const formatDuration = (ms: number): string => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
};
const printQuotaBar = (
name: string,
quota: CopilotQuotaDetail | undefined,
resetInfo?: string,
): void => {
if (!quota) {
console.log(chalk.bold(name));
console.log(chalk.gray("N/A"));
console.log();
return;
}
if (quota.unlimited) {
renderUnlimitedBar(name).forEach((line) => console.log(line));
console.log();
return;
}
const used = quota.entitlement - quota.remaining;
renderUsageBar(name, used, quota.entitlement, resetInfo).forEach((line) =>
console.log(line),
);
console.log();
};
export const showUsage = async (state: ChatState): Promise<void> => {
const stats = usageStore.getStats();
const provider = getProvider(state.currentProvider);
const sessionDuration = Date.now() - stats.sessionStartTime;
console.log();
// User info and quota for Copilot
if (state.currentProvider === "copilot") {
const userInfo = await getUserInfo();
const copilotUsage = await getCopilotUsage();
if (copilotUsage) {
const resetDate = copilotUsage.quota_reset_date;
printQuotaBar(
"Premium Requests",
copilotUsage.quota_snapshots.premium_interactions,
`Resets ${resetDate}`,
);
printQuotaBar(
"Chat",
copilotUsage.quota_snapshots.chat,
`Resets ${resetDate}`,
);
printQuotaBar(
"Completions",
copilotUsage.quota_snapshots.completions,
`Resets ${resetDate}`,
);
}
console.log(chalk.bold("Account"));
console.log(`${chalk.gray("Provider:")} ${provider.displayName}`);
console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`);
if (userInfo) {
console.log(`${chalk.gray("User:")} ${userInfo.login}`);
}
if (copilotUsage) {
console.log(`${chalk.gray("Plan:")} ${copilotUsage.copilot_plan}`);
}
console.log();
} else {
console.log(chalk.bold("Provider"));
console.log(`${chalk.gray("Name:")} ${provider.displayName}`);
console.log(`${chalk.gray("Model:")} ${state.currentModel ?? "auto"}`);
console.log();
}
// Session stats with bar
console.log(chalk.bold("Current Session"));
renderUsageBar(
"Tokens",
stats.totalTokens,
stats.totalTokens || 1,
`${formatNumber(stats.promptTokens)} prompt + ${formatNumber(stats.completionTokens)} completion`,
)
.slice(1)
.forEach((line) => console.log(line));
console.log(`${chalk.gray("Requests:")} ${formatNumber(stats.requestCount)}`);
console.log(`${chalk.gray("Duration:")} ${formatDuration(sessionDuration)}`);
if (stats.requestCount > 0) {
const avgTokensPerRequest = Math.round(
stats.totalTokens / stats.requestCount,
);
console.log(
`${chalk.gray("Avg tokens/request:")} ${formatNumber(avgTokensPerRequest)}`,
);
}
console.log();
};

View File

@@ -0,0 +1,25 @@
/**
* Dashboard Config Builder
*/
import os from "os";
import { getConfig } from "@services/config";
import { DASHBOARD_TITLE } from "@constants/dashboard";
import type { DashboardConfig } from "@/types/dashboard";
export const buildDashboardConfig = async (
version: string,
): Promise<DashboardConfig> => {
const configMgr = await getConfig();
const username = os.userInfo().username;
const cwd = process.cwd();
const provider = configMgr.get("provider") as string;
return {
title: DASHBOARD_TITLE,
version,
user: username,
cwd,
provider,
};
};

View File

@@ -0,0 +1,33 @@
/**
* Dashboard Display
*
* Renders and displays the main dashboard UI.
*/
import { DASHBOARD_LAYOUT } from "@constants/dashboard";
import { buildDashboardConfig } from "@commands/components/dashboard/build-config";
import { renderHeader } from "@commands/components/dashboard/render-header";
import { renderContent } from "@commands/components/dashboard/render-content";
import { renderFooter } from "@commands/components/dashboard/render-footer";
const getTerminalWidth = (): number => {
return process.stdout.columns || DASHBOARD_LAYOUT.DEFAULT_WIDTH;
};
const renderDashboard = async (version: string): Promise<string> => {
const config = await buildDashboardConfig(version);
const width = getTerminalWidth();
const header = renderHeader(config, width);
const content = renderContent(config, width, DASHBOARD_LAYOUT.CONTENT_HEIGHT);
const footer = renderFooter(width);
return [header, content, footer].join("\n");
};
export const displayDashboard = async (version: string): Promise<void> => {
console.clear();
const dashboard = await renderDashboard(version);
console.log(dashboard);
process.exit(0);
};

View File

@@ -0,0 +1,41 @@
/**
* Dashboard Content Renderer
*/
import { DASHBOARD_LAYOUT, DASHBOARD_BORDER } from "@constants/dashboard";
import { renderLeftContent } from "@commands/components/dashboard/render-left-content";
import { renderRightContent } from "@commands/components/dashboard/render-right-content";
import type { DashboardConfig } from "@/types/dashboard";
const padContent = (content: string[], height: number): string[] => {
const padded = [...content];
while (padded.length < height) {
padded.push("");
}
return padded;
};
export const renderContent = (
config: DashboardConfig,
width: number,
height: number,
): string => {
const dividerPos = Math.floor(width * DASHBOARD_LAYOUT.LEFT_COLUMN_RATIO);
const leftWidth = dividerPos - DASHBOARD_LAYOUT.PADDING;
const rightWidth = width - dividerPos - DASHBOARD_LAYOUT.PADDING;
const leftContent = padContent(renderLeftContent(config), height);
const rightContent = padContent(renderRightContent(), height);
const lines: string[] = [];
for (let i = 0; i < height; i++) {
const left = (leftContent[i] || "").padEnd(leftWidth);
const right = (rightContent[i] || "").padEnd(rightWidth);
lines.push(
`${DASHBOARD_BORDER.VERTICAL} ${left} ${DASHBOARD_BORDER.VERTICAL} ${right} ${DASHBOARD_BORDER.VERTICAL}`,
);
}
return lines.join("\n");
};

View File

@@ -0,0 +1,31 @@
/**
* Dashboard Footer Renderer
*/
import chalk from "chalk";
import {
DASHBOARD_BORDER,
DASHBOARD_QUICK_COMMANDS,
} from "@constants/dashboard";
export const renderFooter = (width: number): string => {
const dashCount = Math.max(0, width - 2);
const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount);
const borderLine = `${DASHBOARD_BORDER.BOTTOM_LEFT}${dashes}${DASHBOARD_BORDER.BOTTOM_RIGHT}`;
const commandLines = DASHBOARD_QUICK_COMMANDS.map(
({ command, description }) =>
` ${chalk.cyan(command.padEnd(18))} ${description}`,
);
const lines = [
borderLine,
"",
chalk.dim("Quick Commands:"),
...commandLines,
"",
chalk.dim("Press Ctrl+C to exit • Type 'codetyper chat' to begin"),
];
return lines.join("\n");
};

View File

@@ -0,0 +1,18 @@
/**
* Dashboard Header Renderer
*/
import chalk from "chalk";
import { DASHBOARD_BORDER } from "@constants/dashboard";
import type { DashboardConfig } from "@/types/dashboard";
export const renderHeader = (
config: DashboardConfig,
width: number,
): string => {
const title = ` ${config.title} ${config.version} `;
const dashCount = Math.max(0, width - title.length - 2);
const dashes = DASHBOARD_BORDER.HORIZONTAL.repeat(dashCount);
return `${DASHBOARD_BORDER.TOP_LEFT}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL}${DASHBOARD_BORDER.HORIZONTAL} ${chalk.cyan.bold(title)}${dashes}${DASHBOARD_BORDER.TOP_RIGHT}`;
};

View File

@@ -0,0 +1,24 @@
/**
* Dashboard Left Content Renderer
*/
import chalk from "chalk";
import { DASHBOARD_LOGO } from "@constants/dashboard";
import type { DashboardConfig } from "@/types/dashboard";
export const renderLeftContent = (config: DashboardConfig): string[] => {
const lines: string[] = [];
lines.push("");
lines.push(chalk.green(`Welcome back ${config.user}!`));
lines.push("");
const coloredLogo = DASHBOARD_LOGO.map((line) => chalk.cyan.bold(line));
lines.push(...coloredLogo);
lines.push("");
lines.push(chalk.cyan.bold(`${config.provider.toUpperCase()}`));
lines.push(chalk.dim(`${config.user}@codetyper`));
return lines;
};

View File

@@ -0,0 +1,21 @@
/**
* Dashboard Right Content Renderer
*/
import chalk from "chalk";
import { DASHBOARD_COMMANDS } from "@constants/dashboard";
export const renderRightContent = (): string[] => {
const lines: string[] = [];
lines.push(chalk.bold("Ready to code"));
lines.push("");
for (const { command, description } of DASHBOARD_COMMANDS) {
lines.push(chalk.cyan(command));
lines.push(` ${description}`);
lines.push("");
}
return lines;
};

View File

@@ -0,0 +1,55 @@
import { tui } from "@tui-solid/index";
import { getProviderInfo } from "@services/chat-tui-service";
import type { ChatServiceState } from "@services/chat-tui-service";
import type { AgentConfig } from "@/types/agent-config";
import type { PermissionScope, LearningScope } from "@/types/tui";
export interface RenderAppSolidProps {
sessionId: string;
handleSubmit: (message: string) => Promise<void>;
handleCommand: (command: string) => Promise<void>;
handleModelSelect: (model: string) => Promise<void>;
handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise<void>;
handleThemeSelect: (theme: string) => void;
handlePermissionResponse?: (
allowed: boolean,
scope?: PermissionScope,
) => void;
handleLearningResponse?: (
save: boolean,
scope?: LearningScope,
editedContent?: string,
) => void;
handleExit: () => void;
showBanner: boolean;
state: ChatServiceState;
plan?: {
id: string;
title: string;
items: Array<{ id: string; text: string; completed: boolean }>;
} | null;
}
export const renderAppSolid = async (
props: RenderAppSolidProps,
): Promise<void> => {
const { displayName, model: defaultModel } = getProviderInfo(
props.state.provider,
);
const currentModel = props.state.model ?? defaultModel;
await tui({
sessionId: props.sessionId,
provider: displayName,
model: currentModel,
onSubmit: props.handleSubmit,
onCommand: props.handleCommand,
onModelSelect: props.handleModelSelect,
onThemeSelect: props.handleThemeSelect,
onPermissionResponse: props.handlePermissionResponse ?? (() => {}),
onLearningResponse: props.handleLearningResponse ?? (() => {}),
plan: props.plan,
});
props.handleExit();
};

View File

@@ -0,0 +1,104 @@
import { tui, appStore } from "@tui/index";
import { getProviderInfo } from "@services/chat-tui-service";
import { addServer, connectServer } from "@services/mcp/index";
import type { ChatServiceState } from "@services/chat-tui-service";
import type { AgentConfig } from "@/types/agent-config";
import type { PermissionScope, LearningScope } from "@/types/tui";
import type { ProviderModel } from "@/types/providers";
import type { MCPAddFormData } from "@/types/mcp";
interface AgentOption {
id: string;
name: string;
description?: string;
}
export interface RenderAppProps {
sessionId?: string;
handleSubmit: (message: string) => Promise<void>;
handleCommand: (command: string) => Promise<void>;
handleModelSelect: (model: string) => Promise<void>;
handleAgentSelect: (agentId: string, agent: AgentConfig) => Promise<void>;
handleThemeSelect: (theme: string) => void;
handleProviderSelect?: (providerId: string) => Promise<void>;
handleCascadeToggle?: (enabled: boolean) => Promise<void>;
handleMCPAdd?: (data: MCPAddFormData) => Promise<void>;
handlePermissionResponse?: (
allowed: boolean,
scope?: PermissionScope,
) => void;
handleLearningResponse?: (
save: boolean,
scope?: LearningScope,
editedContent?: string,
) => void;
handleExit: () => void;
showBanner: boolean;
state: ChatServiceState;
availableModels?: ProviderModel[];
agents?: AgentOption[];
initialPrompt?: string;
theme?: string;
cascadeEnabled?: boolean;
plan?: {
id: string;
title: string;
items: Array<{ id: string; text: string; completed: boolean }>;
} | null;
}
const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise<void> => {
const serverArgs = data.args.trim()
? data.args.trim().split(/\s+/)
: undefined;
await addServer(
data.name,
{
command: data.command,
args: serverArgs,
enabled: true,
},
data.isGlobal,
);
await connectServer(data.name);
};
export const renderApp = async (props: RenderAppProps): Promise<void> => {
const { displayName, model: defaultModel } = getProviderInfo(
props.state.provider,
);
const currentModel = props.state.model ?? defaultModel;
await tui({
sessionId: props.sessionId,
provider: displayName,
model: currentModel,
theme: props.theme,
cascadeEnabled: props.cascadeEnabled,
availableModels: props.availableModels,
agents: props.agents,
initialPrompt: props.initialPrompt,
onSubmit: props.handleSubmit,
onCommand: props.handleCommand,
onModelSelect: props.handleModelSelect,
onThemeSelect: props.handleThemeSelect,
onProviderSelect: props.handleProviderSelect,
onCascadeToggle: props.handleCascadeToggle,
onAgentSelect: async (agentId: string) => {
const agent = props.agents?.find((a) => a.id === agentId);
if (agent) {
await props.handleAgentSelect(agentId, agent as AgentConfig);
}
},
onMCPAdd: props.handleMCPAdd ?? defaultHandleMCPAdd,
onPermissionResponse: props.handlePermissionResponse ?? (() => {}),
onLearningResponse: props.handleLearningResponse ?? (() => {}),
plan: props.plan,
});
props.handleExit();
};
export { appStore };

View File

@@ -0,0 +1,195 @@
import { renderApp, appStore } from "@commands/components/execute/execute";
import type { RenderAppProps } from "@commands/components/execute/execute";
import {
initializeChatService,
loadModels,
handleModelSelect as serviceHandleModelSelect,
executePrintMode,
setupPermissionHandler,
cleanupPermissionHandler,
executeCommand,
handleMessage,
} from "@services/chat-tui-service";
import type { ChatServiceState } from "@services/chat-tui-service";
import type { ChatTUIOptions } from "@interfaces/ChatTUIOptions";
import type { AgentConfig } from "@/types/agent-config";
import { getConfig } from "@services/config";
import { getThinkingMessage } from "@constants/status-messages";
import {
enterFullscreen,
registerExitHandlers,
exitFullscreen,
clearScreen,
} from "@utils/terminal";
import { createCallbacks } from "@commands/chat-tui";
import { agentLoader } from "@services/agent-loader";
interface ExecuteContext {
state: ChatServiceState | null;
}
const createHandleExit = (): (() => void) => (): void => {
cleanupPermissionHandler();
exitFullscreen();
clearScreen();
console.log("Goodbye!");
process.exit(0);
};
const createHandleModelSelect =
(ctx: ExecuteContext) =>
async (model: string): Promise<void> => {
if (!ctx.state) return;
await serviceHandleModelSelect(ctx.state, model, createCallbacks());
};
const createHandleAgentSelect =
(ctx: ExecuteContext) =>
async (agentId: string, agent: AgentConfig): Promise<void> => {
if (!ctx.state) return;
(ctx.state as ChatServiceState & { currentAgent?: string }).currentAgent =
agentId;
if (agent.prompt) {
const basePrompt = ctx.state.systemPrompt;
ctx.state.systemPrompt = `${agent.prompt}\n\n${basePrompt}`;
if (
ctx.state.messages.length > 0 &&
ctx.state.messages[0].role === "system"
) {
ctx.state.messages[0].content = ctx.state.systemPrompt;
}
}
};
const createHandleThemeSelect =
() =>
(themeName: string): void => {
getConfig().then((config) => {
config.set("theme", themeName);
config.save();
});
};
const createHandleProviderSelect =
(ctx: ExecuteContext) =>
async (providerId: string): Promise<void> => {
if (!ctx.state) return;
ctx.state.provider = providerId as "copilot" | "ollama";
const config = await getConfig();
config.set("provider", providerId as "copilot" | "ollama");
await config.save();
};
const createHandleCascadeToggle =
() =>
async (enabled: boolean): Promise<void> => {
const config = await getConfig();
config.set("cascadeEnabled", enabled);
await config.save();
};
const createHandleCommand =
(ctx: ExecuteContext, handleExit: () => void) =>
async (command: string): Promise<void> => {
if (!ctx.state) return;
if (["exit", "quit", "q"].includes(command.toLowerCase())) {
handleExit();
return;
}
await executeCommand(ctx.state, command, createCallbacks());
};
const createHandleSubmit =
(ctx: ExecuteContext, handleCommand: (command: string) => Promise<void>) =>
async (message: string): Promise<void> => {
if (!ctx.state) return;
if (message.startsWith("/")) {
const [command] = message.slice(1).split(/\s+/);
await handleCommand(command);
return;
}
// Set initial thinking message (streaming will update this)
appStore.setThinkingMessage(getThinkingMessage());
try {
await handleMessage(ctx.state, message, createCallbacks());
} finally {
// Clean up any remaining state after message handling
appStore.setThinkingMessage(null);
appStore.setCurrentToolCall(null);
appStore.setMode("idle");
}
};
const execute = async (options: ChatTUIOptions): Promise<void> => {
const ctx: ExecuteContext = {
state: null,
};
const { state, session } = await initializeChatService(options);
ctx.state = state;
if (options.printMode && options.initialPrompt) {
await executePrintMode(state, options.initialPrompt);
return;
}
setupPermissionHandler();
const models = await loadModels(state.provider);
const agents = await agentLoader.getAvailableAgents(process.cwd());
const config = await getConfig();
const savedTheme = config.get("theme");
// Register exit handlers to ensure terminal cleanup on abrupt termination
registerExitHandlers();
enterFullscreen();
const handleExit = createHandleExit();
const handleModelSelectFn = createHandleModelSelect(ctx);
const handleAgentSelectFn = createHandleAgentSelect(ctx);
const handleThemeSelectFn = createHandleThemeSelect();
const handleProviderSelectFn = createHandleProviderSelect(ctx);
const handleCascadeToggleFn = createHandleCascadeToggle();
const handleCommand = createHandleCommand(ctx, handleExit);
const handleSubmit = createHandleSubmit(ctx, handleCommand);
// Only pass sessionId if resuming/continuing - otherwise show Home view first
const isResuming = options.continueSession || options.resumeSession;
const savedCascadeEnabled = config.get("cascadeEnabled");
const renderProps: RenderAppProps = {
sessionId: isResuming ? session.id : undefined,
handleSubmit,
handleCommand,
handleModelSelect: handleModelSelectFn,
handleAgentSelect: handleAgentSelectFn,
handleThemeSelect: handleThemeSelectFn,
handleProviderSelect: handleProviderSelectFn,
handleCascadeToggle: handleCascadeToggleFn,
handleExit,
showBanner: true,
state,
availableModels: models,
agents: agents.map((a) => ({
id: a.id,
name: a.name,
description: a.description,
})),
initialPrompt: options.initialPrompt,
theme: savedTheme,
cascadeEnabled: savedCascadeEnabled ?? true,
};
await renderApp(renderProps);
};
export default execute;

View File

@@ -0,0 +1,8 @@
/**
* Dashboard Command
*
* Re-exports the modular dashboard implementation.
*/
export { displayDashboard } from "@commands/components/dashboard/display";
export type { DashboardConfig } from "@/types/dashboard";

25
src/commands/handlers.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Command handlers - Route commands to appropriate implementations
*/
import { errorMessage } from "@utils/terminal";
import { COMMAND_REGISTRY, isValidCommand } from "@commands/handlers/registry";
import type { CommandOptions } from "@/types/index";
export const handleCommand = async (
command: string,
options: CommandOptions,
): Promise<void> => {
try {
if (!isValidCommand(command)) {
errorMessage(`Unknown command: ${command}`);
process.exit(1);
}
const handler = COMMAND_REGISTRY[command];
await handler(options);
} catch (error) {
errorMessage(`Command failed: ${error}`);
throw error;
}
};

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;
}
};

319
src/commands/mcp.ts Normal file
View File

@@ -0,0 +1,319 @@
/**
* MCP Command - Manage MCP servers
*
* Usage:
* codetyper mcp list - List configured servers
* codetyper mcp add <name> - Add a new server
* codetyper mcp remove <name> - Remove a server
* codetyper mcp connect [name] - Connect to server(s)
* codetyper mcp disconnect [name] - Disconnect from server(s)
* codetyper mcp status - Show connection status
* codetyper mcp tools - List available tools
*/
import chalk from "chalk";
import { errorMessage, infoMessage, successMessage } from "@utils/terminal";
import {
initializeMCP,
getMCPConfig,
addServer,
removeServer,
connectServer,
disconnectServer,
connectAllServers,
disconnectAllServers,
getServerInstances,
getAllTools,
} from "@services/mcp/index";
/**
* MCP command handler
*/
export const mcpCommand = async (args: string[]): Promise<void> => {
const subcommand = args[0] || "status";
const handlers: Record<string, (args: string[]) => Promise<void>> = {
list: handleList,
add: handleAdd,
remove: handleRemove,
connect: handleConnect,
disconnect: handleDisconnect,
status: handleStatus,
tools: handleTools,
help: handleHelp,
};
const handler = handlers[subcommand];
if (!handler) {
errorMessage(`Unknown subcommand: ${subcommand}`);
await handleHelp([]);
return;
}
await handler(args.slice(1));
};
/**
* List configured servers
*/
const handleList = async (_args: string[]): Promise<void> => {
await initializeMCP();
const config = await getMCPConfig();
const servers = Object.entries(config.servers);
if (servers.length === 0) {
infoMessage("No MCP servers configured.");
infoMessage("Add a server with: codetyper mcp add <name>");
return;
}
console.log(chalk.bold("\nConfigured MCP Servers:\n"));
for (const [name, server] of servers) {
const enabled =
server.enabled !== false ? chalk.green("✓") : chalk.gray("○");
console.log(` ${enabled} ${chalk.cyan(name)}`);
console.log(
` Command: ${server.command} ${(server.args || []).join(" ")}`,
);
if (server.transport && server.transport !== "stdio") {
console.log(` Transport: ${server.transport}`);
}
console.log();
}
};
/**
* Add a new server
*/
const handleAdd = async (args: string[]): Promise<void> => {
const name = args[0];
if (!name) {
errorMessage("Server name required");
infoMessage(
"Usage: codetyper mcp add <name> --command <cmd> [--args <args>]",
);
return;
}
// Parse options
let command = "";
const serverArgs: string[] = [];
let isGlobal = false;
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg === "--command" || arg === "-c") {
command = args[++i] || "";
} else if (arg === "--args" || arg === "-a") {
// Collect remaining args
while (args[i + 1] && !args[i + 1].startsWith("--")) {
serverArgs.push(args[++i]);
}
} else if (arg === "--global" || arg === "-g") {
isGlobal = true;
}
}
if (!command) {
// Interactive mode - ask for command
infoMessage("Adding MCP server interactively...");
infoMessage("Example: npx @modelcontextprotocol/server-sqlite");
// For now, require command flag
errorMessage("Command required. Use --command <cmd>");
return;
}
try {
await addServer(
name,
{
command,
args: serverArgs.length > 0 ? serverArgs : undefined,
enabled: true,
},
isGlobal,
);
successMessage(`Added MCP server: ${name}`);
infoMessage(`Connect with: codetyper mcp connect ${name}`);
} catch (err) {
errorMessage(`Failed to add server: ${err}`);
}
};
/**
* Remove a server
*/
const handleRemove = async (args: string[]): Promise<void> => {
const name = args[0];
if (!name) {
errorMessage("Server name required");
return;
}
const isGlobal = args.includes("--global") || args.includes("-g");
try {
await removeServer(name, isGlobal);
successMessage(`Removed MCP server: ${name}`);
} catch (err) {
errorMessage(`Failed to remove server: ${err}`);
}
};
/**
* Connect to server(s)
*/
const handleConnect = async (args: string[]): Promise<void> => {
const name = args[0];
if (name) {
// Connect to specific server
try {
infoMessage(`Connecting to ${name}...`);
const instance = await connectServer(name);
successMessage(`Connected to ${name}`);
console.log(` Tools: ${instance.tools.length}`);
console.log(` Resources: ${instance.resources.length}`);
} catch (err) {
errorMessage(`Failed to connect: ${err}`);
}
} else {
// Connect to all servers
infoMessage("Connecting to all servers...");
const results = await connectAllServers();
for (const [serverName, instance] of results) {
if (instance.state === "connected") {
successMessage(
`${serverName}: Connected (${instance.tools.length} tools)`,
);
} else {
errorMessage(`${serverName}: ${instance.error || "Failed"}`);
}
}
}
};
/**
* Disconnect from server(s)
*/
const handleDisconnect = async (args: string[]): Promise<void> => {
const name = args[0];
if (name) {
await disconnectServer(name);
successMessage(`Disconnected from ${name}`);
} else {
await disconnectAllServers();
successMessage("Disconnected from all servers");
}
};
/**
* Show connection status
*/
const handleStatus = async (_args: string[]): Promise<void> => {
await initializeMCP();
const instances = getServerInstances();
if (instances.size === 0) {
infoMessage("No MCP servers configured.");
return;
}
console.log(chalk.bold("\nMCP Server Status:\n"));
for (const [name, instance] of instances) {
const stateColors: Record<string, (s: string) => string> = {
connected: chalk.green,
connecting: chalk.yellow,
disconnected: chalk.gray,
error: chalk.red,
};
const colorFn = stateColors[instance.state] || chalk.white;
const status = colorFn(instance.state.toUpperCase());
console.log(` ${chalk.cyan(name)}: ${status}`);
if (instance.state === "connected") {
console.log(` Tools: ${instance.tools.length}`);
console.log(` Resources: ${instance.resources.length}`);
}
if (instance.error) {
console.log(` Error: ${chalk.red(instance.error)}`);
}
console.log();
}
};
/**
* List available tools
*/
const handleTools = async (_args: string[]): Promise<void> => {
await connectAllServers();
const tools = getAllTools();
if (tools.length === 0) {
infoMessage("No tools available. Connect to MCP servers first.");
return;
}
console.log(chalk.bold("\nAvailable MCP Tools:\n"));
// Group by server
const byServer = new Map<string, typeof tools>();
for (const item of tools) {
const existing = byServer.get(item.server) || [];
existing.push(item);
byServer.set(item.server, existing);
}
for (const [server, serverTools] of byServer) {
console.log(chalk.cyan(` ${server}:`));
for (const { tool } of serverTools) {
console.log(` - ${chalk.white(tool.name)}`);
if (tool.description) {
console.log(` ${chalk.gray(tool.description)}`);
}
}
console.log();
}
};
/**
* Show help
*/
const handleHelp = async (_args: string[]): Promise<void> => {
console.log(`
${chalk.bold("MCP (Model Context Protocol) Management")}
${chalk.cyan("Usage:")}
codetyper mcp <command> [options]
${chalk.cyan("Commands:")}
list List configured servers
add <name> Add a new server
--command, -c <cmd> Command to run
--args, -a <args> Arguments for command
--global, -g Add to global config
remove <name> Remove a server
--global, -g Remove from global config
connect [name] Connect to server(s)
disconnect [name] Disconnect from server(s)
status Show connection status
tools List available tools from connected servers
${chalk.cyan("Examples:")}
codetyper mcp add sqlite -c npx -a @modelcontextprotocol/server-sqlite
codetyper mcp connect sqlite
codetyper mcp tools
`);
};
export default mcpCommand;

10
src/commands/runner.ts Normal file
View File

@@ -0,0 +1,10 @@
/**
* Autonomous task runner - executes agent tasks
*/
export { execute } from "@commands/runner/execute";
export { createPlan } from "@commands/runner/create-plan";
export { executePlan } from "@commands/runner/execute-plan";
export { displayPlan, getStepIcon } from "@commands/runner/display-plan";
export { displayHeader } from "@commands/runner/display-header";
export { delay } from "@commands/runner/utils";

View File

@@ -0,0 +1,45 @@
/**
* Plan creation utilities
*/
import {
RUNNER_DELAYS,
MOCK_STEPS,
DEFAULT_FILE,
ESTIMATED_TIME_PER_STEP,
} from "@constants/runner";
import { delay } from "@commands/runner/utils";
import type { AgentType, ExecutionPlan, PlanStep } from "@/types/index";
export const createPlan = async (
task: string,
_agent: AgentType,
files: string[],
): Promise<ExecutionPlan> => {
await delay(RUNNER_DELAYS.PLANNING);
const targetFile = files[0] ?? DEFAULT_FILE;
const steps: PlanStep[] = [
{
...MOCK_STEPS.READ,
file: targetFile,
},
{
...MOCK_STEPS.EDIT,
file: targetFile,
dependencies: [...MOCK_STEPS.EDIT.dependencies],
},
{
...MOCK_STEPS.EXECUTE,
dependencies: [...MOCK_STEPS.EXECUTE.dependencies],
},
];
return {
steps,
intent: "code",
summary: task,
estimatedTime: steps.length * ESTIMATED_TIME_PER_STEP,
};
};

View File

@@ -0,0 +1,27 @@
/**
* Runner header display utilities
*/
import chalk from "chalk";
import { headerMessage, filePath } from "@utils/terminal";
import type { RunnerOptions } from "@/types/runner";
export const displayHeader = (options: RunnerOptions): void => {
const { task, agent, files, dryRun } = options;
headerMessage("Running Task");
console.log(chalk.bold("Agent:") + ` ${chalk.cyan(agent)}`);
console.log(chalk.bold("Task:") + ` ${task}`);
if (files.length > 0) {
console.log(
chalk.bold("Files:") + ` ${files.map((f) => filePath(f)).join(", ")}`,
);
}
console.log(
chalk.bold("Mode:") +
` ${dryRun ? chalk.yellow("Dry Run") : chalk.green("Execute")}`,
);
console.log();
};

View File

@@ -0,0 +1,34 @@
/**
* Plan display utilities
*/
import chalk from "chalk";
import { filePath } from "@utils/terminal";
import { STEP_ICONS, DEFAULT_STEP_ICON } from "@constants/runner";
import type { ExecutionPlan, PlanStep } from "@/types/index";
export const getStepIcon = (type: PlanStep["type"]): string =>
STEP_ICONS[type] ?? DEFAULT_STEP_ICON;
export const displayPlan = (plan: ExecutionPlan): void => {
console.log("\n" + chalk.bold.underline("Execution Plan:"));
console.log(chalk.gray(`${plan.summary}`));
console.log();
plan.steps.forEach((step, index) => {
const icon = getStepIcon(step.type);
const deps = step.dependencies
? chalk.gray(` (depends on: ${step.dependencies.join(", ")})`)
: "";
console.log(
`${icon} ${chalk.bold(`Step ${index + 1}:`)} ${step.description}${deps}`,
);
if (step.file) {
console.log(` ${filePath(step.file)}`);
}
if (step.tool) {
console.log(` ${chalk.gray(`Tool: ${step.tool}`)}`);
}
});
console.log();
};

View File

@@ -0,0 +1,35 @@
/**
* Plan execution utilities
*/
import { failSpinner, succeedSpinner, startSpinner } from "@utils/terminal";
import { RUNNER_DELAYS } from "@constants/runner";
import { getStepIcon } from "@commands/runner/display-plan";
import { delay } from "@commands/runner/utils";
import type { ExecutionPlan, PlanStep } from "@/types/index";
import type { StepContext } from "@/types/runner";
const executeStep = async (context: StepContext): Promise<void> => {
const { step, current, total } = context;
const icon = getStepIcon(step.type);
const message = `${icon} Step ${current}/${total}: ${step.description}`;
startSpinner(message);
try {
await delay(RUNNER_DELAYS.STEP_EXECUTION);
succeedSpinner(message);
} catch (error) {
failSpinner(message);
throw error;
}
};
export const executePlan = async (plan: ExecutionPlan): Promise<void> => {
const total = plan.steps.length;
for (let i = 0; i < plan.steps.length; i++) {
const step: PlanStep = plan.steps[i];
await executeStep({ step, current: i + 1, total });
}
};

View File

@@ -0,0 +1,118 @@
/**
* Main runner execution function
*/
import {
askConfirm,
failSpinner,
successMessage,
succeedSpinner,
headerMessage,
startSpinner,
infoMessage,
errorMessage,
warningMessage,
} from "@utils/terminal";
import { RUNNER_DELAYS, RUNNER_MESSAGES } from "@constants/runner";
import { displayHeader } from "@commands/runner/display-header";
import { displayPlan } from "@commands/runner/display-plan";
import { createPlan } from "@commands/runner/create-plan";
import { executePlan } from "@commands/runner/execute-plan";
import { delay } from "@commands/runner/utils";
import type { CommandOptions, AgentType } from "@/types/index";
import type { RunnerOptions } from "@/types/runner";
const parseOptions = (options: CommandOptions): RunnerOptions | null => {
const {
task,
agent = "coder",
files = [],
dryRun = false,
autoApprove = false,
} = options;
if (!task) {
errorMessage(RUNNER_MESSAGES.TASK_REQUIRED);
return null;
}
return {
task,
agent: agent as AgentType,
files,
dryRun,
autoApprove,
};
};
const runDiscoveryPhase = async (): Promise<void> => {
startSpinner(RUNNER_MESSAGES.DISCOVERY_START);
await delay(RUNNER_DELAYS.DISCOVERY);
succeedSpinner(RUNNER_MESSAGES.DISCOVERY_COMPLETE);
};
const runPlanningPhase = async (
task: string,
agent: AgentType,
files: string[],
) => {
startSpinner(RUNNER_MESSAGES.PLANNING_START);
const plan = await createPlan(task, agent, files);
succeedSpinner(`Plan created with ${plan.steps.length} steps`);
return plan;
};
const confirmExecution = async (autoApprove: boolean): Promise<boolean> => {
if (autoApprove) {
return true;
}
const approved = await askConfirm(RUNNER_MESSAGES.CONFIRM_EXECUTE);
if (!approved) {
warningMessage(RUNNER_MESSAGES.EXECUTION_CANCELLED);
return false;
}
return true;
};
export const execute = async (options: CommandOptions): Promise<void> => {
const runnerOptions = parseOptions(options);
if (!runnerOptions) {
return;
}
const { task, agent, files, dryRun, autoApprove } = runnerOptions;
displayHeader(runnerOptions);
try {
await runDiscoveryPhase();
const plan = await runPlanningPhase(task, agent, files);
displayPlan(plan);
if (dryRun) {
infoMessage(RUNNER_MESSAGES.DRY_RUN_INFO);
return;
}
const shouldExecute = await confirmExecution(autoApprove);
if (!shouldExecute) {
return;
}
headerMessage("Executing Plan");
await executePlan(plan);
successMessage(`\n${RUNNER_MESSAGES.TASK_COMPLETE}`);
} catch (error) {
failSpinner(RUNNER_MESSAGES.TASK_FAILED);
errorMessage(`Error: ${error}`);
throw error;
}
};

View File

@@ -0,0 +1,6 @@
/**
* Runner utility functions
*/
export const delay = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));

5
src/constants/agent.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Agent constants
*/
export const MAX_ITERATIONS = 50;

View File

@@ -0,0 +1,23 @@
/**
* Auto-Scroll Constants
*
* Constants for auto-scroll behavior in the TUI
*/
/** Distance from bottom (in lines) to consider "at bottom" */
export const BOTTOM_THRESHOLD = 3;
/** Settling time after operations complete (ms) */
export const SETTLE_TIMEOUT_MS = 300;
/** Timeout for marking auto-scroll events (ms) */
export const AUTO_SCROLL_MARK_TIMEOUT_MS = 250;
/** Default scroll lines per keyboard event */
export const KEYBOARD_SCROLL_LINES = 3;
/** Default scroll lines per page event */
export const PAGE_SCROLL_LINES = 10;
/** Mouse scroll lines per wheel event */
export const MOUSE_SCROLL_LINES = 3;

103
src/constants/banner.ts Normal file
View File

@@ -0,0 +1,103 @@
/**
* Banner constants for CodeTyper CLI
*/
// ASCII art for "codetyper" using block characters
export const BANNER_LINES = [
" __ __ ",
" _______ _____/ /__ / /___ ______ ___ _____ ",
" / ___/ / / / _ \\/ _ \\/ __/ / / / __ \\/ _ \\/ ___/ ",
"/ /__/ /_/ / __/ __/ /_/ /_/ / /_/ / __/ / ",
"\\___/\\____/\\___/\\___/\\__/\\__, / .___/\\___/_/ ",
" /____/_/ ",
] as const;
// Alternative minimal banner
export const BANNER_MINIMAL = [
"╭───────────────────────────────────────╮",
"│ ▄▀▀ ▄▀▄ █▀▄ ██▀ ▀█▀ ▀▄▀ █▀▄ ██▀ █▀▄ │",
"│ ▀▄▄ ▀▄▀ █▄▀ █▄▄ █ █ █▀ █▄▄ █▀▄ │",
"╰───────────────────────────────────────╯",
] as const;
// Block-style banner (similar to opencode)
export const BANNER_BLOCKS = [
"█▀▀ █▀█ █▀▄ █▀▀ ▀█▀ █▄█ █▀█ █▀▀ █▀█",
"█ █ █ █ █ █▀▀ █ █ █▀▀ █▀▀ █▀▄",
"▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀",
] as const;
// Gradient colors for banner (cyan to blue)
export const GRADIENT_COLORS = [
"\x1b[96m", // Bright cyan
"\x1b[36m", // Cyan
"\x1b[94m", // Bright blue
"\x1b[34m", // Blue
"\x1b[95m", // Bright magenta
"\x1b[35m", // Magenta
] as const;
// Banner style to lines mapping
export const BANNER_STYLE_MAP: Record<string, readonly string[]> = {
default: BANNER_LINES,
minimal: BANNER_MINIMAL,
blocks: BANNER_BLOCKS,
} as const;
// Large ASCII art banner
export const BANNER = `
,gggg, _,gggggg,_ ,gggggggggggg, ,ggggggg, ,ggggggggggggggg ,ggg, gg ,ggggggggggg, ,ggggggg, ,ggggggggggg,
,88"""Y8b, ,d8P""d8P"Y8b, dP"""88""""""Y8b, ,dP"""""""Y8bdP""""""88"""""""dP""Y8a 88 dP"""88""""""Y8, ,dP"""""""Y8bdP"""88""""""Y8,
d8" \`Y8,d8' Y8 "8b,dPYb, 88 \`8b, d8' a Y8Yb,_ 88 Yb, \`88 88 Yb, 88 \`8b d8' a Y8Yb, 88 \`8b
d8' 8b d8d8' \`Ybaaad88P' \`" 88 \`8b 88 "Y8P' \`"" 88 \`" 88 88 \`" 88 ,8P 88 "Y8P' \`" 88 ,8P
,8I "Y88P'8P \`"""Y8 88 Y8 \`8baaaa 88 88 88 88aaaad8P" \`8baaaa 88aaaad8P"
I8' 8b d8 88 d8,d8P"""" 88 88 88 88""""" ,d8P"""" 88""""Yb,
d8 Y8, ,8P 88 ,8Pd8" 88 88 ,88 88 d8" 88 "8b
Y8, \`Y8, ,8P' 88 ,8P'Y8, gg, 88 Y8b,___,d888 88 Y8, 88 \`8i
\`Yba,,_____, \`Y8b,,__,,d8P' 88______,dP' \`Yba,,_____, "Yb,,8P "Y88888P"88, 88 \`Yba,,_____, 88 Yb,
\`"Y8888888 \`"Y8888P"' 888888888P" \`"Y8888888 "Y8P' ,ad8888 88 \`"Y8888888 88 Y8
d8P" 88
,d8' 88
d8' 88
88 88
Y8,_ _,88
"Y888P"
`;
// Welcome message with help information
export const WELCOME_MESSAGE = `
🤖 CodeTyper AI Agent - Autonomous Code Generation Assistant
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Default Provider: GitHub Copilot (gpt-4)
Getting Started:
codetyper chat Start interactive chat
codetyper run "your task" Execute autonomous task
codetyper classify "prompt" Analyze intent
codetyper config show View configuration
Commands:
chat Interactive REPL session
run <task> Execute task autonomously
classify <prompt> Classify user intent
plan <intent> Generate execution plan
validate <plan> Validate plan safety
config Manage configuration
serve Start JSON-RPC server
Options:
--help, -h Show help
--version, -V Show version
Chat Commands:
/help Show help
/models View available LLM providers
/provider Switch LLM provider
/files List context files
/clear Clear conversation
/exit Exit chat
💡 Tip: Use 'codetyper chat' then '/models' to see all available providers
📖 Docs: Run 'codetyper --help <command>' for detailed information
`;

30
src/constants/bash.ts Normal file
View File

@@ -0,0 +1,30 @@
/**
* Bash tool constants
*/
export const BASH_DEFAULTS = {
MAX_OUTPUT_LENGTH: 30000,
TIMEOUT: 120000,
KILL_DELAY: 1000,
} as const;
export const BASH_SIGNALS = {
TERMINATE: "SIGTERM",
KILL: "SIGKILL",
} as const;
export const BASH_MESSAGES = {
PERMISSION_DENIED: "Permission denied by user",
TIMED_OUT: (timeout: number) => `Command timed out after ${timeout}ms`,
ABORTED: "Command aborted",
EXIT_CODE: (code: number) => `Command exited with code ${code}`,
TRUNCATED: "\n\n... (truncated)",
} as const;
export const BASH_DESCRIPTION = `Execute a shell command. Use this tool to run commands like git, npm, mkdir, etc.
Guidelines:
- Always provide a clear description of what the command does
- Use absolute paths when possible
- Be careful with destructive commands (rm, etc.)
- Commands that modify the filesystem will require user approval`;

View File

@@ -0,0 +1,20 @@
/** Quiet bash commands - read-only exploration operations */
export const QUIET_BASH_PATTERNS = [
/^ls\b/,
/^cat\b/,
/^head\b/,
/^tail\b/,
/^find\b/,
/^grep\b/,
/^rg\b/,
/^fd\b/,
/^tree\b/,
/^pwd\b/,
/^echo\b/,
/^which\b/,
/^file\b/,
/^stat\b/,
/^wc\b/,
/^du\b/,
/^df\b/,
];

View File

@@ -0,0 +1,106 @@
/**
* Chat service constants
*/
export const CHAT_TRUNCATE_DEFAULTS = {
MAX_LINES: 10,
MAX_LENGTH: 500,
} as const;
export const FILE_SIZE_LIMITS = {
MAX_CONTEXT_FILE_SIZE: 100000,
} as const;
export const DIFF_PATTERNS = [
/@@\s*-\d+/m,
/---\s+[ab]?\//m,
/\+\+\+\s+[ab]?\//m,
] as const;
export const GLOB_IGNORE_PATTERNS = [
"**/node_modules/**",
"**/.git/**",
] as const;
export const CHAT_MESSAGES = {
CONVERSATION_CLEARED: "Conversation cleared",
SESSION_SAVED: "Session saved",
LEARNING_SAVED: (scope: string) => `Learning saved (${scope})`,
LEARNING_SKIPPED: "Learning skipped",
NO_LEARNINGS:
"No learnings saved yet. Use /remember to save learnings about your project.",
NO_CONVERSATION:
"No conversation to create learning from. Start a conversation first.",
NO_LEARNINGS_DETECTED:
'No learnings detected from the last exchange. Try being more explicit about preferences (e.g., "always use TypeScript", "prefer functional style").',
UNKNOWN_COMMAND: (cmd: string) => `Unknown command: ${cmd}`,
FILE_NOT_FOUND: (pattern: string) => `File not found: ${pattern}`,
FILE_TOO_LARGE: (name: string, size: number) =>
`File too large: ${name} (${Math.round(size / 1024)}KB)`,
FILE_IS_BINARY: (name: string) => `Cannot add binary file: ${name}`,
FILE_ADDED: (name: string) => `Added to context: ${name}`,
FILE_ADD_FAILED: (error: unknown) => `Failed to add file: ${error}`,
FILE_READ_FAILED: (error: unknown) => `Failed to read file: ${error}`,
ANALYZE_FILES: "Analyze the files I've added to the context.",
GITHUB_ISSUES_FOUND: (count: number, issues: string) =>
`Found ${count} GitHub issue(s): ${issues}`,
COMPACTION_STARTING: "Summarizing conversation history...",
COMPACTION_CONTINUING: "Continuing with your request...",
} as const;
export const AUTH_MESSAGES = {
ALREADY_LOGGED_IN: "Already logged in. Use /logout first to re-authenticate.",
AUTH_SUCCESS: "Successfully authenticated with GitHub Copilot!",
AUTH_FAILED: (error: string) => `Authentication failed: ${error}`,
AUTH_START_FAILED: (error: string) =>
`Failed to start authentication: ${error}`,
LOGGED_OUT:
"Logged out from GitHub Copilot. Run /login to authenticate again.",
NOT_LOGGED_IN: "Not logged in. Run /login to authenticate.",
NO_LOGIN_REQUIRED: (provider: string) =>
`Provider ${provider} doesn't require login.`,
NO_LOGOUT_SUPPORT: (provider: string) =>
`Provider ${provider} doesn't support logout.`,
OLLAMA_NO_AUTH: "Ollama is a local provider - no authentication required.",
COPILOT_AUTH_INSTRUCTIONS: (uri: string, code: string) =>
`To authenticate with GitHub Copilot:\n\n1. Open: ${uri}\n2. Enter code: ${code}\n\nWaiting for authentication...`,
LOGGED_IN_AS: (login: string, name?: string) =>
`Logged in as: ${login}${name ? ` (${name})` : ""}`,
} as const;
export const MODEL_MESSAGES = {
MODEL_AUTO: "Model set to auto - the provider will choose the best model.",
MODEL_CHANGED: (model: string) => `Model changed to: ${model}`,
} as const;
// Re-export HELP_TEXT from prompts for backward compatibility
export { HELP_TEXT } from "@prompts/ui/help";
export const LEARNING_CONFIDENCE_THRESHOLD = 0.7;
export const MAX_LEARNINGS_DISPLAY = 20;
export type CommandName =
| "help"
| "h"
| "clear"
| "c"
| "save"
| "s"
| "context"
| "usage"
| "u"
| "model"
| "models"
| "agent"
| "a"
| "theme"
| "mcp"
| "mode"
| "whoami"
| "login"
| "logout"
| "provider"
| "p"
| "status"
| "remember"
| "learnings";

View File

@@ -0,0 +1,85 @@
/**
* Command suggestion constants
*/
import type { SuggestionPriority } from "@/types/command-suggestion";
export const PROJECT_FILES = {
PACKAGE_JSON: "package.json",
YARN_LOCK: "yarn.lock",
PNPM_LOCK: "pnpm-lock.yaml",
BUN_LOCK: "bun.lockb",
CARGO_TOML: "Cargo.toml",
GO_MOD: "go.mod",
PYPROJECT: "pyproject.toml",
REQUIREMENTS: "requirements.txt",
MAKEFILE: "Makefile",
DOCKERFILE: "Dockerfile",
} as const;
export const PRIORITY_ORDER: Record<SuggestionPriority, number> = {
high: 0,
medium: 1,
low: 2,
};
export const PRIORITY_ICONS: Record<SuggestionPriority, string> = {
high: "⚡",
medium: "→",
low: "·",
};
export const FILE_PATTERNS = {
PACKAGE_JSON: /package\.json$/,
TSCONFIG: /tsconfig.*\.json$/,
SOURCE_FILES: /\.(ts|tsx|js|jsx)$/,
CARGO_TOML: /Cargo\.toml$/,
GO_MOD: /go\.mod$/,
PYTHON_DEPS: /requirements.*\.txt$|pyproject\.toml$/,
DOCKER: /Dockerfile$|docker-compose.*\.ya?ml$/,
MAKEFILE: /Makefile$/,
MIGRATIONS: /migrations?\/.*\.(sql|ts|js)$/,
ENV_EXAMPLE: /\.env\.example$|\.env\.sample$/,
LINTER_CONFIG: /\.eslintrc|\.prettierrc|eslint\.config|prettier\.config/,
TEST_FILE: /\.test\.|\.spec\.|__tests__/,
} as const;
export const CONTENT_PATTERNS = {
DEPENDENCIES: /\"dependencies\"/,
DEV_DEPENDENCIES: /\"devDependencies\"/,
PEER_DEPENDENCIES: /\"peerDependencies\"/,
} as const;
export const SUGGESTION_MESSAGES = {
INSTALL_DEPS: "Install dependencies",
REBUILD_PROJECT: "Rebuild the project",
RUN_TESTS: "Run tests",
START_DEV: "Start development server",
BUILD_RUST: "Build the Rust project",
TIDY_GO: "Tidy Go modules",
INSTALL_PYTHON_EDITABLE: "Install Python package in editable mode",
INSTALL_PYTHON_DEPS: "Install Python dependencies",
DOCKER_COMPOSE_BUILD: "Rebuild and start Docker containers",
DOCKER_BUILD: "Rebuild Docker image",
RUN_MAKE: "Run make",
RUN_MIGRATE: "Run database migrations",
CREATE_ENV: "Create local .env file",
RUN_LINT: "Run linter to check for issues",
} as const;
export const SUGGESTION_REASONS = {
PACKAGE_JSON_MODIFIED: "package.json was modified",
TSCONFIG_CHANGED: "TypeScript configuration changed",
TEST_FILE_MODIFIED: "Test file was modified",
SOURCE_FILE_MODIFIED: "Source file was modified",
CARGO_MODIFIED: "Cargo.toml was modified",
GO_MOD_MODIFIED: "go.mod was modified",
PYTHON_DEPS_CHANGED: "Python dependencies changed",
REQUIREMENTS_MODIFIED: "requirements.txt was modified",
DOCKER_COMPOSE_CHANGED: "Docker Compose configuration changed",
DOCKERFILE_MODIFIED: "Dockerfile was modified",
MAKEFILE_MODIFIED: "Makefile was modified",
MIGRATION_MODIFIED: "Migration file was added or modified",
ENV_TEMPLATE_MODIFIED: "Environment template was modified",
LINTER_CONFIG_CHANGED: "Linter configuration changed",
} as const;

114
src/constants/components.ts Normal file
View File

@@ -0,0 +1,114 @@
/**
* UI component constants
*/
// Box drawing characters
export const BoxChars = {
// Single line
single: {
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
bottomRight: "┘",
horizontal: "─",
vertical: "│",
leftT: "├",
rightT: "┤",
topT: "┬",
bottomT: "┴",
cross: "┼",
},
// Double line
double: {
topLeft: "╔",
topRight: "╗",
bottomLeft: "╚",
bottomRight: "╝",
horizontal: "═",
vertical: "║",
leftT: "╠",
rightT: "╣",
topT: "╦",
bottomT: "╩",
cross: "╬",
},
// Rounded
rounded: {
topLeft: "╭",
topRight: "╮",
bottomLeft: "╰",
bottomRight: "╯",
horizontal: "─",
vertical: "│",
leftT: "├",
rightT: "┤",
topT: "┬",
bottomT: "┴",
cross: "┼",
},
// Bold
bold: {
topLeft: "┏",
topRight: "┓",
bottomLeft: "┗",
bottomRight: "┛",
horizontal: "━",
vertical: "┃",
leftT: "┣",
rightT: "┫",
topT: "┳",
bottomT: "┻",
cross: "╋",
},
} as const;
// Default box options
export const BOX_DEFAULTS = {
style: "rounded" as const,
padding: 1,
align: "left" as const,
} as const;
// Tool icon mapping
export const TOOL_ICONS = {
bash: "bash",
read: "read",
write: "write",
edit: "edit",
default: "default",
} as const;
// State color mapping
export const STATE_COLORS = {
pending: "DIM",
running: "primary",
success: "success",
error: "error",
} as const;
// Role configuration for message display
export const ROLE_CONFIG = {
user: { label: "You", colorKey: "primary" },
assistant: { label: "CodeTyper", colorKey: "success" },
system: { label: "System", colorKey: "textMuted" },
tool: { label: "Tool", colorKey: "warning" },
} as const;
// Status indicator configuration
export const STATUS_INDICATORS = {
success: { iconKey: "success", colorKey: "success" },
error: { iconKey: "error", colorKey: "error" },
warning: { iconKey: "warning", colorKey: "warning" },
info: { iconKey: "info", colorKey: "info" },
pending: { iconKey: "pending", colorKey: "textMuted" },
running: { iconKey: "running", colorKey: "primary" },
} as const;
// Tool call icon configuration
export const TOOL_CALL_ICONS = {
bash: { iconKey: "bash", colorKey: "warning" },
read: { iconKey: "read", colorKey: "info" },
write: { iconKey: "write", colorKey: "success" },
edit: { iconKey: "edit", colorKey: "primary" },
default: { iconKey: "gear", colorKey: "textMuted" },
} as const;

213
src/constants/copilot.ts Normal file
View File

@@ -0,0 +1,213 @@
import type { ProviderModel, ProviderName } from "@/types/providers";
// Provider identification
export const COPILOT_PROVIDER_NAME: ProviderName = "copilot";
export const COPILOT_DISPLAY_NAME = "GitHub Copilot";
// GitHub Copilot API endpoints
export const COPILOT_AUTH_URL =
"https://api.github.com/copilot_internal/v2/token";
export const COPILOT_MODELS_URL = "https://api.githubcopilot.com/models";
// GitHub OAuth endpoints for device flow
export const GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98";
export const GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
export const GITHUB_ACCESS_TOKEN_URL =
"https://github.com/login/oauth/access_token";
// Cache and retry configuration
export const COPILOT_MODELS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
export const COPILOT_MAX_RETRIES = 3;
export const COPILOT_INITIAL_RETRY_DELAY = 1000; // 1 second
// Default model
export const COPILOT_DEFAULT_MODEL = "gpt-5-mini";
// Unlimited fallback model (used when quota is exceeded)
export const COPILOT_UNLIMITED_MODEL = "gpt-4o";
// Copilot messages
export const COPILOT_MESSAGES = {
QUOTA_EXCEEDED_SWITCHING: (from: string, to: string) =>
`Quota exceeded for ${from}. Switching to unlimited model: ${to}`,
MODEL_SWITCHED: (from: string, to: string) =>
`Model switched: ${from}${to} (quota exceeded)`,
FORMAT_MULTIPLIER: (multiplier: number) =>
multiplier === 0 ? "Unlimited" : `${multiplier}x`,
} as const;
// Model cost multipliers from GitHub Copilot
// 0x = Unlimited (no premium request usage)
// Lower multiplier = cheaper, Higher multiplier = more expensive
export const MODEL_COST_MULTIPLIERS: Record<string, number> = {
// Unlimited models (0x)
"gpt-4o": 0,
"gpt-4o-mini": 0,
"gpt-5-mini": 0,
"grok-code-fast-1": 0,
"raptor-mini": 0,
// Low cost models (0.33x)
"claude-haiku-4.5": 0.33,
"gemini-3-flash-preview": 0.33,
"gpt-5.1-codex-mini-preview": 0.33,
// Standard cost models (1.0x)
"claude-sonnet-4": 1.0,
"claude-sonnet-4.5": 1.0,
"gemini-2.5-pro": 1.0,
"gemini-3-pro-preview": 1.0,
"gpt-4.1": 1.0,
"gpt-5": 1.0,
"gpt-5-codex-preview": 1.0,
"gpt-5.1": 1.0,
"gpt-5.1-codex": 1.0,
"gpt-5.1-codex-max": 1.0,
"gpt-5.2": 1.0,
"gpt-5.2-codex": 1.0,
// Premium models (3.0x)
"claude-opus-4.5": 3.0,
};
// Models that are unlimited (0x cost multiplier)
export const UNLIMITED_MODELS = new Set([
"gpt-4o",
"gpt-4o-mini",
"gpt-5-mini",
"grok-code-fast-1",
"raptor-mini",
]);
// Model context sizes (input tokens, output tokens)
export interface ModelContextSize {
input: number;
output: number;
}
export const MODEL_CONTEXT_SIZES: Record<string, ModelContextSize> = {
// Claude models
"claude-haiku-4.5": { input: 128000, output: 16000 },
"claude-opus-4.5": { input: 128000, output: 16000 },
"claude-sonnet-4": { input: 128000, output: 16000 },
"claude-sonnet-4.5": { input: 128000, output: 16000 },
// Gemini models
"gemini-2.5-pro": { input: 109000, output: 64000 },
"gemini-3-flash-preview": { input: 109000, output: 64000 },
"gemini-3-pro-preview": { input: 109000, output: 64000 },
// GPT-4 models
"gpt-4.1": { input: 111000, output: 16000 },
"gpt-4o": { input: 64000, output: 4000 },
// GPT-5 models
"gpt-5": { input: 128000, output: 128000 },
"gpt-5-mini": { input: 128000, output: 64000 },
"gpt-5-codex-preview": { input: 128000, output: 128000 },
"gpt-5.1": { input: 128000, output: 64000 },
"gpt-5.1-codex": { input: 128000, output: 128000 },
"gpt-5.1-codex-max": { input: 128000, output: 128000 },
"gpt-5.1-codex-mini-preview": { input: 128000, output: 128000 },
"gpt-5.2": { input: 128000, output: 64000 },
"gpt-5.2-codex": { input: 272000, output: 128000 },
// Other models
"grok-code-fast-1": { input: 109000, output: 64000 },
"raptor-mini": { input: 200000, output: 64000 },
};
// Default context size for unknown models
export const DEFAULT_CONTEXT_SIZE: ModelContextSize = {
input: 128000,
output: 16000,
};
// Get context size for a model
export const getModelContextSize = (modelId: string): ModelContextSize =>
MODEL_CONTEXT_SIZES[modelId] ?? DEFAULT_CONTEXT_SIZE;
// Fallback models when API is unavailable
export const COPILOT_FALLBACK_MODELS: ProviderModel[] = [
{
id: "gpt-4o",
name: "GPT-4o",
maxTokens: 4000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 0,
isUnlimited: true,
},
{
id: "gpt-5-mini",
name: "GPT-5 mini",
maxTokens: 64000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 0,
isUnlimited: true,
},
{
id: "claude-sonnet-4",
name: "Claude Sonnet 4",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "claude-sonnet-4.5",
name: "Claude Sonnet 4.5",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "claude-opus-4.5",
name: "Claude Opus 4.5",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 3.0,
isUnlimited: false,
},
{
id: "gpt-4.1",
name: "GPT-4.1",
maxTokens: 16000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "gpt-5",
name: "GPT-5",
maxTokens: 128000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "gemini-2.5-pro",
name: "Gemini 2.5 Pro",
maxTokens: 64000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 1.0,
isUnlimited: false,
},
{
id: "grok-code-fast-1",
name: "Grok Code Fast 1",
maxTokens: 64000,
supportsTools: true,
supportsStreaming: true,
costMultiplier: 0,
isUnlimited: true,
},
];

View File

@@ -0,0 +1,42 @@
/**
* Dashboard Constants
*/
export const DASHBOARD_TITLE = "CodeTyper";
export const DASHBOARD_LAYOUT = {
DEFAULT_WIDTH: 120,
CONTENT_HEIGHT: 15,
LEFT_COLUMN_RATIO: 0.35,
PADDING: 3,
} as const;
export const DASHBOARD_LOGO = [
" ██████╗███████╗",
" ██╔════╝██╔════╝",
" ██║ ███████╗",
" ██║ ╚════██║",
" ╚██████╗███████║",
" ╚═════╝╚══════╝",
] as const;
export const DASHBOARD_COMMANDS = [
{ command: "codetyper chat", description: "Start interactive chat" },
{ command: "codetyper run <task>", description: "Execute autonomous task" },
{ command: "/help", description: "Show all commands in chat" },
] as const;
export const DASHBOARD_QUICK_COMMANDS = [
{ command: "codetyper chat", description: "Start interactive chat" },
{ command: "codetyper run", description: "Execute autonomous task" },
{ command: "codetyper --help", description: "Show all commands" },
] as const;
export const DASHBOARD_BORDER = {
TOP_LEFT: "╭",
TOP_RIGHT: "╮",
BOTTOM_LEFT: "╰",
BOTTOM_RIGHT: "╯",
HORIZONTAL: "─",
VERTICAL: "│",
} as const;

13
src/constants/diff.ts Normal file
View File

@@ -0,0 +1,13 @@
/**
* Diff utility constants
*/
// Default context lines for hunks
export const DIFF_CONTEXT_LINES = 3;
// Line type prefixes
export const LINE_PREFIXES = {
add: "+",
remove: "-",
context: " ",
} as const;

25
src/constants/edit.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Edit tool constants
*/
export const EDIT_MESSAGES = {
NOT_FOUND:
"Could not find the text to replace. Make sure old_string matches exactly.",
MULTIPLE_OCCURRENCES: (count: number) =>
`old_string appears ${count} times. Use replace_all=true or provide more context to make it unique.`,
PERMISSION_DENIED: "Permission denied by user",
} as const;
export const EDIT_TITLES = {
FAILED: (path: string) => `Edit failed: ${path}`,
CANCELLED: (path: string) => `Edit cancelled: ${path}`,
SUCCESS: (path: string) => `Edited: ${path}`,
EDITING: (name: string) => `Editing ${name}`,
} as const;
export const EDIT_DESCRIPTION = `Edit a file by replacing specific text. The old_string must match exactly.
Guidelines:
- old_string must be unique in the file (or use replace_all)
- Preserve indentation exactly as it appears in the file
- Requires user approval for edits`;

View File

@@ -0,0 +1,30 @@
/**
* Embedding Constants
*
* Configuration for semantic learning retrieval
*/
export const EMBEDDING_DEFAULTS = {
MODEL: "nomic-embed-text",
FALLBACK_MODEL: "all-minilm",
DIMENSIONS: 768,
} as const;
export const EMBEDDING_ENDPOINTS = {
EMBED: "/api/embed",
} as const;
export const EMBEDDING_TIMEOUTS = {
EMBED: 30000,
} as const;
export const EMBEDDING_SEARCH = {
TOP_K: 10,
MIN_SIMILARITY: 0.3,
CACHE_TTL_MS: 300000, // 5 minutes
} as const;
export const EMBEDDING_STORAGE = {
INDEX_FILE: "embeddings.json",
VERSION: 1,
} as const;

View File

@@ -0,0 +1,123 @@
/**
* File Picker constants
*/
export const IGNORED_PATTERNS = [
// Version control
".git",
".svn",
".hg",
// AI/Code assistants
".claude",
".coder",
".codetyper",
".cursor",
".copilot",
".aider",
// Build outputs / binaries
"node_modules",
"dist",
"build",
"bin",
"obj",
"target",
".next",
".nuxt",
".output",
"out",
// Cache directories
".cache",
".turbo",
".parcel-cache",
".vite",
// Test/Coverage
"coverage",
".nyc_output",
// Python
"__pycache__",
".venv",
"venv",
".env",
// OS files
".DS_Store",
"thumbs.db",
// IDE/Editor
".idea",
".vscode",
// Misc
".terraform",
".serverless",
] as const;
export const BINARY_EXTENSIONS = [
// Executables
".exe",
".dll",
".so",
".dylib",
".bin",
".app",
// Images
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".ico",
".webp",
".svg",
".tiff",
// Audio/Video
".mp3",
".mp4",
".wav",
".avi",
".mov",
".mkv",
".flac",
".ogg",
// Archives
".zip",
".tar",
".gz",
".rar",
".7z",
".bz2",
// Documents
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
// Fonts
".ttf",
".otf",
".woff",
".woff2",
".eot",
// Database
".db",
".sqlite",
".sqlite3",
// Other binary
".pyc",
".pyo",
".class",
".o",
".a",
".lib",
".node",
".wasm",
] as const;
export type BinaryExtension = (typeof BINARY_EXTENSIONS)[number];
export type IgnoredPattern = (typeof IGNORED_PATTERNS)[number];
export const FILE_PICKER_DEFAULTS = {
MAX_DEPTH: 2,
MAX_RESULTS: 15,
INITIAL_DEPTH: 0,
} as const;

2
src/constants/files.ts Normal file
View File

@@ -0,0 +1,2 @@
// File-related constants
// MAX_FILE_SIZE, etc.

1
src/constants/general.ts Normal file
View File

@@ -0,0 +1 @@
export const PROVIDER_NAME_COPILOT = "copilot";

View File

@@ -0,0 +1,31 @@
/**
* GitHub Issue constants
*/
export const ISSUE_PATTERNS = [
/\bissue\s*#?(\d+)\b/gi,
/\bfix\s+#(\d+)\b/gi,
/\bclose\s+#(\d+)\b/gi,
/\bresolve\s+#(\d+)\b/gi,
/(?<!\w)#(\d+)(?!\w)/g,
] as const;
export const GITHUB_ISSUE_DEFAULTS = {
MAX_ISSUE_NUMBER: 100000,
MIN_ISSUE_NUMBER: 1,
} as const;
export const GITHUB_ISSUE_MESSAGES = {
CONTEXT_HEADER: "The user is referencing the following GitHub issue(s):",
SECTION_SEPARATOR: "\n\n---\n\n",
USER_REQUEST_PREFIX: "User request: ",
UNKNOWN_AUTHOR: "unknown",
} as const;
export const GH_CLI_COMMANDS = {
GET_REMOTE_URL: "git remote get-url origin 2>/dev/null",
VIEW_ISSUE: (issueNumber: number) =>
`gh issue view ${issueNumber} --json number,title,state,body,author,labels,url 2>/dev/null`,
} as const;
export const GITHUB_REMOTE_IDENTIFIER = "github.com";

58
src/constants/glob.ts Normal file
View File

@@ -0,0 +1,58 @@
/**
* Glob tool constants
*/
export const GLOB_DEFAULTS = {
DOT: false,
ONLY_FILES: true,
ONLY_DIRECTORIES: false,
} as const;
export const GLOB_IGNORE_PATTERNS = [
// Version control
"**/.git/**",
"**/.svn/**",
"**/.hg/**",
// AI/Code assistants
"**/.claude/**",
"**/.coder/**",
"**/.codetyper/**",
"**/.cursor/**",
"**/.copilot/**",
"**/.aider/**",
// Build outputs / binaries
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/bin/**",
"**/obj/**",
"**/target/**",
"**/.next/**",
"**/.nuxt/**",
"**/.output/**",
"**/out/**",
// Cache directories
"**/.cache/**",
"**/.turbo/**",
"**/.parcel-cache/**",
"**/.vite/**",
// Test/Coverage
"**/coverage/**",
"**/.nyc_output/**",
// Python
"**/__pycache__/**",
"**/.venv/**",
"**/venv/**",
"**/.env/**",
// IDE/Editor
"**/.idea/**",
"**/.vscode/**",
// Misc
"**/.terraform/**",
"**/.serverless/**",
] as const;
export const GLOB_MESSAGES = {
FAILED: (error: unknown) => `Glob failed: ${error}`,
LIST_FAILED: (error: unknown) => `List failed: ${error}`,
} as const;

28
src/constants/grep.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* Grep tool constants
*/
export const GREP_DEFAULTS = {
MAX_RESULTS: 100,
DEFAULT_PATTERN: "**/*",
NO_MATCHES_EXIT_CODE: 1,
} as const;
export const GREP_IGNORE_PATTERNS = [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
] as const;
export const GREP_MESSAGES = {
NO_MATCHES: "No matches found",
SEARCH_FAILED: (error: unknown) => `Search failed: ${error}`,
RIPGREP_FAILED: (message: string) => `ripgrep failed: ${message}`,
} as const;
export const GREP_COMMANDS = {
RIPGREP: (pattern: string, directory: string) =>
`rg --line-number --no-heading "${pattern}" "${directory}"`,
} as const;

45
src/constants/handlers.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* Constants for command handlers
*/
import type { ConfigKey, ConfigAction } from "@/types/handlers";
import type { Provider } from "@/types/index";
export const VALID_CONFIG_KEYS: readonly ConfigKey[] = [
"provider",
"model",
"maxIterations",
"timeout",
] as const;
export const VALID_PROVIDERS: readonly Provider[] = [
"copilot",
"ollama",
] as const;
export const VALID_CONFIG_ACTIONS: readonly ConfigAction[] = [
"show",
"path",
"set",
] as const;
export const CONFIG_VALIDATION = {
MIN_TIMEOUT_MS: 1000,
MIN_ITERATIONS: 1,
} as const;
export const INTENT_KEYWORDS = {
fix: ["fix", "bug"],
test: ["test", "spec"],
refactor: ["refactor", "improve"],
code: ["add", "implement"],
document: ["document", "comment"],
} as const;
export const CLASSIFICATION_CONFIDENCE = {
HIGH: 0.9,
MEDIUM: 0.85,
DEFAULT: 0.8,
LOW: 0.75,
THRESHOLD: 0.7,
} as const;

View File

@@ -0,0 +1,20 @@
export const HELP_COMMANDS: [string, string][] = [
["/help, /h", "Show this help message"],
["/clear, /c", "Clear conversation history"],
["/files, /f", "List files in context"],
["/remove <file>, /rm", "Remove file from context"],
["/context", "Show current context size"],
["/compact", "Compact conversation history"],
["/history", "Show conversation history"],
["/models, /m", "Show available models"],
["/model <name>", "Switch to a different model"],
["/providers, /p", "Show all providers status"],
["/provider <name>", "Switch to a different provider"],
["/agent, /a", "Select agent"],
["/usage, /u", "Show token usage statistics"],
["/mcp [cmd]", "MCP server status/connect/disconnect/tools"],
["/session", "Show current session info"],
["/sessions", "List all saved sessions"],
["/save, /s", "Save current session"],
["/exit, /quit, /q", "Exit chat"],
];

View File

@@ -0,0 +1,35 @@
/**
* Home Screen Constants
* Constants for the welcome/home screen TUI layout
*/
/** Layout constants for home screen */
export const HOME_LAYOUT = {
maxWidth: 75,
topPadding: 3,
bottomPadding: 2,
horizontalPadding: 2,
logoGap: 1,
} as const;
/** Input placeholders shown in the prompt box */
export const PLACEHOLDERS = [
"Fix a TODO in the codebase",
"What is the tech stack of this project?",
"Fix broken tests",
"Explain how this function works",
"Refactor this code for readability",
"Add error handling to this function",
];
/** Keyboard hints displayed below the prompt box */
export const KEYBOARD_HINTS = {
agents: { key: "tab", label: "agents" },
commands: { key: "ctrl+p", label: "commands" },
} as const;
/** MCP status indicators */
export const MCP_INDICATORS = {
connected: "⊙",
error: "⊙",
} as const;

4
src/constants/home.ts Normal file
View File

@@ -0,0 +1,4 @@
export const HOME_VARS = {
title: "Welcome to CodeTyper - Your AI Coding Assistant",
subTitle: "Type a prompt below to start a new session",
};

View File

@@ -0,0 +1,30 @@
/**
* Input editor constants
*/
// Default prompts
export const INPUT_EDITOR_DEFAULTS = {
prompt: "\x1b[36m> \x1b[0m",
continuationPrompt: "\x1b[90m│ \x1b[0m",
} as const;
// ANSI escape sequences
export const ANSI = {
hideCursor: "\x1b[?25l",
showCursor: "\x1b[?25h",
clearLine: "\x1b[2K",
moveUp: (n: number) => `\x1b[${n}A`,
moveDown: (n: number) => `\x1b[${n}B`,
moveRight: (n: number) => `\x1b[${n}C`,
carriageReturn: "\r",
} as const;
// Special key sequences for Alt+Enter
export const ALT_ENTER_SEQUENCES = ["\x1b\r", "\x1b\n"] as const;
// Pasted text styling
export const PASTE_STYLE = {
// Gray/dim style for pasted text placeholder
start: "\x1b[90m",
end: "\x1b[0m",
} as const;

96
src/constants/learning.ts Normal file
View File

@@ -0,0 +1,96 @@
/**
* Learning Service constants
*/
import type { LearningCategory } from "@/types/learning";
export const LEARNING_PATTERNS = [
// User preferences
/always use (\w+)/i,
/prefer (\w+) over (\w+)/i,
/use (\.\w+) files?/i,
/don't use (\w+)/i,
/never use (\w+)/i,
/code in (\w+)/i,
/write in (\w+)/i,
// Project structure
/put .+ in (.+) directory/i,
/files? should be in (.+)/i,
/follow (.+) pattern/i,
/use (.+) architecture/i,
// Coding style
/use (.+) naming convention/i,
/follow (.+) style/i,
/indent with (\w+)/i,
/use (single|double) quotes/i,
// Testing
/use (.+) for testing/i,
/tests? should (.+)/i,
// Dependencies
/use (.+) library/i,
/prefer (.+) package/i,
] as const;
export const LEARNING_KEYWORDS = [
"always",
"never",
"prefer",
"convention",
"standard",
"pattern",
"rule",
"style",
"remember",
"important",
"must",
"should",
] as const;
export const ACKNOWLEDGMENT_PATTERNS = [
/i('ll| will) (use|follow|apply) (.+)/i,
/using (.+) as (you|per your) (requested|preference)/i,
/following (.+) (convention|pattern|style)/i,
/noted.+ (will|going to) (.+)/i,
] as const;
export const ACKNOWLEDGMENT_PHRASES = [
"i understand",
"got it",
"noted",
] as const;
export const LEARNING_DEFAULTS = {
BASE_PATTERN_CONFIDENCE: 0.7,
BASE_KEYWORD_CONFIDENCE: 0.5,
KEYWORD_CONFIDENCE_INCREMENT: 0.1,
ACKNOWLEDGMENT_CONFIDENCE: 0.8,
CONFIDENCE_BOOST: 0.2,
MAX_CONFIDENCE: 1.0,
MIN_KEYWORDS_FOR_LEARNING: 2,
MAX_CONTENT_LENGTH: 80,
TRUNCATE_LENGTH: 77,
MAX_SLICE_LENGTH: 100,
} as const;
export const LEARNING_CONTEXTS = {
USER_PREFERENCE: "User preference",
CONVENTION_IDENTIFIED: "Convention identified",
MULTIPLE_INDICATORS: "Multiple preference indicators",
CONVENTION_CONFIRMED: "Convention confirmed by assistant",
PREFERENCE_ACKNOWLEDGED: "Preference acknowledged by assistant",
} as const;
export const CATEGORY_PATTERNS: Record<string, LearningCategory> = {
prefer: "preference",
use: "preference",
directory: "architecture",
architecture: "architecture",
style: "style",
naming: "style",
indent: "style",
test: "testing",
};

28
src/constants/login.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* Login flow constants and messages
*/
export const LOGIN_MESSAGES = {
COPILOT_ALREADY_CONFIGURED: "✓ Copilot is already configured and working!",
COPILOT_STARTING_AUTH: "\nStarting GitHub device flow authentication...\n",
COPILOT_AUTH_INSTRUCTIONS: "To authenticate with GitHub Copilot:\n",
COPILOT_WAITING: "Waiting for authentication (press Ctrl+C to cancel)...\n",
COPILOT_SUCCESS: "\n✓ GitHub Copilot authenticated successfully!",
OLLAMA_SUCCESS: "\n✓ Connected to Ollama!",
OLLAMA_NO_MODELS: "\nNo models found. Pull a model with: ollama pull <model>",
AVAILABLE_MODELS: "\nAvailable models:",
VALIDATION_FAILED: "\n✗ Validation failed:",
AUTH_FAILED: "\n✗ Authentication failed:",
CONNECTION_FAILED: "\n✗ Failed to connect:",
UNKNOWN_PROVIDER: "Unknown provider:",
} as const;
export const LOGIN_PROMPTS = {
RECONFIGURE: "Do you want to re-authenticate?",
OLLAMA_HOST: "Ollama host URL:",
} as const;
export const AUTH_STEP_PREFIXES = {
OPEN_URL: " 1. Open:",
ENTER_CODE: " 2. Enter code:",
} as const;

View File

@@ -0,0 +1,41 @@
/**
* Mouse Handler Constants
*
* Constants for terminal mouse event handling
*/
// Mouse event button codes for SGR encoding
export const MOUSE_WHEEL_CODES = {
UP: 64,
DOWN: 65,
} as const;
// Default scroll lines per wheel event
export const MOUSE_SCROLL_LINES = 3;
// SGR mouse sequence pattern: \x1b[<Cb;Cx;Cy(M|m)
export const SGR_MOUSE_REGEX = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
// X10 mouse sequence pattern: \x1b[M followed by 3 bytes
export const X10_MOUSE_REGEX = /\x1b\[M[\x20-\xff]{3}/g;
// Partial/incomplete sequence patterns for cleanup
export const PARTIAL_SGR_REGEX = /\x1b\[<[\d;]*$/;
export const PARTIAL_X10_REGEX = /\x1b\[M.{0,2}$/;
// Mouse tracking escape sequences
export const MOUSE_TRACKING_SEQUENCES = {
ENABLE_BUTTON: "\x1b[?1000h",
ENABLE_SGR: "\x1b[?1006h",
DISABLE_SGR: "\x1b[?1006l",
DISABLE_BUTTON: "\x1b[?1000l",
} as const;
// Scroll direction type
export type MouseScrollDirection = "up" | "down";
// Button code to scroll direction mapping
export const MOUSE_BUTTON_TO_SCROLL: Record<number, MouseScrollDirection> = {
[MOUSE_WHEEL_CODES.UP]: "up",
[MOUSE_WHEEL_CODES.DOWN]: "down",
} as const;

View File

@@ -0,0 +1,38 @@
/**
* Mouse Scroll Constants
*
* Terminal escape sequences for mouse mode handling
*/
// Mouse mode enable/disable escape sequences
export const MOUSE_ESCAPE_SEQUENCES = {
ENABLE: "\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h",
DISABLE: "\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l",
} as const;
// Mouse button codes
export const MOUSE_BUTTON_CODES = {
SGR_SCROLL_UP: 64,
SGR_SCROLL_DOWN: 65,
X10_SCROLL_UP: 96, // 32 + 64
X10_SCROLL_DOWN: 97, // 32 + 65
} as const;
// SGR mouse mode regex pattern
export const SGR_MOUSE_PATTERN = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
// X10/Normal mouse mode prefix
export const X10_MOUSE_PREFIX = "\x1b[M";
export const X10_MIN_LENGTH = 6;
export const X10_BUTTON_OFFSET = 3;
// Scroll direction type
export type ScrollDirection = "up" | "down";
// Mouse button to scroll direction mapping
export const MOUSE_BUTTON_TO_DIRECTION: Record<number, ScrollDirection> = {
[MOUSE_BUTTON_CODES.SGR_SCROLL_UP]: "up",
[MOUSE_BUTTON_CODES.SGR_SCROLL_DOWN]: "down",
[MOUSE_BUTTON_CODES.X10_SCROLL_UP]: "up",
[MOUSE_BUTTON_CODES.X10_SCROLL_DOWN]: "down",
} as const;

32
src/constants/ollama.ts Normal file
View File

@@ -0,0 +1,32 @@
/**
* Ollama provider constants
*/
export const OLLAMA_PROVIDER_NAME = "ollama" as const;
export const OLLAMA_DISPLAY_NAME = "Ollama (Local)";
export const OLLAMA_DEFAULTS = {
BASE_URL: "http://localhost:11434",
MODEL: "deepseek-coder:6.7b",
} as const;
export const OLLAMA_ENDPOINTS = {
TAGS: "/api/tags",
CHAT: "/api/chat",
PULL: "/api/pull",
} as const;
export const OLLAMA_TIMEOUTS = {
VALIDATION: 5000,
CHAT: 120000,
} as const;
export const OLLAMA_CHAT_OPTIONS = {
DEFAULT_TEMPERATURE: 0.3,
DEFAULT_MAX_TOKENS: 4096,
} as const;
export const OLLAMA_ERRORS = {
NOT_RUNNING: (baseUrl: string) =>
`Ollama not running at ${baseUrl}. Start it with: ollama serve`,
} as const;

12
src/constants/paste.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* Constants for paste virtual text feature
*/
/** Minimum number of lines to trigger paste summary */
export const PASTE_LINE_THRESHOLD = 3;
/** Minimum character count to trigger paste summary */
export const PASTE_CHAR_THRESHOLD = 150;
/** Format for paste placeholder display */
export const PASTE_PLACEHOLDER_FORMAT = "[Pasted ~{lineCount} lines]";

96
src/constants/paths.ts Normal file
View File

@@ -0,0 +1,96 @@
/**
* XDG-compliant storage paths
*
* Follows the XDG Base Directory Specification:
* - XDG_CONFIG_HOME: User configuration (~/.config)
* - XDG_DATA_HOME: User data (~/.local/share)
* - XDG_CACHE_HOME: Cache data (~/.cache)
* - XDG_STATE_HOME: State data (~/.local/state)
*
* See: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
*/
import { homedir } from "os";
import { join } from "path";
const APP_NAME = "codetyper";
/**
* XDG base directories with fallbacks
*/
export const XDG = {
config: process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
data: process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"),
cache: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"),
state: process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"),
} as const;
/**
* Application directories
*/
export const DIRS = {
/** Configuration directory (~/.config/codetyper) */
config: join(XDG.config, APP_NAME),
/** Data directory (~/.local/share/codetyper) */
data: join(XDG.data, APP_NAME),
/** Cache directory (~/.cache/codetyper) */
cache: join(XDG.cache, APP_NAME),
/** State directory (~/.local/state/codetyper) */
state: join(XDG.state, APP_NAME),
/** Sessions directory (~/.local/share/codetyper/sessions) */
sessions: join(XDG.data, APP_NAME, "sessions"),
} as const;
/**
* Application files
*/
export const FILES = {
/** Main configuration file */
config: join(DIRS.config, "config.json"),
/** Keybindings configuration */
keybindings: join(DIRS.config, "keybindings.json"),
/** Provider credentials (stored in data, not config) */
credentials: join(DIRS.data, "credentials.json"),
/** Command history */
history: join(DIRS.data, "history.json"),
/** Models cache */
modelsCache: join(DIRS.cache, "models.json"),
/** Frecency cache for file/command suggestions */
frecency: join(DIRS.cache, "frecency.json"),
/** Key-value state storage */
kvStore: join(DIRS.state, "kv.json"),
/** Global settings (permissions, etc.) */
settings: join(DIRS.config, "settings.json"),
} as const;
/**
* Local project config directory name
*/
export const LOCAL_CONFIG_DIR = ".codetyper";
export const IGNORE_FOLDERS = [
"**/node_modules/**",
"**/.git/**",
"**/.codetyper/**",
"**/.vscode/**",
"**/.idea/**",
"**/__pycache__/**",
"**/.DS_Store/**",
"**/dist/**",
"**/build/**",
"**/out/**",
"**/.next/**",
"**/.nuxt/**",
"**/venv/**",
];

View File

@@ -0,0 +1 @@
export const FILE_REFERENCE_PATTERN = /@(?:"([^"]+)"|'([^']+)'|(\S+))/g;

View File

@@ -0,0 +1,127 @@
import type { TaskType } from "@/types/provider-quality";
export const QUALITY_THRESHOLDS = {
HIGH: 0.85,
MEDIUM: 0.6,
LOW: 0.4,
INITIAL: 0.5,
} as const;
export const SCORE_ADJUSTMENTS = {
APPROVAL: 0.05,
CORRECTION: -0.08,
USER_REJECTION: -0.15,
MINOR_ISSUE: -0.02,
MAJOR_ISSUE: -0.05,
} as const;
export const TASK_TYPE_PATTERNS: Record<TaskType, RegExp[]> = {
code_generation: [
/create\s+(a\s+)?function/i,
/write\s+(a\s+)?code/i,
/implement/i,
/generate\s+(a\s+)?/i,
/add\s+(a\s+)?(new\s+)?feature/i,
/build\s+(a\s+)?/i,
],
refactoring: [
/refactor/i,
/clean\s*up/i,
/restructure/i,
/reorganize/i,
/simplify/i,
/improve\s+(the\s+)?code/i,
],
bug_fix: [
/fix/i,
/bug/i,
/error/i,
/issue/i,
/not\s+working/i,
/broken/i,
/problem/i,
/debug/i,
],
documentation: [
/document/i,
/comment/i,
/readme/i,
/explain\s+.*\s+code/i,
/add\s+.*\s+docs/i,
/jsdoc/i,
/tsdoc/i,
],
testing: [
/test/i,
/spec/i,
/unit\s+test/i,
/integration/i,
/coverage/i,
/mock/i,
],
explanation: [
/explain/i,
/what\s+(does|is)/i,
/how\s+(does|do)/i,
/why/i,
/understand/i,
/clarify/i,
],
review: [
/review/i,
/check/i,
/evaluate/i,
/assess/i,
/audit/i,
/pr\s+review/i,
],
general: [],
};
export const NEGATIVE_FEEDBACK_PATTERNS = [
/fix\s+this/i,
/that'?s?\s+(wrong|incorrect)/i,
/not\s+(good|right|correct|working)/i,
/doesn'?t?\s+work/i,
/incorrect/i,
/broken/i,
/bad\s+(code|response)/i,
/try\s+again/i,
/redo/i,
/wrong/i,
];
export const POSITIVE_FEEDBACK_PATTERNS = [
/thanks/i,
/thank\s+you/i,
/perfect/i,
/great/i,
/works/i,
/good\s+(job|work)/i,
/excellent/i,
/awesome/i,
/exactly/i,
];
export const DEFAULT_QUALITY_SCORES: Record<TaskType, number> = {
code_generation: QUALITY_THRESHOLDS.INITIAL,
refactoring: QUALITY_THRESHOLDS.INITIAL,
bug_fix: QUALITY_THRESHOLDS.INITIAL,
documentation: QUALITY_THRESHOLDS.INITIAL,
testing: QUALITY_THRESHOLDS.INITIAL,
explanation: QUALITY_THRESHOLDS.INITIAL,
review: QUALITY_THRESHOLDS.INITIAL,
general: QUALITY_THRESHOLDS.INITIAL,
};
export const PROVIDER_IDS = {
OLLAMA: "ollama",
COPILOT: "copilot",
} as const;
export const CASCADE_CONFIG = {
MIN_AUDIT_THRESHOLD: QUALITY_THRESHOLDS.HIGH,
MAX_SKIP_THRESHOLD: QUALITY_THRESHOLDS.LOW,
DECAY_RATE: 0.01,
DECAY_INTERVAL_MS: 7 * 24 * 60 * 60 * 1000, // 1 week
} as const;

Some files were not shown because too many files have changed in this diff Show More