feat: replace /connect claude with /connect codex for OpenAI OAuth (#527)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -3,15 +3,18 @@
|
||||
|
||||
import {
|
||||
exchangeCodeForTokens,
|
||||
startAnthropicOAuth,
|
||||
validateAnthropicCredentials,
|
||||
} from "../../auth/anthropic-oauth";
|
||||
extractAccountIdFromToken,
|
||||
OPENAI_OAUTH_CONFIG,
|
||||
startLocalOAuthServer,
|
||||
startOpenAIOAuth,
|
||||
} from "../../auth/openai-oauth";
|
||||
import {
|
||||
ANTHROPIC_PROVIDER_NAME,
|
||||
checkAnthropicOAuthEligibility,
|
||||
createOrUpdateAnthropicProvider,
|
||||
removeAnthropicProvider,
|
||||
} from "../../providers/anthropic-provider";
|
||||
checkOpenAICodexEligibility,
|
||||
createOrUpdateOpenAICodexProvider,
|
||||
getOpenAICodexProvider,
|
||||
OPENAI_CODEX_PROVIDER_NAME,
|
||||
removeOpenAICodexProvider,
|
||||
} from "../../providers/openai-codex-provider";
|
||||
import {
|
||||
createOrUpdateZaiProvider,
|
||||
getZaiProvider,
|
||||
@@ -35,6 +38,7 @@ export interface ConnectCommandContext {
|
||||
buffersRef: { current: Buffers };
|
||||
refreshDerived: () => void;
|
||||
setCommandRunning: (running: boolean) => void;
|
||||
onCodexConnected?: () => void; // Callback to show model selector after successful connection
|
||||
}
|
||||
|
||||
// Helper to add a command result to buffers
|
||||
@@ -85,13 +89,13 @@ function updateCommandResult(
|
||||
|
||||
/**
|
||||
* Handle /connect command
|
||||
* Usage: /connect claude [code]
|
||||
* Usage: /connect codex
|
||||
*
|
||||
* Flow:
|
||||
* 1. User runs `/connect claude` - opens browser for authorization
|
||||
* 2. User authorizes on claude.ai, gets redirected to Anthropic's callback page
|
||||
* 3. User copies the authorization code from the URL
|
||||
* 4. User runs `/connect claude <code>` to complete the exchange
|
||||
* 1. User runs `/connect codex` - starts local server and opens browser for authorization
|
||||
* 2. User authorizes in browser, gets redirected back to local server
|
||||
* 3. Server automatically exchanges code for tokens and API key
|
||||
* 4. Provider is created and user sees success message
|
||||
*/
|
||||
export async function handleConnect(
|
||||
ctx: ConnectCommandContext,
|
||||
@@ -99,8 +103,6 @@ export async function handleConnect(
|
||||
): Promise<void> {
|
||||
const parts = msg.trim().split(/\s+/);
|
||||
const provider = parts[1]?.toLowerCase();
|
||||
// Join all remaining parts in case the code#state got split across lines
|
||||
const authCode = parts.slice(2).join(""); // Optional authorization code
|
||||
|
||||
// Validate provider argument
|
||||
if (!provider) {
|
||||
@@ -108,18 +110,18 @@ export async function handleConnect(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Usage: /connect <provider> [options]\n\nAvailable providers:\n • claude - Connect via OAuth to authenticate without an API key\n • zai <api_key> - Connect to Zai with your API key",
|
||||
"Usage: /connect <provider> [options]\n\nAvailable providers:\n \u2022 codex - Connect via OAuth to authenticate with ChatGPT Plus/Pro\n \u2022 zai <api_key> - Connect to Zai with your API key",
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider !== "claude" && provider !== "zai") {
|
||||
if (provider !== "codex" && provider !== "zai") {
|
||||
addCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
`Error: Unknown provider "${provider}"\n\nAvailable providers: claude, zai\nUsage: /connect <provider> [options]`,
|
||||
`Error: Unknown provider "${provider}"\n\nAvailable providers: codex, zai\nUsage: /connect <provider> [options]`,
|
||||
false,
|
||||
);
|
||||
return;
|
||||
@@ -131,28 +133,31 @@ export async function handleConnect(
|
||||
return;
|
||||
}
|
||||
|
||||
// If authorization code is provided, complete the OAuth flow
|
||||
if (authCode && authCode.length > 0) {
|
||||
await completeOAuthFlow(ctx, msg, authCode);
|
||||
return;
|
||||
}
|
||||
// Handle /connect codex
|
||||
await handleConnectCodex(ctx, msg);
|
||||
}
|
||||
|
||||
// Check if already connected
|
||||
if (
|
||||
settingsManager.hasAnthropicOAuth() &&
|
||||
!settingsManager.isAnthropicTokenExpired()
|
||||
) {
|
||||
/**
|
||||
* Handle /connect codex - OpenAI Codex OAuth with local server
|
||||
*/
|
||||
async function handleConnectCodex(
|
||||
ctx: ConnectCommandContext,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
// Check if already connected (provider exists on backend)
|
||||
const existingProvider = await getOpenAICodexProvider();
|
||||
if (existingProvider) {
|
||||
addCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Already connected to Claude via OAuth.\n\nUse /disconnect to remove the current connection first.",
|
||||
"Already connected to OpenAI Codex via OAuth.\n\nUse /disconnect codex to remove the current connection first.",
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the OAuth flow (step 1)
|
||||
// Start the OAuth flow
|
||||
ctx.setCommandRunning(true);
|
||||
|
||||
// Show initial status
|
||||
@@ -167,20 +172,20 @@ export async function handleConnect(
|
||||
|
||||
try {
|
||||
// 1. Check eligibility before starting OAuth flow
|
||||
const eligibility = await checkAnthropicOAuthEligibility();
|
||||
const eligibility = await checkOpenAICodexEligibility();
|
||||
if (!eligibility.eligible) {
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Claude OAuth requires a Pro or Enterprise plan\n\n` +
|
||||
`\u2717 OpenAI Codex 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`,
|
||||
`If you have an OpenAI API key, you can use it directly by setting:\n` +
|
||||
` export OPENAI_API_KEY=your-key`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
@@ -188,18 +193,46 @@ export async function handleConnect(
|
||||
}
|
||||
|
||||
// 2. Start OAuth flow - generate PKCE and authorization URL
|
||||
const { authorizationUrl, state, codeVerifier } =
|
||||
await startAnthropicOAuth();
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
"Starting OAuth flow...\nA browser window will open for authorization.",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
// 3. Store state for validation when user returns with code
|
||||
settingsManager.storeOAuthState(state, codeVerifier, "anthropic");
|
||||
const { authorizationUrl, state, codeVerifier, redirectUri } =
|
||||
await startOpenAIOAuth(OPENAI_OAUTH_CONFIG.defaultPort);
|
||||
|
||||
// 4. Try to open browser
|
||||
let browserOpened = false;
|
||||
// 3. Store state for validation
|
||||
settingsManager.storeOAuthState(state, codeVerifier, redirectUri, "openai");
|
||||
|
||||
// 4. Start local server to receive callback
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`Starting local OAuth server on port ${OPENAI_OAUTH_CONFIG.defaultPort}...\n\n` +
|
||||
`Opening browser for authorization...\n` +
|
||||
`If the browser doesn't open automatically, visit:\n\n` +
|
||||
`${authorizationUrl}`,
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
// Start the server and wait for callback
|
||||
const serverPromise = startLocalOAuthServer(
|
||||
state,
|
||||
OPENAI_OAUTH_CONFIG.defaultPort,
|
||||
);
|
||||
|
||||
// 5. Try to open browser
|
||||
try {
|
||||
const { default: open } = await import("open");
|
||||
const subprocess = await open(authorizationUrl, { wait: false });
|
||||
browserOpened = true;
|
||||
// Handle errors from the spawned process (e.g., xdg-open not found in containers)
|
||||
subprocess.on("error", () => {
|
||||
// Silently ignore - user can still manually visit the URL
|
||||
@@ -208,143 +241,81 @@ export async function handleConnect(
|
||||
// If auto-open fails, user can still manually visit the URL
|
||||
}
|
||||
|
||||
// 5. Show instructions
|
||||
const browserMsg = browserOpened
|
||||
? "Opening browser for authorization..."
|
||||
: "Please open the following URL in your browser:";
|
||||
|
||||
// 6. Wait for callback
|
||||
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" +
|
||||
"Copy the entire value and run:\n\n" +
|
||||
" /connect claude <code#state>\n\n" +
|
||||
"Example: /connect claude abc123...#def456...",
|
||||
`Waiting for authorization...\n\n` +
|
||||
`Please complete the sign-in process in your browser.\n` +
|
||||
`The page will redirect automatically when done.\n\n` +
|
||||
`If needed, visit:\n${authorizationUrl}`,
|
||||
true,
|
||||
"finished",
|
||||
"running",
|
||||
);
|
||||
} catch (error) {
|
||||
// Clear any partial state
|
||||
settingsManager.clearOAuthState();
|
||||
|
||||
const { result, server } = await serverPromise;
|
||||
|
||||
// Close the server
|
||||
server.close();
|
||||
|
||||
// 7. Exchange code for tokens
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Failed to start OAuth flow: ${getErrorMessage(error)}`,
|
||||
false,
|
||||
"finished",
|
||||
"Authorization received! Exchanging code for tokens...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
} finally {
|
||||
ctx.setCommandRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete OAuth flow after user provides authorization code
|
||||
* Accepts either:
|
||||
* - Just the code: "n3nzU6B7gMep..."
|
||||
* - Code#state format: "n3nzU6B7gMep...#9ba626d8..."
|
||||
*/
|
||||
async function completeOAuthFlow(
|
||||
ctx: ConnectCommandContext,
|
||||
msg: string,
|
||||
authCodeInput: string,
|
||||
): Promise<void> {
|
||||
// Show initial status
|
||||
const cmdId = addCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Exchanging authorization code for tokens...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
ctx.setCommandRunning(true);
|
||||
|
||||
try {
|
||||
// 1. Get stored OAuth state
|
||||
const storedState = settingsManager.getOAuthState();
|
||||
if (!storedState) {
|
||||
throw new Error(
|
||||
"No pending OAuth flow found. Please run '/connect claude' first to start the authorization.",
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check if state is too old (5 minute timeout)
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
if (Date.now() - storedState.timestamp > fiveMinutes) {
|
||||
settingsManager.clearOAuthState();
|
||||
throw new Error(
|
||||
"OAuth session expired. Please run '/connect claude' again to start a new authorization.",
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Parse code#state format if provided
|
||||
let authCode = authCodeInput;
|
||||
let stateFromInput: string | undefined;
|
||||
if (authCodeInput.includes("#")) {
|
||||
const [code, stateVal] = authCodeInput.split("#");
|
||||
authCode = code ?? authCodeInput;
|
||||
stateFromInput = stateVal;
|
||||
// Validate state matches what we stored
|
||||
if (stateVal && stateVal !== storedState.state) {
|
||||
throw new Error(
|
||||
"State mismatch - the authorization may have been tampered with. Please try again.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Use state from input if provided, otherwise use stored state
|
||||
const stateToUse = stateFromInput || storedState.state;
|
||||
|
||||
// 4. Exchange code for tokens
|
||||
const tokens = await exchangeCodeForTokens(
|
||||
authCode,
|
||||
storedState.codeVerifier,
|
||||
stateToUse,
|
||||
result.code,
|
||||
codeVerifier,
|
||||
redirectUri,
|
||||
);
|
||||
|
||||
// 5. Update status
|
||||
// 8. Extract account ID from JWT
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
"Validating credentials...",
|
||||
"Extracting account information...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
// 6. Validate tokens work
|
||||
const isValid = await validateAnthropicCredentials(tokens.access_token);
|
||||
if (!isValid) {
|
||||
let accountId: string;
|
||||
try {
|
||||
accountId = extractAccountIdFromToken(tokens.access_token);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Token validation failed - the token may not have the required permissions.",
|
||||
`Failed to extract account ID from token. This may indicate an incompatible account type. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Store tokens locally
|
||||
settingsManager.storeAnthropicTokens(tokens);
|
||||
|
||||
// 8. Update status for provider creation
|
||||
// 9. Create or update provider in Letta with OAuth config
|
||||
// Backend handles request transformation to ChatGPT backend API
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
"Creating Anthropic provider...",
|
||||
"Creating OpenAI Codex provider...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
// 9. Create or update provider in Letta with the access token
|
||||
await createOrUpdateAnthropicProvider(tokens.access_token);
|
||||
await createOrUpdateOpenAICodexProvider({
|
||||
access_token: tokens.access_token,
|
||||
id_token: tokens.id_token,
|
||||
refresh_token: tokens.refresh_token,
|
||||
account_id: accountId,
|
||||
expires_at: Date.now() + tokens.expires_in * 1000,
|
||||
});
|
||||
|
||||
// 10. Clear OAuth state
|
||||
settingsManager.clearOAuthState();
|
||||
@@ -355,27 +326,36 @@ async function completeOAuthFlow(
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✓ Successfully connected to Claude via OAuth!\n\n` +
|
||||
`Provider '${ANTHROPIC_PROVIDER_NAME}' created/updated in Letta.\n` +
|
||||
`Your OAuth tokens are stored securely in ~/.letta/settings.json`,
|
||||
`\u2713 Successfully connected to OpenAI Codex!\n\n` +
|
||||
`Provider '${OPENAI_CODEX_PROVIDER_NAME}' created/updated in Letta.\n` +
|
||||
`Your ChatGPT Plus/Pro subscription is now linked.`,
|
||||
true,
|
||||
"finished",
|
||||
);
|
||||
|
||||
// 12. Show model selector to let user switch to a ChatGPT Plus/Pro model
|
||||
if (ctx.onCodexConnected) {
|
||||
// Small delay to let the success message render first
|
||||
setTimeout(() => ctx.onCodexConnected?.(), 500);
|
||||
}
|
||||
} catch (error) {
|
||||
// Clear any partial state
|
||||
settingsManager.clearOAuthState();
|
||||
|
||||
// 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` +
|
||||
`\u2717 OpenAI Codex 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`;
|
||||
`If you have an OpenAI API key, you can use it directly by setting:\n` +
|
||||
` export OPENAI_API_KEY=your-key`;
|
||||
} else {
|
||||
displayMessage = `✗ Failed to connect: ${errorMessage}`;
|
||||
displayMessage = `\u2717 Failed to connect: ${errorMessage}`;
|
||||
}
|
||||
|
||||
updateCommandResult(
|
||||
@@ -409,7 +389,7 @@ export async function handleDisconnect(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Usage: /disconnect <provider>\n\nAvailable providers: claude, zai",
|
||||
"Usage: /disconnect <provider>\n\nAvailable providers: codex, claude, zai",
|
||||
false,
|
||||
);
|
||||
return;
|
||||
@@ -421,7 +401,13 @@ export async function handleDisconnect(
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle /disconnect claude
|
||||
// Handle /disconnect codex
|
||||
if (provider === "codex") {
|
||||
await handleDisconnectCodex(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle /disconnect claude (legacy - for users who connected before)
|
||||
if (provider === "claude") {
|
||||
await handleDisconnectClaude(ctx, msg);
|
||||
return;
|
||||
@@ -432,25 +418,26 @@ export async function handleDisconnect(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
`Error: Unknown provider "${provider}"\n\nAvailable providers: claude, zai\nUsage: /disconnect <provider>`,
|
||||
`Error: Unknown provider "${provider}"\n\nAvailable providers: codex, claude, zai\nUsage: /disconnect <provider>`,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle /disconnect claude
|
||||
* Handle /disconnect codex
|
||||
*/
|
||||
async function handleDisconnectClaude(
|
||||
async function handleDisconnectCodex(
|
||||
ctx: ConnectCommandContext,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
// Check if connected
|
||||
if (!settingsManager.hasAnthropicOAuth()) {
|
||||
// Check if provider exists on backend
|
||||
const existingProvider = await getOpenAICodexProvider();
|
||||
if (!existingProvider) {
|
||||
addCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Not currently connected to Claude via OAuth.\n\nUse /connect claude to authenticate.",
|
||||
"Not currently connected to OpenAI Codex via OAuth.\n\nUse /connect codex to authenticate.",
|
||||
false,
|
||||
);
|
||||
return;
|
||||
@@ -461,7 +448,7 @@ async function handleDisconnectClaude(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Disconnecting from Claude OAuth...",
|
||||
"Disconnecting from OpenAI Codex OAuth...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
@@ -469,37 +456,117 @@ async function handleDisconnectClaude(
|
||||
ctx.setCommandRunning(true);
|
||||
|
||||
try {
|
||||
// Remove provider from Letta
|
||||
await removeAnthropicProvider();
|
||||
|
||||
// Clear local tokens
|
||||
settingsManager.clearAnthropicOAuth();
|
||||
// Remove provider from Letta backend
|
||||
await removeOpenAICodexProvider();
|
||||
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✓ Disconnected from Claude OAuth.\n\n` +
|
||||
`Provider '${ANTHROPIC_PROVIDER_NAME}' removed from Letta.`,
|
||||
`\u2713 Disconnected from OpenAI Codex OAuth.\n\n` +
|
||||
`Provider '${OPENAI_CODEX_PROVIDER_NAME}' removed from Letta.`,
|
||||
true,
|
||||
"finished",
|
||||
);
|
||||
} catch (error) {
|
||||
// Still clear local tokens even if provider removal fails
|
||||
settingsManager.clearAnthropicOAuth();
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`\u2717 Failed to disconnect from OpenAI Codex: ${getErrorMessage(error)}`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
} finally {
|
||||
ctx.setCommandRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle /disconnect claude (legacy provider removal)
|
||||
* This allows users who connected Claude before it was replaced with Codex
|
||||
* to remove the old claude-pro-max provider
|
||||
*/
|
||||
async function handleDisconnectClaude(
|
||||
ctx: ConnectCommandContext,
|
||||
msg: string,
|
||||
): Promise<void> {
|
||||
const CLAUDE_PROVIDER_NAME = "claude-pro-max";
|
||||
|
||||
// Show running status
|
||||
const cmdId = addCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Checking for Claude provider...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
ctx.setCommandRunning(true);
|
||||
|
||||
try {
|
||||
// Check if claude-pro-max provider exists
|
||||
const { listProviders } = await import(
|
||||
"../../providers/openai-codex-provider"
|
||||
);
|
||||
const providers = await listProviders();
|
||||
const claudeProvider = providers.find(
|
||||
(p) => p.name === CLAUDE_PROVIDER_NAME,
|
||||
);
|
||||
|
||||
if (!claudeProvider) {
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`No Claude provider found.\n\nThe '${CLAUDE_PROVIDER_NAME}' provider does not exist in your Letta account.`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove provider from Letta
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
"Removing Claude provider...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
const { deleteOpenAICodexProvider } = await import(
|
||||
"../../providers/openai-codex-provider"
|
||||
);
|
||||
await deleteOpenAICodexProvider(claudeProvider.id);
|
||||
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✓ Disconnected from Claude OAuth.\n\n` +
|
||||
`Warning: Failed to remove provider from Letta: ${getErrorMessage(error)}\n` +
|
||||
`Your local OAuth tokens have been removed.`,
|
||||
`✓ Disconnected from Claude.\n\n` +
|
||||
`Provider '${CLAUDE_PROVIDER_NAME}' has been removed from Letta.\n\n` +
|
||||
`Note: /connect claude has been replaced with /connect codex for OpenAI ChatGPT Plus/Pro.`,
|
||||
true,
|
||||
"finished",
|
||||
);
|
||||
} catch (error) {
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Failed to disconnect from Claude: ${getErrorMessage(error)}`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
} finally {
|
||||
ctx.setCommandRunning(false);
|
||||
}
|
||||
@@ -546,7 +613,7 @@ async function handleDisconnectZai(
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✓ Disconnected from Zai.\n\n` +
|
||||
`\u2713 Disconnected from Zai.\n\n` +
|
||||
`Provider '${ZAI_PROVIDER_NAME}' removed from Letta.`,
|
||||
true,
|
||||
"finished",
|
||||
@@ -557,7 +624,7 @@ async function handleDisconnectZai(
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Failed to disconnect from Zai: ${getErrorMessage(error)}`,
|
||||
`\u2717 Failed to disconnect from Zai: ${getErrorMessage(error)}`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
@@ -615,9 +682,9 @@ export async function handleConnectZai(
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✓ Successfully connected to Zai!\n\n` +
|
||||
`\u2713 Successfully connected to Zai!\n\n` +
|
||||
`Provider '${ZAI_PROVIDER_NAME}' created in Letta.\n\n` +
|
||||
`The models are populated in /model → "All Available Models"`,
|
||||
`The models are populated in /model \u2192 "All Available Models"`,
|
||||
true,
|
||||
"finished",
|
||||
);
|
||||
@@ -627,7 +694,7 @@ export async function handleConnectZai(
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Failed to create Zai provider: ${getErrorMessage(error)}`,
|
||||
`\u2717 Failed to create Zai provider: ${getErrorMessage(error)}`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
|
||||
@@ -265,7 +265,7 @@ export const commands: Record<string, Command> = {
|
||||
|
||||
// === Session management (order 40-49) ===
|
||||
"/connect": {
|
||||
desc: "Connect an existing account (/connect zai <api-key>)",
|
||||
desc: "Connect an existing account (/connect codex or /connect zai <api-key>)",
|
||||
order: 40,
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx
|
||||
@@ -273,7 +273,7 @@ export const commands: Record<string, Command> = {
|
||||
},
|
||||
},
|
||||
"/disconnect": {
|
||||
desc: "Disconnect an existing account (/disconnect zai)",
|
||||
desc: "Disconnect an existing account (/disconnect codex|claude|zai)",
|
||||
order: 41,
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx
|
||||
|
||||
Reference in New Issue
Block a user