feat: add handling for when failed to refresh access token (#168)

Co-authored-by: Charles Packer <packercharles@gmail.com>
This commit is contained in:
Shelley Pham
2025-12-10 14:19:08 -08:00
committed by GitHub
parent ed5c6d71b7
commit 6db2fcfc05
5 changed files with 38 additions and 16 deletions

View File

@@ -1,5 +1,5 @@
import Letta from "@letta-ai/letta-client"; import Letta from "@letta-ai/letta-client";
import { refreshAccessToken } from "../auth/oauth"; import { LETTA_CLOUD_API_URL, refreshAccessToken } from "../auth/oauth";
import { settingsManager } from "../settings-manager"; import { settingsManager } from "../settings-manager";
export async function getClient() { export async function getClient() {
@@ -40,14 +40,17 @@ export async function getClient() {
} }
} }
// Check if refresh token is missing for Letta Cloud
const baseURL = const baseURL =
process.env.LETTA_BASE_URL || process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL || settings.env?.LETTA_BASE_URL ||
"https://api.letta.com"; LETTA_CLOUD_API_URL;
if (!apiKey && baseURL === "https://api.letta.com") { if (!apiKey && baseURL === LETTA_CLOUD_API_URL) {
console.error("Missing LETTA_API_KEY"); console.error("Missing LETTA_API_KEY");
console.error("Run 'letta setup' to configure authentication"); console.error(
"Run 'letta setup' to configure authentication or set your LETTA_API_KEY environment variable",
);
process.exit(1); process.exit(1);
} }

View File

