From 50c249e36d15a17ffcc202ca96d8d2fdacfba404 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 27 Oct 2025 23:41:34 -0700 Subject: [PATCH] feat: add API key caching via settings.json (#16) Co-authored-by: Letta --- src/agent/client.ts | 34 +++++++++++++++++++++++++++++++--- src/agent/create.ts | 2 +- src/agent/message.ts | 2 +- src/agent/modify.ts | 2 +- src/cli/App.tsx | 6 +++--- src/headless.ts | 2 +- src/index.ts | 10 +++++++--- src/settings.ts | 1 + 8 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/agent/client.ts b/src/agent/client.ts index 49b505b..7fe8ecb 100644 --- a/src/agent/client.ts +++ b/src/agent/client.ts @@ -1,13 +1,41 @@ import { LettaClient } from "@letta-ai/letta-client"; +import { loadSettings, updateSettings } from "../settings"; -export function getClient() { - const token = process.env.LETTA_API_KEY; +export async function getClient() { + const settings = await loadSettings(); + + const token = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY; if (!token) { console.error("Missing LETTA_API_KEY"); + console.error( + "Set it via environment variable or add it to ~/.letta/settings.json:", + ); + console.error(' { "env": { "LETTA_API_KEY": "sk-let-..." } }'); process.exit(1); } - const baseUrl = process.env.LETTA_BASE_URL || "https://api.letta.com"; + const baseUrl = + process.env.LETTA_BASE_URL || + settings.env?.LETTA_BASE_URL || + "https://api.letta.com"; + + // Auto-cache: if env vars are set but not in settings, write them to settings + let needsUpdate = false; + const updatedEnv = { ...settings.env }; + + if (process.env.LETTA_API_KEY && !settings.env?.LETTA_API_KEY) { + updatedEnv.LETTA_API_KEY = process.env.LETTA_API_KEY; + needsUpdate = true; + } + + if (process.env.LETTA_BASE_URL && !settings.env?.LETTA_BASE_URL) { + updatedEnv.LETTA_BASE_URL = process.env.LETTA_BASE_URL; + needsUpdate = true; + } + + if (needsUpdate) { + await updateSettings({ env: updatedEnv }); + } return new LettaClient({ token, baseUrl }); } diff --git a/src/agent/create.ts b/src/agent/create.ts index 3d93bfa..3dd9147 100644 --- a/src/agent/create.ts +++ b/src/agent/create.ts @@ -17,7 +17,7 @@ export async function createAgent( name = "letta-cli-agent", model = "anthropic/claude-sonnet-4-5-20250929", ) { - const client = getClient(); + const client = await getClient(); // Get loaded tool names (tools are already registered with Letta) const toolNames = [ diff --git a/src/agent/message.ts b/src/agent/message.ts index 0c757f6..2fdbbb5 100644 --- a/src/agent/message.ts +++ b/src/agent/message.ts @@ -14,7 +14,7 @@ export async function sendMessageStream( // add more later: includePings, request timeouts, etc. } = { streamTokens: true, background: true }, ): Promise> { - const client = getClient(); + const client = await getClient(); return client.agents.messages.createStream(agentId, { messages: messages, streamTokens: opts.streamTokens ?? true, diff --git a/src/agent/modify.ts b/src/agent/modify.ts index 10f1150..fec375b 100644 --- a/src/agent/modify.ts +++ b/src/agent/modify.ts @@ -20,7 +20,7 @@ export async function updateAgentLLMConfig( modelHandle: string, updateArgs?: Record, ): Promise { - const client = getClient(); + const client = await getClient(); // Step 1: Update model (top-level field) await client.agents.modify(agentId, { model: modelHandle }); diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 55942b2..5486500 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -343,7 +343,7 @@ export default function App({ const fetchConfig = async () => { try { const { getClient } = await import("../agent/client"); - const client = getClient(); + const client = await getClient(); const agent = await client.agents.retrieve(agentId); setLlmConfig(agent.llmConfig); } catch (error) { @@ -541,7 +541,7 @@ export default function App({ setInterruptRequested(true); try { - const client = getClient(); + const client = await getClient(); // Send cancel request to backend await client.agents.messages.cancel(agentId); @@ -746,7 +746,7 @@ export default function App({ // Check for pending approvals before sending message if (CHECK_PENDING_APPROVALS_BEFORE_SEND) { try { - const client = getClient(); + const client = await getClient(); const { pendingApproval: existingApproval } = await getResumeData( client, agentId, diff --git a/src/headless.ts b/src/headless.ts index c6787c0..a1fb405 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -45,7 +45,7 @@ export async function handleHeadlessCommand(argv: string[]) { process.exit(1); } - const client = getClient(); + const client = await getClient(); // Resolve agent (same logic as interactive mode) let agent: Letta.AgentState | null = null; diff --git a/src/index.ts b/src/index.ts index bfc2bc5..6e51819 100755 --- a/src/index.ts +++ b/src/index.ts @@ -104,9 +104,13 @@ async function main() { const isHeadless = values.prompt || values.run || !process.stdin.isTTY; // Validate API key early before any UI rendering - const apiKey = process.env.LETTA_API_KEY; + const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY; if (!apiKey) { console.error("Missing LETTA_API_KEY"); + console.error( + "Set it via environment variable or add it to ~/.letta/settings.json:", + ); + console.error(' { "env": { "LETTA_API_KEY": "sk-let-..." } }'); process.exit(1); } @@ -158,7 +162,7 @@ async function main() { if (isHeadless) { // For headless mode, load tools synchronously await loadTools(); - const client = getClient(); + const client = await getClient(); await upsertToolsToServer(client); const { handleHeadlessCommand } = await import("./headless"); @@ -193,7 +197,7 @@ async function main() { await loadTools(); setLoadingState("upserting"); - const client = getClient(); + const client = await getClient(); await upsertToolsToServer(client); setLoadingState("initializing"); diff --git a/src/settings.ts b/src/settings.ts index 67c2639..9a742eb 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -14,6 +14,7 @@ export interface Settings { tokenStreaming: boolean; globalSharedBlockIds: Record; // label -> blockId mapping (persona, human; style moved to project settings) permissions?: PermissionRules; + env?: Record; } const DEFAULT_SETTINGS: Settings = {