mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-22 02:03:42 +00:00
Refactor main.ts to use SDK createAgentSession
- Add continueSession, restoreFromSession, additionalHookPaths, additionalCustomToolPaths, scopedModels to CreateAgentSessionOptions - Return CreateAgentSessionResult with session, customToolsResult, and modelFallbackMessage - main.ts now focuses on CLI arg parsing and mode routing - Reduced main.ts from ~490 to ~340 lines
This commit is contained in:
parent
bfc1c44791
commit
4398596d41
3 changed files with 367 additions and 448 deletions
|
|
@ -82,6 +82,8 @@ export interface CreateAgentSessionOptions {
|
||||||
model?: Model<any>;
|
model?: Model<any>;
|
||||||
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
|
/** Thinking level. Default: from settings, else 'off' (clamped to model capabilities) */
|
||||||
thinkingLevel?: ThinkingLevel;
|
thinkingLevel?: ThinkingLevel;
|
||||||
|
/** Models available for cycling (Ctrl+P in interactive mode) */
|
||||||
|
scopedModels?: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }>;
|
||||||
|
|
||||||
// === API Key ===
|
// === API Key ===
|
||||||
/** API key resolver. Default: defaultGetApiKey() */
|
/** API key resolver. Default: defaultGetApiKey() */
|
||||||
|
|
@ -94,12 +96,16 @@ export interface CreateAgentSessionOptions {
|
||||||
// === Tools ===
|
// === Tools ===
|
||||||
/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
|
/** Built-in tools to use. Default: codingTools [read, bash, edit, write] */
|
||||||
tools?: Tool[];
|
tools?: Tool[];
|
||||||
/** Custom tools. Default: discovered from cwd/.pi/tools/ + agentDir/tools/ */
|
/** Custom tools (replaces discovery). */
|
||||||
customTools?: Array<{ path?: string; tool: CustomAgentTool }>;
|
customTools?: Array<{ path?: string; tool: CustomAgentTool }>;
|
||||||
|
/** Additional custom tool paths to load (merged with discovery). */
|
||||||
|
additionalCustomToolPaths?: string[];
|
||||||
|
|
||||||
// === Hooks ===
|
// === Hooks ===
|
||||||
/** Hooks. Default: discovered from cwd/.pi/hooks/ + agentDir/hooks/ */
|
/** Hooks (replaces discovery). */
|
||||||
hooks?: Array<{ path?: string; factory: HookFactory }>;
|
hooks?: Array<{ path?: string; factory: HookFactory }>;
|
||||||
|
/** Additional hook paths to load (merged with discovery). */
|
||||||
|
additionalHookPaths?: string[];
|
||||||
|
|
||||||
// === Context ===
|
// === Context ===
|
||||||
/** Skills. Default: discovered from multiple locations */
|
/** Skills. Default: discovered from multiple locations */
|
||||||
|
|
@ -112,12 +118,29 @@ export interface CreateAgentSessionOptions {
|
||||||
// === Session ===
|
// === Session ===
|
||||||
/** Session file path, or false to disable persistence. Default: auto in agentDir/sessions/ */
|
/** Session file path, or false to disable persistence. Default: auto in agentDir/sessions/ */
|
||||||
sessionFile?: string | false;
|
sessionFile?: string | false;
|
||||||
|
/** Continue most recent session for cwd. */
|
||||||
|
continueSession?: boolean;
|
||||||
|
/** Restore model/thinking from session (default: true when continuing). */
|
||||||
|
restoreFromSession?: boolean;
|
||||||
|
|
||||||
// === Settings ===
|
// === Settings ===
|
||||||
/** Settings overrides (merged with agentDir/settings.json) */
|
/** Settings overrides (merged with agentDir/settings.json) */
|
||||||
settings?: Partial<Settings>;
|
settings?: Partial<Settings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from createAgentSession */
|
||||||
|
export interface CreateAgentSessionResult {
|
||||||
|
/** The created session */
|
||||||
|
session: AgentSession;
|
||||||
|
/** Custom tools result (for UI context setup in interactive mode) */
|
||||||
|
customToolsResult: {
|
||||||
|
tools: LoadedCustomTool[];
|
||||||
|
setUIContext: (uiContext: any, hasUI: boolean) => void;
|
||||||
|
};
|
||||||
|
/** Warning if session was restored with a different model than saved */
|
||||||
|
modelFallbackMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Re-exports
|
// Re-exports
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -394,16 +417,21 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // Minimal - uses defaults
|
* // Minimal - uses defaults
|
||||||
* const session = await createAgentSession();
|
* const { session } = await createAgentSession();
|
||||||
*
|
*
|
||||||
* // With explicit model
|
* // With explicit model
|
||||||
* const session = await createAgentSession({
|
* const { session } = await createAgentSession({
|
||||||
* model: findModel('anthropic', 'claude-sonnet-4-20250514'),
|
* model: findModel('anthropic', 'claude-sonnet-4-20250514'),
|
||||||
* thinkingLevel: 'high',
|
* thinkingLevel: 'high',
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
|
* // Continue previous session
|
||||||
|
* const { session, modelFallbackMessage } = await createAgentSession({
|
||||||
|
* continueSession: true,
|
||||||
|
* });
|
||||||
|
*
|
||||||
* // Full control
|
* // Full control
|
||||||
* const session = await createAgentSession({
|
* const { session } = await createAgentSession({
|
||||||
* model: myModel,
|
* model: myModel,
|
||||||
* getApiKey: async () => process.env.MY_KEY,
|
* getApiKey: async () => process.env.MY_KEY,
|
||||||
* systemPrompt: 'You are helpful.',
|
* systemPrompt: 'You are helpful.',
|
||||||
|
|
@ -414,48 +442,90 @@ function createLoadedHooksFromDefinitions(definitions: Array<{ path?: string; fa
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<AgentSession> {
|
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
|
||||||
const cwd = options.cwd ?? process.cwd();
|
const cwd = options.cwd ?? process.cwd();
|
||||||
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
const agentDir = options.agentDir ?? getDefaultAgentDir();
|
||||||
|
|
||||||
// === Settings ===
|
// === Settings ===
|
||||||
const settingsManager = new SettingsManager(agentDir);
|
const settingsManager = new SettingsManager(agentDir);
|
||||||
|
|
||||||
|
// === Session Manager ===
|
||||||
|
const sessionManager = new SessionManager(options.continueSession ?? false, undefined);
|
||||||
|
if (options.sessionFile === false) {
|
||||||
|
sessionManager.disable();
|
||||||
|
} else if (typeof options.sessionFile === "string") {
|
||||||
|
sessionManager.setSessionFile(options.sessionFile);
|
||||||
|
}
|
||||||
|
|
||||||
// === Model Resolution ===
|
// === Model Resolution ===
|
||||||
let model = options.model;
|
let model = options.model;
|
||||||
|
let modelFallbackMessage: string | undefined;
|
||||||
|
const shouldRestoreFromSession = options.restoreFromSession ?? (options.continueSession || options.sessionFile);
|
||||||
|
|
||||||
|
// If continuing/restoring, try to get model from session first
|
||||||
|
if (!model && shouldRestoreFromSession) {
|
||||||
|
const savedModel = sessionManager.loadModel();
|
||||||
|
if (savedModel) {
|
||||||
|
const restoredModel = findModel(savedModel.provider, savedModel.modelId);
|
||||||
|
if (restoredModel) {
|
||||||
|
const key = await getApiKeyForModel(restoredModel);
|
||||||
|
if (key) {
|
||||||
|
model = restoredModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we couldn't restore, we'll fall back below and set fallback message
|
||||||
|
if (!model) {
|
||||||
|
modelFallbackMessage = `Could not restore model ${savedModel.provider}/${savedModel.modelId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no model, try settings default
|
||||||
if (!model) {
|
if (!model) {
|
||||||
// Try settings default
|
|
||||||
const defaultProvider = settingsManager.getDefaultProvider();
|
const defaultProvider = settingsManager.getDefaultProvider();
|
||||||
const defaultModelId = settingsManager.getDefaultModel();
|
const defaultModelId = settingsManager.getDefaultModel();
|
||||||
if (defaultProvider && defaultModelId) {
|
if (defaultProvider && defaultModelId) {
|
||||||
model = findModel(defaultProvider, defaultModelId) ?? undefined;
|
const settingsModel = findModel(defaultProvider, defaultModelId);
|
||||||
// Verify it has an API key
|
if (settingsModel) {
|
||||||
if (model) {
|
const key = await getApiKeyForModel(settingsModel);
|
||||||
const key = await getApiKeyForModel(model);
|
if (key) {
|
||||||
if (!key) {
|
model = settingsModel;
|
||||||
model = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fall back to first available
|
// Fall back to first available
|
||||||
if (!model) {
|
if (!model) {
|
||||||
const available = await discoverAvailableModels();
|
const available = await discoverAvailableModels();
|
||||||
if (available.length === 0) {
|
if (available.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No models available. Set an API key environment variable " +
|
"No models available. Set an API key environment variable " +
|
||||||
"(ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) or provide a model explicitly.",
|
"(ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) or provide a model explicitly.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
model = available[0];
|
model = available[0];
|
||||||
|
if (modelFallbackMessage) {
|
||||||
|
modelFallbackMessage += `. Using ${model.provider}/${model.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Thinking Level Resolution ===
|
// === Thinking Level Resolution ===
|
||||||
let thinkingLevel = options.thinkingLevel;
|
let thinkingLevel = options.thinkingLevel;
|
||||||
|
|
||||||
|
// If continuing/restoring, try to get thinking level from session
|
||||||
|
if (thinkingLevel === undefined && shouldRestoreFromSession) {
|
||||||
|
const savedThinking = sessionManager.loadThinkingLevel();
|
||||||
|
if (savedThinking) {
|
||||||
|
thinkingLevel = savedThinking as ThinkingLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to settings default
|
||||||
if (thinkingLevel === undefined) {
|
if (thinkingLevel === undefined) {
|
||||||
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? "off";
|
thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? "off";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp to model capabilities
|
// Clamp to model capabilities
|
||||||
if (!model.reasoning) {
|
if (!model.reasoning) {
|
||||||
thinkingLevel = "off";
|
thinkingLevel = "off";
|
||||||
|
|
@ -487,13 +557,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||||
setUIContext: () => {},
|
setUIContext: () => {},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Discover custom tools
|
// Discover custom tools, merging with additional paths
|
||||||
const result = await discoverAndLoadCustomTools(
|
const configuredPaths = [...settingsManager.getCustomToolPaths(), ...(options.additionalCustomToolPaths ?? [])];
|
||||||
settingsManager.getCustomToolPaths(),
|
const result = await discoverAndLoadCustomTools(configuredPaths, cwd, Object.keys(allTools), agentDir);
|
||||||
cwd,
|
|
||||||
Object.keys(allTools),
|
|
||||||
agentDir,
|
|
||||||
);
|
|
||||||
for (const { path, error } of result.errors) {
|
for (const { path, error } of result.errors) {
|
||||||
console.error(`Failed to load custom tool "${path}": ${error}`);
|
console.error(`Failed to load custom tool "${path}": ${error}`);
|
||||||
}
|
}
|
||||||
|
|
@ -508,8 +574,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||||
hookRunner = new HookRunner(loadedHooks, cwd, settingsManager.getHookTimeout());
|
hookRunner = new HookRunner(loadedHooks, cwd, settingsManager.getHookTimeout());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Discover hooks
|
// Discover hooks, merging with additional paths
|
||||||
const { hooks, errors } = await discoverAndLoadHooks(settingsManager.getHookPaths(), cwd, agentDir);
|
const configuredPaths = [...settingsManager.getHookPaths(), ...(options.additionalHookPaths ?? [])];
|
||||||
|
const { hooks, errors } = await discoverAndLoadHooks(configuredPaths, cwd, agentDir);
|
||||||
for (const { path, error } of errors) {
|
for (const { path, error } of errors) {
|
||||||
console.error(`Failed to load hook "${path}": ${error}`);
|
console.error(`Failed to load hook "${path}": ${error}`);
|
||||||
}
|
}
|
||||||
|
|
@ -544,15 +611,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||||
// === Slash Commands ===
|
// === Slash Commands ===
|
||||||
const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);
|
const slashCommands = options.slashCommands ?? discoverSlashCommands(cwd, agentDir);
|
||||||
|
|
||||||
// === Session Manager ===
|
|
||||||
const sessionManager = new SessionManager(false, undefined);
|
|
||||||
if (options.sessionFile === false) {
|
|
||||||
sessionManager.disable();
|
|
||||||
} else if (typeof options.sessionFile === "string") {
|
|
||||||
sessionManager.setSessionFile(options.sessionFile);
|
|
||||||
}
|
|
||||||
// If undefined, SessionManager uses auto-detection based on cwd
|
|
||||||
|
|
||||||
// === Create Agent ===
|
// === Create Agent ===
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
initialState: {
|
initialState: {
|
||||||
|
|
@ -578,16 +636,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// === Load messages if continuing session ===
|
||||||
|
if (shouldRestoreFromSession) {
|
||||||
|
const messages = sessionManager.loadMessages();
|
||||||
|
if (messages.length > 0) {
|
||||||
|
agent.replaceMessages(messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Create Session ===
|
// === Create Session ===
|
||||||
const session = new AgentSession({
|
const session = new AgentSession({
|
||||||
agent,
|
agent,
|
||||||
sessionManager,
|
sessionManager,
|
||||||
settingsManager,
|
settingsManager,
|
||||||
|
scopedModels: options.scopedModels,
|
||||||
fileCommands: slashCommands,
|
fileCommands: slashCommands,
|
||||||
hookRunner,
|
hookRunner,
|
||||||
customTools: customToolsResult.tools,
|
customTools: customToolsResult.tools,
|
||||||
skillsSettings: settingsManager.getSkillsSettings(),
|
skillsSettings: settingsManager.getSkillsSettings(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return session;
|
return {
|
||||||
|
session,
|
||||||
|
customToolsResult,
|
||||||
|
modelFallbackMessage,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export {
|
||||||
type BuildSystemPromptOptions,
|
type BuildSystemPromptOptions,
|
||||||
buildSystemPrompt,
|
buildSystemPrompt,
|
||||||
type CreateAgentSessionOptions,
|
type CreateAgentSessionOptions,
|
||||||
|
type CreateAgentSessionResult,
|
||||||
// Factory
|
// Factory
|
||||||
createAgentSession,
|
createAgentSession,
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
/**
|
/**
|
||||||
* Main entry point for the coding agent
|
* Main entry point for the coding agent CLI.
|
||||||
|
*
|
||||||
|
* This file handles CLI argument parsing and translates them into
|
||||||
|
* createAgentSession() options. The SDK does the heavy lifting.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Agent, type Attachment, ProviderTransport, type ThinkingLevel } from "@mariozechner/pi-agent-core";
|
import type { Attachment, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||||
import { setOAuthStorage, supportsXhigh } from "@mariozechner/pi-ai";
|
import { setOAuthStorage, supportsXhigh } from "@mariozechner/pi-ai";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
||||||
|
|
@ -12,17 +15,12 @@ import { processFileArguments } from "./cli/file-processor.js";
|
||||||
import { listModels } from "./cli/list-models.js";
|
import { listModels } from "./cli/list-models.js";
|
||||||
import { selectSession } from "./cli/session-picker.js";
|
import { selectSession } from "./cli/session-picker.js";
|
||||||
import { getModelsPath, getOAuthPath, VERSION } from "./config.js";
|
import { getModelsPath, getOAuthPath, VERSION } from "./config.js";
|
||||||
import { AgentSession } from "./core/agent-session.js";
|
import type { AgentSession } from "./core/agent-session.js";
|
||||||
import { discoverAndLoadCustomTools, type LoadedCustomTool } from "./core/custom-tools/index.js";
|
import type { LoadedCustomTool } from "./core/custom-tools/index.js";
|
||||||
import { exportFromFile } from "./core/export-html.js";
|
import { exportFromFile } from "./core/export-html.js";
|
||||||
import { discoverAndLoadHooks, HookRunner, wrapToolsWithHooks } from "./core/hooks/index.js";
|
import { resolveModelScope, type ScopedModel } from "./core/model-resolver.js";
|
||||||
import { messageTransformer } from "./core/messages.js";
|
import { type CreateAgentSessionOptions, createAgentSession } from "./core/sdk.js";
|
||||||
import { findModel, getApiKeyForModel, getAvailableModels } from "./core/model-config.js";
|
|
||||||
import { resolveModelScope, restoreModelFromSession, type ScopedModel } from "./core/model-resolver.js";
|
|
||||||
import { discoverSlashCommands } from "./core/sdk.js";
|
|
||||||
import { SessionManager } from "./core/session-manager.js";
|
|
||||||
import { SettingsManager } from "./core/settings-manager.js";
|
import { SettingsManager } from "./core/settings-manager.js";
|
||||||
import { buildSystemPrompt } from "./core/system-prompt.js";
|
|
||||||
import { allTools, codingTools } from "./core/tools/index.js";
|
import { allTools, codingTools } from "./core/tools/index.js";
|
||||||
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
|
||||||
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
|
||||||
|
|
@ -70,7 +68,6 @@ async function checkForNewVersion(currentVersion: string): Promise<string | null
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch {
|
} catch {
|
||||||
// Silently fail - don't disrupt the user experience
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +77,7 @@ async function runInteractiveMode(
|
||||||
session: AgentSession,
|
session: AgentSession,
|
||||||
version: string,
|
version: string,
|
||||||
changelogMarkdown: string | null,
|
changelogMarkdown: string | null,
|
||||||
modelFallbackMessage: string | null,
|
modelFallbackMessage: string | undefined,
|
||||||
versionCheckPromise: Promise<string | null>,
|
versionCheckPromise: Promise<string | null>,
|
||||||
initialMessages: string[],
|
initialMessages: string[],
|
||||||
customTools: LoadedCustomTool[],
|
customTools: LoadedCustomTool[],
|
||||||
|
|
@ -91,25 +88,20 @@ async function runInteractiveMode(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mode = new InteractiveMode(session, version, changelogMarkdown, customTools, setToolUIContext, fdPath);
|
const mode = new InteractiveMode(session, version, changelogMarkdown, customTools, setToolUIContext, fdPath);
|
||||||
|
|
||||||
// Initialize TUI (subscribes to agent events internally)
|
|
||||||
await mode.init();
|
await mode.init();
|
||||||
|
|
||||||
// Handle version check result when it completes (don't block)
|
|
||||||
versionCheckPromise.then((newVersion) => {
|
versionCheckPromise.then((newVersion) => {
|
||||||
if (newVersion) {
|
if (newVersion) {
|
||||||
mode.showNewVersionNotification(newVersion);
|
mode.showNewVersionNotification(newVersion);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Render any existing messages (from --continue mode)
|
|
||||||
mode.renderInitialMessages(session.state);
|
mode.renderInitialMessages(session.state);
|
||||||
|
|
||||||
// Show model fallback warning at the end of the chat if applicable
|
|
||||||
if (modelFallbackMessage) {
|
if (modelFallbackMessage) {
|
||||||
mode.showWarning(modelFallbackMessage);
|
mode.showWarning(modelFallbackMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process initial message with attachments if provided (from @file args)
|
|
||||||
if (initialMessage) {
|
if (initialMessage) {
|
||||||
try {
|
try {
|
||||||
await session.prompt(initialMessage, { attachments: initialAttachments });
|
await session.prompt(initialMessage, { attachments: initialAttachments });
|
||||||
|
|
@ -119,7 +111,6 @@ async function runInteractiveMode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process remaining initial messages if provided (from CLI args)
|
|
||||||
for (const message of initialMessages) {
|
for (const message of initialMessages) {
|
||||||
try {
|
try {
|
||||||
await session.prompt(message);
|
await session.prompt(message);
|
||||||
|
|
@ -129,11 +120,8 @@ async function runInteractiveMode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interactive loop
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const userInput = await mode.getUserInput();
|
const userInput = await mode.getUserInput();
|
||||||
|
|
||||||
// Process the message
|
|
||||||
try {
|
try {
|
||||||
await session.prompt(userInput);
|
await session.prompt(userInput);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
|
@ -154,11 +142,10 @@ async function prepareInitialMessage(parsed: Args): Promise<{
|
||||||
|
|
||||||
const { textContent, imageAttachments } = await processFileArguments(parsed.fileArgs);
|
const { textContent, imageAttachments } = await processFileArguments(parsed.fileArgs);
|
||||||
|
|
||||||
// Combine file content with first plain text message (if any)
|
|
||||||
let initialMessage: string;
|
let initialMessage: string;
|
||||||
if (parsed.messages.length > 0) {
|
if (parsed.messages.length > 0) {
|
||||||
initialMessage = textContent + parsed.messages[0];
|
initialMessage = textContent + parsed.messages[0];
|
||||||
parsed.messages.shift(); // Remove first message as it's been combined
|
parsed.messages.shift();
|
||||||
} else {
|
} else {
|
||||||
initialMessage = textContent;
|
initialMessage = textContent;
|
||||||
}
|
}
|
||||||
|
|
@ -169,386 +156,6 @@ async function prepareInitialMessage(parsed: Args): Promise<{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(args: string[]) {
|
|
||||||
// Configure OAuth storage to use the coding-agent's configurable path
|
|
||||||
// This must happen before any OAuth operations
|
|
||||||
configureOAuthStorage();
|
|
||||||
|
|
||||||
const parsed = parseArgs(args);
|
|
||||||
|
|
||||||
if (parsed.version) {
|
|
||||||
console.log(VERSION);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.help) {
|
|
||||||
printHelp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle --list-models flag: list available models and exit
|
|
||||||
if (parsed.listModels !== undefined) {
|
|
||||||
const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
|
|
||||||
await listModels(searchPattern);
|
|
||||||
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 } = await prepareInitialMessage(parsed);
|
|
||||||
|
|
||||||
// Determine if we're in interactive mode (needed for theme watcher)
|
|
||||||
const isInteractive = !parsed.print && parsed.mode === undefined;
|
|
||||||
|
|
||||||
// Initialize theme (before any TUI rendering)
|
|
||||||
const settingsManager = new SettingsManager();
|
|
||||||
const themeName = settingsManager.getTheme();
|
|
||||||
initTheme(themeName, isInteractive);
|
|
||||||
|
|
||||||
// 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 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 skillsSettings = settingsManager.getSkillsSettings();
|
|
||||||
if (parsed.noSkills) {
|
|
||||||
skillsSettings.enabled = false;
|
|
||||||
}
|
|
||||||
if (parsed.skills && parsed.skills.length > 0) {
|
|
||||||
skillsSettings.includeSkills = parsed.skills;
|
|
||||||
}
|
|
||||||
const systemPrompt = buildSystemPrompt({
|
|
||||||
customPrompt: parsed.systemPrompt,
|
|
||||||
selectedTools: parsed.tools,
|
|
||||||
appendSystemPrompt: parsed.appendSystemPrompt,
|
|
||||||
skillsSettings,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle session restoration
|
|
||||||
let modelFallbackMessage: string | null = null;
|
|
||||||
|
|
||||||
if (parsed.continue || parsed.resume || parsed.session) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clamp thinking level to model capabilities
|
|
||||||
if (initialModel) {
|
|
||||||
if (!initialModel.reasoning) {
|
|
||||||
initialThinking = "off";
|
|
||||||
} else if (initialThinking === "xhigh" && !supportsXhigh(initialModel)) {
|
|
||||||
initialThinking = "high";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine which tools to use
|
|
||||||
let selectedTools = parsed.tools ? parsed.tools.map((name) => allTools[name]) : codingTools;
|
|
||||||
|
|
||||||
// Discover and load hooks from:
|
|
||||||
// 1. ~/.pi/agent/hooks/*.ts (global)
|
|
||||||
// 2. cwd/.pi/hooks/*.ts (project-local)
|
|
||||||
// 3. Explicit paths in settings.json
|
|
||||||
// 4. CLI --hook flags
|
|
||||||
let hookRunner: HookRunner | null = null;
|
|
||||||
const cwd = process.cwd();
|
|
||||||
const configuredHookPaths = [...settingsManager.getHookPaths(), ...(parsed.hooks ?? [])];
|
|
||||||
const { hooks, errors } = await discoverAndLoadHooks(configuredHookPaths, cwd);
|
|
||||||
|
|
||||||
// Report hook loading errors
|
|
||||||
for (const { path, error } of errors) {
|
|
||||||
console.error(chalk.red(`Failed to load hook "${path}": ${error}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hooks.length > 0) {
|
|
||||||
const timeout = settingsManager.getHookTimeout();
|
|
||||||
hookRunner = new HookRunner(hooks, cwd, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discover and load custom tools from:
|
|
||||||
// 1. ~/.pi/agent/tools/*.ts (global)
|
|
||||||
// 2. cwd/.pi/tools/*.ts (project-local)
|
|
||||||
// 3. Explicit paths in settings.json
|
|
||||||
// 4. CLI --tool flags
|
|
||||||
const configuredToolPaths = [...settingsManager.getCustomToolPaths(), ...(parsed.customTools ?? [])];
|
|
||||||
const builtInToolNames = Object.keys(allTools);
|
|
||||||
const {
|
|
||||||
tools: loadedCustomTools,
|
|
||||||
errors: toolErrors,
|
|
||||||
setUIContext: setToolUIContext,
|
|
||||||
} = await discoverAndLoadCustomTools(configuredToolPaths, cwd, builtInToolNames);
|
|
||||||
|
|
||||||
// Report custom tool loading errors
|
|
||||||
for (const { path, error } of toolErrors) {
|
|
||||||
console.error(chalk.red(`Failed to load custom tool "${path}": ${error}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add custom tools to selected tools
|
|
||||||
if (loadedCustomTools.length > 0) {
|
|
||||||
const customToolInstances = loadedCustomTools.map((lt) => lt.tool);
|
|
||||||
selectedTools = [...selectedTools, ...customToolInstances] as typeof selectedTools;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap tools with hook callbacks (built-in and custom)
|
|
||||||
if (hookRunner) {
|
|
||||||
selectedTools = wrapToolsWithHooks(selectedTools, hookRunner);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load previous messages if continuing, resuming, or using --session
|
|
||||||
if (parsed.continue || parsed.resume || parsed.session) {
|
|
||||||
const messages = sessionManager.loadMessages();
|
|
||||||
if (messages.length > 0) {
|
|
||||||
agent.replaceMessages(messages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load file commands for slash command expansion
|
|
||||||
const fileCommands = discoverSlashCommands();
|
|
||||||
|
|
||||||
// Create session
|
|
||||||
const session = new AgentSession({
|
|
||||||
agent,
|
|
||||||
sessionManager,
|
|
||||||
settingsManager,
|
|
||||||
scopedModels,
|
|
||||||
fileCommands,
|
|
||||||
hookRunner,
|
|
||||||
customTools: loadedCustomTools,
|
|
||||||
skillsSettings,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
loadedCustomTools,
|
|
||||||
setToolUIContext,
|
|
||||||
initialMessage,
|
|
||||||
initialAttachments,
|
|
||||||
fdPath,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Non-interactive mode (--print flag or --mode flag)
|
|
||||||
await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
|
|
||||||
// Clean up and exit (file watchers keep process alive)
|
|
||||||
stopThemeWatcher();
|
|
||||||
// Wait for stdout to fully flush before exiting
|
|
||||||
if (process.stdout.writableLength > 0) {
|
|
||||||
await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
|
|
||||||
}
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 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) */
|
/** Get changelog markdown to display (only for new sessions with updates) */
|
||||||
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {
|
function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager): string | null {
|
||||||
if (parsed.continue || parsed.resume) {
|
if (parsed.continue || parsed.resume) {
|
||||||
|
|
@ -560,13 +167,11 @@ function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager):
|
||||||
const entries = parseChangelog(changelogPath);
|
const entries = parseChangelog(changelogPath);
|
||||||
|
|
||||||
if (!lastVersion) {
|
if (!lastVersion) {
|
||||||
// First run - show all entries
|
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
settingsManager.setLastChangelogVersion(VERSION);
|
settingsManager.setLastChangelogVersion(VERSION);
|
||||||
return entries.map((e) => e.content).join("\n\n");
|
return entries.map((e) => e.content).join("\n\n");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check for new entries since last version
|
|
||||||
const newEntries = getNewEntries(entries, lastVersion);
|
const newEntries = getNewEntries(entries, lastVersion);
|
||||||
if (newEntries.length > 0) {
|
if (newEntries.length > 0) {
|
||||||
settingsManager.setLastChangelogVersion(VERSION);
|
settingsManager.setLastChangelogVersion(VERSION);
|
||||||
|
|
@ -576,3 +181,245 @@ function getChangelogForDisplay(parsed: Args, settingsManager: SettingsManager):
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Build CreateAgentSessionOptions from CLI args */
|
||||||
|
async function buildSessionOptions(parsed: Args, scopedModels: ScopedModel[]): Promise<CreateAgentSessionOptions> {
|
||||||
|
const options: CreateAgentSessionOptions = {};
|
||||||
|
|
||||||
|
// Model from CLI
|
||||||
|
if (parsed.provider && parsed.model) {
|
||||||
|
// Will be resolved by SDK
|
||||||
|
// For now, we need to find it ourselves since SDK expects Model object
|
||||||
|
const { findModel } = await import("./core/model-config.js");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
options.model = model;
|
||||||
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
||||||
|
// Use first scoped model
|
||||||
|
options.model = scopedModels[0].model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thinking level
|
||||||
|
if (parsed.thinking) {
|
||||||
|
options.thinkingLevel = parsed.thinking;
|
||||||
|
} else if (scopedModels.length > 0 && !parsed.continue && !parsed.resume) {
|
||||||
|
options.thinkingLevel = scopedModels[0].thinkingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scoped models for Ctrl+P cycling
|
||||||
|
if (scopedModels.length > 0) {
|
||||||
|
options.scopedModels = scopedModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API key from CLI
|
||||||
|
if (parsed.apiKey) {
|
||||||
|
options.getApiKey = async () => parsed.apiKey!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// System prompt
|
||||||
|
if (parsed.systemPrompt && parsed.appendSystemPrompt) {
|
||||||
|
options.systemPrompt = (defaultPrompt) => {
|
||||||
|
// Custom prompt with append
|
||||||
|
return parsed.systemPrompt + "\n\n" + parsed.appendSystemPrompt;
|
||||||
|
};
|
||||||
|
} else if (parsed.systemPrompt) {
|
||||||
|
options.systemPrompt = parsed.systemPrompt;
|
||||||
|
} else if (parsed.appendSystemPrompt) {
|
||||||
|
options.systemPrompt = (defaultPrompt) => defaultPrompt + "\n\n" + parsed.appendSystemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
if (parsed.tools) {
|
||||||
|
options.tools = parsed.tools.map((name) => allTools[name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills
|
||||||
|
if (parsed.noSkills) {
|
||||||
|
options.skills = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional hook paths from CLI
|
||||||
|
if (parsed.hooks && parsed.hooks.length > 0) {
|
||||||
|
options.additionalHookPaths = parsed.hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional custom tool paths from CLI
|
||||||
|
if (parsed.customTools && parsed.customTools.length > 0) {
|
||||||
|
options.additionalCustomToolPaths = parsed.customTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session handling
|
||||||
|
if (parsed.noSession) {
|
||||||
|
options.sessionFile = false;
|
||||||
|
} else if (parsed.session) {
|
||||||
|
options.sessionFile = parsed.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue session
|
||||||
|
if (parsed.continue && !parsed.resume) {
|
||||||
|
options.continueSession = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function main(args: string[]) {
|
||||||
|
// Configure OAuth storage first
|
||||||
|
configureOAuthStorage();
|
||||||
|
|
||||||
|
const parsed = parseArgs(args);
|
||||||
|
|
||||||
|
// === Early exits ===
|
||||||
|
|
||||||
|
if (parsed.version) {
|
||||||
|
console.log(VERSION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.help) {
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.listModels !== undefined) {
|
||||||
|
const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
|
||||||
|
await listModels(searchPattern);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Validation ===
|
||||||
|
|
||||||
|
if (parsed.mode === "rpc" && parsed.fileArgs.length > 0) {
|
||||||
|
console.error(chalk.red("Error: @file arguments are not supported in RPC mode"));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Prepare inputs ===
|
||||||
|
|
||||||
|
const { initialMessage, initialAttachments } = await prepareInitialMessage(parsed);
|
||||||
|
const isInteractive = !parsed.print && parsed.mode === undefined;
|
||||||
|
const mode = parsed.mode || "text";
|
||||||
|
|
||||||
|
// Initialize theme early
|
||||||
|
const settingsManager = new SettingsManager();
|
||||||
|
initTheme(settingsManager.getTheme(), isInteractive);
|
||||||
|
|
||||||
|
// Resolve scoped models from --models flag
|
||||||
|
let scopedModels: ScopedModel[] = [];
|
||||||
|
if (parsed.models && parsed.models.length > 0) {
|
||||||
|
scopedModels = await resolveModelScope(parsed.models);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle --resume: show session picker
|
||||||
|
let sessionFileFromResume: string | undefined;
|
||||||
|
if (parsed.resume) {
|
||||||
|
const { SessionManager } = await import("./core/session-manager.js");
|
||||||
|
const tempSessionManager = new SessionManager(false, undefined);
|
||||||
|
const selectedSession = await selectSession(tempSessionManager);
|
||||||
|
if (!selectedSession) {
|
||||||
|
console.log(chalk.dim("No session selected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionFileFromResume = selectedSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Build session options ===
|
||||||
|
|
||||||
|
const sessionOptions = await buildSessionOptions(parsed, scopedModels);
|
||||||
|
|
||||||
|
// Apply resume session file
|
||||||
|
if (sessionFileFromResume) {
|
||||||
|
sessionOptions.sessionFile = sessionFileFromResume;
|
||||||
|
sessionOptions.restoreFromSession = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Create session ===
|
||||||
|
|
||||||
|
const { session, customToolsResult, modelFallbackMessage } = await createAgentSession(sessionOptions);
|
||||||
|
|
||||||
|
// === Validate for non-interactive mode ===
|
||||||
|
|
||||||
|
if (!isInteractive && !session.model) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp thinking level to model capabilities (for CLI override case)
|
||||||
|
if (session.model && parsed.thinking) {
|
||||||
|
let effectiveThinking = parsed.thinking;
|
||||||
|
if (!session.model.reasoning) {
|
||||||
|
effectiveThinking = "off";
|
||||||
|
} else if (effectiveThinking === "xhigh" && !supportsXhigh(session.model)) {
|
||||||
|
effectiveThinking = "high";
|
||||||
|
}
|
||||||
|
if (effectiveThinking !== session.thinkingLevel) {
|
||||||
|
session.setThinkingLevel(effectiveThinking);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Route to mode ===
|
||||||
|
|
||||||
|
if (mode === "rpc") {
|
||||||
|
await runRpcMode(session);
|
||||||
|
} else if (isInteractive) {
|
||||||
|
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => null);
|
||||||
|
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)")}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fdPath = await ensureTool("fd");
|
||||||
|
|
||||||
|
await runInteractiveMode(
|
||||||
|
session,
|
||||||
|
VERSION,
|
||||||
|
changelogMarkdown,
|
||||||
|
modelFallbackMessage,
|
||||||
|
versionCheckPromise,
|
||||||
|
parsed.messages,
|
||||||
|
customToolsResult.tools,
|
||||||
|
customToolsResult.setUIContext,
|
||||||
|
initialMessage,
|
||||||
|
initialAttachments,
|
||||||
|
fdPath,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await runPrintMode(session, mode, parsed.messages, initialMessage, initialAttachments);
|
||||||
|
stopThemeWatcher();
|
||||||
|
if (process.stdout.writableLength > 0) {
|
||||||
|
await new Promise<void>((resolve) => process.stdout.once("drain", resolve));
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue