From 0104fe4b3b3f0c6c8f57e4a613bc42e080dd8a17 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Sun, 28 Dec 2025 23:17:42 -0800 Subject: [PATCH] feat: add --name/-n option to resume agent by name (#411) Co-authored-by: Letta --- src/cli/App.tsx | 9 +++- src/index.ts | 124 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 5f3c6f4..896a57f 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -5412,7 +5412,14 @@ Plan file path: ${planFilePath}`; })} Resume this agent with: - letta --agent {agentId} + + {/* Show -n "name" if agent has name and is pinned, otherwise --agent */} + {agentName && + (settingsManager.getLocalPinnedAgents().includes(agentId) || + settingsManager.getGlobalPinnedAgents().includes(agentId)) + ? `letta -n "${agentName}"` + : `letta --agent ${agentId}`} + )} diff --git a/src/index.ts b/src/index.ts index 9a7c378..b234c8c 100755 --- a/src/index.ts +++ b/src/index.ts @@ -42,6 +42,7 @@ OPTIONS --init-blocks Comma-separated memory blocks to initialize when using --new (e.g., "persona,skills") --base-tools Comma-separated base tools to attach when using --new (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) @@ -211,6 +212,82 @@ function getModelForToolLoading( 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 { // Initialize settings manager (loads settings once into memory) await settingsManager.initialize(); @@ -240,6 +317,7 @@ async function main(): Promise { "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" }, toolset: { type: "string" }, @@ -311,7 +389,8 @@ async function main(): Promise { const forceNew = (values.new as boolean | undefined) ?? false; const initBlocksRaw = values["init-blocks"] as string | undefined; const baseToolsRaw = values["base-tools"] as string | undefined; - const specifiedAgentId = (values.agent as string | undefined) ?? null; + let specifiedAgentId = (values.agent as string | undefined) ?? null; + const specifiedAgentName = (values.name as string | undefined) ?? null; const specifiedModel = (values.model as string | undefined) ?? undefined; const systemPromptId = (values.system as string | undefined) ?? undefined; const specifiedToolset = (values.toolset as string | undefined) ?? undefined; @@ -410,6 +489,10 @@ async function main(): Promise { 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 (shouldContinue) { console.error("Error: --from-af cannot be used with --continue"); process.exit(1); @@ -428,6 +511,18 @@ async function main(): Promise { } } + // 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 = @@ -508,6 +603,33 @@ async function main(): Promise { 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");