From 19b56633408b10871081646b4f6350666c2efbab Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 9 Jan 2026 06:00:20 +0100 Subject: [PATCH] Revert "Remove Anthropic OAuth support" This reverts commit f5e6bcac1b74dd66567310b51a2134ecda7d6676. --- anthropic-oauth-test-payload.json | 28 ----- anthropic-oauth-test.sh | 37 ------ packages/ai/src/cli.ts | 13 ++ packages/ai/src/utils/oauth/anthropic.ts | 118 ++++++++++++++++++ packages/ai/src/utils/oauth/index.ts | 11 ++ packages/ai/src/utils/oauth/types.ts | 7 +- packages/coding-agent/README.md | 3 +- .../coding-agent/examples/extensions/snake.ts | 18 +-- .../coding-agent/src/core/auth-storage.ts | 7 ++ .../coding-agent/src/core/system-prompt.ts | 1 - 10 files changed, 159 insertions(+), 84 deletions(-) delete mode 100644 anthropic-oauth-test-payload.json delete mode 100755 anthropic-oauth-test.sh create mode 100644 packages/ai/src/utils/oauth/anthropic.ts diff --git a/anthropic-oauth-test-payload.json b/anthropic-oauth-test-payload.json deleted file mode 100644 index 7d7f645a..00000000 --- a/anthropic-oauth-test-payload.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "model": "claude-opus-4-5-20251101", - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "Warmup", - "cache_control": { - "type": "ephemeral" - } - } - ] - } - ], - "system": [ - { - "type": "text", - "text": "You are Claude Code, Anthropic's official CLI for Claude.", - "cache_control": { - "type": "ephemeral" - } - } - ], - "max_tokens": 32000, - "stream": true -} diff --git a/anthropic-oauth-test.sh b/anthropic-oauth-test.sh deleted file mode 100755 index 9c45f677..00000000 --- a/anthropic-oauth-test.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ -z "${ATO:-}" ]]; then - printf '%s\n' "ATO is not set. Export ATO with the OAuth token." >&2 - exit 1 -fi - -payload_path="${1:-/Users/badlogic/workspaces/pi-mono/anthropic-oauth-test-payload.json}" - -if [[ ! -f "$payload_path" ]]; then - printf '%s\n' "Payload file not found: $payload_path" >&2 - exit 1 -fi - -curl -sS -D - -o /tmp/anthropic-oauth-test.json \ - -X POST "https://api.anthropic.com/v1/messages?beta=true" \ - -H "accept: application/json" \ - -H "anthropic-beta: claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14" \ - -H "anthropic-dangerous-direct-browser-access: true" \ - -H "anthropic-version: 2023-06-01" \ - -H "authorization: Bearer $ATO" \ - -H "content-type: application/json" \ - -H "user-agent: claude-cli/2.1.2 (external, cli)" \ - -H "x-app: cli" \ - -H "x-stainless-arch: arm64" \ - -H "x-stainless-helper-method: stream" \ - -H "x-stainless-lang: js" \ - -H "x-stainless-os: MacOS" \ - -H "x-stainless-package-version: 0.70.0" \ - -H "x-stainless-retry-count: 0" \ - -H "x-stainless-runtime: node" \ - -H "x-stainless-runtime-version: v25.2.1" \ - -H "x-stainless-timeout: 600" \ - --data-binary "@$payload_path" - -printf '%s\n' "Response body saved to /tmp/anthropic-oauth-test.json" diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index 7c62af58..1a865c1c 100644 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs"; import { createInterface } from "readline"; +import { loginAnthropic } from "./utils/oauth/anthropic.js"; import { loginGitHubCopilot } from "./utils/oauth/github-copilot.js"; import { loginAntigravity } from "./utils/oauth/google-antigravity.js"; import { loginGeminiCli } from "./utils/oauth/google-gemini-cli.js"; @@ -38,6 +39,17 @@ async function login(provider: OAuthProvider): Promise { let credentials: OAuthCredentials; switch (provider) { + case "anthropic": + credentials = await loginAnthropic( + (url) => { + console.log(`\nOpen this URL in your browser:\n${url}\n`); + }, + async () => { + return await promptFn("Paste the authorization code:"); + }, + ); + break; + case "github-copilot": credentials = await loginGitHubCopilot({ onAuth: (url, instructions) => { @@ -110,6 +122,7 @@ Commands: list List available providers Providers: + anthropic Anthropic (Claude Pro/Max) github-copilot GitHub Copilot google-gemini-cli Google Gemini CLI google-antigravity Antigravity (Gemini 3, Claude, GPT-OSS) diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts new file mode 100644 index 00000000..74a2228c --- /dev/null +++ b/packages/ai/src/utils/oauth/anthropic.ts @@ -0,0 +1,118 @@ +/** + * Anthropic OAuth flow (Claude Pro/Max) + */ + +import { generatePKCE } from "./pkce.js"; +import type { OAuthCredentials } from "./types.js"; + +const decode = (s: string) => atob(s); +const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"); +const AUTHORIZE_URL = "https://claude.ai/oauth/authorize"; +const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"; +const REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"; +const SCOPES = "org:create_api_key user:profile user:inference"; + +/** + * Login with Anthropic OAuth (device code flow) + * + * @param onAuthUrl - Callback to handle the authorization URL (e.g., open browser) + * @param onPromptCode - Callback to prompt user for the authorization code + */ +export async function loginAnthropic( + onAuthUrl: (url: string) => void, + onPromptCode: () => Promise, +): Promise { + const { verifier, challenge } = await generatePKCE(); + + // Build authorization URL + const authParams = new URLSearchParams({ + code: "true", + client_id: CLIENT_ID, + response_type: "code", + redirect_uri: REDIRECT_URI, + scope: SCOPES, + code_challenge: challenge, + code_challenge_method: "S256", + state: verifier, + }); + + const authUrl = `${AUTHORIZE_URL}?${authParams.toString()}`; + + // Notify caller with URL to open + onAuthUrl(authUrl); + + // Wait for user to paste authorization code (format: code#state) + const authCode = await onPromptCode(); + const splits = authCode.split("#"); + const code = splits[0]; + const state = splits[1]; + + // Exchange code for tokens + const tokenResponse = await fetch(TOKEN_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + grant_type: "authorization_code", + client_id: CLIENT_ID, + code: code, + state: state, + redirect_uri: REDIRECT_URI, + code_verifier: verifier, + }), + }); + + if (!tokenResponse.ok) { + const error = await tokenResponse.text(); + throw new Error(`Token exchange failed: ${error}`); + } + + const tokenData = (await tokenResponse.json()) as { + access_token: string; + refresh_token: string; + expires_in: number; + }; + + // Calculate expiry time (current time + expires_in seconds - 5 min buffer) + const expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000; + + // Save credentials + return { + refresh: tokenData.refresh_token, + access: tokenData.access_token, + expires: expiresAt, + }; +} + +/** + * Refresh Anthropic OAuth token + */ +export async function refreshAnthropicToken(refreshToken: string): Promise { + const response = await fetch(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + grant_type: "refresh_token", + client_id: CLIENT_ID, + refresh_token: refreshToken, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Anthropic token refresh failed: ${error}`); + } + + const data = (await response.json()) as { + access_token: string; + refresh_token: string; + expires_in: number; + }; + + return { + refresh: data.refresh_token, + access: data.access_token, + expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000, + }; +} diff --git a/packages/ai/src/utils/oauth/index.ts b/packages/ai/src/utils/oauth/index.ts index 09b68f9c..1f9ff1a2 100644 --- a/packages/ai/src/utils/oauth/index.ts +++ b/packages/ai/src/utils/oauth/index.ts @@ -9,6 +9,8 @@ * - Antigravity (Gemini 3, Claude, GPT-OSS via Google Cloud) */ +// Anthropic +export { loginAnthropic, refreshAnthropicToken } from "./anthropic.js"; // GitHub Copilot export { getGitHubCopilotBaseUrl, @@ -38,6 +40,7 @@ export * from "./types.js"; // High-level API // ============================================================================ +import { refreshAnthropicToken } from "./anthropic.js"; import { refreshGitHubCopilotToken } from "./github-copilot.js"; import { refreshAntigravityToken } from "./google-antigravity.js"; import { refreshGoogleCloudToken } from "./google-gemini-cli.js"; @@ -59,6 +62,9 @@ export async function refreshOAuthToken( let newCredentials: OAuthCredentials; switch (provider) { + case "anthropic": + newCredentials = await refreshAnthropicToken(credentials.refresh); + break; case "github-copilot": newCredentials = await refreshGitHubCopilotToken(credentials.refresh, credentials.enterpriseUrl); break; @@ -122,6 +128,11 @@ export async function getOAuthApiKey( */ export function getOAuthProviders(): OAuthProviderInfo[] { return [ + { + id: "anthropic", + name: "Anthropic (Claude Pro/Max)", + available: true, + }, { id: "openai-codex", name: "ChatGPT Plus/Pro (Codex Subscription)", diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 3bc42ecf..245d93f6 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -8,7 +8,12 @@ export type OAuthCredentials = { accountId?: string; }; -export type OAuthProvider = "github-copilot" | "google-gemini-cli" | "google-antigravity" | "openai-codex"; +export type OAuthProvider = + | "anthropic" + | "github-copilot" + | "google-gemini-cli" + | "google-antigravity" + | "openai-codex"; export type OAuthPrompt = { message: string; diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 622c3574..9e15f499 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -158,7 +158,7 @@ Add API keys to `~/.pi/agent/auth.json`: | Provider | Auth Key | Environment Variable | |----------|--------------|---------------------| -| Anthropic | `anthropic` | `ANTHROPIC_API_KEY`, `ANTHROPIC_OAUTH_TOKEN` | +| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | | OpenAI | `openai` | `OPENAI_API_KEY` | | Google | `google` | `GEMINI_API_KEY` | | Mistral | `mistral` | `MISTRAL_API_KEY` | @@ -176,6 +176,7 @@ Use `/login` to authenticate with subscription-based or free-tier providers: | Provider | Models | Cost | |----------|--------|------| +| Anthropic (Claude Pro/Max) | Claude models via your subscription | Subscription | | GitHub Copilot | GPT-4o, Claude, Gemini via Copilot subscription | Subscription | | Google Gemini CLI | Gemini 2.0/2.5 models | Free (Google account) | | Google Antigravity | Gemini 3, Claude, GPT-OSS | Free (Google account) | diff --git a/packages/coding-agent/examples/extensions/snake.ts b/packages/coding-agent/examples/extensions/snake.ts index 0c345ef7..4378f758 100644 --- a/packages/coding-agent/examples/extensions/snake.ts +++ b/packages/coding-agent/examples/extensions/snake.ts @@ -156,23 +156,9 @@ class SnakeComponent { this.onClose(); return; } - // Any other key resumes (including P) + // Any other key resumes this.paused = false; this.startGame(); - this.version++; - this.tui.requestRender(); - return; - } - - // P to pause without closing - if (data === "p" || data === "P") { - this.paused = true; - if (this.interval) { - clearInterval(this.interval); - this.interval = null; - } - this.version++; - this.tui.requestRender(); return; } @@ -289,7 +275,7 @@ class SnakeComponent { } else if (this.state.gameOver) { footer = `${red(bold("GAME OVER!"))} Press ${bold("R")} to restart, ${bold("Q")} to quit`; } else { - footer = `↑↓←→ or WASD to move, ${bold("P")} pause, ${bold("ESC")} save+quit, ${bold("Q")} quit`; + footer = `↑↓←→ or WASD to move, ${bold("ESC")} pause, ${bold("Q")} quit`; } lines.push(this.padLine(boxLine(footer), width)); diff --git a/packages/coding-agent/src/core/auth-storage.ts b/packages/coding-agent/src/core/auth-storage.ts index 66aa43e7..09c0ac02 100644 --- a/packages/coding-agent/src/core/auth-storage.ts +++ b/packages/coding-agent/src/core/auth-storage.ts @@ -9,6 +9,7 @@ import { getEnvApiKey, getOAuthApiKey, + loginAnthropic, loginAntigravity, loginGeminiCli, loginGitHubCopilot, @@ -169,6 +170,12 @@ export class AuthStorage { let credentials: OAuthCredentials; switch (provider) { + case "anthropic": + credentials = await loginAnthropic( + (url) => callbacks.onAuth({ url }), + () => callbacks.onPrompt({ message: "Paste the authorization code:" }), + ); + break; case "github-copilot": credentials = await loginGitHubCopilot({ onAuth: (url, instructions) => callbacks.onAuth({ url, instructions }), diff --git a/packages/coding-agent/src/core/system-prompt.ts b/packages/coding-agent/src/core/system-prompt.ts index 95114a6b..7a0aaece 100644 --- a/packages/coding-agent/src/core/system-prompt.ts +++ b/packages/coding-agent/src/core/system-prompt.ts @@ -303,6 +303,5 @@ Documentation: prompt += `\nCurrent date and time: ${dateTime}`; prompt += `\nCurrent working directory: ${resolvedCwd}`; - prompt = "You are a helpful assistant. Be concise."; return prompt; }