import { parseArgs } from "node:util"; import { getClient } from "../../agent/client"; function printUsage(): void { console.log( ` Usage: letta blocks list --agent [--limit ] letta blocks copy --block-id [--label ] [--agent ] [--override] letta blocks attach --block-id [--agent ] [--read-only] [--override] Notes: - Output is JSON only. - Uses CLI auth; override with LETTA_API_KEY/LETTA_BASE_URL if needed. - Default target agent for copy/attach is LETTA_AGENT_ID. `.trim(), ); } function parseLimit(value: unknown, fallback: number): number { if (typeof value !== "string" || value.length === 0) return fallback; const parsed = Number.parseInt(value, 10); return Number.isNaN(parsed) ? fallback : parsed; } function getAgentId(agentFromArgs?: string, agentIdFromArgs?: string): string { return agentFromArgs || agentIdFromArgs || process.env.LETTA_AGENT_ID || ""; } const BLOCKS_OPTIONS = { help: { type: "boolean", short: "h" }, agent: { type: "string" }, "agent-id": { type: "string" }, limit: { type: "string" }, "block-id": { type: "string" }, label: { type: "string" }, override: { type: "boolean" }, "read-only": { type: "boolean" }, } as const; function parseBlocksArgs(argv: string[]) { return parseArgs({ args: argv, options: BLOCKS_OPTIONS, strict: true, allowPositionals: true, }); } type CopyBlockResult = { sourceBlock: Awaited< ReturnType>["blocks"]["retrieve"]> >; newBlock: Awaited< ReturnType>["blocks"]["create"]> >; attachResult: Awaited< ReturnType< Awaited>["agents"]["blocks"]["attach"] > >; detachedBlock?: Awaited< ReturnType>["blocks"]["retrieve"]> >; }; type AttachBlockResult = { attachResult: Awaited< ReturnType< Awaited>["agents"]["blocks"]["attach"] > >; detachedBlock?: Awaited< ReturnType>["blocks"]["retrieve"]> >; }; async function copyBlock( client: Awaited>, blockId: string, options?: { labelOverride?: string; targetAgentId?: string; override?: boolean; }, ): Promise { const currentAgentId = getAgentId(options?.targetAgentId); if (!currentAgentId) { throw new Error( "Missing agent id. Set LETTA_AGENT_ID or pass --agent/--agent-id.", ); } let detachedBlock: | Awaited> | undefined; const sourceBlock = await client.blocks.retrieve(blockId); const targetLabel = options?.labelOverride || sourceBlock.label || "migrated-block"; if (options?.override) { const currentBlocksResponse = await client.agents.blocks.list(currentAgentId); const currentBlocks = Array.isArray(currentBlocksResponse) ? currentBlocksResponse : (currentBlocksResponse as { items?: unknown[] }).items || []; const conflictingBlock = currentBlocks.find( (b: { label?: string }) => b.label === targetLabel, ); if (conflictingBlock) { console.error( `Detaching existing block with label "${targetLabel}" (${conflictingBlock.id})...`, ); detachedBlock = conflictingBlock as Awaited< ReturnType >; try { await client.agents.blocks.detach(conflictingBlock.id, { agent_id: currentAgentId, }); } catch (detachError) { throw new Error( `Failed to detach existing block "${targetLabel}": ${ detachError instanceof Error ? detachError.message : String(detachError) }`, ); } } } let newBlock: Awaited>; try { newBlock = await client.blocks.create({ label: targetLabel, value: sourceBlock.value, description: sourceBlock.description || undefined, limit: sourceBlock.limit, }); } catch (createError) { if (detachedBlock) { console.error( `Create failed, reattaching original block "${detachedBlock.label}"...`, ); try { await client.agents.blocks.attach(detachedBlock.id, { agent_id: currentAgentId, }); console.error("Original block reattached successfully."); } catch { console.error( `WARNING: Failed to reattach original block! Block ID: ${detachedBlock.id}`, ); } } throw createError; } let attachResult: Awaited>; try { attachResult = await client.agents.blocks.attach(newBlock.id, { agent_id: currentAgentId, }); } catch (attachError) { if (detachedBlock) { console.error( `Attach failed, reattaching original block "${detachedBlock.label}"...`, ); try { await client.agents.blocks.attach(detachedBlock.id, { agent_id: currentAgentId, }); console.error("Original block reattached successfully."); } catch { console.error( `WARNING: Failed to reattach original block! Block ID: ${detachedBlock.id}`, ); } } throw attachError; } return { sourceBlock, newBlock, attachResult, detachedBlock }; } async function attachBlock( client: Awaited>, blockId: string, options?: { readOnly?: boolean; targetAgentId?: string; override?: boolean }, ): Promise { const currentAgentId = getAgentId(options?.targetAgentId); if (!currentAgentId) { throw new Error( "Missing agent id. Set LETTA_AGENT_ID or pass --agent/--agent-id.", ); } let detachedBlock: | Awaited> | undefined; if (options?.override) { const sourceBlock = await client.blocks.retrieve(blockId); const sourceLabel = sourceBlock.label; const currentBlocksResponse = await client.agents.blocks.list(currentAgentId); const currentBlocks = Array.isArray(currentBlocksResponse) ? currentBlocksResponse : (currentBlocksResponse as { items?: unknown[] }).items || []; const conflictingBlock = currentBlocks.find( (b: { label?: string }) => b.label === sourceLabel, ); if (conflictingBlock) { console.error( `Detaching existing block with label "${sourceLabel}" (${conflictingBlock.id})...`, ); detachedBlock = conflictingBlock as Awaited< ReturnType >; try { await client.agents.blocks.detach(conflictingBlock.id, { agent_id: currentAgentId, }); } catch (detachError) { throw new Error( `Failed to detach existing block "${sourceLabel}": ${ detachError instanceof Error ? detachError.message : String(detachError) }`, ); } } } let attachResult: Awaited>; try { attachResult = await client.agents.blocks.attach(blockId, { agent_id: currentAgentId, }); } catch (attachError) { if (detachedBlock) { console.error( `Attach failed, reattaching original block "${detachedBlock.label}"...`, ); try { await client.agents.blocks.attach(detachedBlock.id, { agent_id: currentAgentId, }); console.error("Original block reattached successfully."); } catch { console.error( `WARNING: Failed to reattach original block! Block ID: ${detachedBlock.id}`, ); } } throw attachError; } if (options?.readOnly) { console.warn( "Note: read_only flag is set on the block itself, not per-agent. " + "Use the block update API to set read_only if needed.", ); } return { attachResult, detachedBlock }; } export async function runBlocksSubcommand(argv: string[]): Promise { let parsed: ReturnType; try { parsed = parseBlocksArgs(argv); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Error: ${message}`); printUsage(); return 1; } const [action] = parsed.positionals; if (parsed.values.help || !action || action === "help") { printUsage(); return 0; } try { const client = await getClient(); if (action === "list") { const agentId = parsed.values.agent || parsed.values["agent-id"] || ""; if (!agentId || typeof agentId !== "string") { console.error("Missing required --agent ."); return 1; } const result = await client.agents.blocks.list(agentId, { limit: parseLimit(parsed.values.limit, 1000), }); console.log(JSON.stringify(result, null, 2)); return 0; } if (action === "copy") { const blockId = parsed.values["block-id"]; if (!blockId || typeof blockId !== "string") { console.error("Missing required --block-id ."); return 1; } const agentId = getAgentId( parsed.values.agent, parsed.values["agent-id"], ); const result = await copyBlock(client, blockId, { labelOverride: typeof parsed.values.label === "string" ? parsed.values.label : undefined, targetAgentId: agentId, override: parsed.values.override === true, }); console.log(JSON.stringify(result, null, 2)); return 0; } if (action === "attach") { const blockId = parsed.values["block-id"]; if (!blockId || typeof blockId !== "string") { console.error("Missing required --block-id ."); return 1; } const agentId = getAgentId( parsed.values.agent, parsed.values["agent-id"], ); const result = await attachBlock(client, blockId, { readOnly: parsed.values["read-only"] === true, override: parsed.values.override === true, targetAgentId: agentId, }); console.log(JSON.stringify(result, null, 2)); return 0; } } catch (error) { console.error(error instanceof Error ? error.message : String(error)); return 1; } console.error(`Unknown action: ${action}`); printUsage(); return 1; }