Remove old implementation files (main.ts, cli.ts, tui-renderer.ts), rename new files

This commit is contained in:
Mario Zechner 2025-12-09 01:21:28 +01:00
parent 1a6a1a8acf
commit 6c9a264b63
8 changed files with 206 additions and 6396 deletions

File diff suppressed because it is too large Load diff

View file

@ -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));

View file

@ -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));

View file

@ -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";

View file

@ -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