@@ -5,11 +5,13 @@
import Letta from "@letta-ai/letta-client"; import Letta from "@letta-ai/letta-client";
export const LETTA_CLOUD_API_URL = "https://api.letta.com";
export const OAUTH_CONFIG = { export const OAUTH_CONFIG = {
clientId: "ci-let-724dea7e98f4af6f8f370f4b1466200c", clientId: "ci-let-724dea7e98f4af6f8f370f4b1466200c",
clientSecret: "", // Not needed for device code flow clientSecret: "", // Not needed for device code flow
authBaseUrl: "https://app.letta.com", authBaseUrl: "https://app.letta.com",
apiBaseUrl: "https://api.letta.com", apiBaseUrl: LETTA_CLOUD_API_URL,
} as const; } as const;
export interface DeviceCodeResponse { export interface DeviceCodeResponse {

View File

@@ -148,7 +148,7 @@ export function SetupUI({ onComplete }: SetupUIProps) {
<Text>{asciiLogo}</Text> <Text>{asciiLogo}</Text>
<Text bold>Welcome to Letta Code!</Text> <Text bold>Welcome to Letta Code!</Text>
<Text> </Text> <Text> </Text>
<Text>Please choose how you'd like to authenticate:</Text> <Text>Let's get you authenticated:</Text>
<Text> </Text> <Text> </Text>
<Box> <Box>
<Text color={selectedOption === 0 ? "cyan" : undefined}> <Text color={selectedOption === 0 ? "cyan" : undefined}>

View File

@@ -3,6 +3,7 @@ import { Box, Text, useInput } from "ink";
import SpinnerLib from "ink-spinner"; import SpinnerLib from "ink-spinner";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { LETTA_CLOUD_API_URL } from "../../auth/oauth";
import type { PermissionMode } from "../../permissions/mode"; import type { PermissionMode } from "../../permissions/mode";
import { permissionMode } from "../../permissions/mode"; import { permissionMode } from "../../permissions/mode";
import { settingsManager } from "../../settings-manager"; import { settingsManager } from "../../settings-manager";
@@ -113,7 +114,7 @@ export function Input({
const serverUrl = const serverUrl =
process.env.LETTA_BASE_URL || process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL || settings.env?.LETTA_BASE_URL ||
"https://api.letta.com"; LETTA_CLOUD_API_URL;
// Handle escape key for interrupt (when streaming) or double-escape-to-clear (when not) // Handle escape key for interrupt (when streaming) or double-escape-to-clear (when not)
useInput((_input, key) => { useInput((_input, key) => {

View File

@@ -5,6 +5,7 @@ import { getResumeData, type ResumeData } from "./agent/check-approval";
import { getClient } from "./agent/client"; import { getClient } from "./agent/client";
import { initializeLoadedSkillsFlag, setAgentContext } from "./agent/context"; import { initializeLoadedSkillsFlag, setAgentContext } from "./agent/context";
import type { AgentProvenance } from "./agent/create"; import type { AgentProvenance } from "./agent/create";
import { LETTA_CLOUD_API_URL } from "./auth/oauth";
import { permissionMode } from "./permissions/mode"; import { permissionMode } from "./permissions/mode";
import { settingsManager } from "./settings-manager"; import { settingsManager } from "./settings-manager";
import { loadTools, upsertToolsToServer } from "./tools/manager"; import { loadTools, upsertToolsToServer } from "./tools/manager";
@@ -46,15 +47,15 @@ OPTIONS
BEHAVIOR BEHAVIOR
By default, letta auto-resumes the last agent used in the current directory By default, letta auto-resumes the last agent used in the current directory
(stored in .letta/settings.local.json). (stored in .letta/settings.local.json).
Memory blocks (persona, human, project, skills) are shared between agents: Memory blocks (persona, human, project, skills) are shared between agents:
- Global blocks (persona, human) are shared across all agents - Global blocks (persona, human) are shared across all agents
- Local blocks (project, skills) are shared within the current directory - Local blocks (project, skills) are shared within the current directory
Use --new to create a new agent that reuses your global persona/human blocks. Use --new to create a new agent that reuses your global persona/human blocks.
Use --fresh-blocks to create a completely isolated agent with new blocks. Use --fresh-blocks to create a completely isolated agent with new blocks.
If no credentials are configured, you'll be prompted to authenticate via If no credentials are configured, you'll be prompted to authenticate via
Letta Cloud OAuth on first run. Letta Cloud OAuth on first run.
@@ -64,10 +65,10 @@ EXAMPLES
letta --new # New agent, keeps your persona/human blocks letta --new # New agent, keeps your persona/human blocks
letta --fresh-blocks # New agent, all blocks fresh (full isolation) letta --fresh-blocks # New agent, all blocks fresh (full isolation)
letta --agent agent_123 letta --agent agent_123
# inside the interactive session # inside the interactive session
/logout # Clear credentials and exit /logout # Clear credentials and exit
# headless with JSON output (includes stats) # headless with JSON output (includes stats)
letta -p "hello" --output-format json letta -p "hello" --output-format json
@@ -265,13 +266,28 @@ async function main() {
const baseURL = const baseURL =
process.env.LETTA_BASE_URL || process.env.LETTA_BASE_URL ||
settings.env?.LETTA_BASE_URL || settings.env?.LETTA_BASE_URL ||
"https://api.letta.com"; LETTA_CLOUD_API_URL;
if (!apiKey && baseURL === "https://api.letta.com") { // Check if refresh token is missing for Letta Cloud (only when not using env var)
if (
!isHeadless &&
baseURL === LETTA_CLOUD_API_URL &&
!settings.refreshToken
) {
// For interactive mode, show setup flow
const { runSetup } = await import("./auth/setup");
await runSetup();
// After setup, restart main flow
return main();
}
if (!apiKey && baseURL === LETTA_CLOUD_API_URL) {
// For headless mode, error out (assume automation context) // For headless mode, error out (assume automation context)
if (isHeadless) { if (isHeadless) {
console.error("Missing LETTA_API_KEY"); console.error("Missing LETTA_API_KEY");
console.error("Run 'letta' in interactive mode to authenticate"); console.error(
"Run 'letta' in interactive mode to authenticate or export the missing environment variable",
);
process.exit(1); process.exit(1);
} }