import { fstatSync } from "node:fs"; import { homedir } from "node:os"; import { basename } from "node:path"; import type { ToolPolicy } from "@cline/core"; import { registerDisposable } from "@cline/shared"; import type { Command } from "commander"; import { CommanderError, commanderToParsedArgs, createProgram, } from "./commands/program"; import { autoUpdateOnStartup, getPreferredKanbanInstaller, } from "./commands/update"; import { CLI_DEFAULT_CHECKPOINT_CONFIG } from "./runtime/defaults"; import { getCliBuildInfo } from "./utils/common"; import { buildCliCompactionConfig, CLI_COMPACTION_MODE_EXPECTED_TEXT, } from "./utils/compaction-mode"; import { refreshCliFeatureFlagsInBackground, setCliFeatureFlagsAccountContext, } from "./utils/feature-flags"; import { configureSandboxEnvironment, normalizeAutoApproveArgs, resolveWorkspaceRoot, } from "./utils/helpers"; import { c, installStreamErrorGuards, setCurrentOutputMode, writeErr, writeln, } from "./utils/output"; import { ensureOAuthProviderApiKey, getPersistedProviderApiKey, isOAuthProvider, normalizeProviderId, } from "./utils/provider-auth"; import { resolveCliReasoning } from "./utils/reasoning"; import { rewriteTeamPrompt, TEAM_COMMAND_USAGE } from "./utils/team-command"; import { captureCliExtensionActivated, getCliTelemetryService, identifyTelemetryAccount, } from "./utils/telemetry"; import type { Config } from "./utils/types"; import { runConnectWizard } from "./wizards/connect"; import { runMcpWizard } from "./wizards/mcp"; import { runScheduleWizard } from "./wizards/schedule"; export function stdinHasPipedInput(): boolean { if (process.stdin.isTTY) return false; try { const stats = fstatSync(0); return stats.isFIFO() || stats.isFile(); } catch { return false; } } async function createProviderSettingsManager() { const { ProviderSettingsManager } = await import("@cline/core"); return new ProviderSettingsManager(); } async function loadCliRuntimeModules() { const [coreServer, prompt, runAgentModule] = await Promise.all([ import("@cline/core"), import("./runtime/prompt"), import("./runtime/run-agent"), ]); return { coreServer, resolveSystemPrompt: prompt.resolveSystemPrompt, runAgent: runAgentModule.runAgent, }; } async function loadInteractiveRuntimeModule() { const { runInteractive } = await import("./runtime/run-interactive"); return runInteractive; } /** * Two-pass approach for --config: a quick scan of process.argv extracts the * config directory before commander parses, because setClineDir() must run * before any code that reads the home/config directory. * * Recognizes both Commander spellings: * --config * --config= * * Exported for unit testing; callers in this file should use this rather * than reimplementing the scan. */ export function resolveConfigDirArg(argv: string[]): string | undefined { for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === "--config") { const value = argv[i + 1]?.trim(); return value ? value : undefined; } if (arg?.startsWith("--config=")) { const value = arg.slice("--config=".length).trim(); return value ? value : undefined; } } return undefined; } function collectOption(value: string, previous: string[] = []): string[] { return [...previous, value]; } // Shells strip quote characters before argv reaches us, so a prompt that was // typed in quotes is only observable when it remains one argv token with spaces. function promptArgLooksQuoted(arg: string | undefined): boolean { return !!arg && /\s/.test(arg); } function writePromptArgError(args: string[]): void { const renderedArgs = args.join(" "); writeErr( `Unknown command or unquoted prompt: ${renderedArgs}\nPrompt text must be passed as a single quoted argument, for example: cline "fix the tests". Use "cline --help" to see available commands and flags.`, ); } export async function runCli(): Promise { installStreamErrorGuards(); autoUpdateOnStartup(); const cliArgs = process.argv.slice(2); const configDir = resolveConfigDirArg(cliArgs); const { setClineDir, setHomeDir } = await import("@cline/shared/storage"); if (configDir) { setClineDir(configDir); } setHomeDir(homedir()); // Capture activation telemetry only after config/home directory selection // has been applied, so the telemetry singleton's persisted distinct-id // (and any other storage it touches) lands under the user-selected // `--config ` rather than the default home/config location. captureCliExtensionActivated(); let launchConfigView = false; const normalizedArgs = normalizeAutoApproveArgs(cliArgs); // Subcommand routing via Commander const ctx: { exitCode?: number; resumeSessionId?: string } = {}; const io = { writeln, writeErr }; const program = createProgram(); // Re-enable built-in help/version output for the routing program program.configureOutput({ writeOut: (str: string) => process.stdout.write(str), writeErr: () => {}, }); // Default action handles non-subcommand args (e.g. prompt text) program.action(() => {}); // Auth subcommand: defines its own options so commander parses them // directly. The short flags -p/-m intentionally shadow the root's -p (plan) // and -m (model); commander scopes options per-command so there is no // conflict. const authCmd = program .command("auth") .description("Authenticate a provider and configure what model is used") .argument("[provider]", "Provider id (positional shorthand for -p)") .option("-p, --provider ", "Provider ID") .option("-k, --apikey ", "API key") .option("-m, --modelid ", "Model ID") .option("-b, --baseurl ", "Base URL") .option("--azure-api-version ", "Azure API version") .option("--config ", "configuration directory") .option("-c, --cwd ", "Working directory") .option( "--data-dir ", "Use isolated local state at instead of ~/.cline (enables sandbox mode)", ) .option("-v, --verbose", "Show verbose output") .action(async (positionalProvider: string | undefined) => { const opts = authCmd.opts<{ provider?: string; apikey?: string; modelid?: string; baseurl?: string; azureApiVersion?: string; config?: string; cwd?: string; dataDir?: string; verbose?: boolean; }>(); // Honor --config inside the action as a defense-in-depth measure. // The early pre-pass in runCli() also calls setClineDir(), but only // for argv tokens it can spot before commander runs. Reapplying // here ensures opts.config (parsed by commander, including the // --config= form) is always respected before any provider // settings manager is constructed against ~/.cline. if (opts.config?.trim()) { const { setClineDir } = await import("@cline/shared/storage"); setClineDir(opts.config.trim()); } // Honor --data-dir before constructing the provider settings manager // so writes land under the chosen data dir instead of ~/.cline. configureSandboxEnvironment({ enabled: !!opts.dataDir || process.env.CLINE_SANDBOX?.trim() === "1", cwd: opts.cwd ?? process.cwd(), explicitDir: opts.dataDir, }); const { runAuthCommand } = await import("./commands/auth"); const providerSettingsManager = await createProviderSettingsManager(); ctx.exitCode = await runAuthCommand({ providerSettingsManager, explicitProvider: opts.provider ?? positionalProvider, apikey: opts.apikey, modelid: opts.modelid, baseurl: opts.baseurl, azureApiVersion: opts.azureApiVersion, io, }); }); const createConfigRuntimeCommand = async () => { const { createConfigCommand } = await import("./commands/config"); let configCmd: Command; configCmd = createConfigCommand( () => resolveWorkspaceRoot(program.opts().cwd ?? process.cwd()), () => { const outputMode = program.opts().json || configCmd.opts().json ? ("json" as const) : ("text" as const); setCurrentOutputMode(outputMode); return outputMode; }, io, (code) => { ctx.exitCode = code; }, () => { launchConfigView = true; }, ); return configCmd; }; program .command("config") .description("Show current configuration") .option("--json", "Output as JSON") .option("--config ", "configuration directory") .allowUnknownOption() .allowExcessArguments() .passThroughOptions() .action(async (_opts: unknown, cmd: Command) => { const realCmd = await createConfigRuntimeCommand(); await realCmd.parseAsync(cmd.args, { from: "user" }); }); const pluginCmd = program .command("plugin") .description("Manage Cline Plugins") .action(() => { pluginCmd.help(); }); const pluginInstallCmd = pluginCmd .command("install") .alias("i") .description( "Install a Cline Plugin from an official keyword, npm, git, URL, or a local path", ) .argument( "", "official keyword, npm package, git URL, plugin file URL, or local plugin path", ) .option("--npm", "Treat source as an npm package") .option("--git", "Treat source as a git repository") .option("--force", "Replace an existing install for the same source") .option("--json", "Output as JSON") .option("--cwd ", "Install to /.cline/plugins") .action(async (source: string) => { const opts = pluginInstallCmd.opts<{ npm?: boolean; git?: boolean; force?: boolean; json?: boolean; cwd?: string; }>(); const sourceTypes = [ opts.npm ? ("npm" as const) : undefined, opts.git ? ("git" as const) : undefined, ].filter((sourceType) => sourceType !== undefined); if (sourceTypes.length > 1) { writeErr("plugin install accepts only one source type flag"); ctx.exitCode = 1; return; } const { runPluginInstallCommand } = await import("./commands/plugin"); ctx.exitCode = await runPluginInstallCommand({ source, sourceType: sourceTypes[0], cwd: opts.cwd, force: opts.force === true, json: opts.json === true || program.opts().json === true, io, }); }); const pluginUninstallCmd = pluginCmd .command("uninstall") .alias("remove") .alias("rm") .description("Uninstall a Cline Plugin by name or path") .argument("", "plugin package name, installed slug, or plugin path") .option("--json", "Output as JSON") .option( "--cwd ", "Search /.cline/plugins before global plugins", ) .action(async (name: string) => { const opts = pluginUninstallCmd.opts<{ json?: boolean; cwd?: string; }>(); const { runPluginUninstallCommand } = await import("./commands/plugin"); ctx.exitCode = await runPluginUninstallCommand({ name, cwd: opts.cwd, json: opts.json === true || program.opts().json === true, io, }); }); const skillCmd = program .command("skill") .description("Manage Cline Skills via the open skills CLI (npx skills)") .allowUnknownOption() .passThroughOptions() .argument("[args...]", "arguments forwarded to the skills CLI") .addHelpText( "after", "\nForwards to the open skills CLI via npx. Examples:\n" + " cline skill add Add a skill into Cline\n" + " cline skill install Alias for add\n" + " cline skill list List installed skills\n" + " cline skill remove Remove installed skills\n" + " cline skill uninstall Alias for remove\n" + "\nadd/install and remove/uninstall default to '--agent cline' unless you pass your own --agent.\n" + "Run 'npx skills --help' for the full command reference.", ) .action(async () => { const { runSkillCommand } = await import("./commands/skill"); ctx.exitCode = await runSkillCommand(skillCmd.args, io); }); const connectCmd = program .command("connect") .description("Connect to an external channel") .argument("[channel]", "Channel to connect Cline CLI to") .option("--stop", "Kill all current channel connections") .allowUnknownOption() .passThroughOptions() .addHelpText( "after", "\nRun 'connect --help' for channel-specific options.", ) .action(async (adapter: string | undefined) => { const { formatAdapterList, runConnectAdapter, runStopAllConnectors, runStopConnector, } = await import("./commands/connect"); const opts = connectCmd.opts(); if (opts.stop) { if (adapter) { ctx.exitCode = await runStopConnector(adapter, io); } else { ctx.exitCode = await runStopAllConnectors(io); } } else if (adapter) { // connectCmd.args = [adapter, ...passthroughFlags]. Pass only the // connector-specific flags (everything after the adapter name). ctx.exitCode = await runConnectAdapter( adapter, connectCmd.args.slice(1), io, ); } else if (process.stdin.isTTY && process.stdout.isTTY) { ctx.exitCode = await runConnectWizard(); } else { writeln(`\nAdapters:\n${formatAdapterList()}`); connectCmd.help(); } }); const mcpCmd = program .command("mcp") .description("Manage MCP servers") .action(async () => { if (process.stdin.isTTY && process.stdout.isTTY) { ctx.exitCode = await runMcpWizard(); } else { writeln( "MCP wizard requires a TTY. Use cline config mcp to list servers.", ); } }); const mcpInstallCmd = mcpCmd .command("install") .alias("add") .description("Open the MCP add wizard with server fields prefilled") .argument("", "MCP server name") .argument( "[targetArgs...]", "URL for remote transports, or command and args after -- for stdio", ) .option( "--transport ", "stdio, sse, http, streamable-http, or streamableHttp (default: stdio)", ) .option("--header
", "Remote MCP request header", collectOption, []) .option("--yes", "Install noninteractively without opening the wizard") .option("--json", "Output as JSON") .action(async (name: string, targetArgs: string[]) => { const opts = mcpInstallCmd.opts<{ header?: string[]; json?: boolean; transport?: string; yes?: boolean; }>(); const { runMcpInstallCommand } = await import("./commands/mcp"); ctx.exitCode = await runMcpInstallCommand({ name, headers: opts.header, targetArgs, transport: opts.transport, json: opts.json === true || program.opts().json === true, yes: opts.yes === true, io, }); }); const createDoctorRuntimeCommand = async () => { const { createDoctorCommand } = await import("./commands/doctor"); return createDoctorCommand(io, (code) => { ctx.exitCode = code; }); }; program .command("doctor") .description("Diagnose and fix configuration issues") .allowUnknownOption() .allowExcessArguments() .passThroughOptions() .addHelpText( "after", "\nCommands:\n fix Kill all running processes\n log Open the CLI log file\n", ) .action(async (_opts: unknown, cmd: Command) => { const doctorCmd = await createDoctorRuntimeCommand(); await doctorCmd.parseAsync(cmd.args, { from: "user" }); }); const historyCmd = program .command("history") .alias("h") .description("List session history or manage saved sessions") .option("--json", "Output as JSON") .option("--limit ", "Maximum number of sessions to show", "50") .option("--page ", "Page number for paginated results") .option("--config ", "configuration directory") .action(async () => { const opts = historyCmd.opts(); const limit = Number.parseInt(opts.limit, 10); const outputMode = program.opts().json || opts.json ? ("json" as const) : ("text" as const); const { runHistoryList } = await import("./commands/history"); const result = await runHistoryList({ limit, outputMode, io, }); if (typeof result === "string") { ctx.resumeSessionId = result; // JSON listing should never return a session id; if it does, still exit here so // we never fall through to agent bootstrap (which can block on stdin in CI). if (outputMode === "json") { ctx.exitCode = 0; } } else { // Always set exit code for numeric results so `ctx.exitCode` is never left // undefined (that would fall through and load the full CLI runtime). ctx.exitCode = result ?? 0; } }); const historyDeleteCmd = historyCmd .command("delete") .description("Delete a session from history") .option("--session-id ", "Session ID to delete") .action(async () => { const opts = historyDeleteCmd.opts(); if (!opts.sessionId) { writeErr("history delete requires --session-id "); ctx.exitCode = 0; return; } const outputMode = program.opts().json || historyCmd.opts().json ? ("json" as const) : ("text" as const); const { runHistoryDelete } = await import("./commands/history"); ctx.exitCode = await runHistoryDelete(opts.sessionId, outputMode, io); }); const historyUpdateCmd = historyCmd .command("update") .description("Update a session in history") .option("--metadata ", "Metadata as JSON string") .option("--prompt ", "New prompt text") .option("--session-id ", "Session ID to update") .option("--title ", "New title") .action(async () => { const opts = historyUpdateCmd.opts(); if (!opts.sessionId) { writeErr("history update requires --session-id "); ctx.exitCode = 1; return; } const outputMode = program.opts().json || historyCmd.opts().json ? ("json" as const) : ("text" as const); const { runHistoryUpdate } = await import("./commands/history"); ctx.exitCode = await runHistoryUpdate( opts.sessionId, opts.prompt, opts.title, opts.metadata, outputMode, io, ); }); const historyExportCmd = historyCmd .command("export ") .description("Export a session as a standalone HTML file") .option("-o, --output ", "Output HTML file path") .action(async (sessionId: string) => { const opts = historyExportCmd.opts(); const outputMode = program.opts().json || historyCmd.opts().json ? ("json" as const) : ("text" as const); const { runHistoryExport } = await import("./commands/history"); ctx.exitCode = await runHistoryExport( sessionId, opts.output, outputMode, io, ); }); program .command("hook") .description("Handle a hook payload from stdin") .allowUnknownOption() .allowExcessArguments() .action(async () => { const { runHookCommand } = await import("./commands/hook"); ctx.exitCode = await runHookCommand(io); }); const createScheduleRuntimeCommand = async () => { const { createScheduleCommand } = await import("./commands/schedule"); return createScheduleCommand(io, (code) => { ctx.exitCode = code; }); }; const createHubRuntimeCommand = async () => { const { createHubCommand } = await import("./commands/hub"); return createHubCommand(io, (code) => { ctx.exitCode = code; }); }; program .command("schedule") .description("Manage scheduled tasks") .allowUnknownOption() .allowExcessArguments() .passThroughOptions() .action(async (_opts: unknown, cmd: Command) => { if ( cmd.args.length === 0 && process.stdin.isTTY && process.stdout.isTTY ) { ctx.exitCode = await runScheduleWizard(); return; } const scheduleCmd = await createScheduleRuntimeCommand(); await scheduleCmd.parseAsync(cmd.args, { from: "user" }); }); program .command("hub") .description("Manage the local hub daemon") .allowUnknownOption() .allowExcessArguments() .passThroughOptions() .action(async (_opts: unknown, cmd: Command) => { const hubCmd = await createHubRuntimeCommand(); await hubCmd.parseAsync(cmd.args, { from: "user" }); }); const dashboardCmd = program .command("dashboard") .description("Start the Cline Hub dashboard and open it in a browser") .option("--config ", "configuration directory") .option("-c, --cwd ", "Workspace root", process.cwd()) .option( "--data-dir ", "Use isolated local state at instead of ~/.cline (enables sandbox mode)", ) .option("--host ", "Dashboard bind host") .option("--port ", "Dashboard HTTP/WebSocket port") .option("--public-url ", "Public dashboard URL") .option("--room-secret ", "Invite secret for browser access") .option("--no-open", "Start the dashboard without opening a browser") .action(async () => { const opts = dashboardCmd.opts<{ config?: string; cwd?: string; dataDir?: string; host?: string; port?: string; publicUrl?: string; roomSecret?: string; open?: boolean; }>(); const { runDashboardCommand } = await import("./commands/dashboard"); ctx.exitCode = await runDashboardCommand({ configDir: opts.config, cwd: opts.cwd, dataDir: opts.dataDir, host: opts.host, port: opts.port, publicUrl: opts.publicUrl, roomSecret: opts.roomSecret, openBrowser: opts.open !== false, io, }); }); const updateCmd = program .command("update") .description("Check for updates and install if available") .allowUnknownOption() .allowExcessArguments() .option("-v, --verbose", "Show verbose output") .option("--config ", "configuration directory") .action(async () => { const { checkForUpdates } = await import("./commands/update"); ctx.exitCode = await checkForUpdates({ verbose: updateCmd.opts().verbose === true, }); }); program .command("version") .description("Show Cline CLI version number") .action(async () => { const { showVersion } = await import("./commands/help"); showVersion(); ctx.exitCode = 0; }); program .command("kanban") .description("Run the kanban app") .action(async () => { const { launchKanban } = await import("./commands/kanban"); ctx.exitCode = await launchKanban({ preferredInstaller: getPreferredKanbanInstaller(), }); }); try { await program.parseAsync(normalizedArgs, { from: "user" }); } catch (err: unknown) { if (err instanceof CommanderError) { if (err.exitCode !== 0) { writeErr(err.message); process.exitCode = err.exitCode; return; } return; } throw err; } if (ctx.exitCode !== undefined) { process.exitCode = ctx.exitCode; return; } const rootOpts = program.opts<{ kanban?: boolean; tui?: boolean; update?: boolean; verbose?: boolean; }>(); if (rootOpts.update) { if (rootOpts.kanban || rootOpts.tui || program.args.length > 0) { writeErr("Use --update without a prompt or task flags."); process.exitCode = 1; return; } const { checkForUpdates } = await import("./commands/update"); process.exitCode = await checkForUpdates({ verbose: rootOpts.verbose === true, }); return; } if (rootOpts.kanban) { if (rootOpts.tui) { writeErr("Use either --kanban or --tui, not both."); process.exitCode = 1; return; } if (program.args.length > 0) { writeErr("Use --kanban without a prompt."); process.exitCode = 1; return; } const { launchKanban } = await import("./commands/kanban"); process.exitCode = await launchKanban({ preferredInstaller: getPreferredKanbanInstaller(), }); return; } // Default flow: no subcommand matched, or fall-through from config/history. let args = commanderToParsedArgs(program); let resumeSessionId: string | undefined = ctx.resumeSessionId; if (resumeSessionId) { // The history picker already created (and tore down) an OpenTUI renderer // in this process; starting the interactive TUI here would create a // second one, which can crash natively during teardown. Resume in a // fresh `cline --id ` child process instead. const { spawnHistoryResume } = await import("./utils/history-resume"); const childExitCode = await spawnHistoryResume({ sessionId: resumeSessionId, normalizedArgs, remainingArgs: program.args, configDir, }); if (childExitCode !== undefined) { process.exitCode = childExitCode; return; } args = { ...args, interactive: true, prompt: undefined, }; } if (args.id !== undefined) { const sessionId = args.id.trim(); if (!sessionId) { writeErr("--id requires "); process.exitCode = 1; return; } resumeSessionId = sessionId; process.env.CLINE_HOOK_AGENT_RESUME = "1"; args = { ...args, interactive: true, prompt: undefined, }; } else { delete process.env.CLINE_HOOK_AGENT_RESUME; } if (launchConfigView) { args = { ...args, interactive: true, prompt: undefined, }; } if (args.invalidThinkingLevel) { writeErr( `invalid thinking level "${args.invalidThinkingLevel}" (expected "none", "low", "medium", "high", or "xhigh")`, ); process.exitCode = 1; return; } if (args.invalidCompactionMode) { writeErr( `invalid compaction mode "${args.invalidCompactionMode}" (expected ${CLI_COMPACTION_MODE_EXPECTED_TEXT})`, ); process.exitCode = 1; return; } if (args.invalidAutoApprove) { writeErr( `invalid auto-approve value "${args.invalidAutoApprove}" (expected "true" or "false")`, ); process.exitCode = 1; return; } if (args.invalidTimeoutSeconds) { writeErr( `invalid timeout "${args.invalidTimeoutSeconds}" (expected integer >= 1)`, ); process.exitCode = 1; return; } if (args.invalidRetries) { writeln( `${c.dim}[warn] ignoring invalid --retries value "${args.invalidRetries}" (expected integer >= 1)${c.reset}`, ); } if (args.hooksDir?.trim()) { process.env.CLINE_HOOKS_DIR = args.hooksDir.trim(); } if (args.prompt && !args.interactive) { if (program.args.length > 1 || !promptArgLooksQuoted(program.args[0])) { writePromptArgError(program.args); process.exitCode = 1; return; } } setCurrentOutputMode(args.outputMode); const defaultToolAutoApprove = true; const effectiveToolAutoApprove = args.autoApproveOverride ?? defaultToolAutoApprove; const toolPolicies: Record = { "*": { autoApprove: effectiveToolAutoApprove, }, }; if (args.outputMode === "json" && (args.interactive || !args.prompt)) { writeErr( "JSON output mode requires a prompt argument or piped stdin (interactive mode is unsupported)", ); process.exitCode = 1; return; } // ACP mode: mutually exclusive with interactive/piped modes. // Enters the Agent Client Protocol stdio transport and never falls through. if (args.acpMode) { const { runAcpMode } = await import("./acp/index"); await runAcpMode(); return; } if (args.worktree) { if ( !args.prompt && !resumeSessionId && !stdinHasPipedInput() && (!process.stdin.isTTY || !process.stdout.isTTY) ) { writeErr("--worktree without a prompt requires an interactive terminal."); process.exitCode = 1; return; } if (resumeSessionId) { const { getSessionRow } = await import("./session/session"); const session = await getSessionRow(resumeSessionId); if (!session) { writeErr(`Session not found: ${resumeSessionId}`); process.exitCode = 1; return; } } const { createTaskWorktree } = await import("./utils/worktree"); const sourceCwd = args.cwd ?? process.cwd(); const result = await createTaskWorktree({ cwd: sourceCwd }); if (!result.success || !result.path) { writeErr(`--worktree failed: ${result.message}`); process.exitCode = 1; return; } writeln(`Created worktree at ${result.path}`); args = { ...args, cwd: result.path, }; } const cwd = args.cwd ?? process.cwd(); const workspaceRoot = resolveWorkspaceRoot(cwd); // Sandbox mode is enabled implicitly whenever --data-dir is provided, or // when CLINE_SANDBOX=1 is set in the environment (in which case the data // dir falls back to $CLINE_SANDBOX_DATA_DIR or /tmp/cline-sandbox). const sandboxEnabled = !!args.dataDir || process.env.CLINE_SANDBOX?.trim() === "1"; const sandboxDataDir = configureSandboxEnvironment({ enabled: sandboxEnabled, cwd, explicitDir: args.dataDir, }); // Keep command-style subcommands on a narrow path. Runtime-only imports pull // in provider resolution, config services, and session startup wiring that // should only load when the CLI is actually starting an agent session. const providerSettingsManager = await createProviderSettingsManager(); const { coreServer, coreServer: { createUserInstructionConfigService }, resolveSystemPrompt, runAgent, } = await loadCliRuntimeModules(); // Register the SDK early logger as early as possible — before any // provider settings reads — so the full startup sequence is captured. // These components operate before/outside ClineCore sessions, so the // session-scoped logger can't reach them. const { createCliLoggerAdapter } = await import("./logging/adapter"); const loggerAdapter = createCliLoggerAdapter({ runtime: "cli", component: "main", }); coreServer.setSdkLogger(loggerAdapter.core); const userInstructionService = createUserInstructionConfigService({ skills: { workspacePath: workspaceRoot, includePluginSkills: true, cwd, }, rules: { workspacePath: workspaceRoot }, workflows: { workspacePath: workspaceRoot }, }); await userInstructionService.start().catch(() => {}); let userInstructionServiceDisposed = false; const stopUserInstructionService = () => { if (userInstructionServiceDisposed) { return; } userInstructionServiceDisposed = true; userInstructionService.stop(); }; registerDisposable(stopUserInstructionService); try { const persistedClineAccountId = providerSettingsManager .getProviderSettings("cline") ?.auth?.accountId?.trim(); if (persistedClineAccountId) { setCliFeatureFlagsAccountContext({ id: persistedClineAccountId }); } refreshCliFeatureFlagsInBackground(); const lastUsedProviderSettings = providerSettingsManager.getLastUsedProviderSettings({ isClinePassEnabled: true, }); const provider = normalizeProviderId( args.provider?.trim() || lastUsedProviderSettings?.provider || "cline", ); let selectedProviderSettings = providerSettingsManager.getProviderSettings(provider); // Apply locally persisted Cline account identity so subsequent events // (task.*, workspace.initialized) carry user_id when available. // Note: user.extension_activated fires anonymously earlier in startup // and cannot be retroactively updated; this is by design for // lightweight subcommand and pre-auth CLI flows. See CLINE-2406. if (provider === "cline") { const savedAccountId = selectedProviderSettings?.auth?.accountId; if (savedAccountId) { identifyTelemetryAccount({ id: savedAccountId, provider: "cline" }); } } const persistedApiKey = getPersistedProviderApiKey( provider, selectedProviderSettings, ); const providedApiKey = args.key?.trim() || undefined; let apiKey = providedApiKey || persistedApiKey || undefined; const isYoloMode = args.mode === "yolo"; const isZenMode = args.mode === "zen"; // In headless mode (yolo / json / piped stdin without --tui), // don't attempt browser-based OAuth. Authentication may still resolve at // runtime from environment-based provider auth or persisted OAuth tokens. const isHeadless = isYoloMode || isZenMode || args.outputMode === "json" || (!process.stdin.isTTY && !args.interactive); const isInteractive = (args.interactive || !args.prompt) && !isHeadless; if (!apiKey && isOAuthProvider(provider) && !isHeadless && !isInteractive) { const oauthResult = await ensureOAuthProviderApiKey({ providerId: provider, currentApiKey: apiKey, existingSettings: selectedProviderSettings, providerSettingsManager, io: { writeln, writeErr }, }); selectedProviderSettings = oauthResult?.selectedProviderSettings ?? selectedProviderSettings; apiKey = oauthResult?.apiKey ?? apiKey; } let knownModels: Config["knownModels"]; try { const persistedProviderConfig = providerSettingsManager.getProviderConfig( provider, { includeKnownModels: false, }, ); const catalogOptions = isInteractive ? { loadLatestOnInit: true, loadPrivateOnAuth: true, failOnError: false, } : undefined; const resolvedProviderConfig = await coreServer.resolveProviderConfig( provider, catalogOptions, persistedProviderConfig, ); knownModels = resolvedProviderConfig?.knownModels; } catch (error) { const message = error instanceof Error ? error.message : String(error); writeln( `${c.dim}[model-catalog] catalog resolution failed (${message})${c.reset}`, ); } const knownModelIds = knownModels ? Object.keys(knownModels) : []; const resolvedReasoning = resolveCliReasoning({ thinking: args.thinking, thinkingExplicitlySet: args.thinkingExplicitlySet, reasoningEffort: args.reasoningEffort, persistedReasoning: selectedProviderSettings?.reasoning, }); const cliBuildInfo = getCliBuildInfo(); const { createCliLoggerAdapter } = await import("./logging/adapter"); const loggerAdapter = createCliLoggerAdapter({ runtime: "cli", component: "main", }); loggerAdapter.core.log("CLI run started", { interactive: args.interactive === true, hasPrompt: !!args.prompt?.trim(), cwd, }); const config: Config = { providerId: provider, modelId: args.model ?? selectedProviderSettings?.model ?? knownModelIds[0] ?? "anthropic/claude-sonnet-4.6", apiKey: apiKey ?? "", knownModels, systemPrompt: await resolveSystemPrompt({ cwd, explicitSystemPrompt: args.systemPrompt, providerId: provider, mode: args.mode ?? "act", }), execution: { maxConsecutiveMistakes: args.retries ?? 3, }, checkpoint: CLI_DEFAULT_CHECKPOINT_CONFIG, compaction: buildCliCompactionConfig(args.compactionMode), timeoutSeconds: args.timeoutSeconds, sandbox: sandboxEnabled, sandboxDataDir, verbose: args.verbose, thinking: resolvedReasoning.thinking, reasoningEffort: resolvedReasoning.reasoningEffort, outputMode: args.outputMode, mode: args.mode, logger: loggerAdapter.core, loggerConfig: loggerAdapter.runtimeConfig, telemetry: getCliTelemetryService(loggerAdapter.core), defaultToolAutoApprove, toolPolicies, enableSpawnAgent: !isYoloMode, enableAgentTeams: !isYoloMode, enableTools: true, cwd, workspaceRoot, extensionContext: { client: { name: "cline-cli", version: cliBuildInfo.version, platform: "cli", platformVersion: cliBuildInfo.version, isMultiRoot: false, }, workspace: { rootPath: workspaceRoot, cwd, workspaceName: basename(cwd), ide: "Terminal Shell", platform: process.platform, }, logger: loggerAdapter.core, }, teamName: !isYoloMode ? args.teamName?.trim() || undefined : undefined, }; try { // For OAuth providers, don't write the resolved key into apiKey; // the token lives in auth.accessToken and apiKey is reserved for // migrated/manual keys. const persistApiKey = // Persist explicit `-k/--key` even for OAuth-capable providers. providedApiKey ? { apiKey: providedApiKey } : apiKey && !isOAuthProvider(provider) ? { apiKey } : {}; providerSettingsManager.saveProviderSettings({ ...(selectedProviderSettings ?? {}), provider, model: config.modelId, ...persistApiKey, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); writeln( `${c.dim}[provider-settings] failed to persist selection (${message})${c.reset}`, ); } // Check for piped input (skip when stdin is not a real pipe/file, e.g. headless CI). // Guard `isTTY` first so we never block on fd 0 when stdin is a terminal (and avoid // redundant fstat work). `stdinHasPipedInput` also checks `isTTY`, but callers may hit // inconsistent state in tests or embedded hosts. if (!process.stdin.isTTY && stdinHasPipedInput() && !args.interactive) { const chunks: Buffer[] = []; for await (const chunk of process.stdin) { chunks.push(chunk as Buffer); } const pipedInput = Buffer.concat(chunks).toString("utf-8").trim(); if (pipedInput) { const prompt = args.prompt ? `${args.prompt}\n\n${pipedInput}` : pipedInput; const rewrittenTeamPrompt = rewriteTeamPrompt(prompt); if (rewrittenTeamPrompt.kind === "usage") { writeln(TEAM_COMMAND_USAGE); return; } const pipedEffectivePrompt = rewrittenTeamPrompt.kind === "rewritten" ? rewrittenTeamPrompt.prompt : prompt; if (isZenMode) { const { runZen } = await import("./runtime/run-zen"); await runZen(pipedEffectivePrompt, config, userInstructionService); return; } await runAgent(pipedEffectivePrompt, config, userInstructionService); return; } } // Interactive mode: zen is incompatible because there is no terminal UI // to surface results and nothing waits for the background task. if (args.interactive || !args.prompt) { if (isZenMode) { writeErr( args.interactive ? "--zen is not compatible with interactive mode." : "--zen requires a prompt.", ); process.exitCode = 1; return; } const runInteractive = await loadInteractiveRuntimeModule(); let initialView: "chat" | "config" | undefined; if (launchConfigView) { initialView = "config"; } else if (resumeSessionId) { initialView = "chat"; } const initialClineProviderSettings = provider === "cline" ? selectedProviderSettings : undefined; let initialNotice: | import("./kanban-migration/notice").CliMigrationNotice | undefined; let markInitialNoticeShown: | (( notice: import("./kanban-migration/notice").CliMigrationNotice, ) => void) | undefined; if (!launchConfigView && process.stdin.isTTY && process.stdout.isTTY) { const { getClineCliMigrationNotice, markClineCliMigrationNoticeShown } = await import("./kanban-migration/notice"); initialNotice = getClineCliMigrationNotice(undefined, process.env, { activeProviderId: provider, }); if (initialNotice) { markInitialNoticeShown = () => { markClineCliMigrationNoticeShown(); }; } } await runInteractive(config, userInstructionService, resumeSessionId, { initialPrompt: args.prompt, clineApiBaseUrl: initialClineProviderSettings?.baseUrl, clineProviderSettings: initialClineProviderSettings, initialView, initialNotice, onInitialNoticeShown: markInitialNoticeShown, }); return; } // Single prompt mode const rewrittenTeamPrompt = rewriteTeamPrompt(args.prompt); if (rewrittenTeamPrompt.kind === "usage") { writeln(TEAM_COMMAND_USAGE); return; } const effectivePrompt = rewrittenTeamPrompt.kind === "rewritten" ? rewrittenTeamPrompt.prompt : args.prompt; // Zen mode: dispatch the task to the background hub and exit. The CLI // does not stay connected to stream output; completion is delivered via // the hub's existing ui.notify broadcast (picked up by the menubar app // when installed). if (isZenMode) { const { runZen } = await import("./runtime/run-zen"); await runZen(effectivePrompt, config, userInstructionService); return; } await runAgent(effectivePrompt, config, userInstructionService); // Exit once agent is done in non-interactive mode return; } finally { stopUserInstructionService(); } }