Skip to content
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CodexBar 🎚️ - May your tokens never run out.

Tiny macOS 14+ menu bar app that keeps your Codex, Claude, Cursor, Gemini, Antigravity, Droid (Factory), Copilot, z.ai, Kiro, Vertex AI, Augment, Amp, JetBrains AI, and OpenRouter limits visible (session + weekly where available) and shows when each window resets. One status item per provider (or Merge Icons mode with a provider switcher and optional Overview tab); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.
Tiny macOS 14+ menu bar app that keeps your Codex, Claude, Cursor, Gemini, Jules, Antigravity, Droid (Factory), Copilot, z.ai, Kiro, Vertex AI, Augment, Amp, JetBrains AI, and OpenRouter limits visible (session + weekly where available) and shows when each window resets. One status item per provider (or Merge Icons mode with a provider switcher and optional Overview tab); enable what you use from Settings. No Dock icon, minimal UI, dynamic bar icons in the menu bar.

<img src="codexbar.png" alt="CodexBar menu screenshot" width="520" />

Expand All @@ -26,15 +26,15 @@ Linux support via Omarchy: community Waybar module and TUI, driven by the `codex

### First run
- Open Settings → Providers and enable what you use.
- Install/sign in to the provider sources you rely on (e.g. `codex`, `claude`, `gemini`, browser cookies, or OAuth; Antigravity requires the Antigravity app running).
- Install/sign in to the provider sources you rely on (e.g. `codex`, `claude`, `gemini`, `jules`, browser cookies, or OAuth; Antigravity requires the Antigravity app running).
- Optional: Settings → Providers → Codex → OpenAI cookies (Automatic or Manual) to add dashboard extras.

## Providers

- [Codex](docs/codex.md) — Local Codex CLI RPC (+ PTY fallback) and optional OpenAI web dashboard extras.
- [Claude](docs/claude.md) — OAuth API or browser cookies (+ CLI PTY fallback); session + weekly usage.
- [Cursor](docs/cursor.md) — Browser session cookies for plan + usage + billing resets.
- [Gemini](docs/gemini.md) — OAuth-backed quota API using Gemini CLI credentials (no browser cookies).
- [Gemini / Jules](docs/gemini.md) — OAuth-backed quota API using CLI credentials (no browser cookies).
- [Antigravity](docs/antigravity.md) — Local language server probe (experimental); no external auth.
- [Droid](docs/factory.md) — Browser cookies + WorkOS token flows for Factory usage + billing.
- [Copilot](docs/copilot.md) — GitHub device flow + Copilot internal usage API.
Expand Down Expand Up @@ -75,6 +75,7 @@ Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it re
- **Keychain access (prompted by macOS)**:
- Chrome cookie import needs the “Chrome Safe Storage” key to decrypt cookies.
- Claude OAuth credentials (written by the Claude CLI) are read from Keychain when present.
- Gemini/Jules CLI credentials are read from Keychain or local config.
- z.ai API token is stored in Keychain from Preferences → Providers; Copilot stores its API token in Keychain during device flow.
- **How do I prevent those keychain alerts?**
- Open **Keychain Access.app** → login keychain → search the item (e.g., “Claude Code-credentials”).
Expand All @@ -86,7 +87,7 @@ Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it re
- Find the browser’s “Safe Storage” key (e.g., “Chrome Safe Storage”, “Brave Safe Storage”, “Firefox”, “Microsoft Edge Safe Storage”).
- Open the item → **Access Control** → add `CodexBar.app` under “Always allow access by these applications”.
- This removes the prompt when CodexBar decrypts cookies for that browser.
- **Files & Folders prompts (folder/volume access)**: CodexBar launches provider CLIs (codex/claude/gemini/antigravity). If those CLIs read a project directory or external drive, macOS may ask CodexBar for that folder/volume (e.g., Desktop or an external volume). This is driven by the CLI’s working directory, not background disk scanning.
- **Files & Folders prompts (folder/volume access)**: CodexBar launches provider CLIs (codex/claude/gemini/jules/antigravity). If those CLIs read a project directory or external drive, macOS may ask CodexBar for that folder/volume (e.g., Desktop or an external volume). This is driven by the CLI’s working directory, not background disk scanning.
- **What we do not request**: no Screen Recording, Accessibility, or Automation permissions; no passwords are stored (browser cookies are reused when you opt in).

## Docs
Expand Down
41 changes: 41 additions & 0 deletions Sources/CodexBar/Providers/Jules/JulesProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct JulesProviderImplementation: ProviderImplementation {
let id: UsageProvider = .jules
let supportsLoginFlow: Bool = true

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { context in
let versionText = context.store.version(for: context.provider) ?? "not detected"
let email = context.store.snapshot(for: .jules)?.identity?.accountEmail ?? ""
let plan = context.store.snapshot(for: .jules)?.identity?.loginMethod ?? ""

var detail = "\(context.metadata.cliName) \(versionText)"
if !email.isEmpty {
detail += " • \(email)"
}
if !plan.isEmpty {
detail += " (\(plan))"
}
return detail
}
}

@MainActor
func runLoginFlow(context: ProviderLoginContext) async -> Bool {
// Run `jules login` in Terminal.
// We can't easily automate this since it's an interactive login,
// so we just show an alert or instructions.
let alert = NSAlert()
alert.messageText = "Jules Login"
alert.informativeText = "To use Jules, you must be logged in via the CLI. Run 'jules login' in your terminal."
alert.addButton(withTitle: "OK")
alert.runModal()
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum ProviderImplementationRegistry {
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
case .jules: JulesProviderImplementation()
}
}

Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-jules.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ final class UsageStore {
codexBinary: nil,
claudeBinary: nil,
geminiBinary: nil,
julesBinary: nil,
effectivePATH: PathBuilder.effectivePATH(purposes: [.rpc, .tty, .nodeTooling]),
loginShellPATH: LoginShellPathCache.shared.current?.joined(separator: ":"))
Task { @MainActor [weak self] in
Expand Down Expand Up @@ -1181,6 +1182,7 @@ extension UsageStore {
.kimi: "Kimi debug log not yet implemented",
.kimik2: "Kimi K2 debug log not yet implemented",
.jetbrains: "JetBrains AI debug log not yet implemented",
.jules: "Jules debug log not yet implemented",
]
let text: String
switch provider {
Expand Down Expand Up @@ -1243,7 +1245,7 @@ extension UsageStore {
let hasAny = resolution != nil
let source = resolution?.source.rawValue ?? "none"
text = "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .factory, .copilot, .vertexai, .kiro, .kimi, .kimik2, .jetbrains:
case .gemini, .antigravity, .opencode, .factory, .copilot, .vertexai, .kiro, .kimi, .kimik2, .jetbrains, .jules:
text = unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ struct TokenAccountCLIContext {
return self.makeSnapshot(
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(
ideBasePath: nil))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp:
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp, .jules:
return nil
}
}
Expand Down
25 changes: 25 additions & 0 deletions Sources/CodexBarCore/PathEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,55 @@ public struct PathDebugSnapshot: Equatable, Sendable {
public let codexBinary: String?
public let claudeBinary: String?
public let geminiBinary: String?
public let julesBinary: String?
public let effectivePATH: String
public let loginShellPATH: String?

public static let empty = PathDebugSnapshot(
codexBinary: nil,
claudeBinary: nil,
geminiBinary: nil,
julesBinary: nil,
effectivePATH: "",
loginShellPATH: nil)

public init(
codexBinary: String?,
claudeBinary: String?,
geminiBinary: String? = nil,
julesBinary: String? = nil,
effectivePATH: String,
loginShellPATH: String?)
{
self.codexBinary = codexBinary
self.claudeBinary = claudeBinary
self.geminiBinary = geminiBinary
self.julesBinary = julesBinary
self.effectivePATH = effectivePATH
self.loginShellPATH = loginShellPATH
}
}

public enum BinaryLocator {
public static func resolveJulesBinary(
env: [String: String] = ProcessInfo.processInfo.environment,
loginPATH: [String]? = LoginShellPathCache.shared.current,
commandV: (String, String?, TimeInterval, FileManager) -> String? = ShellCommandLocator.commandV,
aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = ShellCommandLocator
.resolveAlias,
fileManager: FileManager = .default,
home: String = NSHomeDirectory()) -> String?
{
self.resolveBinary(
name: "jules",
overrideKey: "JULES_CLI_PATH",
env: env,
loginPATH: loginPATH,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fileManager,
home: home)
}
public static func resolveClaudeBinary(
env: [String: String] = ProcessInfo.processInfo.environment,
loginPATH: [String]? = LoginShellPathCache.shared.current,
Expand Down Expand Up @@ -367,11 +390,13 @@ public enum PathBuilder {
let codex = BinaryLocator.resolveCodexBinary(env: env, loginPATH: login, home: home)
let claude = BinaryLocator.resolveClaudeBinary(env: env, loginPATH: login, home: home)
let gemini = BinaryLocator.resolveGeminiBinary(env: env, loginPATH: login, home: home)
let jules = BinaryLocator.resolveJulesBinary(env: env, loginPATH: login, home: home)
let loginString = login?.joined(separator: ":")
return PathDebugSnapshot(
codexBinary: codex,
claudeBinary: claude,
geminiBinary: gemini,
julesBinary: jules,
effectivePATH: effective,
loginShellPATH: loginString)
}
Expand Down
41 changes: 41 additions & 0 deletions Sources/CodexBarCore/Providers/Jules/JulesProviderDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import CodexBarMacroSupport
import Foundation

@ProviderDescriptorRegistration
@ProviderDescriptorDefinition
public enum JulesProviderDescriptor {
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .jules,
metadata: ProviderMetadata(
id: .jules,
displayName: "Jules",
sessionLabel: "Sessions",
weeklyLabel: "Daily",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Jules usage",
cliName: "jules",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: "https://jules.google.com",
statusPageURL: nil,
statusLinkURL: nil),
branding: ProviderBranding(
iconStyle: .gemini,
iconResourceName: "ProviderIcon-jules",
color: ProviderColor(red: 66 / 255, green: 133 / 255, blue: 244 / 255)), // Google Blue
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Jules cost summary is not supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .cli],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [JulesFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "jules",
versionDetector: { _ in ProviderVersionDetector.genericVersion(command: "jules", argument: "--version") }))
}
}
Loading