From 6eff0309cabac052a0546a0c138bc91158af8752 Mon Sep 17 00:00:00 2001 From: jnjpng Date: Tue, 23 Dec 2025 12:01:31 -0800 Subject: [PATCH] fix: pro enterprise plan messaging (#355) --- bun.lock | 1 + src/cli/commands/connect.ts | 85 +++++++++++++++++++++----- src/cli/components/OAuthCodeDialog.tsx | 25 +++++++- src/providers/anthropic-provider.ts | 73 ++++++++++++++++++++++ 4 files changed, 166 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 741eb46..cfe8d93 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@letta-ai/letta-code", diff --git a/src/cli/commands/connect.ts b/src/cli/commands/connect.ts index 68d359d..4ebb331 100644 --- a/src/cli/commands/connect.ts +++ b/src/cli/commands/connect.ts @@ -8,10 +8,12 @@ import { } from "../../auth/anthropic-oauth"; import { ANTHROPIC_PROVIDER_NAME, + checkAnthropicOAuthEligibility, createOrUpdateAnthropicProvider, removeAnthropicProvider, } from "../../providers/anthropic-provider"; import { settingsManager } from "../../settings-manager"; +import { getErrorMessage } from "../../utils/error"; import type { Buffers, Line } from "../helpers/accumulator"; // tiny helper for unique ids @@ -141,15 +143,46 @@ export async function handleConnect( // Start the OAuth flow (step 1) ctx.setCommandRunning(true); + // Show initial status + const cmdId = addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + "Checking account eligibility...", + true, + "running", + ); + try { - // 1. Start OAuth flow - generate PKCE and authorization URL + // 1. Check eligibility before starting OAuth flow + const eligibility = await checkAnthropicOAuthEligibility(); + if (!eligibility.eligible) { + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `✗ Claude OAuth requires a Pro or Enterprise plan\n\n` + + `This feature is only available for Letta Pro or Enterprise customers.\n` + + `Current plan: ${eligibility.billing_tier}\n\n` + + `To upgrade your plan, visit:\n\n` + + ` https://app.letta.com/settings/organization/usage\n\n` + + `If you have an Anthropic API key, you can use it directly by setting:\n` + + ` export ANTHROPIC_API_KEY=your-key`, + false, + "finished", + ); + return; + } + + // 2. Start OAuth flow - generate PKCE and authorization URL const { authorizationUrl, state, codeVerifier } = await startAnthropicOAuth(); - // 2. Store state for validation when user returns with code + // 3. Store state for validation when user returns with code settingsManager.storeOAuthState(state, codeVerifier, "anthropic"); - // 3. Try to open browser + // 4. Try to open browser let browserOpened = false; try { const { default: open } = await import("open"); @@ -163,14 +196,15 @@ export async function handleConnect( // If auto-open fails, user can still manually visit the URL } - // 4. Show instructions + // 5. Show instructions const browserMsg = browserOpened ? "Opening browser for authorization..." : "Please open the following URL in your browser:"; - addCommandResult( + updateCommandResult( ctx.buffersRef, ctx.refreshDerived, + cmdId, msg, `${browserMsg}\n\n${authorizationUrl}\n\n` + "After authorizing, you'll be redirected to a page showing: code#state\n" + @@ -178,17 +212,20 @@ export async function handleConnect( " /connect claude \n\n" + "Example: /connect claude abc123...#def456...", true, + "finished", ); } catch (error) { // Clear any partial state settingsManager.clearOAuthState(); - addCommandResult( + updateCommandResult( ctx.buffersRef, ctx.refreshDerived, + cmdId, msg, - `✗ Failed to start OAuth flow: ${error instanceof Error ? error.message : String(error)}`, + `✗ Failed to start OAuth flow: ${getErrorMessage(error)}`, false, + "finished", ); } finally { ctx.setCommandRunning(false); @@ -261,7 +298,7 @@ async function completeOAuthFlow( stateToUse, ); - // 4. Update status + // 5. Update status updateCommandResult( ctx.buffersRef, ctx.refreshDerived, @@ -272,7 +309,7 @@ async function completeOAuthFlow( "running", ); - // 5. Validate tokens work + // 6. Validate tokens work const isValid = await validateAnthropicCredentials(tokens.access_token); if (!isValid) { throw new Error( @@ -280,10 +317,10 @@ async function completeOAuthFlow( ); } - // 6. Store tokens locally + // 7. Store tokens locally settingsManager.storeAnthropicTokens(tokens); - // 7. Update status for provider creation + // 8. Update status for provider creation updateCommandResult( ctx.buffersRef, ctx.refreshDerived, @@ -294,13 +331,13 @@ async function completeOAuthFlow( "running", ); - // 8. Create or update provider in Letta with the access token + // 9. Create or update provider in Letta with the access token await createOrUpdateAnthropicProvider(tokens.access_token); - // 9. Clear OAuth state + // 10. Clear OAuth state settingsManager.clearOAuthState(); - // 10. Success! + // 11. Success! updateCommandResult( ctx.buffersRef, ctx.refreshDerived, @@ -313,12 +350,28 @@ async function completeOAuthFlow( "finished", ); } catch (error) { + // Check if this is a plan upgrade requirement error from provider creation + const errorMessage = getErrorMessage(error); + + let displayMessage: string; + if (errorMessage === "PLAN_UPGRADE_REQUIRED") { + displayMessage = + `✗ Claude OAuth requires a Pro or Enterprise plan\n\n` + + `This feature is only available for Letta Pro or Enterprise customers.\n` + + `To upgrade your plan, visit:\n\n` + + ` https://app.letta.com/settings/organization/usage\n\n` + + `If you have an Anthropic API key, you can use it directly by setting:\n` + + ` export ANTHROPIC_API_KEY=your-key`; + } else { + displayMessage = `✗ Failed to connect: ${errorMessage}`; + } + updateCommandResult( ctx.buffersRef, ctx.refreshDerived, cmdId, msg, - `✗ Failed to connect: ${error instanceof Error ? error.message : String(error)}`, + displayMessage, false, "finished", ); @@ -401,7 +454,7 @@ export async function handleDisconnect( cmdId, msg, `✓ Disconnected from Claude OAuth.\n\n` + - `Warning: Failed to remove provider from Letta: ${error instanceof Error ? error.message : String(error)}\n` + + `Warning: Failed to remove provider from Letta: ${getErrorMessage(error)}\n` + `Your local OAuth tokens have been removed.`, true, "finished", diff --git a/src/cli/components/OAuthCodeDialog.tsx b/src/cli/components/OAuthCodeDialog.tsx index 4712731..7a7d638 100644 --- a/src/cli/components/OAuthCodeDialog.tsx +++ b/src/cli/components/OAuthCodeDialog.tsx @@ -7,6 +7,7 @@ import { } from "../../auth/anthropic-oauth"; import { ANTHROPIC_PROVIDER_NAME, + checkAnthropicOAuthEligibility, createOrUpdateAnthropicProvider, } from "../../providers/anthropic-provider"; import { settingsManager } from "../../settings-manager"; @@ -21,6 +22,7 @@ type Props = { type FlowState = | "initializing" + | "checking_eligibility" | "waiting_for_code" | "exchanging" | "validating" @@ -58,6 +60,23 @@ export const OAuthCodeDialog = memo( return; } + // Check eligibility before starting OAuth flow + setFlowState("checking_eligibility"); + const eligibility = await checkAnthropicOAuthEligibility(); + if (!eligibility.eligible) { + onComplete( + false, + `✗ Claude OAuth requires a Pro or Enterprise plan\n\n` + + `This feature is only available for Letta Pro or Enterprise customers.\n` + + `Current plan: ${eligibility.billing_tier}\n\n` + + `To upgrade your plan, visit:\n\n` + + ` https://app.letta.com/settings/organization/usage\n\n` + + `If you have an Anthropic API key, you can use it directly by setting:\n` + + ` export ANTHROPIC_API_KEY=your-key`, + ); + return; + } + // Start OAuth flow const { authorizationUrl, @@ -248,11 +267,13 @@ export const OAuthCodeDialog = memo( } }; - if (flowState === "initializing") { + if (flowState === "initializing" || flowState === "checking_eligibility") { return ( - Starting Claude OAuth flow... + {flowState === "checking_eligibility" + ? "Checking account eligibility..." + : "Starting Claude OAuth flow..."} ); diff --git a/src/providers/anthropic-provider.ts b/src/providers/anthropic-provider.ts index 3f05cb2..8fadae3 100644 --- a/src/providers/anthropic-provider.ts +++ b/src/providers/anthropic-provider.ts @@ -17,6 +17,19 @@ interface ProviderResponse { base_url?: string; } +interface BalanceResponse { + total_balance: number; + monthly_credit_balance: number; + purchased_credit_balance: number; + billing_tier: string; +} + +interface EligibilityCheckResult { + eligible: boolean; + billing_tier: string; + reason?: string; +} + /** * Get the Letta API base URL and auth token */ @@ -53,6 +66,29 @@ async function providersRequest( if (!response.ok) { const errorText = await response.text(); + + // Check if this is a pro/enterprise plan limitation error + if (response.status === 403) { + try { + const errorData = JSON.parse(errorText); + if ( + errorData.error && + typeof errorData.error === "string" && + errorData.error.includes("only available for pro or enterprise") + ) { + throw new Error("PLAN_UPGRADE_REQUIRED"); + } + } catch (parseError) { + // If it's not valid JSON or doesn't match our pattern, fall through to generic error + if ( + parseError instanceof Error && + parseError.message === "PLAN_UPGRADE_REQUIRED" + ) { + throw parseError; + } + } + } + throw new Error(`Provider API error (${response.status}): ${errorText}`); } @@ -187,3 +223,40 @@ export async function removeAnthropicProvider(): Promise { await deleteAnthropicProvider(existing.id); } } + +/** + * Check if user is eligible for Anthropic OAuth + * Requires Pro or Enterprise billing tier + */ +export async function checkAnthropicOAuthEligibility(): Promise { + try { + const balance = await providersRequest( + "GET", + "/v1/metadata/balance", + ); + + const billingTier = balance.billing_tier.toLowerCase(); + + // OAuth is available for pro and enterprise tiers + if (billingTier === "pro" || billingTier === "enterprise") { + return { + eligible: true, + billing_tier: balance.billing_tier, + }; + } + + return { + eligible: false, + billing_tier: balance.billing_tier, + reason: `Claude OAuth requires a Pro or Enterprise plan. Current plan: ${balance.billing_tier}`, + }; + } catch (error) { + // If we can't check eligibility, allow the flow to continue + // The provider creation will handle the error appropriately + console.warn("Failed to check Anthropic OAuth eligibility:", error); + return { + eligible: true, + billing_tier: "unknown", + }; + } +}