diff --git a/README.md b/README.md index f84a00c..e08c7b1 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Check the [ADE](https://app.letta.com) to see if your agent is attempting to use ## Documentation - [Getting Started](docs/getting-started.md) -- [Self-Hosted Setup](docs/selfhosted-setup.md) - Run with your own Letta server +- [Docker Server Setup](docs/selfhosted-setup.md) - Run with your own Letta server - [Configuration Reference](docs/configuration.md) - [Slack Setup](docs/slack-setup.md) - [Discord Setup](docs/discord-setup.md) diff --git a/SKILL.md b/SKILL.md index 62172dc..58df5c4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -61,7 +61,7 @@ The wizard will guide you through: | Variable | Description | Default | |----------|-------------|---------| -| `LETTA_API_KEY` | API key from app.letta.com | Required (unless self-hosted) | +| `LETTA_API_KEY` | API key from app.letta.com | Required (unless using a Docker server) | | `LETTA_BASE_URL` | API endpoint | `https://api.letta.com` | ### Agent Selection @@ -329,9 +329,9 @@ The agent can verify success by checking: - Config file exists at `~/.lettabot/config.yaml` - User can message bot on configured channel(s) -## Self-Hosted Letta +## Docker Server Letta -To use a self-hosted Letta server: +To use a Letta Docker server: ```bash # Run Letta Docker diff --git a/TESTING.md b/TESTING.md index bafff0f..1488b8b 100644 --- a/TESTING.md +++ b/TESTING.md @@ -58,7 +58,7 @@ describe('myFunction', () => { ## E2E Tests -E2E tests verify the full message flow against a real Letta Cloud agent. +E2E tests verify the full message flow against a real Letta API agent. ### Setup @@ -73,7 +73,7 @@ Without these, E2E tests are automatically skipped. ### Test Agent -We use a dedicated test agent named "greg" on Letta Cloud. This agent: +We use a dedicated test agent named "greg" on Letta API. This agent: - Has minimal configuration - Is only used for automated testing - Should not have any sensitive data @@ -137,7 +137,7 @@ Tests run automatically via GitHub Actions (`.github/workflows/test.yml`): | Job | Trigger | What it tests | |-----|---------|---------------| | `unit` | All PRs and pushes | Unit tests only | -| `e2e` | Pushes to main | Full E2E with Letta Cloud | +| `e2e` | Pushes to main | Full E2E with Letta API | E2E tests only run on `main` because they require secrets that aren't available to fork PRs. diff --git a/docs/README.md b/docs/README.md index 59ba2f0..1a476ac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ LettaBot is a multi-channel AI assistant powered by [Letta](https://letta.com) t ## Guides - [Getting Started](./getting-started.md) - Installation and basic setup -- [Self-Hosted Setup](./selfhosted-setup.md) - Run with your own Letta server +- [Docker Server Setup](./selfhosted-setup.md) - Run with your own Letta server - [Configuration Reference](./configuration.md) - All config options - [Commands Reference](./commands.md) - Bot commands reference - [CLI Tools](./cli-tools.md) - Agent/operator CLI tools @@ -62,7 +62,7 @@ LettaBot uses a **single agent with unified memory** across all channels: ┌──────────────────────────┐ │ Letta Server │ │ (api.letta.com or │ - │ self-hosted Docker) │ + │ Docker/custom) │ │ │ │ • Agent Memory │ │ • LLM Inference │ diff --git a/docs/configuration.md b/docs/configuration.md index 0af420a..fb77518 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,8 +21,8 @@ For global installs (`npm install -g`), either: ```yaml # Server connection server: - mode: cloud # 'cloud' or 'selfhosted' - apiKey: letta_... # Required for cloud mode + mode: api # 'api' or 'docker' (legacy: 'cloud'/'selfhosted') + apiKey: letta_... # Required for api mode # Agent settings (single agent mode) # For multiple agents, use `agents:` array instead -- see Multi-Agent section @@ -98,15 +98,15 @@ api: | Option | Type | Description | |--------|------|-------------| -| `server.mode` | `'cloud'` \| `'selfhosted'` | Connection mode | -| `server.apiKey` | string | API key for Letta Cloud | -| `server.baseUrl` | string | URL for self-hosted server (e.g., `http://localhost:8283`) | +| `server.mode` | `'api'` \| `'docker'` | Connection mode (legacy aliases: `'cloud'`, `'selfhosted'`) | +| `server.apiKey` | string | API key for Letta API | +| `server.baseUrl` | string | URL for Docker/custom server (e.g., `http://localhost:8283`) | -### Self-Hosted Mode +### Docker Server Mode ```yaml server: - mode: selfhosted + mode: docker baseUrl: http://localhost:8283 ``` @@ -142,7 +142,7 @@ Use the `agents:` array instead of the top-level `agent:` and `channels:` keys: ```yaml server: - mode: cloud + mode: api apiKey: letta_... agents: diff --git a/docs/railway-deploy.md b/docs/railway-deploy.md index 442efb7..99f562b 100644 --- a/docs/railway-deploy.md +++ b/docs/railway-deploy.md @@ -17,7 +17,7 @@ Deploy LettaBot to [Railway](https://railway.app) for always-on hosting. | Variable | Description | |----------|-------------| -| `LETTA_API_KEY` | Your Letta Cloud API key ([get one here](https://app.letta.com)) | +| `LETTA_API_KEY` | Your Letta API key ([get one here](https://app.letta.com)) | ### Channel Configuration (at least one required) @@ -59,7 +59,7 @@ SLACK_APP_TOKEN=xapp-... On startup, LettaBot: 1. Checks for `LETTA_AGENT_ID` env var - uses if set -2. Otherwise, searches Letta Cloud for an agent named `LETTA_AGENT_NAME` (or legacy `AGENT_NAME`, default: "LettaBot") +2. Otherwise, searches Letta API for an agent named `LETTA_AGENT_NAME` (or legacy `AGENT_NAME`, default: "LettaBot") 3. If found, uses the existing agent (preserves memory!) 4. If not found, creates a new agent on first message diff --git a/docs/selfhosted-setup.md b/docs/selfhosted-setup.md index 79febdd..1fab7be 100644 --- a/docs/selfhosted-setup.md +++ b/docs/selfhosted-setup.md @@ -1,6 +1,6 @@ -# Self-Hosted Letta Server Setup +# Docker Server Setup -Run LettaBot with your own Letta server instead of Letta Cloud. +Run LettaBot with your own Letta Docker/custom server instead of Letta API. ## Prerequisites @@ -42,7 +42,7 @@ curl http://localhost:8283/v1/health lettabot onboard ``` -Select "Enter self-hosted URL" and enter `http://localhost:8283`. +Select "Enter Docker server URL" and enter `http://localhost:8283`. ### Option B: Manual Configuration @@ -50,7 +50,7 @@ Create `lettabot.yaml`: ```yaml server: - mode: selfhosted + mode: docker baseUrl: http://localhost:8283 # apiKey: optional-if-server-requires-auth @@ -79,7 +79,7 @@ lettabot server You should see: ``` [Config] Loaded from /path/to/lettabot.yaml -[Config] Mode: selfhosted, Agent: LettaBot, Model: gpt-4o +[Config] Mode: docker, Agent: LettaBot, Model: gpt-4o Starting LettaBot... LettaBot initialized. Agent ID: (new) [Telegram] Bot started as @YourBotName diff --git a/e2e/bot.e2e.test.ts b/e2e/bot.e2e.test.ts index 5719c7d..b74dd8b 100644 --- a/e2e/bot.e2e.test.ts +++ b/e2e/bot.e2e.test.ts @@ -1,7 +1,7 @@ /** * E2E Tests for LettaBot * - * These tests use a real Letta Cloud agent to verify the full message flow. + * These tests use a real Letta API agent to verify the full message flow. * Requires LETTA_API_KEY and LETTA_E2E_AGENT_ID environment variables. * * Run with: npm run test:e2e @@ -17,7 +17,7 @@ import { join } from 'node:path'; // Skip if no API key (local dev without secrets) const SKIP_E2E = !process.env.LETTA_API_KEY || !process.env.LETTA_E2E_AGENT_ID; -describe.skipIf(SKIP_E2E)('e2e: LettaBot with Letta Cloud', () => { +describe.skipIf(SKIP_E2E)('e2e: LettaBot with Letta API', () => { let bot: LettaBot; let mockAdapter: MockChannelAdapter; let tempDir: string; diff --git a/e2e/models.e2e.test.ts b/e2e/models.e2e.test.ts index c174cfd..6f434bf 100644 --- a/e2e/models.e2e.test.ts +++ b/e2e/models.e2e.test.ts @@ -1,7 +1,7 @@ /** * E2E Tests for Model API * - * Tests model listing and retrieval against Letta Cloud. + * Tests model listing and retrieval against Letta API. * Requires LETTA_API_KEY and LETTA_E2E_AGENT_ID environment variables. * * Run with: npm run test:e2e @@ -13,10 +13,10 @@ import { listModels, getAgentModel } from '../src/tools/letta-api.js'; const SKIP_E2E = !process.env.LETTA_API_KEY || !process.env.LETTA_E2E_AGENT_ID; describe.skipIf(SKIP_E2E)('e2e: Model API', () => { - it('lists available models from Letta Cloud', async () => { + it('lists available models from Letta API', async () => { const models = await listModels(); expect(models.length).toBeGreaterThan(0); - // Known providers should always exist on Letta Cloud + // Known providers should always exist on Letta API const handles = models.map(m => m.handle); expect(handles.some(h => h.includes('anthropic') || h.includes('openai'))).toBe(true); }, 30000); diff --git a/lettabot.example.yaml b/lettabot.example.yaml index e543215..7c1137d 100644 --- a/lettabot.example.yaml +++ b/lettabot.example.yaml @@ -2,15 +2,16 @@ # Copy this to lettabot.yaml and fill in your values. # # Server modes: -# - 'cloud': Use Letta Cloud (api.letta.com) with API key -# - 'selfhosted': Use self-hosted Letta server +# - 'api': Use Letta API (api.letta.com) with API key +# - 'docker': Use a Docker/custom Letta server +# Legacy aliases still accepted: 'cloud', 'selfhosted' server: - mode: cloud - # For cloud mode, set your API key (get one at https://app.letta.com): + mode: api + # For api mode, set your API key (get one at https://app.letta.com): apiKey: sk-let-YOUR-API-KEY - # For selfhosted mode, uncomment and set the base URL: - # mode: selfhosted + # For docker mode, uncomment and set the base URL: + # mode: docker # baseUrl: http://localhost:8283 agent: @@ -19,8 +20,8 @@ agent: # Note: model is configured on the Letta agent server-side. # Select a model during `lettabot onboard` or change it with `lettabot model set `. -# BYOK Providers (optional, cloud mode only) -# These will be synced to Letta Cloud on startup +# BYOK Providers (optional, api mode only) +# These will be synced to Letta API on startup # providers: # - id: anthropic # name: lc-anthropic diff --git a/src/auth/oauth.ts b/src/auth/oauth.ts index 12ecd4b..81aae95 100644 --- a/src/auth/oauth.ts +++ b/src/auth/oauth.ts @@ -1,5 +1,5 @@ /** - * OAuth 2.0 utilities for Letta Cloud authentication + * OAuth 2.0 utilities for Letta API authentication * Uses Device Code Flow for CLI authentication * * Ported from @letta-ai/letta-code @@ -7,13 +7,15 @@ import Letta from "@letta-ai/letta-client"; -export const LETTA_CLOUD_API_URL = "https://api.letta.com"; +export const LETTA_API_URL = "https://api.letta.com"; +// Backward-compatible alias for older imports. +export const LETTA_CLOUD_API_URL = LETTA_API_URL; export const OAUTH_CONFIG = { clientId: "ci-let-724dea7e98f4af6f8f370f4b1466200c", clientSecret: "", // Not needed for device code flow authBaseUrl: "https://app.letta.com", - apiBaseUrl: LETTA_CLOUD_API_URL, + apiBaseUrl: LETTA_API_URL, } as const; export interface DeviceCodeResponse { diff --git a/src/cli.ts b/src/cli.ts index bd0a767..8a648b6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ */ // Config loaded from lettabot.yaml -import { loadConfig, applyConfigToEnv } from './config/index.js'; +import { loadConfig, applyConfigToEnv, serverModeLabel } from './config/index.js'; const config = loadConfig(); applyConfigToEnv(config); import { existsSync, readFileSync, writeFileSync } from 'node:fs'; @@ -48,7 +48,7 @@ async function configure() { // Show current config from YAML const configRows = [ - ['Server Mode', config.server.mode], + ['Server Mode', serverModeLabel(config.server.mode)], ['API Key', config.server.apiKey ? '✓ Set' : '✗ Not set'], ['Agent Name', config.agent.name], ['Telegram', config.channels.telegram?.enabled ? '✓ Enabled' : '✗ Disabled'], diff --git a/src/commands/model.ts b/src/commands/model.ts index e815220..44352d2 100644 --- a/src/commands/model.ts +++ b/src/commands/model.ts @@ -9,7 +9,7 @@ import { getAgentModel, updateAgentModel } from '../tools/letta-api.js'; import { buildModelOptions, handleModelSelection, getBillingTier } from '../utils/model-selection.js'; -import { isLettaCloudUrl } from '../utils/server.js'; +import { isLettaApiUrl } from '../utils/server.js'; import { Store } from '../core/store.js'; /** @@ -79,11 +79,11 @@ export async function modelInteractive(): Promise { p.log.info(`Current model: ${currentModel}`); } - // Determine if self-hosted + // Determine if using Docker/custom server const baseUrl = process.env.LETTA_BASE_URL; - const isSelfHosted = !!baseUrl && !isLettaCloudUrl(baseUrl); + const isSelfHosted = !!baseUrl && !isLettaApiUrl(baseUrl); - // Get billing tier for cloud users + // Get billing tier for Letta API users let billingTier: string | null = null; if (!isSelfHosted) { const spinner = p.spinner(); diff --git a/src/config/io.ts b/src/config/io.ts index 7dcebc6..9f69b4d 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -9,7 +9,8 @@ import { homedir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import YAML from 'yaml'; import type { LettaBotConfig, ProviderConfig } from './types.js'; -import { DEFAULT_CONFIG } from './types.js'; +import { DEFAULT_CONFIG, canonicalizeServerMode, isApiServerMode, isDockerServerMode } from './types.js'; +import { LETTA_API_URL } from '../auth/oauth.js'; // Config file locations (checked in order) const CONFIG_PATHS = [ @@ -64,14 +65,22 @@ export function loadConfig(): LettaBotConfig { // Re-extract from document AST to preserve the original string representation. fixLargeGroupIds(content, parsed); - // Merge with defaults - return { + // Merge with defaults and canonicalize server mode. + const merged = { ...DEFAULT_CONFIG, ...parsed, server: { ...DEFAULT_CONFIG.server, ...parsed.server }, agent: { ...DEFAULT_CONFIG.agent, ...parsed.agent }, channels: { ...DEFAULT_CONFIG.channels, ...parsed.channels }, }; + + return { + ...merged, + server: { + ...merged.server, + mode: canonicalizeServerMode(merged.server.mode), + }, + }; } catch (err) { console.error(`[Config] Failed to load ${configPath}:`, err); return { ...DEFAULT_CONFIG }; @@ -107,7 +116,7 @@ export function configToEnv(config: LettaBotConfig): Record { const env: Record = {}; // Server - if (config.server.mode === 'selfhosted' && config.server.baseUrl) { + if (isDockerServerMode(config.server.mode) && config.server.baseUrl) { env.LETTA_BASE_URL = config.server.baseUrl; } if (config.server.apiKey) { @@ -309,10 +318,10 @@ export function applyConfigToEnv(config: LettaBotConfig): void { } /** - * Create BYOK providers on Letta Cloud + * Create BYOK providers on Letta API */ export async function syncProviders(config: Partial & Pick): Promise { - if (config.server.mode !== 'cloud' || !config.server.apiKey) { + if (!isApiServerMode(config.server.mode) || !config.server.apiKey) { return; } @@ -321,7 +330,7 @@ export async function syncProviders(config: Partial & Pick { + it('canonicalizes legacy server mode aliases', () => { + expect(canonicalizeServerMode('cloud')).toBe('api'); + expect(canonicalizeServerMode('api')).toBe('api'); + expect(canonicalizeServerMode('selfhosted')).toBe('docker'); + expect(canonicalizeServerMode('docker')).toBe('docker'); + expect(isApiServerMode('cloud')).toBe(true); + expect(isDockerServerMode('selfhosted')).toBe(true); + }); + it('should normalize legacy single-agent config to one-entry array', () => { const config: LettaBotConfig = { server: { mode: 'cloud' }, diff --git a/src/config/types.ts b/src/config/types.ts index 3774238..c3bd517 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -2,10 +2,29 @@ * LettaBot Configuration Types * * Two modes: - * 1. Self-hosted: Uses baseUrl (e.g., http://localhost:8283), no API key - * 2. Letta Cloud: Uses apiKey, optional BYOK providers + * 1. Docker server: Uses baseUrl (e.g., http://localhost:8283), no API key + * 2. Letta API: Uses apiKey, optional BYOK providers */ +export type ServerMode = 'api' | 'docker' | 'cloud' | 'selfhosted'; +export type CanonicalServerMode = 'api' | 'docker'; + +export function canonicalizeServerMode(mode?: ServerMode): CanonicalServerMode { + return mode === 'docker' || mode === 'selfhosted' ? 'docker' : 'api'; +} + +export function isDockerServerMode(mode?: ServerMode): boolean { + return canonicalizeServerMode(mode) === 'docker'; +} + +export function isApiServerMode(mode?: ServerMode): boolean { + return canonicalizeServerMode(mode) === 'api'; +} + +export function serverModeLabel(mode?: ServerMode): string { + return canonicalizeServerMode(mode); +} + /** * Configuration for a single agent in multi-agent mode. * Each agent has its own name, channels, and features. @@ -50,11 +69,12 @@ export interface AgentConfig { export interface LettaBotConfig { // Server connection server: { - // 'cloud' (api.letta.com) or 'selfhosted' - mode: 'cloud' | 'selfhosted'; - // Only for selfhosted mode + // Canonical values: 'api' or 'docker' + // Legacy aliases accepted for compatibility: 'cloud', 'selfhosted' + mode: ServerMode; + // Only for docker mode baseUrl?: string; - // Only for cloud mode + // Only for api mode apiKey?: string; }; @@ -71,7 +91,7 @@ export interface LettaBotConfig { model?: string; }; - // BYOK providers (cloud mode only) + // BYOK providers (api mode only) providers?: ProviderConfig[]; // Channel configurations @@ -272,7 +292,7 @@ export interface GoogleConfig { // Default config export const DEFAULT_CONFIG: LettaBotConfig = { server: { - mode: 'cloud', + mode: 'api', }, agent: { name: 'LettaBot', diff --git a/src/main.ts b/src/main.ts index 036dcf4..8a44a96 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,16 +14,23 @@ import { createApiServer } from './api/server.js'; import { loadOrGenerateApiKey } from './api/auth.js'; // Load YAML config and apply to process.env (overrides .env values) -import { loadConfig, applyConfigToEnv, syncProviders, resolveConfigPath } from './config/index.js'; -import { isLettaCloudUrl } from './utils/server.js'; +import { + loadConfig, + applyConfigToEnv, + syncProviders, + resolveConfigPath, + isDockerServerMode, + serverModeLabel, +} from './config/index.js'; +import { isLettaApiUrl } from './utils/server.js'; import { getDataDir, getWorkingDir, hasRailwayVolume } from './utils/paths.js'; const yamlConfig = loadConfig(); const configSource = existsSync(resolveConfigPath()) ? resolveConfigPath() : 'defaults + environment variables'; console.log(`[Config] Loaded from ${configSource}`); if (yamlConfig.agents?.length) { - console.log(`[Config] Mode: ${yamlConfig.server.mode}, Agents: ${yamlConfig.agents.map(a => a.name).join(', ')}`); + console.log(`[Config] Mode: ${serverModeLabel(yamlConfig.server.mode)}, Agents: ${yamlConfig.agents.map(a => a.name).join(', ')}`); } else { - console.log(`[Config] Mode: ${yamlConfig.server.mode}, Agent: ${yamlConfig.agent.name}`); + console.log(`[Config] Mode: ${serverModeLabel(yamlConfig.server.mode)}, Agent: ${yamlConfig.agent.name}`); } if (yamlConfig.agent?.model) { console.warn('[Config] WARNING: agent.model in lettabot.yaml is deprecated and ignored. Use `lettabot model set ` instead.'); @@ -98,8 +105,8 @@ async function refreshTokensIfNeeded(): Promise { return; } - // OAuth tokens only work with Letta Cloud - skip if using custom server - if (!isLettaCloudUrl(process.env.LETTA_BASE_URL)) { + // OAuth tokens only work with Letta API - skip if using custom server + if (!isLettaApiUrl(process.env.LETTA_BASE_URL)) { return; } @@ -435,11 +442,11 @@ const globalConfig = { cronEnabled: process.env.CRON_ENABLED === 'true', // Legacy env var fallback }; -// Validate LETTA_API_KEY is set for cloud mode (selfhosted mode doesn't require it) -if (yamlConfig.server.mode !== 'selfhosted' && !process.env.LETTA_API_KEY) { - console.error('\n Error: LETTA_API_KEY is required for Letta Cloud.'); +// Validate LETTA_API_KEY is set for API mode (docker mode doesn't require it) +if (!isDockerServerMode(yamlConfig.server.mode) && !process.env.LETTA_API_KEY) { + console.error('\n Error: LETTA_API_KEY is required for Letta API.'); console.error(' Get your API key from https://app.letta.com and set it as an environment variable.'); - console.error(' Or use selfhosted mode: run "lettabot onboard" and select "Enter self-hosted URL".\n'); + console.error(' Or use docker mode: run "lettabot onboard" and select "Enter Docker server URL".\n'); process.exit(1); } diff --git a/src/onboard.ts b/src/onboard.ts index 286d947..f2bdd1f 100644 --- a/src/onboard.ts +++ b/src/onboard.ts @@ -6,9 +6,9 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; import * as p from '@clack/prompts'; -import { saveConfig, syncProviders } from './config/index.js'; +import { saveConfig, syncProviders, isApiServerMode } from './config/index.js'; import type { AgentConfig, LettaBotConfig, ProviderConfig } from './config/types.js'; -import { isLettaCloudUrl } from './utils/server.js'; +import { isLettaApiUrl } from './utils/server.js'; import { CHANNELS, getChannelHint, isSignalCliInstalled, setupTelegram, setupSlack, setupDiscord, setupWhatsApp, setupSignal } from './channels/setup.js'; // ============================================================================ @@ -118,7 +118,7 @@ async function saveConfigFromEnv(config: any, configPath: string): Promise const lettabotConfig: Partial & Pick = { server: { - mode: isLettaCloudUrl(config.baseUrl) ? 'cloud' : 'selfhosted', + mode: isLettaApiUrl(config.baseUrl) ? 'api' : 'docker', baseUrl: config.baseUrl, apiKey: config.apiKey, }, @@ -207,7 +207,7 @@ async function saveConfigFromEnv(config: any, configPath: string): Promise interface OnboardConfig { // Auth - authMethod: 'keep' | 'oauth' | 'apikey' | 'selfhosted' | 'skip'; + authMethod: 'keep' | 'oauth' | 'apikey' | 'docker' | 'selfhosted' | 'skip'; apiKey?: string; baseUrl?: string; billingTier?: string; @@ -288,6 +288,7 @@ interface OnboardConfig { } const isPlaceholder = (val?: string) => !val || /^(your_|sk-\.\.\.|placeholder|example)/i.test(val); +const isDockerAuthMethod = (method: OnboardConfig['authMethod']) => method === 'docker' || method === 'selfhosted'; // ============================================================================ // Step Functions @@ -298,12 +299,12 @@ async function stepAuth(config: OnboardConfig, env: Record): Pro const { saveTokens, loadTokens, getOrCreateDeviceId, getDeviceName } = await import('./auth/tokens.js'); const baseUrl = config.baseUrl || env.LETTA_BASE_URL || process.env.LETTA_BASE_URL; - const isLettaCloud = isLettaCloudUrl(baseUrl); + const isLettaApi = isLettaApiUrl(baseUrl); const existingTokens = loadTokens(); // Check both env and config for existing API key const realApiKey = config.apiKey || (isPlaceholder(env.LETTA_API_KEY) ? undefined : env.LETTA_API_KEY); - const validOAuthToken = isLettaCloud ? existingTokens?.accessToken : undefined; + const validOAuthToken = isLettaApi ? existingTokens?.accessToken : undefined; const hasExistingAuth = !!realApiKey || !!validOAuthToken; const displayKey = realApiKey || validOAuthToken; @@ -316,9 +317,9 @@ async function stepAuth(config: OnboardConfig, env: Record): Pro const authOptions = [ ...(hasExistingAuth ? [{ value: 'keep', label: getAuthLabel(), hint: displayKey?.slice(0, 20) + '...' }] : []), - ...(isLettaCloud ? [{ value: 'oauth', label: 'Login to Letta Platform', hint: 'Opens browser' }] : []), + ...(isLettaApi ? [{ value: 'oauth', label: 'Login to Letta Platform', hint: 'Opens browser' }] : []), { value: 'apikey', label: 'Enter API Key manually', hint: 'Paste your key' }, - { value: 'selfhosted', label: 'Enter self-hosted URL', hint: 'Local Letta server' }, + { value: 'docker', label: 'Enter Docker server URL', hint: 'Local/custom Letta server' }, { value: 'skip', label: 'Skip', hint: 'Continue without auth' }, ]; @@ -390,7 +391,7 @@ async function stepAuth(config: OnboardConfig, env: Record): Pro config.apiKey = apiKey; env.LETTA_API_KEY = apiKey; } - } else if (authMethod === 'selfhosted') { + } else if (authMethod === 'docker' || authMethod === 'selfhosted') { const serverUrl = await p.text({ message: 'Letta server URL', placeholder: 'http://localhost:8283', @@ -403,7 +404,7 @@ async function stepAuth(config: OnboardConfig, env: Record): Pro env.LETTA_BASE_URL = url; process.env.LETTA_BASE_URL = url; // Set immediately so model listing works - // Clear any cloud API key since we're using self-hosted + // Clear API key since we're using a Docker/custom server. delete env.LETTA_API_KEY; delete process.env.LETTA_API_KEY; } else if (authMethod === 'keep') { @@ -458,14 +459,14 @@ async function stepAuth(config: OnboardConfig, env: Record): Pro } const spinner = p.spinner(); - const serverLabel = config.baseUrl || 'Letta Cloud'; + const serverLabel = config.baseUrl || 'Letta API'; spinner.start(`Checking connection to ${serverLabel}...`); try { const { testConnection } = await import('./tools/letta-api.js'); const ok = await testConnection(); spinner.stop(ok ? `Connected to ${serverLabel}` : 'Connection issue'); - if (!ok && config.authMethod === 'selfhosted') { + if (!ok && isDockerAuthMethod(config.authMethod)) { p.log.warn(`Could not connect to ${config.baseUrl}. Make sure the server is running.`); } } catch { @@ -561,8 +562,8 @@ const BYOK_PROVIDERS = [ ]; async function stepProviders(config: OnboardConfig, env: Record): Promise { - // Only for free tier users on Letta Cloud (not self-hosted, not paid) - if (config.authMethod === 'selfhosted') return; + // Only for free tier users on Letta API (not Docker/custom servers, not paid) + if (isDockerAuthMethod(config.authMethod)) return; if (config.billingTier !== 'free') return; const selectedProviders = await p.multiselect({ @@ -680,10 +681,10 @@ async function stepModel(config: OnboardConfig, env: Record): Pr const spinner = p.spinner(); - // Determine if self-hosted (not Letta Cloud) - const isSelfHosted = config.authMethod === 'selfhosted'; + // Determine if Docker/custom server (not Letta API) + const isSelfHosted = isDockerAuthMethod(config.authMethod); - // Fetch billing tier for Letta Cloud users (if not already fetched) + // Fetch billing tier for Letta API users (if not already fetched) let billingTier: string | null = config.billingTier || null; if (!isSelfHosted && !billingTier) { spinner.start('Checking account...'); @@ -1155,7 +1156,8 @@ function showSummary(config: OnboardConfig): void { keep: 'Keep existing', oauth: 'OAuth login', apikey: config.apiKey ? `API Key (${config.apiKey.slice(0, 10)}...)` : 'API Key', - selfhosted: config.baseUrl ? `Self-hosted (${config.baseUrl})` : 'Self-hosted', + docker: config.baseUrl ? `Docker server (${config.baseUrl})` : 'Docker server', + selfhosted: config.baseUrl ? `Docker server (${config.baseUrl})` : 'Docker server', skip: 'None', }[config.authMethod]; lines.push(`Auth: ${authLabel}`); @@ -1320,12 +1322,12 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise & Pick = { server: { - mode: config.authMethod === 'selfhosted' ? 'selfhosted' : 'cloud', - ...(config.authMethod === 'selfhosted' && config.baseUrl ? { baseUrl: config.baseUrl } : {}), + mode: isDockerAuthMethod(config.authMethod) ? 'docker' : 'api', + ...(isDockerAuthMethod(config.authMethod) && config.baseUrl ? { baseUrl: config.baseUrl } : {}), ...(config.apiKey ? { apiKey: config.apiKey } : {}), }, agents: [agentConfig], @@ -1789,10 +1791,10 @@ export async function onboard(options?: { nonInteractive?: boolean }): Promise 0 && yamlConfig.server.mode === 'cloud') { + // Sync BYOK providers to Letta API. + if (yamlConfig.providers && yamlConfig.providers.length > 0 && isApiServerMode(yamlConfig.server.mode)) { const spinner = p.spinner(); - spinner.start('Syncing BYOK providers to Letta Cloud...'); + spinner.start('Syncing BYOK providers to Letta API...'); try { await syncProviders(yamlConfig); spinner.stop('BYOK providers synced'); diff --git a/src/utils/model-selection.ts b/src/utils/model-selection.ts index d44c140..161770e 100644 --- a/src/utils/model-selection.ts +++ b/src/utils/model-selection.ts @@ -26,11 +26,11 @@ export interface ModelInfo { * Uses /v1/metadata/balance endpoint (same as letta-code) * * @param apiKey - The API key to use - * @param isSelfHosted - If true, skip billing check (self-hosted has no tiers) + * @param isSelfHosted - If true, skip billing check (Docker/custom servers have no tiers) */ export async function getBillingTier(apiKey?: string, isSelfHosted?: boolean): Promise { try { - // Self-hosted servers don't have billing tiers + // Docker/custom servers don't have billing tiers. if (isSelfHosted) { return null; } @@ -39,7 +39,7 @@ export async function getBillingTier(apiKey?: string, isSelfHosted?: boolean): P return 'free'; } - // Always use Letta Cloud for billing check (not process.env.LETTA_BASE_URL) + // Always use Letta API for billing check (not process.env.LETTA_BASE_URL) const response = await fetch('https://api.letta.com/v1/metadata/balance', { headers: { 'Content-Type': 'application/json', @@ -111,7 +111,7 @@ async function fetchByokModels(apiKey?: string): Promise { * * For free users: Show free models first, then BYOK models from API * For paid users: Show featured models first, then all models - * For self-hosted: Fetch models from server + * For Docker/custom servers: fetch models from server */ export async function buildModelOptions(options?: { billingTier?: string | null; @@ -122,7 +122,7 @@ export async function buildModelOptions(options?: { const isSelfHosted = options?.isSelfHosted; const isFreeTier = billingTier?.toLowerCase() === 'free'; - // For self-hosted servers, fetch models from server + // For Docker/custom servers, fetch models from server if (isSelfHosted) { return buildServerModelOptions(); } @@ -182,7 +182,7 @@ export async function buildModelOptions(options?: { } /** - * Build model options from self-hosted server + * Build model options from Docker/custom server */ async function buildServerModelOptions(): Promise> { const { listModels } = await import('../tools/letta-api.js'); diff --git a/src/utils/server.test.ts b/src/utils/server.test.ts index 732ff8b..9fd97e5 100644 --- a/src/utils/server.test.ts +++ b/src/utils/server.test.ts @@ -1,45 +1,50 @@ import { describe, it, expect } from 'vitest'; -import { isLettaCloudUrl } from './server.js'; +import { isLettaApiUrl, isLettaCloudUrl } from './server.js'; -describe('isLettaCloudUrl', () => { - it('returns true for undefined (default is cloud)', () => { - expect(isLettaCloudUrl(undefined)).toBe(true); +describe('isLettaApiUrl', () => { + it('returns true for undefined (default is Letta API)', () => { + expect(isLettaApiUrl(undefined)).toBe(true); }); - it('returns true for Letta Cloud URL', () => { - expect(isLettaCloudUrl('https://api.letta.com')).toBe(true); + it('returns true for Letta API URL', () => { + expect(isLettaApiUrl('https://api.letta.com')).toBe(true); }); - it('returns true for Letta Cloud URL with trailing slash', () => { - expect(isLettaCloudUrl('https://api.letta.com/')).toBe(true); + it('returns true for Letta API URL with trailing slash', () => { + expect(isLettaApiUrl('https://api.letta.com/')).toBe(true); }); - it('returns true for Letta Cloud URL with path', () => { - expect(isLettaCloudUrl('https://api.letta.com/v1/agents')).toBe(true); + it('returns true for Letta API URL with path', () => { + expect(isLettaApiUrl('https://api.letta.com/v1/agents')).toBe(true); }); it('returns false for localhost', () => { - expect(isLettaCloudUrl('http://localhost:8283')).toBe(false); + expect(isLettaApiUrl('http://localhost:8283')).toBe(false); }); it('returns false for 127.0.0.1', () => { - expect(isLettaCloudUrl('http://127.0.0.1:8283')).toBe(false); + expect(isLettaApiUrl('http://127.0.0.1:8283')).toBe(false); }); it('returns false for custom server', () => { - expect(isLettaCloudUrl('https://custom.server.com')).toBe(false); + expect(isLettaApiUrl('https://custom.server.com')).toBe(false); }); it('returns false for docker network URL', () => { - expect(isLettaCloudUrl('http://letta:8283')).toBe(false); + expect(isLettaApiUrl('http://letta:8283')).toBe(false); }); it('returns false for invalid URL', () => { - expect(isLettaCloudUrl('not-a-url')).toBe(false); + expect(isLettaApiUrl('not-a-url')).toBe(false); }); it('returns true for empty string (treated as default)', () => { - // Empty string is falsy, so it's treated like undefined (default to cloud) - expect(isLettaCloudUrl('')).toBe(true); + // Empty string is falsy, so it's treated like undefined (default to Letta API) + expect(isLettaApiUrl('')).toBe(true); + }); + + it('keeps backward-compatible alias behavior', () => { + expect(isLettaCloudUrl('https://api.letta.com')).toBe(true); + expect(isLettaCloudUrl('http://localhost:8283')).toBe(false); }); }); diff --git a/src/utils/server.ts b/src/utils/server.ts index b8ba4cd..6ae81a5 100644 --- a/src/utils/server.ts +++ b/src/utils/server.ts @@ -1,24 +1,27 @@ /** * Letta server URL utilities * - * The heuristic is simple: Letta Cloud lives at a known URL. - * Everything else is self-hosted. + * The heuristic is simple: Letta API lives at a known URL. + * Everything else is a Docker/custom server. */ -import { LETTA_CLOUD_API_URL } from '../auth/oauth.js'; +import { LETTA_API_URL } from '../auth/oauth.js'; /** - * Check if a URL points at Letta Cloud (api.letta.com) + * Check if a URL points at Letta API (api.letta.com) * - * @param url - The base URL to check. When absent, assumes cloud (the default). + * @param url - The base URL to check. When absent, assumes Letta API (the default). */ -export function isLettaCloudUrl(url?: string): boolean { - if (!url) return true; // no URL means the default (cloud) +export function isLettaApiUrl(url?: string): boolean { + if (!url) return true; // no URL means the default (Letta API) try { const given = new URL(url); - const cloud = new URL(LETTA_CLOUD_API_URL); - return given.hostname === cloud.hostname; + const api = new URL(LETTA_API_URL); + return given.hostname === api.hostname; } catch { return false; } } + +// Backward-compatible alias. +export const isLettaCloudUrl = isLettaApiUrl;