From e324d97b183ac73fe36e70db1b430b25353e5ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=98=88=EC=84=9C?= <49704047+yeseoLee@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:52:07 +0900 Subject: [PATCH 1/4] feat(claude): plan an optional OAuth step for CLI Auto Add a `cli-auto-fallback-oauth` step between Web and CLI in the CLI Auto plan, included only when the planning input says OAuth should be attempted. CLI runtime never sets that today, so the plan stays web -> cli on every platform until a caller opts in. Co-Authored-By: Claude Opus 5.5 --- .../Claude/ClaudeSourcePlanner.swift | 11 +++++++---- .../ClaudeSourcePlannerTests.swift | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift index ec65a64fba5..cf8204478e7 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeSourcePlanner.swift @@ -32,6 +32,7 @@ public enum ClaudeSourcePlanReason: String, Equatable, Sendable { case appAutoFallbackCLI = "app-auto-fallback-cli" case appAutoFallbackWeb = "app-auto-fallback-web" case cliAutoPreferredWeb = "cli-auto-preferred-web" + case cliAutoFallbackOAuth = "cli-auto-fallback-oauth" case cliAutoFallbackCLI = "cli-auto-fallback-cli" } @@ -182,10 +183,12 @@ public enum ClaudeSourcePlanner { self.step(.web, reason: .appAutoFallbackWeb, input: input), ] case .cli: - [ - self.step(.web, reason: .cliAutoPreferredWeb, input: input), - self.step(.cli, reason: .cliAutoFallbackCLI, input: input), - ] + // OAuth is only planned where CLI Auto attempts it (Linux); macOS keeps web → cli. + [self.step(.web, reason: .cliAutoPreferredWeb, input: input)] + + (input.hasOAuthCredentials + ? [self.step(.oauth, reason: .cliAutoFallbackOAuth, input: input)] + : []) + + [self.step(.cli, reason: .cliAutoFallbackCLI, input: input)] } case .api: [self.step(.api, reason: .explicitSourceSelection, input: input)] diff --git a/Tests/CodexBarTests/ClaudeSourcePlannerTests.swift b/Tests/CodexBarTests/ClaudeSourcePlannerTests.swift index 05b58dd946f..1f6cb200795 100644 --- a/Tests/CodexBarTests/ClaudeSourcePlannerTests.swift +++ b/Tests/CodexBarTests/ClaudeSourcePlannerTests.swift @@ -41,6 +41,25 @@ struct ClaudeSourcePlannerTests { #expect(plan.preferredStep?.dataSource == .web) } + @Test + func `CLI auto plan prefers OAuth over CLI when web session is unavailable`() { + let plan = ClaudeSourcePlanner.resolve(input: ClaudeSourcePlanningInput( + runtime: .cli, + selectedDataSource: .auto, + webExtrasEnabled: false, + hasWebSession: false, + hasCLI: true, + hasOAuthCredentials: true)) + + #expect(plan.orderedSteps.map(\.inclusionReason) == [ + .cliAutoPreferredWeb, + .cliAutoFallbackOAuth, + .cliAutoFallbackCLI, + ]) + #expect(plan.availableSteps.map(\.dataSource) == [.oauth, .cli]) + #expect(plan.preferredStep?.dataSource == .oauth) + } + @Test func `explicit mode plan is single step`() { let plan = ClaudeSourcePlanner.resolve(input: ClaudeSourcePlanningInput( From 31432efc5397c262d09d95e1ea9f5a149b5ef1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=98=88=EC=84=9C?= <49704047+yeseoLee@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:52:07 +0900 Subject: [PATCH 2/4] fix(claude): try OAuth before the CLI PTY fallback in Linux CLI Auto Linux cannot import browser cookies, so CLI Auto went straight from an unavailable Web step to the Claude CLI PTY fallback, which boots a full interactive `claude` session (~9 s on WSL2) even when Claude Code OAuth credentials are readable. Off macOS, plan OAuth between Web and CLI, check it with the same noninteractive credential read used by explicit sources (logged-out users still get noAvailableStrategy), and let OAuth failures such as rate limits fall back to the CLI. macOS is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../Claude/ClaudeProviderDescriptor.swift | 22 +++++- .../ClaudeBaselineCharacterizationTests.swift | 12 +++ TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift | 77 +++++++++++++++++++ .../ClaudeWebManualCookieLinuxTests.swift | 4 +- TestsLinux/PlatformGatingTests.swift | 10 +-- 5 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index fe99d819e46..61b13879e39 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -371,8 +371,9 @@ public enum ClaudeProviderDescriptor { private static func makePlanningInput(context: ProviderFetchContext) -> ClaudeSourcePlanningInput { let webExtrasEnabled = context.settings?.claude?.webExtrasEnabled ?? false let hasWebSession = Self.hasPlausibleWebSession(context: context) - let shouldAttemptOAuth = context.runtime == .app && - (context.sourceMode == .auto || context.sourceMode == .oauth) + let shouldAttemptOAuth = (context.runtime == .app && + (context.sourceMode == .auto || context.sourceMode == .oauth)) + || Self.shouldAttemptCLIAutoOAuth(context: context) return ClaudeSourcePlanningInput( runtime: context.runtime, @@ -385,6 +386,15 @@ public enum ClaudeProviderDescriptor { hasOAuthCredentials: shouldAttemptOAuth) } + /// CLI Auto tries OAuth before the Claude CLI PTY only off macOS; macOS CLI Auto keeps its web → cli plan. + static func shouldAttemptCLIAutoOAuth(context: ProviderFetchContext) -> Bool { + #if os(macOS) + false + #else + context.runtime == .cli && context.sourceMode == .auto + #endif + } + private static func hasPlausibleWebSession(context: ProviderFetchContext) -> Bool { switch context.sourceMode { case .api, .oauth, .cli: @@ -525,8 +535,11 @@ private struct ClaudePlannedFetchStrategy: ProviderFetchStrategy { return await self.base.isAvailable(context) } guard self.plannedStep.isPlausiblyAvailable else { return false } + // CLI Auto OAuth uses the same noninteractive credential check as explicit sources, so a + // logged-out user still gets `noAvailableStrategy` rather than an OAuth credential error. if self.plannedStep.dataSource == .cli || - (context.runtime == .app && self.plannedStep.dataSource == .web) + (context.runtime == .app && self.plannedStep.dataSource == .web) || + (context.runtime == .cli && self.plannedStep.dataSource == .oauth) { return await self.base.isAvailable(context) } @@ -788,6 +801,9 @@ struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy { } // In Auto mode, fall back to the next strategy (cli/web) when safe credentials are absent or OAuth fails. // Cancellation itself is always terminal. + if ClaudeProviderDescriptor.shouldAttemptCLIAutoOAuth(context: context) { + return true + } return context.runtime == .app && context.sourceMode == .auto } diff --git a/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift b/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift index 397abc9259a..e2b3494a8f9 100644 --- a/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift +++ b/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift @@ -220,6 +220,18 @@ struct ClaudeBaselineCharacterizationTests { #expect(strategyIDs == ["claude.web", "claude.cli"]) } + @Test + func `CLI auto keeps web then CLI on macOS when OAuth credentials are readable`() async { + let env = [ + "CLAUDE_CLI_PATH": "/usr/bin/true", + ClaudeOAuthCredentialsStore.environmentTokenKey: "sk-ant-oat01-characterization", + ] + let context = self.makeContext(runtime: .cli, sourceMode: .auto, env: env) + #expect(!ClaudeProviderDescriptor.shouldAttemptCLIAutoOAuth(context: context)) + let strategyIDs = await self.strategyIDs(runtime: .cli, sourceMode: .auto, env: env) + #expect(strategyIDs == ["claude.web", "claude.cli"]) + } + @Test func `CLI explicit source delegates authentication to the configured Claude executable`() async throws { let invocationLog = FileManager.default.temporaryDirectory diff --git a/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift b/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift new file mode 100644 index 00000000000..6d21c141c04 --- /dev/null +++ b/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift @@ -0,0 +1,77 @@ +#if os(Linux) +import Foundation +import Testing +@testable import CodexBarCore + +struct ClaudeCLIAutoOAuthLinuxTests { + private func makeContext(sourceMode: ProviderSourceMode) -> ProviderFetchContext { + let browserDetection = BrowserDetection(cacheTTL: 0) + return ProviderFetchContext( + runtime: .cli, + sourceMode: sourceMode, + includeCredits: false, + webTimeout: 1, + webDebugDumpHTML: false, + verbose: false, + env: [:], + settings: ProviderSettingsSnapshot.make(), + fetcher: UsageFetcher(environment: [:]), + claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection), + browserDetection: browserDetection) + } + + @Test + func `CLI auto tries OAuth before the Claude CLI PTY fallback off macOS`() async { + // Without browser cookies, CLI Auto used to jump straight to the PTY fallback, which boots a full + // interactive `claude` session (~9s on WSL). The noninteractive OAuth file read must come first. + let context = self.makeContext(sourceMode: .auto) + let ids = await ProviderDescriptorRegistry + .descriptor(for: .claude) + .fetchPlan + .pipeline + .resolveStrategies(context) + .map(\.id) + + let oauthIndex = ids.firstIndex(of: "claude.oauth") + let cliIndex = ids.firstIndex(of: "claude.cli") + #expect(oauthIndex != nil) + #expect(cliIndex != nil) + if let oauthIndex, let cliIndex { + #expect(oauthIndex < cliIndex) + } + } + + @Test + func `CLI auto OAuth failure falls back to the next strategy`() { + let context = self.makeContext(sourceMode: .auto) + #expect(ClaudeOAuthFetchStrategy().shouldFallback( + on: ClaudeOAuthCredentialsError.notFound, + context: context)) + } + + @Test(arguments: [ + ClaudeOAuthFetchError.unauthorized, + ClaudeOAuthFetchError.rateLimited(retryAfter: nil), + ClaudeOAuthFetchError.serverError(503, nil), + ]) + func `CLI auto OAuth request failures fall back to the Claude CLI`(error: ClaudeOAuthFetchError) { + let context = self.makeContext(sourceMode: .auto) + #expect(ClaudeOAuthFetchStrategy().shouldFallback(on: error, context: context)) + } + + @Test + func `CLI auto OAuth cancellation does not fall back to the Claude CLI`() { + let context = self.makeContext(sourceMode: .auto) + #expect(!ClaudeOAuthFetchStrategy().shouldFallback(on: CancellationError(), context: context)) + #expect(!ClaudeOAuthFetchStrategy().shouldFallback( + on: ClaudeOAuthFetchError.networkError(URLError(.cancelled)), + context: context)) + } + + @Test + func `explicit CLI OAuth stays terminal`() { + let context = self.makeContext(sourceMode: .oauth) + #expect(!ClaudeProviderDescriptor.shouldAttemptCLIAutoOAuth(context: context)) + } +} +#endif diff --git a/TestsLinux/ClaudeWebManualCookieLinuxTests.swift b/TestsLinux/ClaudeWebManualCookieLinuxTests.swift index 045476d2977..7a2ba9776d3 100644 --- a/TestsLinux/ClaudeWebManualCookieLinuxTests.swift +++ b/TestsLinux/ClaudeWebManualCookieLinuxTests.swift @@ -153,8 +153,8 @@ struct ClaudeWebManualCookieLinuxTests { await ProviderDescriptorRegistry.descriptor(for: .claude).fetchOutcome(context: context) } } - #expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.cli"]) - #expect(outcome.attempts.map(\.wasAvailable) == [true, true]) + #expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.oauth", "claude.cli"]) + #expect(outcome.attempts.map(\.wasAvailable) == [true, false, true]) #expect(outcome.attempts.first?.errorDescription != nil) let result = try outcome.result.get() #expect(result.strategyID == "claude.cli") diff --git a/TestsLinux/PlatformGatingTests.swift b/TestsLinux/PlatformGatingTests.swift index a0aa7e59c43..68b0e76220d 100644 --- a/TestsLinux/PlatformGatingTests.swift +++ b/TestsLinux/PlatformGatingTests.swift @@ -66,8 +66,8 @@ struct PlatformGatingTests { let result = try outcome.result.get() #expect(result.strategyID == "claude.cli") - #expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.cli"]) - #expect(outcome.attempts.map(\.wasAvailable) == [false, true]) + #expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.oauth", "claude.cli"]) + #expect(outcome.attempts.map(\.wasAvailable) == [false, false, true]) #else #expect(Bool(true)) #endif @@ -98,8 +98,8 @@ struct PlatformGatingTests { #expect(provider == .claude) } } - #expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.cli"]) - #expect(outcome.attempts.map(\.wasAvailable) == [false, false]) + #expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.oauth", "claude.cli"]) + #expect(outcome.attempts.map(\.wasAvailable) == [false, false, false]) #else #expect(Bool(true)) #endif @@ -144,7 +144,7 @@ struct PlatformGatingTests { #expect(provider == .claude) } } - let expectedStrategyIDs = sourceMode == .auto ? ["claude.web", "claude.cli"] : ["claude.cli"] + let expectedStrategyIDs = sourceMode == .auto ? ["claude.web", "claude.oauth", "claude.cli"] : ["claude.cli"] #expect(outcome.attempts.map(\.strategyID) == expectedStrategyIDs) #expect(outcome.attempts.allSatisfy { !$0.wasAvailable }) let invocations = try String(contentsOf: invocationLog, encoding: .utf8) From e302b060adfea4a8d75393851b9a9276fd7d2844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=98=88=EC=84=9C?= <49704047+yeseoLee@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:52:07 +0900 Subject: [PATCH 3/4] docs(claude): document the Linux CLI Auto OAuth step Co-Authored-By: Claude Opus 5.5 --- Sources/CodexBarCLI/CLIHelp.swift | 3 ++- docs/claude.md | 6 +++++- docs/cli.md | 3 ++- docs/providers.md | 4 +++- docs/refactor/claude-current-baseline.md | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/CodexBarCLI/CLIHelp.swift b/Sources/CodexBarCLI/CLIHelp.swift index 17e8fb75111..099942f755c 100644 --- a/Sources/CodexBarCLI/CLIHelp.swift +++ b/Sources/CodexBarCLI/CLIHelp.swift @@ -96,7 +96,8 @@ extension CodexBarCLI { - Codex: OpenAI web dashboard (usage limits, credits remaining, code review remaining, usage breakdown). Auto falls back to Codex CLI only when cookies are missing. - Claude: claude.ai API. - Auto falls back to Claude CLI only when cookies are missing. + Auto tries Web, then CLI; on Linux, it tries OAuth before CLI. + Unavailable or failed sources fall back to the next strategy. - Kilo: app.kilo.ai API. Auto falls back to Kilo CLI when API credentials are missing or unauthorized. Token accounts are loaded from the resolved CodexBar config file. diff --git a/docs/claude.md b/docs/claude.md index 55abbed5a20..4e966100029 100644 --- a/docs/claude.md +++ b/docs/claude.md @@ -37,7 +37,7 @@ Incomplete proxy records with an explicit null `stop_reason`, positive input, ze ### Default selection (debug menu disabled) - If an Admin API key is configured, the Admin API strategy is used for Claude API spend/usage. - App runtime main pipeline: OAuth API → CLI PTY → Web API. -- CLI runtime main pipeline: Web API → CLI PTY. +- CLI runtime main pipeline: Web API → CLI PTY (Linux: Web API → OAuth API → CLI PTY). - Explicit picker modes (OAuth/Web/CLI) bypass automatic fallback. - Explicit OAuth retains the last successful quota measurement through recognized temporary network failures, including localized DNS/offline errors. Its original timestamp remains visible; authentication rejection still @@ -312,6 +312,10 @@ is stored on the token account in the config file and does not change quota fetc Web success returns without launching Claude Code; authentication rejection or a Cloudflare challenge falls back to an available CLI using the existing Auto policy. Cancellation stops without launching the CLI. Explicit Web mode does not fall back. Use explicit OAuth mode for passive polling that must not launch the CLI. +- Linux CLI Auto mode: the plan is Web → OAuth → CLI PTY. OAuth runs when Claude Code OAuth credentials + (`~/.claude/.credentials.json`) are usable without a prompt, so a logged-in user does not launch an interactive + `claude` session. Without usable OAuth credentials, the OAuth step is skipped and CLI remains the fallback. + OAuth request failures such as 401 or 429 also fall back to the CLI; cancellation stops without launching it. - Cookie name required: - `sessionKey` (value prefix `sk-ant-...`). - Cached cookies: Keychain cache `com.steipete.codexbar.cache` (account `cookie.claude`, source + timestamp). diff --git a/docs/cli.md b/docs/cli.md index e454963a9ad..3c5823d2980 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -205,7 +205,8 @@ sources and setup guide. The [provider ID list](provider-ids.md) is generated fr - `--web-debug-dump-html` (writes HTML snapshots to `/tmp` when data is missing) - Claude web: claude.ai API (session + weekly usage, account metadata, and prepaid Usage credits balance when available). - CLI Auto falls back to the installed Claude executable when web credentials are unavailable. This foreground + CLI Auto falls back to the installed Claude executable when web credentials are unavailable (on Linux, readable + Claude Code OAuth credentials are tried first). This foreground command delegates authentication to Claude Code; the app keeps its stricter prompt-free background availability gate for scheduled refreshes. - Command Code auto: `COMMAND_CODE_API_KEY`/config `apiKey` when set, otherwise commandcode.ai browser session cookies on macOS or a configured manual cookie on Linux. Explicit `api`/`web` pins that source. diff --git a/docs/providers.md b/docs/providers.md index b00261134ee..573ed8d485f 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -76,7 +76,7 @@ complete when the available scan window covers fewer days. | Codex | App Auto: OAuth API (`oauth`) → CLI RPC/PTy (`codex-cli`). CLI Auto: Web dashboard (`openai-web`) → CLI RPC/PTy (`codex-cli`). | | OpenAI | Admin API key (`api`) for organization spend/usage; legacy API-key balance fallback. | | Azure OpenAI | API key + endpoint + deployment probe (`api`) for deployment status validation. | -| Claude | Admin API key (`api`) when configured; otherwise App Auto: OAuth API (`oauth`) → CLI PTY (`claude`) → Web API (`web`). CLI Auto: Web API (`web`) → CLI PTY (`claude`). | +| Claude | Admin API key (`api`) when configured; otherwise App Auto: OAuth API (`oauth`) → CLI PTY (`claude`) → Web API (`web`). CLI Auto: Web API (`web`) → CLI PTY (`claude`); on Linux, Web API (`web`) → OAuth API (`oauth`) → CLI PTY (`claude`). | | Gemini | Gemini CLI OAuth quotas (`auto`/`api`, default), or opt-in Gemini app 5-hour/weekly limits via cookies (`web`). | | Antigravity | Local LSP/HTTP probe (`local`). | | Cursor | Web API via cookies → legacy stored session → Cursor.app local auth (`web`). | @@ -217,6 +217,8 @@ complete when the available scan window covers fewer days. - Admin API shows organization spend/messages summaries with the same inline dashboard pattern as OpenAI API. - App Auto: OAuth API (`oauth`) → CLI PTY (`claude`) → Web API (`web`). - CLI Auto: Web API (`web`) → CLI PTY (`claude`). +- Linux CLI Auto: Web API (`web`) → OAuth API (`oauth`) → CLI PTY (`claude`). The OAuth step is skipped without + usable noninteractive credentials; OAuth request failures fall back to the CLI. - Local cost usage: scans `CLAUDE_CONFIG_DIR` when set, otherwise `~/.config/claude/projects`, `~/.claude/projects` (including current Claude Desktop Code/Cowork CLI sessions), and nested Claude Desktop local-agent JSONL files for the configured history window. diff --git a/docs/refactor/claude-current-baseline.md b/docs/refactor/claude-current-baseline.md index 80b7d1e0272..d1ac6c4c9d1 100644 --- a/docs/refactor/claude-current-baseline.md +++ b/docs/refactor/claude-current-baseline.md @@ -53,7 +53,7 @@ The generic provider pipeline currently resolves Claude strategies in this order | app | oauth | `oauth` | No fallback. | | app | cli | `cli` | No fallback. | | app | web | `web` | No fallback. | -| cli | auto | `web -> cli` | Web can fall through to CLI. CLI is terminal. | +| cli | auto | `web -> cli` (Linux: `web -> oauth -> cli`) | Web can fall through to OAuth/CLI. Linux always plans OAuth; it is skipped without usable noninteractive credentials, and its request failures fall through to CLI. Cancellation is terminal. CLI is terminal. | | cli | oauth | `oauth` | No fallback. | | cli | cli | `cli` | No fallback. | | cli | web | `web` | No fallback. | From bb2eb778ed90568c7b2ef15223a3daa905ab0390 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Oct 2026 19:09:09 -0700 Subject: [PATCH 4/4] refactor(claude): simplify Linux Auto OAuth routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the contributor's Web → OAuth → CLI plan on Linux while trimming one-use predicates and duplicate source labels. Assert actual explicit-mode fallback behavior, remove stale help text, and add the 0.77.1 credit. Refs #4613, #4615. Co-authored-by: 이예서 <49704047+yeseoLee@users.noreply.github.com> --- CHANGELOG.md | 4 ++ Sources/CodexBarCLI/CLIHelp.swift | 2 - .../Claude/ClaudeProviderDescriptor.swift | 42 ++++++++----------- .../Claude/ClaudeUsageDataSource.swift | 13 +----- .../ClaudeBaselineCharacterizationTests.swift | 2 - TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift | 4 +- 6 files changed, 25 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 636ab772be1..cd18f4104de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.77.1 — Unreleased +### Fixed + +- Claude: try readable OAuth credentials before starting the CLI in Linux Auto mode, avoiding unnecessary interactive startup while preserving explicit source selection (#4613, #4615). Thanks @yeseoLee! + ## 0.77.0 — 2026-10-16 ### Highlights diff --git a/Sources/CodexBarCLI/CLIHelp.swift b/Sources/CodexBarCLI/CLIHelp.swift index 099942f755c..75de2ca9e9d 100644 --- a/Sources/CodexBarCLI/CLIHelp.swift +++ b/Sources/CodexBarCLI/CLIHelp.swift @@ -96,8 +96,6 @@ extension CodexBarCLI { - Codex: OpenAI web dashboard (usage limits, credits remaining, code review remaining, usage breakdown). Auto falls back to Codex CLI only when cookies are missing. - Claude: claude.ai API. - Auto tries Web, then CLI; on Linux, it tries OAuth before CLI. - Unavailable or failed sources fall back to the next strategy. - Kilo: app.kilo.ai API. Auto falls back to Kilo CLI when API credentials are missing or unauthorized. Token accounts are loaded from the resolved CodexBar config file. diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index 61b13879e39..a7af9f2d04b 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -309,7 +309,9 @@ public enum ClaudeProviderDescriptor { browserDetection: context.browserDetection, hasWebFallback: false)] } - if context.sourceMode == .api || self.hasAutoAdminAPIKey(context: context) { + if context.sourceMode == .api || + (context.sourceMode == .auto && ClaudeAdminAPISettingsReader.apiKey(environment: context.env) != nil) + { return [ClaudeAdminAPIFetchStrategy()] } @@ -343,10 +345,6 @@ public enum ClaudeProviderDescriptor { } } - private static func hasAutoAdminAPIKey(context: ProviderFetchContext) -> Bool { - context.sourceMode == .auto && ClaudeAdminAPISettingsReader.apiKey(environment: context.env) != nil - } - private static func isSelectedOAuthTokenAccount(context: ProviderFetchContext) -> Bool { guard context.selectedTokenAccountID != nil, context.settings?.claude?.usageDataSource == .oauth @@ -369,32 +367,25 @@ public enum ClaudeProviderDescriptor { } private static func makePlanningInput(context: ProviderFetchContext) -> ClaudeSourcePlanningInput { - let webExtrasEnabled = context.settings?.claude?.webExtrasEnabled ?? false - let hasWebSession = Self.hasPlausibleWebSession(context: context) - let shouldAttemptOAuth = (context.runtime == .app && - (context.sourceMode == .auto || context.sourceMode == .oauth)) - || Self.shouldAttemptCLIAutoOAuth(context: context) + #if os(Linux) + let shouldAttemptOAuth = context.sourceMode == .auto || + (context.runtime == .app && context.sourceMode == .oauth) + #else + let shouldAttemptOAuth = context.runtime == .app && + (context.sourceMode == .auto || context.sourceMode == .oauth) + #endif return ClaudeSourcePlanningInput( runtime: context.runtime, selectedDataSource: Self.sourceDataSource(from: context.sourceMode), - webExtrasEnabled: webExtrasEnabled, - hasWebSession: hasWebSession, + webExtrasEnabled: context.settings?.claude?.webExtrasEnabled ?? false, + hasWebSession: Self.hasPlausibleWebSession(context: context), hasCLI: ClaudeCLIResolver.isAvailable(environment: context.env), - // App Auto and explicit OAuth perform one real, noninteractive OAuth attempt. Do not preflight here: + // App Auto, Linux CLI Auto, and explicit OAuth plan a noninteractive attempt. Do not preflight here: // a preflight can mutate cache state and make the real fetch misclassify a typed error. hasOAuthCredentials: shouldAttemptOAuth) } - /// CLI Auto tries OAuth before the Claude CLI PTY only off macOS; macOS CLI Auto keeps its web → cli plan. - static func shouldAttemptCLIAutoOAuth(context: ProviderFetchContext) -> Bool { - #if os(macOS) - false - #else - context.runtime == .cli && context.sourceMode == .auto - #endif - } - private static func hasPlausibleWebSession(context: ProviderFetchContext) -> Bool { switch context.sourceMode { case .api, .oauth, .cli: @@ -801,10 +792,11 @@ struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy { } // In Auto mode, fall back to the next strategy (cli/web) when safe credentials are absent or OAuth fails. // Cancellation itself is always terminal. - if ClaudeProviderDescriptor.shouldAttemptCLIAutoOAuth(context: context) { - return true - } + #if os(Linux) + return context.sourceMode == .auto + #else return context.runtime == .app && context.sourceMode == .auto + #endif } fileprivate static func snapshot( diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeUsageDataSource.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeUsageDataSource.swift index 063de7ffa5f..579259b346a 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeUsageDataSource.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeUsageDataSource.swift @@ -22,17 +22,6 @@ public enum ClaudeUsageDataSource: String, CaseIterable, Identifiable, Sendable } public var sourceLabel: String { - switch self { - case .auto: - "auto" - case .api: - "api" - case .oauth: - "oauth" - case .web: - "web" - case .cli: - "cli" - } + self.rawValue } } diff --git a/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift b/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift index e2b3494a8f9..92f0f2062c5 100644 --- a/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift +++ b/Tests/CodexBarTests/ClaudeBaselineCharacterizationTests.swift @@ -226,8 +226,6 @@ struct ClaudeBaselineCharacterizationTests { "CLAUDE_CLI_PATH": "/usr/bin/true", ClaudeOAuthCredentialsStore.environmentTokenKey: "sk-ant-oat01-characterization", ] - let context = self.makeContext(runtime: .cli, sourceMode: .auto, env: env) - #expect(!ClaudeProviderDescriptor.shouldAttemptCLIAutoOAuth(context: context)) let strategyIDs = await self.strategyIDs(runtime: .cli, sourceMode: .auto, env: env) #expect(strategyIDs == ["claude.web", "claude.cli"]) } diff --git a/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift b/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift index 6d21c141c04..0f61995a474 100644 --- a/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift +++ b/TestsLinux/ClaudeCLIAutoOAuthLinuxTests.swift @@ -71,7 +71,9 @@ struct ClaudeCLIAutoOAuthLinuxTests { @Test func `explicit CLI OAuth stays terminal`() { let context = self.makeContext(sourceMode: .oauth) - #expect(!ClaudeProviderDescriptor.shouldAttemptCLIAutoOAuth(context: context)) + #expect(!ClaudeOAuthFetchStrategy().shouldFallback( + on: ClaudeOAuthCredentialsError.notFound, + context: context)) } } #endif