mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
Remove old implementation files (main.ts, cli.ts, tui-renderer.ts), rename new files
This commit is contained in:
parent
1a6a1a8acf
commit
6c9a264b63
8 changed files with 206 additions and 6396 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI entry point for the refactored coding agent.
|
||||
* Uses main-new.ts with AgentSession and new mode modules.
|
||||
*
|
||||
* Test with: npx tsx src/cli-new.ts [args...]
|
||||
*/
|
||||
import { main } from "./main-new.js";
|
||||
|
||||
main(process.argv.slice(2));
|
||||
|
|
@ -1,23 +1,10 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Suppress punycode deprecation warning from dependencies
|
||||
// This warning comes from old dependencies still using the deprecated punycode module
|
||||
const originalEmit = process.emit;
|
||||
// @ts-expect-error - Monkey-patch emit to filter warnings
|
||||
process.emit = (event, ...args) => {
|
||||
if (event === "warning") {
|
||||
const warning = args[0] as any;
|
||||
if (warning?.name === "DeprecationWarning" && warning?.code === "DEP0040") {
|
||||
return false; // Suppress punycode deprecation
|
||||
}
|
||||
}
|
||||
// @ts-expect-error - Call original with event and args
|
||||
return originalEmit.apply(process, [event, ...args]);
|
||||
};
|
||||
|
||||
/**
|
||||
* CLI entry point for the refactored coding agent.
|
||||
* Uses main.ts with AgentSession and new mode modules.
|
||||
*
|
||||
* Test with: npx tsx src/cli-new.ts [args...]
|
||||
*/
|
||||
import { main } from "./main.js";
|
||||
|
||||
main(process.argv.slice(2)).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
main(process.argv.slice(2));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import chalk from "chalk";
|
||||
import { allTools, type ToolName } from "../core/tools/index.js";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR, getModelsPath } from "../utils/config.js";
|
||||
import { APP_NAME, CONFIG_DIR_NAME, ENV_AGENT_DIR } from "../utils/config.js";
|
||||
|
||||
export type Mode = "text" | "json" | "rpc";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,460 +0,0 @@
|
|||
/**
|
||||
* Main entry point for the coding agent
|
||||
*/
|
||||
|
||||
import { Agent, type Attachment, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import chalk from "chalk";
|
||||
import { type Args, parseArgs, printHelp } from "./cli/args.js";
|
||||
import { processFileArguments } from "./cli/file-processor.js";
|
||||
import { selectSession } from "./cli/session-picker.js";
|
||||
import { AgentSession } from "./core/agent-session.js";
|
||||
import { exportFromFile } from "./core/export-html.js";
|
||||
import { messageTransformer } from "./core/messages.js";
|
||||
import { findModel, getApiKeyForModel, getAvailableModels } from "./core/model-config.js";
|
||||
import { resolveModelScope, restoreModelFromSession, type ScopedModel } from "./core/model-resolver.js";
|
||||
import { SessionManager } from "./core/session-manager.js";
|
||||
import { SettingsManager } from "./core/settings-manager.js";
|
||||
import { loadSlashCommands } from "./core/slash-commands.js";
|
||||
import { buildSystemPrompt, loadProjectContextFiles } from "./core/system-prompt.js";
|
||||
import { allTools, codingTools } from "./core/tools/index.js";
|
||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||
import { initTheme } from "./modes/interactive/theme/theme.js";
|
||||
import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog.js";
|
||||
import { getModelsPath, VERSION } from "./utils/config.js";
|
||||
import { ensureTool } from "./utils/tools-manager.js";
|
||||
|
||||
/** Check npm registry for new version (non-blocking) */
|
||||
async function checkForNewVersion(currentVersion: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest");
|
||||
if (!response.ok) return null;
|
||||
|
||||
const data = (await response.json()) as { version?: string };
|
||||
const latestVersion = data.version;
|
||||
|
||||
if (latestVersion && latestVersion !== currentVersion) {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
// Silently fail - don't disrupt the user experience
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Run interactive mode with TUI */
|
||||
async function runInteractiveMode(
|
||||
session: AgentSession,
|
||||
version: string,
|
||||
changelogMarkdown: string | null,
|
||||
modelFallbackMessage: string | null,
|
||||
versionCheckPromise: Promise<string | null>,
|
||||
initialMessages: string[],
|
||||
initialMessage?: string,
|
||||
initialAttachments?: Attachment[],
|
||||
fdPath: string | null = null,
|
||||
): Promise<void> {
|
||||
const mode = new InteractiveMode(session, version, changelogMarkdown, fdPath);
|
||||
|
||||
// Initialize TUI (subscribes to agent events internally)
|
||||
await mode.init();
|
||||
|
||||
// Handle version check result when it completes (don't block)
|
||||
versionCheckPromise.then((newVersion) => {
|
||||
if (newVersion) {
|
||||
mode.showNewVersionNotification(newVersion);
|
||||
}
|
||||
});
|
||||
|
||||
// Render any existing messages (from --continue mode)
|
||||
mode.renderInitialMessages(session.state);
|
||||
|
||||
// Show model fallback warning at the end of the chat if applicable
|
||||
if (modelFallbackMessage) {
|
||||
mode.showWarning(modelFallbackMessage);
|
||||
}
|
||||
|
||||
// Process initial message with attachments if provided (from @file args)
|
||||
if (initialMessage) {
|
||||
try {
|
||||
await session.prompt(initialMessage, { attachments: initialAttachments });
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
mode.showError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining initial messages if provided (from CLI args)
|
||||
for (const message of initialMessages) {
|
||||
try {
|
||||
await session.prompt(message);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
mode.showError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive loop
|
||||
while (true) {
|
||||
const userInput = await mode.getUserInput();
|
||||
|
||||
// Process the message
|
||||
try {
|
||||
await session.prompt(userInput);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
mode.showError(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Prepare initial message from @file arguments */
|
||||
function prepareInitialMessage(parsed: Args): {
|
||||
initialMessage?: string;
|
||||
initialAttachments?: Attachment[];
|
||||
} {
|
||||
if (parsed.fileArgs.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { textContent, imageAttachments } = processFileArguments(parsed.fileArgs);
|
||||
|
||||
// Combine file content with first plain text message (if any)
|
||||
let initialMessage: string;
|
||||
if (parsed.messages.length > 0) {
|
||||
initialMessage = textContent + parsed.messages[0];
|
||||
parsed.messages.shift(); // Remove first message as it's been combined
|
||||
} else {
|
||||
initialMessage = textContent;
|
||||
}
|
||||
|
||||
return {
|
||||
initialMessage,
|
||||
initialAttachments: imageAttachments.length > 0 ? imageAttachments : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function main(args: string[]) {
|
||||
const parsed = parseArgs(args);
|
||||
|
||||
if (parsed.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle --export flag: convert session file to HTML and exit
|
||||
if (parsed.export) {
|
||||
try {
|
||||
const outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;
|
||||
const result = exportFromFile(parsed.export, outputPath);
|
||||
console.log(`Exported to: ${result}`);
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Failed to export session";
|
||||
console.error(chalk.red(`Error: ${message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate: RPC mode doesn't support @file arguments
|
||||
if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {
|
||||
console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Process @file arguments
|
||||
const { initialMessage, initialAttachments } = prepareInitialMessage(parsed);
|
||||
|
||||
// Initialize theme (before any TUI rendering)
|
||||
const settingsManager = new SettingsManager();
|
||||
const themeName = settingsManager.getTheme();
|
||||
initTheme(themeName);
|
||||
|
||||
// Setup session manager
|
||||
const sessionManager = new SessionManager(parsed.continue && !parsed.resume, parsed.session);
|
||||
|
||||
if (parsed.noSession) {
|
||||
sessionManager.disable();
|
||||
}
|
||||
|
||||
// Handle --resume flag: show session selector
|
||||
if (parsed.resume) {
|
||||
const selectedSession = await selectSession(sessionManager);
|
||||
if (!selectedSession) {
|
||||
console.log(chalk.dim("No session selected"));
|
||||
return;
|
||||
}
|
||||
sessionManager.setSessionFile(selectedSession);
|
||||
}
|
||||
|
||||
// Resolve model scope early if provided
|
||||
let scopedModels: ScopedModel[] = [];
|
||||
if (parsed.models && parsed.models.length > 0) {
|
||||
scopedModels = await resolveModelScope(parsed.models);
|
||||
}
|
||||
|
||||
// Determine mode and output behavior
|
||||
const isInteractive = !parsed.print && parsed.mode === undefined;
|
||||
const mode = parsed.mode || "text";
|
||||
const shouldPrintMessages = isInteractive;
|
||||
|
||||
// Find initial model
|
||||
let initialModel = await findInitialModelForSession(parsed, scopedModels, settingsManager);
|
||||
let initialThinking: ThinkingLevel = "off";
|
||||
|
||||
// Get thinking level from scoped models if applicable
|
||||
if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
||||
initialThinking = scopedModels[0].thinkingLevel;
|
||||
} else {
|
||||
// Try saved thinking level
|
||||
const savedThinking = settingsManager.getDefaultThinkingLevel();
|
||||
if (savedThinking) {
|
||||
initialThinking = savedThinking;
|
||||
}
|
||||
}
|
||||
|
||||
// Non-interactive mode: fail early if no model available
|
||||
if (!isInteractive && !initialModel) {
|
||||
console.error(chalk.red("No models available."));
|
||||
console.error(chalk.yellow("\nSet an API key environment variable:"));
|
||||
console.error(" ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.");
|
||||
console.error(chalk.yellow(`\nOr create ${getModelsPath()}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Non-interactive mode: validate API key exists
|
||||
if (!isInteractive && initialModel) {
|
||||
const apiKey = parsed.apiKey || (await getApiKeyForModel(initialModel));
|
||||
if (!apiKey) {
|
||||
console.error(chalk.red(`No API key found for ${initialModel.provider}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Build system prompt
|
||||
const systemPrompt = buildSystemPrompt(parsed.systemPrompt, parsed.tools, parsed.appendSystemPrompt);
|
||||
|
||||
// Handle session restoration
|
||||
let modelFallbackMessage: string | null = null;
|
||||
|
||||
if (parsed.continue || parsed.resume) {
|
||||
const savedModel = sessionManager.loadModel();
|
||||
if (savedModel) {
|
||||
const result = await restoreModelFromSession(
|
||||
savedModel.provider,
|
||||
savedModel.modelId,
|
||||
initialModel,
|
||||
shouldPrintMessages,
|
||||
);
|
||||
|
||||
if (result.model) {
|
||||
initialModel = result.model;
|
||||
}
|
||||
modelFallbackMessage = result.fallbackMessage;
|
||||
}
|
||||
|
||||
// Load and restore thinking level
|
||||
const thinkingLevel = sessionManager.loadThinkingLevel() as ThinkingLevel;
|
||||
if (thinkingLevel) {
|
||||
initialThinking = thinkingLevel;
|
||||
if (shouldPrintMessages) {
|
||||
console.log(chalk.dim(`Restored thinking level: ${thinkingLevel}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLI --thinking flag takes highest priority
|
||||
if (parsed.thinking) {
|
||||
initialThinking = parsed.thinking;
|
||||
}
|
||||
|
||||
// Determine which tools to use
|
||||
const selectedTools = parsed.tools ? parsed.tools.map((name) => allTools[name]) : codingTools;
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
systemPrompt,
|
||||
model: initialModel as any, // Can be null in interactive mode
|
||||
thinkingLevel: initialThinking,
|
||||
tools: selectedTools,
|
||||
},
|
||||
messageTransformer,
|
||||
queueMode: settingsManager.getQueueMode(),
|
||||
transport: new ProviderTransport({
|
||||
getApiKey: async () => {
|
||||
const currentModel = agent.state.model;
|
||||
if (!currentModel) {
|
||||
throw new Error("No model selected");
|
||||
}
|
||||
|
||||
if (parsed.apiKey) {
|
||||
return parsed.apiKey;
|
||||
}
|
||||
|
||||
const key = await getApiKeyForModel(currentModel);
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
`No API key found for provider "${currentModel.provider}". Please set the appropriate environment variable or update ${getModelsPath()}`,
|
||||
);
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// If initial thinking was requested but model doesn't support it, reset to off
|
||||
if (initialThinking !== "off" && initialModel && !initialModel.reasoning) {
|
||||
agent.setThinkingLevel("off");
|
||||
}
|
||||
|
||||
// Load previous messages if continuing or resuming
|
||||
if (parsed.continue || parsed.resume) {
|
||||
const messages = sessionManager.loadMessages();
|
||||
if (messages.length > 0) {
|
||||
agent.replaceMessages(messages);
|
||||
}
|
||||
}
|
||||
|
||||
// Log loaded context files
|
||||
if (shouldPrintMessages && !parsed.continue && !parsed.resume) {
|
||||
const contextFiles = loadProjectContextFiles();
|
||||
if (contextFiles.length > 0) {
|
||||
console.log(chalk.dim("Loaded project context from:"));
|
||||
for (const { path: filePath } of contextFiles) {
|
||||
console.log(chalk.dim(` - ${filePath}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load file commands for slash command expansion
|
||||
const fileCommands = loadSlashCommands();
|
||||
|
||||
// Create session
|
||||
const session = new AgentSession({
|
||||
agent,
|
||||
sessionManager,
|
||||
settingsManager,
|
||||
scopedModels,
|
||||
fileCommands,
|
||||
});
|
||||
|
||||
// Route to appropriate mode
|
||||
if (mode === "rpc") {
|
||||
await runRpcMode(session);
|
||||
} else if (isInteractive) {
|
||||
// Check for new version in the background
|
||||
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);
|
||||
|
||||
// Check if we should show changelog
|
||||
const changelogMarkdown = getChangelogForDisplay(parsed, settingsManager);
|
||||
|
||||
// Show model scope if provided
|
||||
if (scopedModels.length > 0) {
|
||||
const modelList = scopedModels
|
||||
.map((sm) => {
|
||||
const thinkingStr = sm.thinkingLevel !== "off" ? `:${sm.thinkingLevel}` : "";
|
||||
return `${sm.model.id}${thinkingStr}`;
|
||||
})
|
||||
.join(", ");
|
||||
console.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Ctrl+P to cycle)")}`));
|
||||
}
|
||||
|
||||
// Ensure fd tool is available for file autocomplete
|
||||
const fdPath = await ensureTool("fd");
|
||||
|
||||
await runInteractiveMode(
|
||||
session,
|
||||
VERSION,
|
||||
changelogMarkdown,
|
||||
modelFallbackMessage,
|
||||
versionCheckPromise,
|
||||
parsed.messages,
|
||||
initialMessage,
|
||||
initialAttachments,
|
||||
fdPath,
|
||||
);
|
||||
} else {
|
||||
// Non-interactive mode (--print flag or --mode flag)
|
||||
await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
/** Find initial model based on CLI args, scoped models, settings, or available models */
|
||||
async function findInitialModelForSession(parsed: Args, scopedModels: ScopedModel[], settingsManager: SettingsManager) {
|
||||
// 1. CLI args take priority
|
||||
if (parsed.provider && parsed.model) {
|
||||
const { model, error } = findModel(parsed.provider, parsed.model);
|
||||
if (error) {
|
||||
console.error(chalk.red(error));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!model) {
|
||||
console.error(chalk.red(`Model ${parsed.provider}/${parsed.model} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// 2. Use first model from scoped models (skip if continuing/resuming)
|
||||
if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
||||
return scopedModels[0].model;
|
||||
}
|
||||
|
||||
// 3. Try saved default from settings
|
||||
const defaultProvider = settingsManager.getDefaultProvider();
|
||||
const defaultModelId = settingsManager.getDefaultModel();
|
||||
if (defaultProvider && defaultModelId) {
|
||||
const { model, error } = findModel(defaultProvider, defaultModelId);
|
||||
if (error) {
|
||||
console.error(chalk.red(error));
|
||||
process.exit(1);
|
||||
}
|
||||
if (model) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Try first available model with valid API key
|
||||
const { models: availableModels, error } = await getAvailableModels();
|
||||
|
||||
if (error) {
|
||||
console.error(chalk.red(error));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (availableModels.length > 0) {
|
||||
return availableModels[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Get changelog markdown to display (only for new sessions with updates) */
|
||||
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {
|
||||
if (parsed.continue || parsed.resume) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastVersion = settingsManager.getLastChangelogVersion();
|
||||
const changelogPath = getChangelogPath();
|
||||
const entries = parseChangelog(changelogPath);
|
||||
|
||||
if (!lastVersion) {
|
||||
// First run - show all entries
|
||||
if (entries.length > 0) {
|
||||
settingsManager.setLastChangelogVersion(VERSION);
|
||||
return entries.map((e) => e.content).join("\n\n");
|
||||
}
|
||||
} else {
|
||||
// Check for new entries since last version
|
||||
const newEntries = getNewEntries(entries, lastVersion);
|
||||
if (newEntries.length > 0) {
|
||||
settingsManager.setLastChangelogVersion(VERSION);
|
||||
return newEntries.map((e) => e.content).join("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue