#!/usr/bin/env bun import { parseArgs } from "node:util"; import { APIError } from "@letta-ai/letta-client/core/error"; import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents"; import type { Message } from "@letta-ai/letta-client/resources/agents/messages"; import { getResumeData, type ResumeData } from "./agent/check-approval"; import { getClient } from "./agent/client"; import { initializeLoadedSkillsFlag, setAgentContext, setConversationId as setContextConversationId, } from "./agent/context"; import type { AgentProvenance } from "./agent/create"; import { INCOGNITO_TAG, MEMO_TAG } from "./agent/defaults"; import { ensureSkillsBlocks, ISOLATED_BLOCK_LABELS } from "./agent/memory"; import { LETTA_CLOUD_API_URL } from "./auth/oauth"; import { ConversationSelector } from "./cli/components/ConversationSelector"; import type { ApprovalRequest } from "./cli/helpers/stream"; import { ProfileSelectionInline } from "./cli/profile-selection"; import { permissionMode } from "./permissions/mode"; import { settingsManager } from "./settings-manager"; import { telemetry } from "./telemetry"; import { loadTools } from "./tools/manager"; import { markMilestone } from "./utils/timing"; // Stable empty array constants to prevent new references on every render // These are used as fallbacks when resumeData is null, avoiding the React // anti-pattern of creating new [] on every render which triggers useEffect re-runs const EMPTY_APPROVAL_ARRAY: ApprovalRequest[] = []; const EMPTY_MESSAGE_ARRAY: Message[] = []; /** * Check if pinned agents consist only of default agents (Memo + Incognito). * Used to auto-select Memo for fresh users without showing a selector. */ async function hasOnlyDefaultAgents( pinnedIds: string[], ): Promise<{ onlyDefaults: boolean; memoId: string | null }> { if (pinnedIds.length === 0) return { onlyDefaults: true, memoId: null }; if (pinnedIds.length > 2) return { onlyDefaults: false, memoId: null }; const client = await getClient(); let memoId: string | null = null; for (const id of pinnedIds) { try { const agent = await client.agents.retrieve(id); const tags = agent.tags || []; if (tags.includes(MEMO_TAG)) { memoId = agent.id; } else if (!tags.includes(INCOGNITO_TAG)) { // Found a non-default agent return { onlyDefaults: false, memoId: null }; } } catch { // Agent doesn't exist, skip it } } return { onlyDefaults: true, memoId }; } function printHelp() { // Keep this plaintext (no colors) so output pipes cleanly const usage = ` Letta Code is a general purpose CLI for interacting with Letta agents USAGE # interactive TUI letta Resume default conversation (OG single-threaded experience) letta --new Create a new conversation (for concurrent sessions) letta --continue Resume last session (agent + conversation) directly letta --resume Open agent selector UI to pick agent/conversation letta --new-agent Create a new agent directly (skip profile selector) letta --agent Open a specific agent by ID # headless letta -p "..." One-off prompt in headless mode (no TTY UI) # maintenance letta update Manually check for updates and install if available OPTIONS -h, --help Show this help and exit -v, --version Print version and exit --info Show current directory, skills, and pinned agents --continue Resume last session (agent + conversation) directly -r, --resume Open agent selector UI after loading --new Create new conversation (for concurrent sessions) --new-agent Create new agent directly (skip profile selection) --init-blocks Comma-separated memory blocks to initialize when using --new-agent (e.g., "persona,skills") --base-tools Comma-separated base tools to attach when using --new-agent (e.g., "memory,web_search,conversation_search") -a, --agent Use a specific agent ID -n, --name Resume agent by name (from pinned agents, case-insensitive) -m, --model Model ID or handle (e.g., "opus-4.5" or "anthropic/claude-opus-4-5") -s, --system System prompt ID or subagent name (applies to new or existing agent) --toolset Force toolset: "codex", "default", or "gemini" (overrides model-based auto-selection) -p, --prompt Headless prompt mode --output-format Output format for headless mode (text, json, stream-json) Default: text --input-format Input format for headless mode (stream-json) When set, reads JSON messages from stdin for bidirectional communication --include-partial-messages Emit stream_event wrappers for each chunk (stream-json only) --skills Custom path to skills directory (default: .skills in current directory) --sleeptime Enable sleeptime memory management (only for new agents) --from-af Create agent from an AgentFile (.af) template BEHAVIOR On startup, Letta Code checks for saved profiles: - If profiles exist, you'll be prompted to select one or create a new agent - Profiles can be "pinned" to specific projects for quick access - Use /profile save to bookmark your current agent Profiles are stored in: - Global: ~/.config/letta/settings.json (available everywhere) - Local: .letta/settings.local.json (pinned to project) If no credentials are configured, you'll be prompted to authenticate via Letta Cloud OAuth on first run. EXAMPLES # when installed as an executable letta # Show profile selector or create new letta --new # Create new agent directly letta --agent agent_123 # Open specific agent # inside the interactive session /profile save MyAgent # Save current agent as profile /profiles # Open profile selector /pin # Pin current profile to project /unpin # Unpin profile from project /logout # Clear credentials and exit # headless with JSON output (includes stats) letta -p "hello" --output-format json `.trim(); console.log(usage); } /** * Print info about current directory, skills, and pinned agents */ async function printInfo() { const { join } = await import("node:path"); const { getVersion } = await import("./version"); const { SKILLS_DIR } = await import("./agent/skills"); const { exists } = await import("./utils/fs"); const cwd = process.cwd(); const skillsDir = join(cwd, SKILLS_DIR); const skillsExist = exists(skillsDir); // Load local project settings first await settingsManager.loadLocalProjectSettings(cwd); // Get pinned agents const localPinned = settingsManager.getLocalPinnedAgents(cwd); const globalPinned = settingsManager.getGlobalPinnedAgents(); const localSettings = settingsManager.getLocalProjectSettings(cwd); const lastAgent = localSettings.lastAgent; // Try to fetch agent names from API (if authenticated) const agentNames: Record = {}; const allAgentIds = [ ...new Set([ ...localPinned, ...globalPinned, ...(lastAgent ? [lastAgent] : []), ]), ]; if (allAgentIds.length > 0) { try { const client = await getClient(); // Fetch each agent individually to get accurate names await Promise.all( allAgentIds.map(async (id) => { try { const agent = await client.agents.retrieve(id); agentNames[id] = agent.name; } catch { // Agent not found or error - leave as not found } }), ); } catch { // Not authenticated or API error - just show IDs } } const formatAgent = (id: string) => { const name = agentNames[id]; return name ? `${id} (${name})` : `${id} (not found)`; }; console.log(`Letta Code ${getVersion()}\n`); console.log(`Current directory: ${cwd}`); console.log( `Skills directory: ${skillsDir}${skillsExist ? "" : " (not found)"}`, ); console.log(""); // Show which agent will be resumed if (lastAgent) { console.log(`Will resume: ${formatAgent(lastAgent)}`); } else if (localPinned.length > 0 || globalPinned.length > 0) { console.log("Will resume: (will show selector)"); } else { console.log("Will resume: (will create new agent)"); } console.log(""); // Locally pinned agents if (localPinned.length > 0) { console.log("Locally pinned agents (this project):"); for (const id of localPinned) { const isLast = id === lastAgent; const prefix = isLast ? "→ " : " "; const suffix = isLast ? " (last used)" : ""; console.log(` ${prefix}${formatAgent(id)}${suffix}`); } } else { console.log("Locally pinned agents: (none)"); } console.log(""); // Globally pinned agents if (globalPinned.length > 0) { console.log("Globally pinned agents:"); for (const id of globalPinned) { const isLocal = localPinned.includes(id); console.log(` ${formatAgent(id)}${isLocal ? " (also local)" : ""}`); } } else { console.log("Globally pinned agents: (none)"); } } /** * Helper to determine which model identifier to pass to loadTools() * based on user's model and/or toolset preferences. */ function getModelForToolLoading( specifiedModel?: string, specifiedToolset?: "codex" | "default" | "gemini", ): string | undefined { // If toolset is explicitly specified, use a dummy model from that provider // to trigger the correct toolset loading logic if (specifiedToolset === "codex") { return "openai/gpt-4"; } if (specifiedToolset === "gemini") { return "google/gemini-3-pro"; } if (specifiedToolset === "default") { return "anthropic/claude-sonnet-4"; } // Otherwise, use the specified model (or undefined for auto-detection) return specifiedModel; } /** * Resolve an agent ID by name from pinned agents. * Case-insensitive exact match. If multiple matches, picks the most recently used. */ async function resolveAgentByName( name: string, ): Promise<{ id: string; name: string } | null> { const client = await getClient(); // Get all pinned agents (local first, then global, deduplicated) const localPinned = settingsManager.getLocalPinnedAgents(); const globalPinned = settingsManager.getGlobalPinnedAgents(); const allPinned = [...new Set([...localPinned, ...globalPinned])]; if (allPinned.length === 0) { return null; } // Fetch names for all pinned agents and find matches const matches: { id: string; name: string }[] = []; const normalizedSearchName = name.toLowerCase(); await Promise.all( allPinned.map(async (id) => { try { const agent = await client.agents.retrieve(id); if (agent.name?.toLowerCase() === normalizedSearchName) { matches.push({ id, name: agent.name }); } } catch { // Agent not found or error, skip } }), ); if (matches.length === 0) return null; if (matches.length === 1) return matches[0] ?? null; // Multiple matches - pick most recently used // Check local LRU first const localSettings = settingsManager.getLocalProjectSettings(); const localMatch = matches.find((m) => m.id === localSettings.lastAgent); if (localMatch) return localMatch; // Then global LRU const settings = settingsManager.getSettings(); const globalMatch = matches.find((m) => m.id === settings.lastAgent); if (globalMatch) return globalMatch; // Fallback to first match (preserves local pinned order) return matches[0] ?? null; } /** * Get all pinned agent names for error messages */ async function getPinnedAgentNames(): Promise<{ id: string; name: string }[]> { const client = await getClient(); const localPinned = settingsManager.getLocalPinnedAgents(); const globalPinned = settingsManager.getGlobalPinnedAgents(); const allPinned = [...new Set([...localPinned, ...globalPinned])]; const agents: { id: string; name: string }[] = []; await Promise.all( allPinned.map(async (id) => { try { const agent = await client.agents.retrieve(id); agents.push({ id, name: agent.name || "(unnamed)" }); } catch { // Agent not found, skip } }), ); return agents; } async function main(): Promise { markMilestone("CLI_START"); // Initialize settings manager (loads settings once into memory) await settingsManager.initialize(); const settings = await settingsManager.getSettingsWithSecureTokens(); markMilestone("SETTINGS_LOADED"); // Initialize LSP infrastructure for type checking if (process.env.LETTA_ENABLE_LSP) { try { const { lspManager } = await import("./lsp/manager.js"); await lspManager.initialize(process.cwd()); } catch (error) { console.error("[LSP] Failed to initialize:", error); } } // Initialize telemetry (enabled by default, opt-out via LETTA_CODE_TELEM=0) telemetry.init(); // Check for updates on startup (non-blocking) const { checkAndAutoUpdate } = await import("./updater/auto-update"); checkAndAutoUpdate() .then((result) => { // Surface ENOTEMPTY failures so users know how to fix if (result?.enotemptyFailed) { console.error( "\nAuto-update failed due to filesystem issue (ENOTEMPTY).", ); console.error( "Fix: rm -rf $(npm prefix -g)/lib/node_modules/@letta-ai/letta-code && npm i -g @letta-ai/letta-code\n", ); } }) .catch(() => { // Silently ignore other update failures (network timeouts, etc.) }); // Clean up old overflow files (non-blocking, 24h retention) const { cleanupOldOverflowFiles } = await import("./tools/impl/overflow"); Promise.resolve().then(() => { try { cleanupOldOverflowFiles(process.cwd()); } catch { // Silently ignore cleanup failures } }); // Parse command-line arguments (Bun-idiomatic approach using parseArgs) // Preprocess args to support --conv as alias for --conversation const processedArgs = process.argv.map((arg) => arg === "--conv" ? "--conversation" : arg, ); let values: Record; let positionals: string[]; try { const parsed = parseArgs({ args: processedArgs, options: { help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" }, info: { type: "boolean" }, continue: { type: "boolean" }, // Deprecated - kept for error message resume: { type: "boolean", short: "r" }, // Resume last session (or specific conversation with --conversation) conversation: { type: "string", short: "C" }, // Specific conversation ID to resume (--conv alias supported) default: { type: "boolean" }, // Alias for --conv default (use agent's default conversation) "new-agent": { type: "boolean" }, // Force create a new agent new: { type: "boolean" }, // Deprecated - kept for helpful error message "init-blocks": { type: "string" }, "base-tools": { type: "string" }, agent: { type: "string", short: "a" }, name: { type: "string", short: "n" }, model: { type: "string", short: "m" }, system: { type: "string", short: "s" }, "system-custom": { type: "string" }, "system-append": { type: "string" }, "memory-blocks": { type: "string" }, "block-value": { type: "string", multiple: true }, toolset: { type: "string" }, prompt: { type: "boolean", short: "p" }, run: { type: "boolean" }, tools: { type: "string" }, allowedTools: { type: "string" }, disallowedTools: { type: "string" }, "permission-mode": { type: "string" }, yolo: { type: "boolean" }, "output-format": { type: "string" }, "input-format": { type: "string" }, "include-partial-messages": { type: "boolean" }, skills: { type: "string" }, sleeptime: { type: "boolean" }, "from-af": { type: "string" }, "no-skills": { type: "boolean" }, }, strict: true, allowPositionals: true, }); values = parsed.values; positionals = parsed.positionals; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); // Improve error message for common mistakes if (errorMsg.includes("Unknown option")) { console.error(`Error: ${errorMsg}`); console.error( "\nNote: Flags should use double dashes for full names (e.g., --yolo, not -yolo)", ); } else { console.error(`Error: ${errorMsg}`); } console.error("Run 'letta --help' for usage information."); process.exit(1); } // Check for subcommands const command = positionals[2]; // First positional after node and script // Handle help flag first if (values.help) { printHelp(); process.exit(0); } // Handle version flag if (values.version) { const { getVersion } = await import("./version"); console.log(`${getVersion()} (Letta Code)`); process.exit(0); } // Handle info flag if (values.info) { await printInfo(); process.exit(0); } // Handle update command if (command === "update") { const { manualUpdate } = await import("./updater/auto-update"); const result = await manualUpdate(); console.log(result.message); process.exit(result.success ? 0 : 1); } // --continue: Resume last session (agent + conversation) automatically const shouldContinue = (values.continue as boolean | undefined) ?? false; // --resume: Open agent selector UI after loading const shouldResume = (values.resume as boolean | undefined) ?? false; let specifiedConversationId = (values.conversation as string | undefined) ?? null; // Specific conversation to resume const useDefaultConv = (values.default as boolean | undefined) ?? false; // --default flag const forceNew = (values["new-agent"] as boolean | undefined) ?? false; // Handle --default flag (alias for --conv default) if (useDefaultConv) { if (specifiedConversationId && specifiedConversationId !== "default") { console.error( "Error: --default cannot be used with --conversation (they're mutually exclusive)", ); process.exit(1); } specifiedConversationId = "default"; } // --new: Create a new conversation (for concurrent sessions) const forceNewConversation = (values.new as boolean | undefined) ?? false; const initBlocksRaw = values["init-blocks"] as string | undefined; const baseToolsRaw = values["base-tools"] as string | undefined; let specifiedAgentId = (values.agent as string | undefined) ?? null; // Handle --conv {agent-id} shorthand: --conv agent-xyz → --agent agent-xyz --conv default if (specifiedConversationId?.startsWith("agent-")) { if (specifiedAgentId && specifiedAgentId !== specifiedConversationId) { console.error( `Error: Conflicting agent IDs: --agent ${specifiedAgentId} vs --conv ${specifiedConversationId}`, ); process.exit(1); } specifiedAgentId = specifiedConversationId; specifiedConversationId = "default"; } // Validate --conv default requires --agent if (specifiedConversationId === "default" && !specifiedAgentId) { console.error("Error: --conv default requires --agent "); console.error("Usage: letta --agent agent-xyz --conv default"); console.error(" or: letta --conv agent-xyz (shorthand)"); process.exit(1); } const specifiedAgentName = (values.name as string | undefined) ?? null; const specifiedModel = (values.model as string | undefined) ?? undefined; const systemPromptPreset = (values.system as string | undefined) ?? undefined; const systemCustom = (values["system-custom"] as string | undefined) ?? undefined; // Note: systemAppend is also parsed but only used in headless mode (headless.ts handles it) const memoryBlocksJson = (values["memory-blocks"] as string | undefined) ?? undefined; const specifiedToolset = (values.toolset as string | undefined) ?? undefined; const skillsDirectory = (values.skills as string | undefined) ?? undefined; const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined; const fromAfFile = values["from-af"] as string | undefined; const isHeadless = values.prompt || values.run || !process.stdin.isTTY; // Fail if an unknown command/argument is passed (and we're not in headless mode where it might be a prompt) if (command && !isHeadless) { console.error(`Error: Unknown command or argument "${command}"`); console.error("Run 'letta --help' for usage information."); process.exit(1); } // --init-blocks only makes sense when creating a brand new agent if (initBlocksRaw && !forceNew) { console.error( "Error: --init-blocks can only be used together with --new to control initial memory blocks.", ); process.exit(1); } let initBlocks: string[] | undefined; if (initBlocksRaw !== undefined) { const trimmed = initBlocksRaw.trim(); if (!trimmed || trimmed.toLowerCase() === "none") { // Explicitly requested zero blocks initBlocks = []; } else { initBlocks = trimmed .split(",") .map((name) => name.trim()) .filter((name) => name.length > 0); } } // --base-tools only makes sense when creating a brand new agent if (baseToolsRaw && !forceNew) { console.error( "Error: --base-tools can only be used together with --new to control initial base tools.", ); process.exit(1); } let baseTools: string[] | undefined; if (baseToolsRaw !== undefined) { const trimmed = baseToolsRaw.trim(); if (!trimmed || trimmed.toLowerCase() === "none") { baseTools = []; } else { baseTools = trimmed .split(",") .map((name) => name.trim()) .filter((name) => name.length > 0); } } // Validate toolset if provided if ( specifiedToolset && specifiedToolset !== "codex" && specifiedToolset !== "default" && specifiedToolset !== "gemini" ) { console.error( `Error: Invalid toolset "${specifiedToolset}". Must be "codex", "default", or "gemini".`, ); process.exit(1); } // Validate system prompt options (--system and --system-custom are mutually exclusive) if (systemPromptPreset && systemCustom) { console.error( "Error: --system and --system-custom are mutually exclusive. Use one or the other.", ); process.exit(1); } // Validate system prompt preset if provided (can be a system prompt ID or subagent name) if (systemPromptPreset) { const { SYSTEM_PROMPTS } = await import("./agent/promptAssets"); const { getAllSubagentConfigs } = await import("./agent/subagents"); const validSystemPrompts = SYSTEM_PROMPTS.map((p) => p.id); const subagentConfigs = await getAllSubagentConfigs(); const validSubagentNames = Object.keys(subagentConfigs); const isValidSystemPrompt = validSystemPrompts.includes(systemPromptPreset); const isValidSubagent = validSubagentNames.includes(systemPromptPreset); if (!isValidSystemPrompt && !isValidSubagent) { const allValid = [...validSystemPrompts, ...validSubagentNames]; console.error( `Error: Invalid system prompt "${systemPromptPreset}". Must be one of: ${allValid.join(", ")}.`, ); process.exit(1); } } // Parse memory blocks JSON if provided let memoryBlocks: | Array<{ label: string; value: string; description?: string }> | undefined; if (memoryBlocksJson) { try { memoryBlocks = JSON.parse(memoryBlocksJson); if (!Array.isArray(memoryBlocks)) { throw new Error("memory-blocks must be a JSON array"); } // Validate each block has required fields for (const block of memoryBlocks) { if ( typeof block.label !== "string" || typeof block.value !== "string" ) { throw new Error( "Each memory block must have 'label' and 'value' string fields", ); } } } catch (error) { console.error( `Error: Invalid --memory-blocks JSON: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); } } // Validate --conversation flag (mutually exclusive with agent-selection flags) // Exception: --conv default requires --agent if (specifiedConversationId && specifiedConversationId !== "default") { if (specifiedAgentId) { console.error("Error: --conversation cannot be used with --agent"); process.exit(1); } if (specifiedAgentName) { console.error("Error: --conversation cannot be used with --name"); process.exit(1); } if (forceNew) { console.error("Error: --conversation cannot be used with --new-agent"); process.exit(1); } if (fromAfFile) { console.error("Error: --conversation cannot be used with --from-af"); process.exit(1); } if (shouldResume) { console.error("Error: --conversation cannot be used with --resume"); process.exit(1); } if (shouldContinue) { console.error("Error: --conversation cannot be used with --continue"); process.exit(1); } } // Validate --new flag (create new conversation) if (forceNewConversation) { if (shouldContinue) { console.error("Error: --new cannot be used with --continue"); process.exit(1); } if (specifiedConversationId) { console.error("Error: --new cannot be used with --conversation"); process.exit(1); } if (shouldResume) { console.error("Error: --new cannot be used with --resume"); process.exit(1); } } // Validate --from-af flag if (fromAfFile) { if (specifiedAgentId) { console.error("Error: --from-af cannot be used with --agent"); process.exit(1); } if (specifiedAgentName) { console.error("Error: --from-af cannot be used with --name"); process.exit(1); } if (shouldResume) { console.error("Error: --from-af cannot be used with --resume"); process.exit(1); } if (forceNew) { console.error("Error: --from-af cannot be used with --new"); process.exit(1); } // Verify file exists const { resolve } = await import("node:path"); const { existsSync } = await import("node:fs"); const resolvedPath = resolve(fromAfFile); if (!existsSync(resolvedPath)) { console.error(`Error: AgentFile not found: ${resolvedPath}`); process.exit(1); } } // Validate --name flag if (specifiedAgentName) { if (specifiedAgentId) { console.error("Error: --name cannot be used with --agent"); process.exit(1); } if (forceNew) { console.error("Error: --name cannot be used with --new"); process.exit(1); } } // Check if API key is configured const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY; const baseURL = process.env.LETTA_BASE_URL || settings.env?.LETTA_BASE_URL || LETTA_CLOUD_API_URL; // Check if refresh token is missing for Letta Cloud (only when not using env var) // Skip this check if we already have an API key from env if ( !isHeadless && baseURL === LETTA_CLOUD_API_URL && !settings.refreshToken && !apiKey ) { // For interactive mode, show setup flow const { runSetup } = await import("./auth/setup"); await runSetup(); // After setup, restart main flow return main().catch((err: unknown) => { // Handle top-level errors gracefully without raw stack traces const message = err instanceof Error ? err.message : "An unexpected error occurred"; console.error(`\nError: ${message}`); if (process.env.DEBUG) { console.error(err); } process.exit(1); }); } if (!apiKey && baseURL === LETTA_CLOUD_API_URL) { // For headless mode, error out (assume automation context) if (isHeadless) { console.error("Missing LETTA_API_KEY"); console.error( "Run 'letta' in interactive mode to authenticate or export the missing environment variable", ); process.exit(1); } // For interactive mode, show setup flow console.log("No credentials found. Let's get you set up!\n"); const { runSetup } = await import("./auth/setup"); await runSetup(); // After setup, restart main flow return main(); } // Validate credentials by checking health endpoint const { validateCredentials } = await import("./auth/oauth"); const isValid = await validateCredentials(baseURL, apiKey ?? ""); markMilestone("CREDENTIALS_VALIDATED"); if (!isValid) { // For headless mode, error out with helpful message if (isHeadless) { console.error("Failed to connect to Letta server"); console.error(`Base URL: ${baseURL}`); console.error( "Your credentials may be invalid or the server may be unreachable.", ); console.error( "Delete ~/.config/letta/settings.json then run 'letta' to re-authenticate", ); process.exit(1); } // For interactive mode, show setup flow console.log("Failed to connect to Letta server."); console.log(`Base URL: ${baseURL}\n`); console.log( "Your credentials may be invalid or the server may be unreachable.", ); console.log("Let's reconfigure your setup.\n"); const { runSetup } = await import("./auth/setup"); await runSetup(); // After setup, restart main flow return main(); } // Resolve --name to agent ID if provided if (specifiedAgentName) { // Load local settings for LRU priority await settingsManager.loadLocalProjectSettings(); const resolved = await resolveAgentByName(specifiedAgentName); if (!resolved) { console.error( `Error: No pinned agent found with name "${specifiedAgentName}"`, ); console.error(""); const pinnedAgents = await getPinnedAgentNames(); if (pinnedAgents.length > 0) { console.error("Available pinned agents:"); for (const agent of pinnedAgents) { console.error(` - "${agent.name}" (${agent.id})`); } } else { console.error( "No pinned agents available. Use /pin to pin an agent first.", ); } process.exit(1); } specifiedAgentId = resolved.id; } // Set tool filter if provided (controls which tools are loaded) if (values.tools !== undefined) { const { toolFilter } = await import("./tools/filter"); toolFilter.setEnabledTools(values.tools as string); } // Set CLI permission overrides if provided if (values.allowedTools || values.disallowedTools) { const { cliPermissions } = await import("./permissions/cli"); if (values.allowedTools) { cliPermissions.setAllowedTools(values.allowedTools as string); } if (values.disallowedTools) { cliPermissions.setDisallowedTools(values.disallowedTools as string); } } // Set permission mode if provided (or via --yolo alias) const permissionModeValue = values["permission-mode"] as string | undefined; const yoloMode = values.yolo as boolean | undefined; if (yoloMode || permissionModeValue) { if (yoloMode) { // --yolo is an alias for --permission-mode bypassPermissions permissionMode.setMode("bypassPermissions"); } else if (permissionModeValue) { const mode = permissionModeValue; const validModes = [ "default", "acceptEdits", "plan", "bypassPermissions", ] as const; if (validModes.includes(mode as (typeof validModes)[number])) { permissionMode.setMode(mode as (typeof validModes)[number]); } else { console.error( `Invalid permission mode: ${mode}. Valid modes: ${validModes.join(", ")}`, ); process.exit(1); } } } if (isHeadless) { markMilestone("HEADLESS_MODE_START"); // For headless mode, load tools synchronously (respecting model/toolset when provided) const modelForTools = getModelForToolLoading( specifiedModel, specifiedToolset as "codex" | "default" | undefined, ); await loadTools(modelForTools); markMilestone("TOOLS_LOADED"); const { handleHeadlessCommand } = await import("./headless"); await handleHeadlessCommand(process.argv, specifiedModel, skillsDirectory); return; } markMilestone("TUI_MODE_START"); // Enable enhanced key reporting (Shift+Enter, etc.) BEFORE Ink initializes. // In VS Code/xterm.js this typically requires a short handshake (query + enable). try { const { detectAndEnableKittyProtocol } = await import( "./cli/utils/kittyProtocolDetector" ); await detectAndEnableKittyProtocol(); } catch { // Best-effort: if this fails, the app still runs (Option+Enter remains supported). } // Interactive: lazy-load React/Ink + App markMilestone("REACT_IMPORT_START"); const React = await import("react"); const { render } = await import("ink"); const { useState, useEffect } = React; const AppModule = await import("./cli/App"); const App = AppModule.default; function LoadingApp({ continueSession, forceNew, initBlocks, baseTools, agentIdArg, model, systemPromptPreset, toolset, skillsDirectory, fromAfFile, }: { continueSession: boolean; forceNew: boolean; initBlocks?: string[]; baseTools?: string[]; agentIdArg: string | null; model?: string; systemPromptPreset?: string; toolset?: "codex" | "default" | "gemini"; skillsDirectory?: string; fromAfFile?: string; }) { const [showKeybindingSetup, setShowKeybindingSetup] = useState< boolean | null >(null); const [loadingState, setLoadingState] = useState< | "selecting" | "selecting_global" | "selecting_conversation" | "assembling" | "importing" | "initializing" | "checking" | "ready" >("selecting"); const [agentId, setAgentId] = useState(null); const [agentState, setAgentState] = useState(null); const [conversationId, setConversationId] = useState(null); const [resumeData, setResumeData] = useState(null); const [isResumingSession, setIsResumingSession] = useState(false); const [resumedExistingConversation, setResumedExistingConversation] = useState(false); const [agentProvenance, setAgentProvenance] = useState(null); const [selectedGlobalAgentId, setSelectedGlobalAgentId] = useState< string | null >(null); // Track agent and conversation for conversation selector (--resume flag) const [resumeAgentId, setResumeAgentId] = useState(null); const [resumeAgentName, setResumeAgentName] = useState(null); const [selectedConversationId, setSelectedConversationId] = useState< string | null >(null); // Track when user explicitly requested new agent from selector (not via --new flag) const [userRequestedNewAgent, setUserRequestedNewAgent] = useState(false); // Release notes to display (checked once on mount) const [releaseNotes, setReleaseNotes] = useState(null); // Auto-install Shift+Enter keybinding for VS Code/Cursor/Windsurf (silent, no prompt) useEffect(() => { async function autoInstallKeybinding() { const { detectTerminalType, getKeybindingsPath, keybindingExists, installKeybinding, } = await import("./cli/utils/terminalKeybindingInstaller"); const { loadSettings, updateSettings } = await import("./settings"); const terminal = detectTerminalType(); if (!terminal) { setShowKeybindingSetup(false); return; } const settings = await loadSettings(); const keybindingsPath = getKeybindingsPath(terminal); // Skip if already installed or no valid path if (!keybindingsPath || settings.shiftEnterKeybindingInstalled) { setShowKeybindingSetup(false); return; } // Check if keybinding already exists (user might have added it manually) if (keybindingExists(keybindingsPath)) { await updateSettings({ shiftEnterKeybindingInstalled: true }); setShowKeybindingSetup(false); return; } // Silently install keybinding (no prompt, just like Claude Code) const result = installKeybinding(keybindingsPath); if (result.success) { await updateSettings({ shiftEnterKeybindingInstalled: true }); } setShowKeybindingSetup(false); } async function autoInstallWezTermFix() { const { isWezTerm, wezTermDeleteFixExists, getWezTermConfigPath, installWezTermDeleteFix, } = await import("./cli/utils/terminalKeybindingInstaller"); const { loadSettings, updateSettings } = await import("./settings"); if (!isWezTerm()) return; const settings = await loadSettings(); if (settings.wezTermDeleteFixInstalled) return; const configPath = getWezTermConfigPath(); if (wezTermDeleteFixExists(configPath)) { await updateSettings({ wezTermDeleteFixInstalled: true }); return; } // Silently install the fix const result = installWezTermDeleteFix(); if (result.success) { await updateSettings({ wezTermDeleteFixInstalled: true }); } } autoInstallKeybinding(); autoInstallWezTermFix(); }, []); // Check for release notes to display (runs once on mount) useEffect(() => { async function checkNotes() { const { checkReleaseNotes } = await import("./release-notes"); const notes = await checkReleaseNotes(); setReleaseNotes(notes); } checkNotes(); }, []); // Initialize on mount - check if we should show global agent selector useEffect(() => { async function checkAndStart() { // Load settings await settingsManager.loadLocalProjectSettings(); const localSettings = settingsManager.getLocalProjectSettings(); let globalPinned = settingsManager.getGlobalPinnedAgents(); const client = await getClient(); // ===================================================================== // TOP-LEVEL PATH: --conversation // Conversation ID is unique, so we can derive the agent from it // (except for "default" which requires --agent flag, validated above) // ===================================================================== if (specifiedConversationId) { if (specifiedConversationId === "default") { // "default" requires --agent (validated in flag preprocessing above) // Use the specified agent directly, skip conversation validation // TypeScript can't see the validation above, but specifiedAgentId is guaranteed if (!specifiedAgentId) { throw new Error("Unreachable: --conv default requires --agent"); } setSelectedGlobalAgentId(specifiedAgentId); setSelectedConversationId("default"); setLoadingState("assembling"); return; } // For explicit conversations, derive agent from conversation try { const conversation = await client.conversations.retrieve( specifiedConversationId, ); // Use the agent that owns this conversation setSelectedGlobalAgentId(conversation.agent_id); setSelectedConversationId(specifiedConversationId); setLoadingState("assembling"); return; } catch (error) { if ( error instanceof APIError && (error.status === 404 || error.status === 422) ) { console.error( `Conversation ${specifiedConversationId} not found`, ); process.exit(1); } throw error; } } // ===================================================================== // TOP-LEVEL PATH: --resume // Show conversation selector for last-used agent (local → global fallback) // ===================================================================== if (shouldResume) { const localSession = settingsManager.getLocalLastSession( process.cwd(), ); const localAgentId = localSession?.agentId ?? localSettings.lastAgent; // Try local LRU first if (localAgentId) { try { const agent = await client.agents.retrieve(localAgentId); setResumeAgentId(localAgentId); setResumeAgentName(agent.name ?? null); setLoadingState("selecting_conversation"); return; } catch { // Local agent doesn't exist, try global console.log( `Unable to locate agent ${localAgentId} in .letta/, checking global (~/.letta)`, ); } } else { console.log("No recent agent in .letta/, using global (~/.letta)"); } // Try global LRU const globalSession = settingsManager.getGlobalLastSession(); const globalAgentId = globalSession?.agentId; if (globalAgentId) { try { const agent = await client.agents.retrieve(globalAgentId); setResumeAgentId(globalAgentId); setResumeAgentName(agent.name ?? null); setLoadingState("selecting_conversation"); return; } catch { // Global agent also doesn't exist } } // No valid agent found anywhere console.error("No recent session found in .letta/ or ~/.letta."); console.error("Run 'letta' to get started."); process.exit(1); } // ===================================================================== // TOP-LEVEL PATH: --continue // Resume last session directly (local → global fallback) // ===================================================================== if (continueSession) { const localSession = settingsManager.getLocalLastSession( process.cwd(), ); const localAgentId = localSession?.agentId ?? localSettings.lastAgent; // Try local LRU first if (localAgentId) { try { await client.agents.retrieve(localAgentId); setSelectedGlobalAgentId(localAgentId); if (localSession?.conversationId) { setSelectedConversationId(localSession.conversationId); } setLoadingState("assembling"); return; } catch { // Local agent doesn't exist, try global console.log( `Unable to locate agent ${localAgentId} in .letta/, checking global (~/.letta)`, ); } } else { console.log("No recent agent in .letta/, using global (~/.letta)"); } // Try global LRU const globalSession = settingsManager.getGlobalLastSession(); const globalAgentId = globalSession?.agentId; if (globalAgentId) { try { await client.agents.retrieve(globalAgentId); setSelectedGlobalAgentId(globalAgentId); if (globalSession?.conversationId) { setSelectedConversationId(globalSession.conversationId); } setLoadingState("assembling"); return; } catch { // Global agent also doesn't exist } } // No valid agent found anywhere console.error("No recent session found in .letta/ or ~/.letta."); console.error("Run 'letta' to get started."); process.exit(1); } // ===================================================================== // DEFAULT PATH: No special flags // Check local LRU, then selector, then defaults // ===================================================================== // Check if user would see selector (fresh dir, no bypass flags) const wouldShowSelector = !localSettings.lastAgent && !forceNew && !agentIdArg && !fromAfFile; // Ensure default agents (Memo/Incognito) exist for all users const { ensureDefaultAgents } = await import("./agent/defaults"); if (wouldShowSelector && globalPinned.length === 0) { // New user with no agents - create defaults first, then trigger init // NOTE: Don't set loadingState to "assembling" until we have the agent ID, // otherwise init will run before we've set selectedGlobalAgentId try { const memoAgent = await ensureDefaultAgents(client); // Refresh pinned list after defaults created globalPinned = settingsManager.getGlobalPinnedAgents(); // Auto-select Memo for fresh users if (memoAgent) { setSelectedGlobalAgentId(memoAgent.id); setLoadingState("assembling"); return; } // If memoAgent is null (createDefaultAgents disabled), fall through } catch (err) { console.error( `Failed to create default agents: ${err instanceof Error ? err.message : String(err)}`, ); process.exit(1); } } else { // Existing user - fire and forget, don't block startup ensureDefaultAgents(client).catch(() => { // Silently ignore - defaults may already exist }); } // If there's a local LRU, use it directly if (localSettings.lastAgent) { try { await client.agents.retrieve(localSettings.lastAgent); setLoadingState("assembling"); return; } catch { // LRU agent doesn't exist, show message and fall through to selector console.log( `Unable to locate recently used agent ${localSettings.lastAgent}`, ); } } // Check if we should show selector or auto-select Memo if (wouldShowSelector && globalPinned.length > 0) { // Check if only default agents are pinned const { onlyDefaults, memoId } = await hasOnlyDefaultAgents(globalPinned); if (onlyDefaults && memoId) { // Only defaults pinned - auto-select Memo setSelectedGlobalAgentId(memoId); setLoadingState("assembling"); return; } // Has custom agents - show selector setLoadingState("selecting_global"); return; } setLoadingState("assembling"); } checkAndStart(); }, [ forceNew, agentIdArg, fromAfFile, continueSession, shouldResume, specifiedConversationId, ]); // Main initialization effect - runs after profile selection useEffect(() => { if (loadingState !== "assembling") return; async function init() { const client = await getClient(); // Determine which agent we'll be using (before loading tools) let resumingAgentId: string | null = null; // Priority 1: --agent flag if (agentIdArg) { try { await client.agents.retrieve(agentIdArg); resumingAgentId = agentIdArg; } catch { // Agent doesn't exist, will create new later } } // Priority 1.5: Use agent from conversation selector (--resume flag) if (!resumingAgentId && resumeAgentId) { resumingAgentId = resumeAgentId; } // Priority 2: Use agent selected from global selector (user just picked one) // This takes precedence over stale LRU since user explicitly chose it const shouldCreateNew = forceNew || userRequestedNewAgent; if (!resumingAgentId && !shouldCreateNew && selectedGlobalAgentId) { try { await client.agents.retrieve(selectedGlobalAgentId); resumingAgentId = selectedGlobalAgentId; } catch { // Selected agent doesn't exist - show selector again setLoadingState("selecting_global"); return; } } // Priority 3: LRU from local settings (if not --new or user explicitly requested new from selector) if (!resumingAgentId && !shouldCreateNew) { const localProjectSettings = settingsManager.getLocalProjectSettings(); if (localProjectSettings?.lastAgent) { try { await client.agents.retrieve(localProjectSettings.lastAgent); resumingAgentId = localProjectSettings.lastAgent; } catch { // LRU agent doesn't exist (wrong org, deleted, etc.) // Show selector instead of silently creating a new agent setLoadingState("selecting_global"); return; } } // Priority 4: Try global settings if --continue flag if (!resumingAgentId && continueSession && settings.lastAgent) { try { await client.agents.retrieve(settings.lastAgent); resumingAgentId = settings.lastAgent; } catch { // Global agent doesn't exist - show selector setLoadingState("selecting_global"); return; } } } // Set resuming state early so loading messages are accurate setIsResumingSession(!!resumingAgentId); // Load toolset: use explicit --toolset flag if provided, otherwise derive from model // NOTE: We don't persist toolset per-agent. On resume, toolset is re-derived from model. // If explicit toolset overrides need to persist, see comment in tools/toolset.ts const modelForTools = getModelForToolLoading( model, toolset as "codex" | "default" | undefined, ); await loadTools(modelForTools); setLoadingState("initializing"); const { createAgent } = await import("./agent/create"); const { getModelUpdateArgs } = await import("./agent/model"); let agent: AgentState | null = null; // Priority 1: Import from AgentFile template if (fromAfFile) { setLoadingState("importing"); const { importAgentFromFile } = await import("./agent/import"); const result = await importAgentFromFile({ filePath: fromAfFile, modelOverride: model, stripMessages: true, }); agent = result.agent; setAgentProvenance({ isNew: true, blocks: [], }); } // Priority 2: Try to use --agent specified ID if (!agent && agentIdArg) { try { agent = await client.agents.retrieve(agentIdArg); // Apply --system flag to existing agent if provided if (systemPromptPreset) { const { updateAgentSystemPrompt } = await import( "./agent/modify" ); const result = await updateAgentSystemPrompt( agent.id, systemPromptPreset, ); if (!result.success || !result.agent) { console.error( `Failed to update system prompt: ${result.message}`, ); process.exit(1); } agent = result.agent; } } catch (error) { console.error( `Agent ${agentIdArg} not found (error: ${JSON.stringify(error)})`, ); console.error( "When using --agent, the specified agent ID must exist.", ); console.error("Run 'letta' without --agent to create a new agent."); process.exit(1); } } // Priority 3: Check if --new flag was passed or user requested new from selector if (!agent && shouldCreateNew) { const updateArgs = getModelUpdateArgs(model); const result = await createAgent( undefined, model, undefined, updateArgs, skillsDirectory, true, // parallelToolCalls always enabled sleeptimeFlag ?? settings.enableSleeptime, systemPromptPreset, initBlocks, baseTools, ); agent = result.agent; setAgentProvenance(result.provenance); } // Priority 4: Try to resume from project settings LRU (.letta/settings.local.json) // Note: If LRU retrieval failed in early validation, we already showed selector and returned // This block handles the case where we have a valid resumingAgentId from early validation if (!agent && resumingAgentId) { try { agent = await client.agents.retrieve(resumingAgentId); } catch (error) { // Agent disappeared between validation and now - show selector console.error( `Agent ${resumingAgentId} not found (error: ${JSON.stringify(error)})`, ); setLoadingState("selecting_global"); return; } } // Priority 6: Try to reuse global lastAgent if --continue flag is passed // Note: If global lastAgent retrieval failed in early validation (with --continue), // we already showed selector and returned. This is a safety fallback. if (!agent && continueSession && settings.lastAgent) { try { agent = await client.agents.retrieve(settings.lastAgent); } catch (error) { // Agent disappeared - show selector instead of silently creating console.error( `Previous agent ${settings.lastAgent} not found (error: ${JSON.stringify(error)})`, ); setLoadingState("selecting_global"); return; } } // All paths should have resolved to an agent by now // If not, it's an unexpected state - error out instead of auto-creating if (!agent) { console.error( "No agent found. Use --new-agent to create a new agent.", ); process.exit(1); } // Ensure local project settings are loaded before updating // (they may not have been loaded if we didn't try to resume from project settings) try { settingsManager.getLocalProjectSettings(); } catch { await settingsManager.loadLocalProjectSettings(); } // Save agent ID to both project and global settings settingsManager.updateLocalProjectSettings({ lastAgent: agent.id }); settingsManager.updateSettings({ lastAgent: agent.id }); // Ensure the agent has the required skills blocks (for backwards compatibility) const createdBlocks = await ensureSkillsBlocks(agent.id); if (createdBlocks.length > 0) { console.log("Created missing skills blocks for agent compatibility"); } // Set agent context for tools that need it (e.g., Skill tool) setAgentContext(agent.id, skillsDirectory); // Fire-and-forget: Initialize loaded skills flag (LET-7101) // Don't await - this is just for the skill unload reminder initializeLoadedSkillsFlag().catch(() => { // Ignore errors - not critical }); // Fire-and-forget: Sync skills in background (LET-7101) // This ensures new skills added after agent creation are available // Don't await - user can start typing immediately (async () => { try { const { syncSkillsToAgent, SKILLS_DIR } = await import( "./agent/skills" ); const { join } = await import("node:path"); const resolvedSkillsDirectory = skillsDirectory || join(process.cwd(), SKILLS_DIR); await syncSkillsToAgent(client, agent.id, resolvedSkillsDirectory, { skipIfUnchanged: true, }); } catch (error) { console.warn( `[skills] Background sync failed: ${error instanceof Error ? error.message : String(error)}`, ); } })(); // Check if we're resuming an existing agent // We're resuming if: // 1. We specified an agent ID via --agent flag (agentIdArg) // 2. We used --resume flag (continueSession) // 3. We're reusing a project agent (detected early as resumingAgentId) // 4. We retrieved an agent from LRU (detected by checking if agent already existed) const isResumingProject = !shouldCreateNew && !!resumingAgentId; const isReusingExistingAgent = !shouldCreateNew && !fromAfFile && agent && agent.id; const resuming = !!( continueSession || agentIdArg || isResumingProject || isReusingExistingAgent ); setIsResumingSession(resuming); // If resuming and a model or system prompt was specified, apply those changes if (resuming && (model || systemPromptPreset)) { if (model) { const { resolveModel } = await import("./agent/model"); const modelHandle = resolveModel(model); if (!modelHandle) { console.error(`Error: Invalid model "${model}"`); process.exit(1); } // Optimization: Skip update if agent is already using the specified model const currentModel = agent.llm_config?.model; const currentEndpointType = agent.llm_config?.model_endpoint_type; const currentHandle = `${currentEndpointType}/${currentModel}`; if (currentHandle !== modelHandle) { const { updateAgentLLMConfig } = await import("./agent/modify"); const { getModelUpdateArgs } = await import("./agent/model"); const updateArgs = getModelUpdateArgs(model); await updateAgentLLMConfig(agent.id, modelHandle, updateArgs); // Refresh agent state after model update agent = await client.agents.retrieve(agent.id); } } if (systemPromptPreset) { const { updateAgentSystemPrompt } = await import("./agent/modify"); const result = await updateAgentSystemPrompt( agent.id, systemPromptPreset, ); if (!result.success || !result.agent) { console.error(`Error: ${result.message}`); process.exit(1); } agent = result.agent; } } // Handle conversation: either resume existing or create new // Using definite assignment assertion - all branches below either set this or exit/throw let conversationIdToUse!: string; // Debug: log resume flag status if (process.env.DEBUG) { console.log(`[DEBUG] shouldContinue=${shouldContinue}`); console.log(`[DEBUG] shouldResume=${shouldResume}`); console.log( `[DEBUG] specifiedConversationId=${specifiedConversationId}`, ); } if (specifiedConversationId) { // Use the explicitly specified conversation ID // User explicitly requested this conversation, so error if it doesn't exist conversationIdToUse = specifiedConversationId; setResumedExistingConversation(true); try { // Load message history and pending approvals from the conversation // Re-fetch agent to get fresh message_ids for accurate pending approval detection setLoadingState("checking"); const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, freshAgent, specifiedConversationId, ); setResumeData(data); } catch (error) { // Only treat 404/422 as "not found", rethrow other errors if ( error instanceof APIError && (error.status === 404 || error.status === 422) ) { console.error( `Conversation ${specifiedConversationId} not found`, ); process.exit(1); } throw error; } } else if (shouldContinue) { // Try to load the last session for this agent const lastSession = settingsManager.getLocalLastSession(process.cwd()) ?? settingsManager.getGlobalLastSession(); if (process.env.DEBUG) { console.log(`[DEBUG] lastSession=${JSON.stringify(lastSession)}`); console.log(`[DEBUG] agent.id=${agent.id}`); } let resumedSuccessfully = false; if (lastSession && lastSession.agentId === agent.id) { // Try to resume the exact last conversation // If it no longer exists, fall back to creating new try { // Load message history and pending approvals from the conversation // Re-fetch agent to get fresh message_ids for accurate pending approval detection setLoadingState("checking"); const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, freshAgent, lastSession.conversationId, ); // Only set state after validation succeeds conversationIdToUse = lastSession.conversationId; setResumedExistingConversation(true); setResumeData(data); resumedSuccessfully = true; } catch (error) { // Only treat 404/422 as "not found", rethrow other errors if ( error instanceof APIError && (error.status === 404 || error.status === 422) ) { // Conversation no longer exists, will create new below console.warn( `Previous conversation ${lastSession.conversationId} not found, creating new`, ); } else { throw error; } } } if (!resumedSuccessfully) { // No valid session to resume - error with helpful message console.error( `Attempting to resume conversation ${lastSession?.conversationId ?? "(unknown)"}, but conversation was not found.`, ); console.error( "Resume the default conversation with 'letta', view recent conversations with 'letta --resume', or start a new conversation with 'letta --new'.", ); process.exit(1); } } else if (selectedConversationId) { // User selected a specific conversation from the --resume selector try { setLoadingState("checking"); const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, freshAgent, selectedConversationId, ); conversationIdToUse = selectedConversationId; setResumedExistingConversation(true); setResumeData(data); } catch (error) { if ( error instanceof APIError && (error.status === 404 || error.status === 422) ) { console.error(`Conversation ${selectedConversationId} not found`); process.exit(1); } throw error; } } else if (forceNewConversation) { // --new flag: create a new conversation (for concurrent sessions) const conversation = await client.conversations.create({ agent_id: agent.id, isolated_block_labels: [...ISOLATED_BLOCK_LABELS], }); conversationIdToUse = conversation.id; } else { // Default (including --new-agent): use the agent's "default" conversation conversationIdToUse = "default"; // Load message history from the default conversation setLoadingState("checking"); const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData(client, freshAgent, "default"); setResumeData(data); setResumedExistingConversation(true); } // Save the session (agent + conversation) to settings // Skip for subagents - they shouldn't pollute the LRU settings const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent"; if (!isSubagent) { settingsManager.setLocalLastSession( { agentId: agent.id, conversationId: conversationIdToUse }, process.cwd(), ); settingsManager.setGlobalLastSession({ agentId: agent.id, conversationId: conversationIdToUse, }); } setAgentId(agent.id); setAgentState(agent); setConversationId(conversationIdToUse); // Also set in global context for tools (e.g., Skill tool) to access setContextConversationId(conversationIdToUse); setLoadingState("ready"); } init().catch((err) => { // Handle errors gracefully without showing raw stack traces const message = err instanceof Error ? err.message : "An unexpected error occurred"; console.error(`\nError during initialization: ${message}`); if (process.env.DEBUG) { console.error(err); } process.exit(1); }); }, [ continueSession, forceNew, userRequestedNewAgent, agentIdArg, model, systemPromptPreset, fromAfFile, loadingState, selectedGlobalAgentId, shouldContinue, resumeAgentId, selectedConversationId, ]); // Wait for keybinding auto-install to complete before showing UI if (showKeybindingSetup === null) { return null; } // Don't render anything during initial "selecting" phase - wait for checkAndStart if (loadingState === "selecting") { return null; } // Show conversation selector for --resume flag if (loadingState === "selecting_conversation" && resumeAgentId) { return React.createElement(ConversationSelector, { agentId: resumeAgentId, agentName: resumeAgentName ?? undefined, currentConversationId: "", // No current conversation yet onSelect: (conversationId: string) => { setSelectedConversationId(conversationId); setLoadingState("assembling"); }, onNewConversation: () => { // Start with a new conversation for this agent setLoadingState("assembling"); }, onCancel: () => { process.exit(0); }, }); } // Show global agent selector in fresh repos with global pinned agents if (loadingState === "selecting_global") { return React.createElement(ProfileSelectionInline, { lruAgentId: null, // No LRU in fresh repo loading: false, freshRepoMode: true, // Hides "(global)" labels and simplifies context message onSelect: (agentId: string) => { setSelectedGlobalAgentId(agentId); setLoadingState("assembling"); }, onCreateNew: () => { setUserRequestedNewAgent(true); setLoadingState("assembling"); }, onExit: () => { process.exit(0); }, }); } // At this point, loadingState is not "selecting", "selecting_global", or "selecting_conversation" // (those are handled above), so it's safe to pass to App const appLoadingState = loadingState as Exclude< typeof loadingState, "selecting" | "selecting_global" | "selecting_conversation" >; if (!agentId || !conversationId) { return React.createElement(App, { agentId: "loading", conversationId: "loading", loadingState: appLoadingState, continueSession: isResumingSession, startupApproval: resumeData?.pendingApproval ?? null, startupApprovals: resumeData?.pendingApprovals ?? EMPTY_APPROVAL_ARRAY, messageHistory: resumeData?.messageHistory ?? EMPTY_MESSAGE_ARRAY, resumedExistingConversation, tokenStreaming: settings.tokenStreaming, agentProvenance, releaseNotes, }); } return React.createElement(App, { agentId, agentState, conversationId, loadingState: appLoadingState, continueSession: isResumingSession, startupApproval: resumeData?.pendingApproval ?? null, startupApprovals: resumeData?.pendingApprovals ?? EMPTY_APPROVAL_ARRAY, messageHistory: resumeData?.messageHistory ?? EMPTY_MESSAGE_ARRAY, resumedExistingConversation, tokenStreaming: settings.tokenStreaming, agentProvenance, releaseNotes, }); } markMilestone("REACT_RENDER_START"); render( React.createElement(LoadingApp, { continueSession: shouldContinue, forceNew: forceNew, initBlocks: initBlocks, baseTools: baseTools, agentIdArg: specifiedAgentId, model: specifiedModel, systemPromptPreset: systemPromptPreset, toolset: specifiedToolset as "codex" | "default" | "gemini" | undefined, skillsDirectory: skillsDirectory, fromAfFile: fromAfFile, }), { exitOnCtrlC: false, // We handle CTRL-C manually with double-press guard }, ); } main();