Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
### Highlights
- z.ai: preserve weekly and 5-hour token quotas together, surface the 5-hour lane correctly across the menu/menu bar, and add regression coverage (#662). Thanks to @takumi3488 for the original fix and investigation.
- Cursor: fix a crash in the usage fetch path and add regression coverage (#663). Thanks @anirudhvee for the report and validation!
- Claude: preserve normal CLI fallback precedence across well-known install paths so Finder-launched apps still prefer user-managed and native Homebrew binaries when multiple installs exist.

### Providers & Usage
- z.ai: preserve both weekly and 5-hour token quotas, keep the existing 2-limit behavior unchanged, and render the 5-hour quota as a tertiary row in provider snapshots and CLI/menu cards (#662). Credit to @takumi3488 for the original fix and investigation.
- Cursor: fix the usage fetch path so failed or cancelled requests no longer crash, and add Linux build and regression test coverage fixes (#663).
- Claude: preserve normal CLI fallback precedence across well-known install paths so Finder-launched apps prefer `~/.claude/bin/claude`, then Homebrew, before the bundled `cmux.app` binary when shell-based resolution is unavailable.
- OpenCode / OpenCode Go: treat serialized `_server` auth/account-context failures as invalid credentials so cached browser cookies are cleared and retried instead of surfacing a misleading HTTP 500.

### Menu & Settings
Expand Down
22 changes: 21 additions & 1 deletion Sources/CodexBarCore/PathEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,22 @@ public enum BinaryLocator {
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
wellKnownPaths: self.claudeWellKnownPaths(home: home),
fileManager: fileManager,
home: home)
}

/// Well-known installation paths for the Claude CLI binary.
/// Covers the macOS Terminal installer (cmux.app), ~/.claude/bin, and Homebrew.
static func claudeWellKnownPaths(home: String) -> [String] {
[
"\(home)/.claude/bin/claude",
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
"/Applications/cmux.app/Contents/Resources/bin/claude",
]
}

public static func resolveCodexBinary(
env: [String: String] = ProcessInfo.processInfo.environment,
loginPATH: [String]? = LoginShellPathCache.shared.current,
Expand Down Expand Up @@ -124,6 +136,7 @@ public enum BinaryLocator {
loginPATH: [String]?,
commandV: (String, String?, TimeInterval, FileManager) -> String?,
aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String?,
wellKnownPaths: [String] = [],
fileManager: FileManager,
home: String) -> String?
{
Expand Down Expand Up @@ -164,7 +177,14 @@ public enum BinaryLocator {
return aliasHit
}

// 5) Minimal fallback
// 5) Well-known installation paths (e.g. cmux.app bundle, ~/.claude/bin)
// macOS apps launched from Finder may not inherit the user's shell PATH,
// so check common install locations that the shell-based lookups above may miss.
for candidate in wellKnownPaths where fileManager.isExecutableFile(atPath: candidate) {
return candidate
}

// 6) Minimal fallback
let fallback = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]
if let pathHit = self.find(name, in: fallback, fileManager: fileManager) {
return pathHit
Expand Down
87 changes: 87 additions & 0 deletions Tests/CodexBarTests/PathBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,93 @@ struct PathBuilderTests {
#expect(resolved == aliasPath)
}

@Test
func `resolves claude from well-known cmux path when shell lookups fail`() {
let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude"
let fm = MockFileManager(executables: [cmuxPath])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }

let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/Users/test")
#expect(resolved == cmuxPath)
}

@Test
func `resolves claude from well-known home dir path`() {
let homePath = "/Users/test/.claude/bin/claude"
let fm = MockFileManager(executables: [homePath])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }

let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/Users/test")
#expect(resolved == homePath)
}

@Test
func `prefers user managed well-known path over cmux path`() {
let homePath = "/Users/test/.claude/bin/claude"
let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude"
let fm = MockFileManager(executables: [homePath, cmuxPath])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }

let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/Users/test")
#expect(resolved == homePath)
}

@Test
func `prefers homebrew arm path over usr local fallback`() {
let fm = MockFileManager(executables: [
"/opt/homebrew/bin/claude",
"/usr/local/bin/claude",
])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }

let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/Users/test")
#expect(resolved == "/opt/homebrew/bin/claude")
}

@Test
func `prefers shell PATH over well-known paths`() {
let shellPath = "/custom/bin/claude"
let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude"
let fm = MockFileManager(executables: [shellPath, cmuxPath])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in shellPath }

let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
fileManager: fm,
home: "/Users/test")
#expect(resolved == shellPath)
}

@Test
func `skips alias when command V resolves`() {
let path = "/shell/bin/claude"
Expand Down