From 479e12846e85882d2c934caf8cae22376b2717dc Mon Sep 17 00:00:00 2001 From: jrimmer Date: Thu, 25 Jun 2026 14:02:03 -0700 Subject: [PATCH 01/13] Add Neuralwatt provider --- README.md | 29 +- Sources/CodexBar/MenuCardView.swift | 6 +- Sources/CodexBar/MenuDescriptor.swift | 4 +- .../NeuralWattProviderImplementation.swift | 43 ++ .../NeuralWatt/NeuralWattSettingsStore.swift | 14 + .../ProviderImplementationRegistry.swift | 1 + .../Resources/ProviderIcon-neuralwatt.svg | 3 + Sources/CodexBar/UsageStore.swift | 5 +- .../Config/ProviderConfigEnvironment.swift | 2 + .../CodexBarCore/Logging/LogCategories.swift | 1 + .../NeuralWattProviderDescriptor.swift | 50 ++ .../NeuralWatt/NeuralWattSettingsReader.swift | 61 +++ .../NeuralWatt/NeuralWattUsageFetcher.swift | 437 ++++++++++++++++++ .../Providers/ProviderDescriptor.swift | 1 + .../Providers/ProviderTokenResolver.swift | 12 + .../CodexBarCore/Providers/Providers.swift | 2 + .../TokenAccountSupportCatalog+Data.swift | 7 + .../Vendored/CostUsage/CostUsageScanner.swift | 4 +- .../CodexBarWidgetProvider.swift | 1 + .../CodexBarWidget/CodexBarWidgetViews.swift | 3 + .../MenuCardNeuralWattTests.swift | 55 +++ .../NeuralWattUsageFetcherTests.swift | 373 +++++++++++++++ .../ProviderConfigEnvironmentTests.swift | 13 + .../ProviderEnvironmentResolverTests.swift | 12 + docs/configuration.md | 12 +- docs/neuralwatt.md | 79 ++++ docs/providers.md | 59 +++ 27 files changed, 1276 insertions(+), 13 deletions(-) create mode 100644 Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift create mode 100644 Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift create mode 100644 Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg create mode 100644 Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift create mode 100644 Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift create mode 100644 Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift create mode 100644 Tests/CodexBarTests/MenuCardNeuralWattTests.swift create mode 100644 Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift create mode 100644 docs/neuralwatt.md diff --git a/README.md b/README.md index ebfebf8de2..b550162f3c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CodexBar 🎚️ — May your tokens never run out. +# CodexBar 🎚️ — May your tokens never run out > Every AI coding limit, in your menu bar. @@ -25,36 +25,47 @@ Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and ## Install ### Requirements + - macOS 14+ (Sonoma) ### GitHub Releases + Download: ### Homebrew + ```bash brew install --cask codexbar ``` ### CLI Tarballs (macOS/Linux) + Homebrew formula (Linux today): + ```bash brew install steipete/tap/codexbar ``` + Arch Linux AUR package: + ```bash yay -S codexbar-cli ``` + Or download release tarballs from GitHub Releases: + - macOS: `CodexBarCLI-v-macos-arm64.tar.gz`, `CodexBarCLI-v-macos-x86_64.tar.gz` - Linux (glibc): `CodexBarCLI-v-linux-aarch64.tar.gz`, `CodexBarCLI-v-linux-x86_64.tar.gz` - Linux (static musl): `CodexBarCLI-v-linux-musl-aarch64.tar.gz`, `CodexBarCLI-v-linux-musl-x86_64.tar.gz` ### First run + - Open Settings → Providers and enable what you use. - Install/sign in to the provider sources you rely on: CLIs, browser sessions, OAuth/device flow, API keys, local app files, or provider apps depending on the provider. - Optional: Settings → Providers → Codex → OpenAI cookies (Automatic or Manual) to add dashboard extras. ### Set API keys from the CLI + Provider toggles and API keys live in the resolved CodexBar config file. New installs use `~/.config/codexbar/config.json`; existing `~/.codexbar/config.json` installs still load from the legacy path. You can script the same provider list that Settings → Providers uses: @@ -135,14 +146,17 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow. - [Deepgram](docs/deepgram.md) — API key usage summaries across speech, agent, token, and TTS metrics. - [Poe](docs/poe.md) — API key for current point balance and recent points history. - [Chutes](docs/chutes.md) — API key for subscription usage, rolling and monthly quota windows, and pay-as-you-go quotas. +- [Neuralwatt](docs/neuralwatt.md) — API key for USD credit balance and optional per-key spending allowance from Neuralwatt Cloud. - [ZenMux](docs/zenmux.md) — Management API key for rolling five-hour and seven-day quota windows plus PAYG balance. - Open to new providers: [provider authoring guide](docs/provider.md). ## Icon & Screenshot + The menu bar icon is a tiny usage meter. Bar meaning is provider-specific, and errors/stale data can dim the icon or show an incident indicator. ## Features + - Multi-provider menu bar with per-provider toggles (Settings → Providers). - Provider-specific usage meters with reset countdowns. - Optional Codex web dashboard enrichments (code review remaining, usage breakdown, credits history). @@ -159,9 +173,11 @@ show an incident indicator. - Privacy-first: on-device parsing by default; browser cookies are opt-in and reused (no passwords stored). ## Privacy note + Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it reads a small set of known locations (browser cookies/local storage, provider config files, local JSONL logs) when the related features are enabled. Provider tokens and token-account settings live in the CodexBar config file with restrictive file permissions. See the discussion and audit notes in [issue #12](https://github.com/steipete/CodexBar/issues/12). ## macOS permissions (why they’re needed) + - **Full Disk Access (optional)**: only required to read Safari cookies/local storage for web-based providers. If you don’t grant it, use another supported browser, manual cookies/API keys, OAuth, or CLI/local sources where that provider supports them. - **Keychain access (prompted by macOS)**: - Chromium cookie import needs the browser “Safe Storage” key to decrypt cookies. @@ -183,6 +199,7 @@ Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it re - **What we do not request in the background**: no Screen Recording or Accessibility permissions; user-triggered helper actions may ask macOS for Automation permission to open Terminal. No passwords are stored (browser cookies are reused when you opt in). ## Docs + - Providers overview: [docs/providers.md](docs/providers.md) - Provider authoring: [docs/provider.md](docs/provider.md) - Issue labeling guide: [docs/ISSUE_LABELING.md](docs/ISSUE_LABELING.md) @@ -202,12 +219,14 @@ Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it re - Changelog: [CHANGELOG.md](CHANGELOG.md) ## Getting started (dev) + - Clone the repo and open it in Xcode or run the scripts directly. - Launch once, then toggle providers in Settings → Providers. - Install/sign in to provider sources you rely on (CLIs, browser cookies, OAuth/device flow, API keys, or local app/config files). - Optional: set OpenAI cookies (Automatic or Manual) for Codex dashboard extras. ## Build from source + Requires macOS 14+ and Swift 6.2+. ```bash @@ -216,6 +235,7 @@ open CodexBar.app ``` Dev loop: + ```bash ./Scripts/compile_and_run.sh ./Scripts/compile_and_run.sh --test # also run the sharded test suite before packaging/relaunching @@ -224,20 +244,24 @@ make docs-list # list docs with frontmatter summaries ``` CLI install: + ```bash # after installing CodexBar.app in /Applications ./bin/install-codexbar-cli.sh ``` ## Related + - ✂️ [Trimmy](https://github.com/steipete/Trimmy) — “Paste once, run once.” Flatten multi-line shell snippets so they paste and run. - 🧳 [MCPorter](https://mcporter.dev) — TypeScript toolkit + CLI for Model Context Protocol servers. - 🧿 [oracle](https://askoracle.dev) — Ask the oracle when you're stuck. Invoke GPT-5 Pro with a custom context and files. ## Looking for a Windows version? + - [Win-CodexBar](https://github.com/Finesssee/Win-CodexBar) ## Linux desktop integration? + - [codexbar-waybar](https://github.com/Marouan-chak/codexbar-waybar) — Waybar custom module + GTK4 popover for Hyprland / Sway / other Wayland compositors, built on top of the bundled Linux CLI. - [Codexbar GNOME](https://extensions.gnome.org/extension/9841/codexbar/) — GNOME Shell extension that brings CodexBar usage into the desktop panel. - [codexbar-cinnamon-applet](https://github.com/jacobcalvert/codexbar-cinnamon-applet) — Linux Mint Cinnamon panel applet powered by CodexBar's JSON output. @@ -246,10 +270,13 @@ CLI install: - [codexbar-plasmoid](https://github.com/psimaker/codexbar-plasmoid) — KDE Plasma 6 widget for CodexBar's meter icon, provider switcher, quota windows, pace, credits, local cost, and status, powered by the bundled Linux CLI. ## Status bar & terminal integration + - [showy-quota](https://github.com/enieuwy/showy-quota) — always-on AI plan quota strips for SketchyBar, tmux, and Zellij (standalone WASM plugin), built on `codexbar serve` / the bundled CLI. ## Credits + Inspired by [ccusage](https://github.com/ryoppippi/ccusage) (MIT), specifically the cost usage tracking. ## License + MIT • Peter Steinberger ([steipete](https://twitter.com/steipete)) diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index c564fd34df..51a4fb3dc3 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -1280,7 +1280,7 @@ extension UsageMenuCardView.Model { { primaryDetailLeft = detail } - if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .litellm].contains(input.provider), + if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .neuralwatt, .litellm].contains(input.provider), let detail = primary.resetDescription, !detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { @@ -1309,7 +1309,7 @@ extension UsageMenuCardView.Model { primaryResetText = nil } } - if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .litellm, .zenmux].contains(input.provider), + if [.warp, .kilo, .mimo, .deepseek, .qoder, .mistral, .neuralwatt, .litellm, .zenmux].contains(input.provider), primary.resetsAt == nil { primaryResetText = nil @@ -1377,7 +1377,7 @@ extension UsageMenuCardView.Model { primaryPacePercent = regen.pace.pacePercent primaryPaceOnTop = regen.pace.paceOnTop } - let usesBalanceStatusText = input.provider == .deepseek + let usesBalanceStatusText = input.provider == .deepseek || input.provider == .neuralwatt let primaryStatusText = usesBalanceStatusText ? primaryDetailText : nil if usesBalanceStatusText { primaryDetailText = nil diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift index b08fe8bc90..dbd6af541c 100644 --- a/Sources/CodexBar/MenuDescriptor.swift +++ b/Sources/CodexBar/MenuDescriptor.swift @@ -228,8 +228,8 @@ struct MenuDescriptor { if let primary = snap.primary { let primaryDetail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines) let primaryDescriptionIsDetail = provider == .warp || provider == .kilo || provider == .abacus || - provider == .deepseek || provider == .azureopenai || provider == .mimo || provider == .qoder || - provider == .sub2api + provider == .deepseek || provider == .neuralwatt || provider == .azureopenai || provider == .mimo || + provider == .qoder || provider == .sub2api let primaryWindow = if primaryDescriptionIsDetail { // Some providers use resetDescription for non-reset detail // (e.g., "Unlimited", "X/Y credits"). Avoid rendering it as a "Resets ..." line. diff --git a/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift new file mode 100644 index 0000000000..cd67345e2b --- /dev/null +++ b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift @@ -0,0 +1,43 @@ +import CodexBarCore +import Foundation + +struct NeuralWattProviderImplementation: ProviderImplementation { + let id: UsageProvider = .neuralwatt + + @MainActor + func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { + ProviderPresentation { _ in "api" } + } + + @MainActor + func observeSettings(_ settings: SettingsStore) { + _ = settings.neuralWattAPIKey + } + + @MainActor + func isAvailable(context: ProviderAvailabilityContext) -> Bool { + if NeuralWattSettingsReader.apiKey(environment: context.environment) != nil { + return true + } + if !context.settings.neuralWattAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return true + } + return !context.settings.tokenAccounts(for: .neuralwatt).isEmpty + } + + @MainActor + func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] { + [ + ProviderSettingsFieldDescriptor( + id: "neuralwatt-api-key", + title: "API key", + subtitle: "Stored in the CodexBar config file. Get your key from portal.neuralwatt.com/dashboard/api-keys.", + kind: .secure, + placeholder: "sk-...", + binding: context.stringBinding(\.neuralWattAPIKey), + actions: [], + isVisible: nil, + onActivate: nil), + ] + } +} diff --git a/Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift new file mode 100644 index 0000000000..c5aa7a1625 --- /dev/null +++ b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattSettingsStore.swift @@ -0,0 +1,14 @@ +import CodexBarCore +import Foundation + +extension SettingsStore { + var neuralWattAPIKey: String { + get { self.configSnapshot.providerConfig(for: .neuralwatt)?.sanitizedAPIKey ?? "" } + set { + self.updateProviderConfig(provider: .neuralwatt) { entry in + entry.apiKey = self.normalizedConfigValue(newValue) + } + self.logSecretUpdate(provider: .neuralwatt, field: "apiKey", value: newValue) + } + } +} diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift index 3a62b34e91..29f72879c5 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift @@ -68,6 +68,7 @@ enum ProviderImplementationRegistry { case .deepgram: DeepgramProviderImplementation() case .poe: PoeProviderImplementation() case .chutes: ChutesProviderImplementation() + case .neuralwatt: NeuralWattProviderImplementation() case .crossmodel: CrossModelProviderImplementation() case .clawrouter: ClawRouterProviderImplementation() case .sub2api: Sub2APIProviderImplementation() diff --git a/Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg b/Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg new file mode 100644 index 0000000000..cf43777aca --- /dev/null +++ b/Sources/CodexBar/Resources/ProviderIcon-neuralwatt.svg @@ -0,0 +1,3 @@ + + + diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index efc58a5fe1..1e9690a4ce 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -1154,9 +1154,8 @@ extension UsageStore { case .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .factory, .copilot, .devin, .vertexai, .kilo, .kiro, .kimi, .kimik2, .moonshot, .jetbrains, .perplexity, .mimo, .doubao, .sakana, .abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .qoder, - .stepfun, - .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes, - .clawrouter, .wayfinder, .sub2api, .zenmux: + .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes, + .neuralwatt, .clawrouter, .wayfinder, .sub2api, .zenmux: return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented" } } diff --git a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift index f8fda2b523..75d51fe41e 100644 --- a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift +++ b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift @@ -149,6 +149,8 @@ public enum ProviderConfigEnvironment { OpenRouterSettingsReader.envKey case .elevenlabs: ElevenLabsSettingsReader.apiKeyEnvironmentKey + case .neuralwatt: + NeuralWattSettingsReader.apiKeyEnvironmentKey case .moonshot: MoonshotSettingsReader.apiKeyEnvironmentKeys.first case .kimi: diff --git a/Sources/CodexBarCore/Logging/LogCategories.swift b/Sources/CodexBarCore/Logging/LogCategories.swift index 547df9120e..06718dc06b 100644 --- a/Sources/CodexBarCore/Logging/LogCategories.swift +++ b/Sources/CodexBarCore/Logging/LogCategories.swift @@ -57,6 +57,7 @@ public enum LogCategories { public static let minimaxUsage = "minimax-usage" public static let minimaxWeb = "minimax-web" public static let moonshotUsage = "moonshot-usage" + public static let neuralWattUsage = "neuralwatt-usage" public static let notifications = "notifications" public static let openAIWeb = "openai-web" public static let openAIWebview = "openai-webview" diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift new file mode 100644 index 0000000000..28b9020967 --- /dev/null +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift @@ -0,0 +1,50 @@ +import Foundation + +public enum NeuralWattProviderDescriptor { + public static let descriptor: ProviderDescriptor = Self.makeDescriptor() + + static func makeDescriptor() -> ProviderDescriptor { + ProviderDescriptor( + id: .neuralwatt, + metadata: ProviderMetadata( + id: .neuralwatt, + displayName: "Neuralwatt", + sessionLabel: "Credits", + weeklyLabel: "Spend", + opusLabel: nil, + supportsOpus: false, + supportsCredits: true, + creditsHint: "Energy-based USD credit balance.", + toggleTitle: "Show Neuralwatt usage", + cliName: "neuralwatt", + defaultEnabled: false, + isPrimaryProvider: false, + usesAccountFallback: false, + browserCookieOrder: nil, + dashboardURL: "https://portal.neuralwatt.com/dashboard", + subscriptionDashboardURL: "https://portal.neuralwatt.com/dashboard", + changelogURL: "https://portal.neuralwatt.com/docs/changelog", + statusPageURL: nil, + statusLinkURL: "https://portal.neuralwatt.com/status"), + branding: ProviderBranding( + iconStyle: .neuralwatt, + iconResourceName: "ProviderIcon-neuralwatt", + color: ProviderColor(red: 0.22, green: 0.85, blue: 0.55)), + tokenCost: ProviderTokenCostConfig( + supportsTokenCost: false, + noDataMessage: { "Neuralwatt token cost history is not available via the quota API." }), + fetchPlan: .apiToken( + strategyID: "neuralwatt.api", + resolveToken: { ProviderTokenResolver.neuralWattToken(environment: $0) }, + missingCredentialsError: { NeuralWattUsageError.missingCredentials }, + loadUsage: { apiKey, context in + try await NeuralWattUsageFetcher.fetchUsage( + apiKey: apiKey, + environment: context.env).toUsageSnapshot() + }), + cli: ProviderCLIConfig( + name: "neuralwatt", + aliases: ["nw", "neural"], + versionDetector: nil)) + } +} diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift new file mode 100644 index 0000000000..cea42dab70 --- /dev/null +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattSettingsReader.swift @@ -0,0 +1,61 @@ +import Foundation + +public enum NeuralWattSettingsReader { + public static let apiKeyEnvironmentKey = "NEURALWATT_API_KEY" + public static let apiKeyEnvironmentKeys = [ + Self.apiKeyEnvironmentKey, + ] + public static let apiURLEnvironmentKey = "NEURALWATT_API_URL" + + public static func apiKey(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { + for key in self.apiKeyEnvironmentKeys { + guard let token = self.cleaned(environment[key]) else { continue } + return token + } + return nil + } + + public static func apiURL(environment: [String: String] = ProcessInfo.processInfo.environment) -> URL { + if let override = self.validAPIURL(environment: environment) { + return override + } + return URL(string: "https://api.neuralwatt.com")! + } + + public static func validateEndpointOverrides( + environment: [String: String] = ProcessInfo.processInfo.environment) throws + { + guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return } + guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) == nil else { return } + throw NeuralWattSettingsError.invalidEndpointOverride(self.apiURLEnvironmentKey) + } + + static func cleaned(_ raw: String?) -> String? { + guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { + return nil + } + if (value.hasPrefix("\"") && value.hasSuffix("\"")) || + (value.hasPrefix("'") && value.hasSuffix("'")) + { + value = String(value.dropFirst().dropLast()) + } + value = value.trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } + + private static func validAPIURL(environment: [String: String]) -> URL? { + guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return nil } + return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) + } +} + +public enum NeuralWattSettingsError: LocalizedError, Sendable, Equatable { + case invalidEndpointOverride(String) + + public var errorDescription: String? { + switch self { + case let .invalidEndpointOverride(key): + "Neuralwatt endpoint override \(key) must use HTTPS or a bare host." + } + } +} diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift new file mode 100644 index 0000000000..2b6167c7f8 --- /dev/null +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift @@ -0,0 +1,437 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +// MARK: - Response models + +public struct NeuralWattBalance: Codable, Sendable, Equatable { + public let creditsRemainingUSD: Double? + public let totalCreditsUSD: Double? + public let creditsUsedUSD: Double? + public let accountingMethod: String? + + private enum CodingKeys: String, CodingKey { + case creditsRemainingUSD = "credits_remaining_usd" + case totalCreditsUSD = "total_credits_usd" + case creditsUsedUSD = "credits_used_usd" + case accountingMethod = "accounting_method" + } +} + +public struct NeuralWattUsagePeriod: Codable, Sendable, Equatable { + public let costUSD: Double? + public let requests: Int? + public let tokens: Int? + public let energyKWh: Double? + + private enum CodingKeys: String, CodingKey { + case costUSD = "cost_usd" + case requests + case tokens + case energyKWh = "energy_kwh" + } +} + +public struct NeuralWattUsage: Codable, Sendable, Equatable { + public let lifetime: NeuralWattUsagePeriod? + public let currentMonth: NeuralWattUsagePeriod? + + private enum CodingKeys: String, CodingKey { + case lifetime + case currentMonth = "current_month" + } +} + +public struct NeuralWattLimits: Codable, Sendable, Equatable { + public let overageLimitUSD: Double? + public let rateLimitTier: String? + + private enum CodingKeys: String, CodingKey { + case overageLimitUSD = "overage_limit_usd" + case rateLimitTier = "rate_limit_tier" + } +} + +public struct NeuralWattSubscription: Codable, Sendable, Equatable { + public let plan: String? + public let status: String? + public let billingInterval: String? + public let currentPeriodStart: Date? + public let currentPeriodEnd: Date? + public let autoRenew: Bool? + public let kwhIncluded: Double? + public let kwhUsed: Double? + public let kwhRemaining: Double? + public let inOverage: Bool? + + private enum CodingKeys: String, CodingKey { + case plan + case status + case billingInterval = "billing_interval" + case currentPeriodStart = "current_period_start" + case currentPeriodEnd = "current_period_end" + case autoRenew = "auto_renew" + case kwhIncluded = "kwh_included" + case kwhUsed = "kwh_used" + case kwhRemaining = "kwh_remaining" + case inOverage = "in_overage" + } +} + +public struct NeuralWattKeyAllowance: Codable, Sendable, Equatable { + public let limitUSD: Double? + public let period: String? + public let spentUSD: Double? + public let remainingUSD: Double? + public let blocked: Bool? + + private enum CodingKeys: String, CodingKey { + case limitUSD = "limit_usd" + case period + case spentUSD = "spent_usd" + case remainingUSD = "remaining_usd" + case blocked + } +} + +public struct NeuralWattKey: Codable, Sendable, Equatable { + public let name: String? + public let allowance: NeuralWattKeyAllowance? +} + +public struct NeuralWattQuotaResponse: Decodable, Sendable { + public let snapshotAt: String? + public let balance: NeuralWattBalance? + public let usage: NeuralWattUsage? + public let limits: NeuralWattLimits? + public let subscription: NeuralWattSubscription? + public let key: NeuralWattKey? + + private enum CodingKeys: String, CodingKey { + case snapshotAt = "snapshot_at" + case balance + case usage + case limits + case subscription + case key + } + + private init( + snapshotAt: String?, + balance: NeuralWattBalance?, + usage: NeuralWattUsage?, + limits: NeuralWattLimits?, + subscription: NeuralWattSubscription?, + key: NeuralWattKey?) + { + self.snapshotAt = snapshotAt + self.balance = balance + self.usage = usage + self.limits = limits + self.subscription = subscription + self.key = key + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.snapshotAt = try container.decodeIfPresent(String.self, forKey: .snapshotAt) + self.balance = try container.decodeIfPresent(NeuralWattBalance.self, forKey: .balance) + self.usage = try container.decodeIfPresent(NeuralWattUsage.self, forKey: .usage) + self.limits = try container.decodeIfPresent(NeuralWattLimits.self, forKey: .limits) + // `subscription` is documented as always-present: object when active, `null` otherwise. + self.subscription = try container.decodeIfPresent(NeuralWattSubscription.self, forKey: .subscription) + self.key = try container.decodeIfPresent(NeuralWattKey.self, forKey: .key) + } +} + +// MARK: - Snapshot + +public struct NeuralWattUsageSnapshot: Codable, Sendable, Equatable { + public let creditsRemainingUSD: Double? + public let totalCreditsUSD: Double? + public let creditsUsedUSD: Double? + public let accountingMethod: String? + public let currentMonthCostUSD: Double? + public let currentMonthEnergyKWh: Double? + public let subscription: NeuralWattSubscription? + public let keyAllowance: NeuralWattKeyAllowance? + public let rateLimitTier: String? + public let updatedAt: Date + + public init( + creditsRemainingUSD: Double?, + totalCreditsUSD: Double?, + creditsUsedUSD: Double?, + accountingMethod: String?, + currentMonthCostUSD: Double?, + currentMonthEnergyKWh: Double?, + subscription: NeuralWattSubscription?, + keyAllowance: NeuralWattKeyAllowance?, + rateLimitTier: String?, + updatedAt: Date) + { + self.creditsRemainingUSD = creditsRemainingUSD + self.totalCreditsUSD = totalCreditsUSD + self.creditsUsedUSD = creditsUsedUSD + self.accountingMethod = accountingMethod + self.currentMonthCostUSD = currentMonthCostUSD + self.currentMonthEnergyKWh = currentMonthEnergyKWh + self.subscription = subscription + self.keyAllowance = keyAllowance + self.rateLimitTier = rateLimitTier + self.updatedAt = updatedAt + } + + public var creditUsedPercent: Double { + if self.hasKnownZeroRemainingBalance { + return 100 + } + guard let used = self.effectiveUsedCredits, let total = self.effectiveTotalCredits, total > 0 else { + return 0 + } + return min(100, max(0, used / total * 100)) + } + + private var hasKnownZeroRemainingBalance: Bool { + Self.validNonNegative(self.creditsRemainingUSD) == 0 && self.effectiveTotalCredits == nil + } + + public var effectiveRemainingCredits: Double? { + if let remaining = Self.validNonNegative(self.creditsRemainingUSD) { return remaining } + guard let total = self.effectiveTotalCredits, let used = self.effectiveUsedCredits else { return nil } + return max(0, total - used) + } + + public var effectiveTotalCredits: Double? { + if let total = Self.validPositive(self.totalCreditsUSD) { return total } + guard let remaining = Self.validNonNegative(self.creditsRemainingUSD), + let used = Self.validNonNegative(self.creditsUsedUSD) + else { return nil } + let total = remaining + used + return total > 0 ? total : nil + } + + public var effectiveUsedCredits: Double? { + if let used = Self.validNonNegative(self.creditsUsedUSD) { return used } + guard let total = Self.validPositive(self.totalCreditsUSD), + let remaining = Self.validNonNegative(self.creditsRemainingUSD) + else { return nil } + return max(0, total - remaining) + } + + public var keyAllowanceUsedPercent: Double? { + guard let spent = self.keyAllowance?.spentUSD, let limit = self.keyAllowance?.limitUSD, limit > 0 else { + return nil + } + return min(100, max(0, spent / limit * 100)) + } + + public func toUsageSnapshot() -> UsageSnapshot { + // Neuralwatt is a credit-exhaustion model (like DeepSeek): USD credits deplete + // as you use the API and do not reset on a billing cycle. There is no renewal + // date to surface, so the primary window carries only the balance summary. + let primary = RateWindow( + usedPercent: self.creditUsedPercent, + windowMinutes: nil, + resetsAt: nil, + resetDescription: self.creditSummary) + + var extras: [NamedRateWindow] = [] + if let percent = self.keyAllowanceUsedPercent, let allowance = self.keyAllowance { + let periodTitle = (allowance.period ?? "allowance").capitalized + extras.append(NamedRateWindow( + id: "key-allowance", + title: "Key \(periodTitle)", + window: RateWindow( + usedPercent: percent, + windowMinutes: nil, + resetsAt: nil, + resetDescription: nil))) + } + + let identity = ProviderIdentitySnapshot( + providerID: .neuralwatt, + accountEmail: nil, + accountOrganization: nil, + loginMethod: self.displayLoginMethod) + + return UsageSnapshot( + primary: primary, + secondary: nil, + tertiary: nil, + extraRateWindows: extras.isEmpty ? nil : extras, + subscriptionRenewsAt: nil, + updatedAt: self.updatedAt, + identity: identity) + } + + private var creditSummary: String { + guard let remaining = self.effectiveRemainingCredits else { return "Balance unavailable" } + guard let total = self.effectiveTotalCredits else { + return "\(Self.formatUSD(remaining)) remaining" + } + return "\(Self.formatUSD(remaining)) remaining of \(Self.formatUSD(total))" + } + + private var displayLoginMethod: String? { + // Credits are account-wide; surface the accounting method (Token vs Energy) + // when present. Subscription plan is shown only as supplementary identity. + if let method = self.accountingMethod, !method.isEmpty { + return method.capitalized + } + return self.subscription?.plan?.replacingOccurrences(of: "_", with: " ").capitalized + } + + fileprivate static func validNonNegative(_ value: Double?) -> Double? { + guard let value, value.isFinite, value >= 0 else { return nil } + return value + } + + fileprivate static func validPositive(_ value: Double?) -> Double? { + guard let value, value.isFinite, value > 0 else { return nil } + return value + } + + private static func formatUSD(_ value: Double) -> String { + let formatter = NumberFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.numberStyle = .currency + formatter.currencyCode = "USD" + formatter.maximumFractionDigits = 2 + formatter.minimumFractionDigits = 2 + return formatter.string(from: NSNumber(value: value)) ?? String(format: "$%.2f", value) + } +} + +// MARK: - Errors + +public enum NeuralWattUsageError: LocalizedError, Sendable { + case missingCredentials + case networkError(String) + case apiError(String) + case parseFailed(String) + + public var errorDescription: String? { + switch self { + case .missingCredentials: + "Missing Neuralwatt API key. Set apiKey in the CodexBar config file or NEURALWATT_API_KEY." + case let .networkError(message): + "Neuralwatt network error: \(message)" + case let .apiError(message): + "Neuralwatt API error: \(message)" + case let .parseFailed(message): + "Failed to parse Neuralwatt response: \(message)" + } + } +} + +// MARK: - Fetcher + +public struct NeuralWattUsageFetcher: Sendable { + private static let log = CodexBarLog.logger(LogCategories.neuralWattUsage) + private static let timeoutSeconds: TimeInterval = 15 + + public static func fetchUsage( + apiKey: String, + environment: [String: String] = ProcessInfo.processInfo.environment) async throws -> NeuralWattUsageSnapshot + { + let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { + throw NeuralWattUsageError.missingCredentials + } + try NeuralWattSettingsReader.validateEndpointOverrides(environment: environment) + + let url = Self.quotaURL(baseURL: NeuralWattSettingsReader.apiURL(environment: environment)) + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("Bearer \(trimmed)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.timeoutInterval = Self.timeoutSeconds + + let response: ProviderHTTPResponse + do { + response = try await ProviderHTTPClient.shared.response(for: request) + } catch { + throw NeuralWattUsageError.networkError(error.localizedDescription) + } + + switch response.statusCode { + case 200: + return try Self.parseSnapshot(data: response.data, updatedAt: Date()) + case 401, 403: + throw NeuralWattUsageError.missingCredentials + default: + Self.log.error("Neuralwatt API returned \(response.statusCode)") + throw NeuralWattUsageError.apiError("HTTP \(response.statusCode)") + } + } + + static func _parseSnapshotForTesting(_ data: Data, updatedAt: Date) throws -> NeuralWattUsageSnapshot { + try self.parseSnapshot(data: data, updatedAt: updatedAt) + } + + private static func parseSnapshot(data: Data, updatedAt: Date) throws -> NeuralWattUsageSnapshot { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .custom(Self.decodeISO8601Date) + let decoded: NeuralWattQuotaResponse + do { + decoded = try decoder.decode(NeuralWattQuotaResponse.self, from: data) + } catch { + throw NeuralWattUsageError.parseFailed(error.localizedDescription) + } + + guard let balance = decoded.balance else { + throw NeuralWattUsageError.parseFailed("Missing Neuralwatt balance object") + } + guard NeuralWattUsageSnapshot.validNonNegative(balance.creditsRemainingUSD) != nil || + NeuralWattUsageSnapshot.validNonNegative(balance.creditsUsedUSD) != nil || + NeuralWattUsageSnapshot.validPositive(balance.totalCreditsUSD) != nil + else { + throw NeuralWattUsageError.parseFailed("Missing Neuralwatt credit balance fields") + } + + return NeuralWattUsageSnapshot( + creditsRemainingUSD: balance.creditsRemainingUSD, + totalCreditsUSD: balance.totalCreditsUSD, + creditsUsedUSD: balance.creditsUsedUSD, + accountingMethod: balance.accountingMethod, + currentMonthCostUSD: decoded.usage?.currentMonth?.costUSD, + currentMonthEnergyKWh: decoded.usage?.currentMonth?.energyKWh, + subscription: decoded.subscription, + keyAllowance: decoded.key?.allowance, + rateLimitTier: decoded.limits?.rateLimitTier, + updatedAt: updatedAt) + } + + private static func decodeISO8601Date(from decoder: Decoder) throws -> Date { + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + let standardFormatter = ISO8601DateFormatter() + standardFormatter.formatOptions = [.withInternetDateTime] + if let date = standardFormatter.date(from: value) { + return date + } + + let fractionalFormatter = ISO8601DateFormatter() + fractionalFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + if let date = fractionalFormatter.date(from: value) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid ISO8601 date: \(value)") + } + + private static func quotaURL(baseURL: URL) -> URL { + var url = baseURL + let pathComponents = url.path.split(separator: "/") + if pathComponents.last == "v1" { + url.append(path: "quota") + } else { + url.append(path: "v1/quota") + } + return url + } +} diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift index 9f16b60743..72681c4595 100644 --- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift @@ -108,6 +108,7 @@ public enum ProviderDescriptorRegistry { .deepgram: DeepgramProviderDescriptor.descriptor, .poe: PoeProviderDescriptor.descriptor, .chutes: ChutesProviderDescriptor.descriptor, + .neuralwatt: NeuralWattProviderDescriptor.descriptor, .crossmodel: CrossModelProviderDescriptor.descriptor, .clawrouter: ClawRouterProviderDescriptor.descriptor, .sub2api: Sub2APIProviderDescriptor.descriptor, diff --git a/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift b/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift index b0f7ce6a04..731b72b115 100644 --- a/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift +++ b/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift @@ -109,6 +109,12 @@ public enum ProviderTokenResolver { self.elevenLabsResolution(environment: environment)?.token } + public static func neuralWattToken( + environment: [String: String] = ProcessInfo.processInfo.environment) -> String? + { + self.neuralWattResolution(environment: environment)?.token + } + public static func groqToken(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { self.groqResolution(environment: environment)?.token } @@ -362,6 +368,12 @@ public enum ProviderTokenResolver { self.resolveEnv(ElevenLabsSettingsReader.apiKey(environment: environment)) } + public static func neuralWattResolution( + environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution? + { + self.resolveEnv(NeuralWattSettingsReader.apiKey(environment: environment)) + } + public static func groqResolution( environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution? { diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift index bef804bfc0..6ca3202abc 100644 --- a/Sources/CodexBarCore/Providers/Providers.swift +++ b/Sources/CodexBarCore/Providers/Providers.swift @@ -58,6 +58,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable { case deepgram case poe case chutes + case neuralwatt case crossmodel case clawrouter case sub2api @@ -121,6 +122,7 @@ public enum IconStyle: String, Sendable, CaseIterable { case deepgram case poe case chutes + case neuralwatt case crossmodel case clawrouter case sub2api diff --git a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift index 4b8854592e..d8e5fcadac 100644 --- a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift +++ b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift @@ -136,6 +136,13 @@ extension TokenAccountSupportCatalog { injection: .environment(key: ElevenLabsSettingsReader.apiKeyEnvironmentKey), requiresManualCookieSource: false, cookieName: nil), + .neuralwatt: TokenAccountSupport( + title: "API keys", + subtitle: "Store multiple Neuralwatt API keys.", + placeholder: "sk-...", + injection: .environment(key: NeuralWattSettingsReader.apiKeyEnvironmentKey), + requiresManualCookieSource: false, + cookieName: nil), .groq: TokenAccountSupport( title: "API keys", subtitle: "Store multiple Groq API keys.", diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index de337ed815..2e360cf894 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -825,8 +825,8 @@ enum CostUsageScanner { .copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .kimik2, .moonshot, .augment, .jetbrains, .amp, .ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana, .abacus, .mistral, .deepseek, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .qoder, .stepfun, - .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .crossmodel, .clawrouter, - .sub2api, .wayfinder, .zenmux: + .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .neuralwatt, .crossmodel, + .clawrouter, .sub2api, .wayfinder, .zenmux: return emptyReport } } diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift index 52ae23da1f..f52c056031 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift @@ -125,6 +125,7 @@ enum ProviderChoice: String, AppEnum { case .poe: return nil // Poe not yet supported in widgets case .chutes: return nil // Chutes not yet supported in widgets case .zed: return nil // Zed not yet supported in widgets + case .neuralwatt: return nil // Neuralwatt not yet supported in widgets case .zenmux: return nil // ZenMux not yet supported in widgets } } diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 4457269111..d1a6e541e1 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -346,6 +346,7 @@ private struct ProviderSwitchChip: View { case .poe: "Poe" case .chutes: "Chutes" case .zed: "Zed" + case .neuralwatt: "Neuralwatt" case .zenmux: "ZenMux" } } @@ -1084,6 +1085,8 @@ enum WidgetColors { Color(red: 24 / 255, green: 160 / 255, blue: 88 / 255) case .zed: Color(red: 64 / 255, green: 156 / 255, blue: 255 / 255) + case .neuralwatt: + Color(red: 56 / 255, green: 217 / 255, blue: 140 / 255) case .zenmux: Color(red: 108 / 255, green: 92 / 255, blue: 231 / 255) } diff --git a/Tests/CodexBarTests/MenuCardNeuralWattTests.swift b/Tests/CodexBarTests/MenuCardNeuralWattTests.swift new file mode 100644 index 0000000000..4458eb1439 --- /dev/null +++ b/Tests/CodexBarTests/MenuCardNeuralWattTests.swift @@ -0,0 +1,55 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +struct MenuCardNeuralWattTests { + @Test + func `model shows credit balance as status text without reset wording`() throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let snapshot = NeuralWattUsageSnapshot( + creditsRemainingUSD: 51.00, + totalCreditsUSD: 77.04, + creditsUsedUSD: 26.04, + accountingMethod: "energy", + currentMonthCostUSD: 12.34, + currentMonthEnergyKWh: 0.25, + subscription: nil, + keyAllowance: nil, + rateLimitTier: "standard", + updatedAt: now) + .toUsageSnapshot() + let metadata = try #require(ProviderDefaults.metadata[.neuralwatt]) + + let model = UsageMenuCardView.Model.make(.init( + provider: .neuralwatt, + metadata: metadata, + snapshot: snapshot, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: nil, plan: nil), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + + let primary = try #require(model.metrics.first) + #expect(primary.title == "Credits") + let statusText = primary.statusText?.replacingOccurrences(of: "\u{00A0}", with: "") + #expect(statusText == "$51.00 remaining of $77.04") + #expect(primary.detailText == nil) + #expect(primary.resetText == nil) + #expect(model.metrics.contains { $0.title == "This month" } == false) + #expect(model.metrics.allSatisfy { metric in + metric.resetText?.localizedCaseInsensitiveContains("reset") != true + }) + } +} diff --git a/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift b/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift new file mode 100644 index 0000000000..786b1e3c97 --- /dev/null +++ b/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift @@ -0,0 +1,373 @@ +import Foundation +import Testing +@testable import CodexBarCore + +@Suite(.serialized) +struct NeuralWattUsageFetcherTests { + @Test + func `parses quota response into usage snapshot`() throws { + let body = #""" + { + "snapshot_at": "2026-04-16T18:30:00Z", + "balance": { + "credits_remaining_usd": 32.6774, + "total_credits_usd": 52.34, + "credits_used_usd": 19.6626, + "accounting_method": "energy" + }, + "usage": { + "lifetime": { + "cost_usd": 243.9145, + "requests": 37801, + "tokens": 1235477176, + "energy_kwh": 15.6009 + }, + "current_month": { + "cost_usd": 160.1463, + "requests": 23902, + "tokens": 1116658995, + "energy_kwh": 9.7278 + } + }, + "limits": { + "overage_limit_usd": null, + "rate_limit_tier": "standard" + }, + "subscription": { + "plan": "standard", + "status": "active", + "billing_interval": "month", + "current_period_start": "2026-04-11T05:05:25Z", + "current_period_end": "2026-05-11T05:05:25Z", + "auto_renew": true, + "kwh_included": 20.0, + "kwh_used": 13.9023, + "kwh_remaining": 6.0977, + "in_overage": false + }, + "key": { + "name": "my-production-key", + "allowance": { + "limit_usd": 50.0, + "period": "monthly", + "spent_usd": 12.5, + "remaining_usd": 37.5, + "blocked": false + } + } + } + """# + + let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting( + Data(body.utf8), + updatedAt: Date(timeIntervalSince1970: 1)) + let usage = snapshot.toUsageSnapshot() + + #expect(snapshot.totalCreditsUSD == 52.34) + #expect(snapshot.creditsUsedUSD == 19.6626) + let expectedCreditPercent = 19.6626 / 52.34 * 100 + #expect(abs(snapshot.creditUsedPercent - expectedCreditPercent) < 1e-6) + // Credits exhaust (DeepSeek-style); no kWh window surfaced. + #expect(snapshot.keyAllowanceUsedPercent == 25.0) + #expect(snapshot.currentMonthCostUSD == 160.1463) + let primaryPercent = usage.primary?.usedPercent + #expect(primaryPercent.map { abs($0 - expectedCreditPercent) < 1e-6 } == true) + // No reset cycle — credits deplete, they don't renew. + #expect(usage.primary?.resetsAt == nil) + #expect(usage.subscriptionRenewsAt == nil) + // loginMethod now reflects accounting method when present. + #expect(usage.loginMethod(for: .neuralwatt) == "Energy") + // Only key allowance is surfaced as an extra quota window. Current-month spend is telemetry, + // not a resettable rate window. + #expect(usage.extraRateWindows?.count == 1) + #expect(usage.extraRateWindows?.contains { $0.id == "subscription-kwh" } == false) + #expect(usage.extraRateWindows?.contains { $0.id == "current-month-spend" } == false) + let allowanceWindow = usage.extraRateWindows?.first { $0.id == "key-allowance" } + #expect(allowanceWindow?.title == "Key Monthly") + } + + @Test + func `parses response with null subscription using accounting method`() throws { + let body = #""" + { + "snapshot_at": "2026-04-16T18:30:00Z", + "balance": { + "credits_remaining_usd": 4.5, + "total_credits_usd": 5.0, + "credits_used_usd": 0.5, + "accounting_method": "energy" + }, + "usage": { + "lifetime": {"cost_usd": 0.5, "requests": 10, "tokens": 1000, "energy_kwh": 0.01}, + "current_month": {"cost_usd": 0.5, "requests": 10, "tokens": 1000, "energy_kwh": 0.01} + }, + "limits": {"overage_limit_usd": null, "rate_limit_tier": "free"}, + "subscription": null, + "key": {"name": "trial", "allowance": null} + } + """# + + let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting( + Data(body.utf8), + updatedAt: Date(timeIntervalSince1970: 100)) + let usage = snapshot.toUsageSnapshot() + + #expect(snapshot.creditUsedPercent == 10) + #expect(snapshot.subscription == nil) + #expect(snapshot.keyAllowanceUsedPercent == nil) + #expect(usage.primary?.usedPercent == 10) + #expect(usage.subscriptionRenewsAt == nil) + #expect(usage.loginMethod(for: .neuralwatt) == "Energy") + // No resettable extra quota windows when there is no per-key allowance. + #expect(usage.extraRateWindows == nil) + } + + @Test + func `parses response with missing credits used derived from remaining`() throws { + let body = #""" + { + "balance": { + "credits_remaining_usd": 30.0, + "total_credits_usd": 100.0, + "accounting_method": "energy" + }, + "usage": {"lifetime": {}, "current_month": {}}, + "limits": {}, + "subscription": null, + "key": {"name": "x", "allowance": null} + } + """# + + let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting( + Data(body.utf8), + updatedAt: Date(timeIntervalSince1970: 2)) + // credits_used_usd missing but derived as 100 - 30 = 70. + #expect(snapshot.effectiveUsedCredits == 70) + #expect(snapshot.creditUsedPercent == 70) + } + + @Test + func `marks known zero credit balance as exhausted`() throws { + let body = #""" + { + "balance": { + "credits_remaining_usd": 0.0, + "total_credits_usd": 0.0, + "accounting_method": "energy" + }, + "usage": {"lifetime": {}, "current_month": {}}, + "limits": {}, + "subscription": null, + "key": {"name": "x", "allowance": null} + } + """# + + let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting( + Data(body.utf8), + updatedAt: Date(timeIntervalSince1970: 2)) + let usage = snapshot.toUsageSnapshot() + + #expect(snapshot.effectiveRemainingCredits == 0) + #expect(snapshot.effectiveTotalCredits == nil) + #expect(snapshot.creditUsedPercent == 100) + #expect(usage.primary?.usedPercent == 100) + let resetDescription = usage.primary?.resetDescription?.replacingOccurrences(of: "\u{00A0}", with: "") + #expect(resetDescription == "$0.00 remaining") + } + + @Test + func `parses fractional subscription dates`() throws { + let body = #""" + { + "balance": { + "credits_remaining_usd": 8.0, + "total_credits_usd": 10.0, + "credits_used_usd": 2.0, + "accounting_method": "energy" + }, + "usage": {"lifetime": {}, "current_month": {}}, + "limits": {}, + "subscription": { + "plan": "standard", + "status": "active", + "current_period_start": "2026-04-11T05:05:25.123Z", + "current_period_end": "2026-05-11T05:05:25.456Z" + }, + "key": {"name": "x", "allowance": null} + } + """# + + let snapshot = try NeuralWattUsageFetcher._parseSnapshotForTesting( + Data(body.utf8), + updatedAt: Date(timeIntervalSince1970: 3)) + + #expect(snapshot.subscription?.currentPeriodEnd != nil) + #expect(snapshot.creditUsedPercent == 20) + } + + @Test + func `rejects malformed successful response without balance`() throws { + let body = #"{"error":"temporarily unavailable"}"# + + do { + _ = try NeuralWattUsageFetcher._parseSnapshotForTesting( + Data(body.utf8), + updatedAt: Date(timeIntervalSince1970: 4)) + Issue.record("Expected NeuralWattUsageError.parseFailed") + } catch let error as NeuralWattUsageError { + guard case let .parseFailed(message) = error else { + Issue.record("Expected parseFailed, got \(error)") + return + } + #expect(message.contains("balance")) + } + } + + @Test + func `fetch usage rejects blank API key before request`() async throws { + do { + _ = try await NeuralWattUsageFetcher.fetchUsage( + apiKey: " ", + environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"]) + Issue.record("Expected NeuralWattUsageError.missingCredentials") + } catch let error as NeuralWattUsageError { + guard case .missingCredentials = error else { + Issue.record("Expected missingCredentials, got \(error)") + return + } + } + } + + @Test + func `unauthorized fetch throws missing credentials`() async throws { + let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self) + defer { + if registered { + URLProtocol.unregisterClass(NeuralWattStubURLProtocol.self) + } + NeuralWattStubURLProtocol.handler = nil + } + + NeuralWattStubURLProtocol.handler = { request in + guard let url = request.url else { throw URLError(.badURL) } + return Self.makeResponse(url: url, body: #"{"detail":"bad key"}"#, statusCode: 401) + } + + do { + _ = try await NeuralWattUsageFetcher.fetchUsage( + apiKey: "sk-test", + environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"]) + Issue.record("Expected NeuralWattUsageError.missingCredentials") + } catch let error as NeuralWattUsageError { + guard case .missingCredentials = error else { + Issue.record("Expected missingCredentials, got \(error)") + return + } + } + } + + @Test + func `fetch usage sends bearer authorization header`() async throws { + let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self) + defer { + if registered { + URLProtocol.unregisterClass(NeuralWattStubURLProtocol.self) + } + NeuralWattStubURLProtocol.handler = nil + } + + NeuralWattStubURLProtocol.handler = { request in + guard let url = request.url else { throw URLError(.badURL) } + #expect(url.path == "/v1/quota") + #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer sk-test") + #expect(request.timeoutInterval == 15) + + let body = #""" + { + "balance": {"credits_remaining_usd": 5.0, "total_credits_usd": 10.0, + "credits_used_usd": 5.0, "accounting_method": "energy"}, + "usage": {"lifetime": {}, "current_month": {}}, + "limits": {}, "subscription": null, "key": {"name": "k", "allowance": null} + } + """# + return Self.makeResponse(url: url, body: body, statusCode: 200) + } + + let usage = try await NeuralWattUsageFetcher.fetchUsage( + apiKey: " sk-test ", + environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"]) + + #expect(usage.creditUsedPercent == 50) + } + + @Test + func `non success fetch throws generic HTTP error`() async throws { + let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self) + defer { + if registered { + URLProtocol.unregisterClass(NeuralWattStubURLProtocol.self) + } + NeuralWattStubURLProtocol.handler = nil + } + + NeuralWattStubURLProtocol.handler = { request in + guard let url = request.url else { throw URLError(.badURL) } + return Self.makeResponse(url: url, body: #"{"detail":"bad key"}"#, statusCode: 500) + } + + do { + _ = try await NeuralWattUsageFetcher.fetchUsage( + apiKey: "sk-test", + environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://api.neuralwatt.test"]) + Issue.record("Expected NeuralWattUsageError.apiError") + } catch let error as NeuralWattUsageError { + guard case let .apiError(message) = error else { + Issue.record("Expected apiError, got \(error)") + return + } + #expect(message == "HTTP 500") + } + } + + private static func makeResponse( + url: URL, + body: String, + statusCode: Int = 200) -> (HTTPURLResponse, Data) + { + let response = HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + return (response, Data(body.utf8)) + } +} + +final class NeuralWattStubURLProtocol: URLProtocol { + nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override static func canInit(with request: URLRequest) -> Bool { + request.url?.host == "api.neuralwatt.test" + } + + override static func canonicalRequest(for request: URLRequest) -> URLRequest { + request + } + + override func startLoading() { + guard let handler = Self.handler else { + self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse)) + return + } + do { + let (response, data) = try handler(self.request) + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + self.client?.urlProtocol(self, didLoad: data) + self.client?.urlProtocolDidFinishLoading(self) + } catch { + self.client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() {} +} diff --git a/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift b/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift index 77b4302d01..c8900661e4 100644 --- a/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift +++ b/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift @@ -312,6 +312,19 @@ struct ProviderConfigEnvironmentTests { #expect(ProviderTokenResolver.elevenLabsToken(environment: env) == "xi-token") } + @Test + func `applies API key override for NeuralWatt`() { + let config = ProviderConfig(id: .neuralwatt, apiKey: "sk-neuralwatt-config") + let env = ProviderConfigEnvironment.applyAPIKeyOverride( + base: [:], + provider: .neuralwatt, + config: config) + + #expect(env[NeuralWattSettingsReader.apiKeyEnvironmentKey] == "sk-neuralwatt-config") + #expect(ProviderTokenResolver.neuralWattToken(environment: env) == "sk-neuralwatt-config") + #expect(ProviderConfigEnvironment.supportsAPIKeyOverride(for: .neuralwatt)) + } + @Test func `applies API key override for groq`() { let config = ProviderConfig(id: .groq, apiKey: "gsk-token") diff --git a/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift b/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift index 52bb2fd5cd..604257aea4 100644 --- a/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift +++ b/Tests/CodexBarTests/ProviderEnvironmentResolverTests.swift @@ -15,6 +15,18 @@ struct ProviderEnvironmentResolverTests { #expect(environment[ZaiSettingsReader.apiTokenKey] == "account-token") } + @Test + func `NeuralWatt selected API account overrides saved and ambient credentials`() { + let account = Self.account(token: "sk-neuralwatt-account") + let environment = ProviderEnvironmentResolver.resolve( + base: [NeuralWattSettingsReader.apiKeyEnvironmentKey: "ambient-token"], + provider: .neuralwatt, + config: ProviderConfig(id: .neuralwatt, apiKey: "saved-token"), + selectedAccount: account) + + #expect(environment[NeuralWattSettingsReader.apiKeyEnvironmentKey] == "sk-neuralwatt-account") + } + @Test func `OpenAI account removes project scoping from saved config`() { let account = Self.account(token: "sk-admin-account") diff --git a/docs/configuration.md b/docs/configuration.md index c4e58b2a36..7ee7b003d0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,6 +12,7 @@ CodexBar reads a single JSON config file for CLI and app provider settings. API keys, manual cookie headers, source selection, ordering, and token accounts live here. Keychain is still used for runtime cookie caches, browser Safe Storage access, and provider OAuth/device-flow credentials where those flows require it. ## Location + - `CODEXBAR_CONFIG=/path/to/config.json` when set. - `$XDG_CONFIG_HOME/codexbar/config.json` when `XDG_CONFIG_HOME` is set to an absolute path. Relative values are ignored. @@ -21,6 +22,7 @@ API keys, manual cookie headers, source selection, ordering, and token accounts - Permissions are set to `0600` whenever CodexBar writes the file on macOS and Linux. ## Root shape + ```json { "version": 1, @@ -42,6 +44,7 @@ API keys, manual cookie headers, source selection, ordering, and token accounts ``` ## Provider fields + All provider fields are optional unless noted. - `id` (required): provider identifier. @@ -62,6 +65,7 @@ All provider fields are optional unless noted. - `tokenAccounts`: multi-account tokens for providers in `TokenAccountSupportCatalog`. ## Manual cookies + Use manual cookies when automatic browser import is unavailable, disabled, or too noisy for your setup. The app and CLI both read the same resolved config file, so a manual cookie saved in the UI is also used by `codexbar`, and a cookie written by tooling is shown in the app after reload. @@ -71,7 +75,7 @@ export. In browser DevTools, open the Network tab, select a request for the prov header named `Cookie`. You can paste either the full `Cookie: name=value; other=value` string or just `name=value; other=value`. -If you have a Netscape export, convert each non-comment row to `name=value` and join values with `; `. Do not paste +If you have a Netscape export, convert each non-comment row to `name=value` and join values with `;`. Do not paste the raw `# Netscape HTTP Cookie File` text into `cookieHeader`. Example placeholder config: @@ -176,6 +180,7 @@ Manual cookies are secrets. Keep the CodexBar config file private, leave its per and never paste real cookie values or readable DevTools screenshots into public issues. ### tokenAccounts + ```json { "version": 1, @@ -195,13 +200,16 @@ and never paste real cookie values or readable DevTools screenshots into public z.ai team accounts also use `usageScope`, `organizationId`, and `workspaceID`; see [z.ai](zai.md). ## Provider IDs + Current IDs (see `Sources/CodexBarCore/Providers/Providers.swift`): -`codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `crossmodel`, `clawrouter`, `sub2api`, `wayfinder`, `zenmux`. +`codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `neuralwatt`, `crossmodel`, `clawrouter`, `sub2api`, `wayfinder`, `zenmux`. ## Ordering + The order of `providers` controls display/order in the app and CLI. Reorder the array to change ordering. ## Notes + - Fields not relevant to a provider are ignored. - Omitted providers are appended with defaults during normalization. - Keep the file private; it contains secrets. diff --git a/docs/neuralwatt.md b/docs/neuralwatt.md new file mode 100644 index 0000000000..a811fce323 --- /dev/null +++ b/docs/neuralwatt.md @@ -0,0 +1,79 @@ +--- +summary: "Neuralwatt provider notes: API key setup and quota usage fields." +read_when: + - Adding or modifying the Neuralwatt provider + - Debugging Neuralwatt API keys or quota parsing + - Adjusting Neuralwatt credit labels +--- + +# Neuralwatt Provider + +The Neuralwatt provider reads account quota from the Neuralwatt Cloud API using an API key. +Neuralwatt Cloud is an OpenAI-compatible inference API with energy-based pricing. Credits are a +Deplete-as-you-go USD balance (like DeepSeek): they do **not** reset on a billing cycle, they +simply exhaust as the API is used and are refilled by topping up. The quota endpoint exposes +the USD credit balance, current-month spend fields, and optional per-key spending allowances. + +## Features + +- USD credit balance as the primary usage window (`credits_remaining_usd` / `total_credits_usd`). + The bar fills as credits are consumed; there is **no reset date** since credits deplete, not renew. +- Per-key spending allowance (`spent_usd` / `limit_usd`) as an extra rate window when configured. +- Accounting method (`Token` vs `Energy`) shown as the provider identity label. +- Current calendar-month spend is parsed for future/reporting use, but is not shown as a resettable quota window. + +## Setup + +### CLI + +Store the API key without opening Settings: + +```bash +printf '%s' "$NEURALWATT_API_KEY" | codexbar config set-api-key --provider neuralwatt --stdin +``` + +This trims the piped key, writes it to CodexBar's config file (`~/.config/codexbar/config.json` +by default, or the legacy `~/.codexbar/config.json` when already present), and enables Neuralwatt by +default. Use `--no-enable` to save the key without enabling the provider. + +### Settings + +1. Open **Settings → Providers** +2. Enable **Neuralwatt** +3. Open `https://portal.neuralwatt.com/dashboard/api-keys` +4. Create or copy an API key +5. Paste the key into CodexBar's Neuralwatt provider settings + +### Environment Variables + +CodexBar also accepts these environment variables: + +- `NEURALWATT_API_KEY` + +For tests or self-hosted/proxy setups, override the API base URL with `NEURALWATT_API_URL`. + +## How It Works + +- Endpoint: `GET https://api.neuralwatt.com/v1/quota` +- Auth header: `Authorization: Bearer sk-...` +- Fields used: `balance.credits_remaining_usd`, `balance.total_credits_usd`, + `balance.credits_used_usd`, `balance.accounting_method`, + `usage.current_month.cost_usd`, + `key.allowance.limit_usd`, `key.allowance.spent_usd`, `key.allowance.period` +- `credits_used_usd` is derived as `total_credits_usd − credits_remaining_usd` when the API omits it. +- `subscription` may be `null`; subscription periods are not rendered as resets because Neuralwatt + credits deplete until topped up. + +## Troubleshooting + +### "Missing Neuralwatt API key" + +Set the key with `codexbar config set-api-key --provider neuralwatt --stdin`, add it in +**Settings → Providers → Neuralwatt**, set `NEURALWATT_API_KEY`, or configure a Neuralwatt token +account in CodexBar. + +### "Neuralwatt API error" + +Confirm the API key is valid and that the current network can reach `api.neuralwatt.com`. The +quota endpoint is rate-limited to 1 request per second per customer; CodexBar refreshes on its +normal cycle so this should not be hit in practice. diff --git a/docs/providers.md b/docs/providers.md index 82730b24b3..6827b3ddab 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -12,6 +12,7 @@ CodexBar currently registers 60 provider IDs. Some companies expose multiple sur OpenCode vs OpenCode Go, because the auth source and quota shape differ. ## Fetch strategies (current) + Legend: web (browser cookies/WebView), cli (RPC/PTy or provider CLI), oauth (provider OAuth), api token, local probe, web dashboard. Source labels (CLI/header): `openai-web`, `web`, `oauth`, `api`, `local`, `cli`, plus provider-specific CLI labels (e.g. `codex-cli`, `claude`). @@ -76,10 +77,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` | LiteLLM | API key + base URL → `/key/info`, then `/user/info` or `/team/info` budget usage (`api`). | | Deepgram | API key → project discovery and usage breakdown API (`api`). | | Chutes | API key from config/env → subscription usage and quota API (`api`). | +| Neuralwatt | API key from config/env → `/v1/quota` credit balance and spend (`api`). | | ZenMux | Management API key from config/env → five-hour and seven-day quota windows plus PAYG balance (`api`). | | Zed | Zed editor Keychain session → `cloud.zed.dev/client/users/me` for plan and quota data (`local`). | ## Codex + - App Auto: OAuth API first; falls back to CLI only when OAuth credentials are missing or auth/refresh is invalid. - Web dashboard (optional, off by default): `https://chatgpt.com/codex/settings/usage` via WebView + browser cookies. - Battery saver toggle (currently off by default): reduces routine OpenAI web refreshes but still allows explicit manual refreshes. @@ -90,12 +93,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/codex.md`. ## OpenAI + - API key from `~/.codexbar/config.json`, `OPENAI_ADMIN_KEY`, or `OPENAI_API_KEY`. - Admin API keys are preferred and fetch organization costs plus completion usage for inline Today/7d/configured-window dashboards. - Normal API keys fall back to the legacy credit-grants balance endpoint when organization usage is unavailable. - Details: `docs/openai.md`. ## Azure OpenAI + - API key, endpoint, and deployment from `~/.codexbar/config.json` or `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, and `AZURE_OPENAI_DEPLOYMENT_NAME`. - `AZURE_OPENAI_ENDPOINT` and configured endpoint overrides must be HTTPS URLs or bare hosts normalized to HTTPS; explicit `http://` URLs, user info, and encoded host-delimiter tricks fail closed before `api-key` headers are attached. - Validates the configured deployment with a minimal chat-completions request; it does not expose Azure spend or quota history. @@ -104,6 +109,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/azure-openai.md`. ## Claude + - Admin API: `sk-ant-admin...` key in Settings/config, token accounts, or `ANTHROPIC_ADMIN_KEY`. - 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`). @@ -115,6 +121,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/claude.md`. ## z.ai + - API token from `~/.codexbar/config.json` (`providers[].apiKey`) or `Z_AI_API_KEY` env var. - Supports global and BigModel CN quota hosts; override with `Z_AI_API_HOST` or `Z_AI_QUOTA_URL`. - z.ai endpoint overrides must be HTTPS or bare hosts normalized to HTTPS. `Z_AI_QUOTA_URL` takes precedence for @@ -123,6 +130,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/zai.md`. ## Devin + - Automatic auth reads the current `auth1_session` token and organization metadata from Chrome localStorage. - Manual auth accepts the `Authorization: Bearer ...` value from an app.devin.ai request. - Usage endpoint: `GET /api//billing/quota/usage`. @@ -130,6 +138,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/devin.md`. ## Manus + - Session token via browser `session_id` cookie, manual Settings entry, `MANUS_SESSION_TOKEN`, or `MANUS_COOKIE`. - Credits endpoint: `POST https://api.manus.im/user.v1.UserService/GetAvailableCredits`. - Auto mode prefers cached/browser cookies before env fallback; manual mode accepts either a bare `session_id` value or a full Cookie header. @@ -137,6 +146,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/manus.md`. ## MiniMax + - Coding Plan API token or web session from configured/manual/browser sources. - Supports global and China mainland hosts via provider region settings and environment overrides. - Web-session billing history can render 30-day token charts plus top model/method breakdowns when MiniMax exposes it. @@ -144,6 +154,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/minimax.md`. ## Kimi + - Kimi Code API key via `~/.codexbar/config.json` or `KIMI_CODE_API_KEY`. - Web fallback uses the JWT from `kimi-auth` cookie via manual entry or `KIMI_AUTH_TOKEN` env var. - Shows weekly quota and 5-hour rate limit (300 minutes). @@ -151,6 +162,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kimi.md`. ## Kilo + - API token from `~/.codexbar/config.json` (`providers[].apiKey`) or `KILO_API_KEY`. - Auto mode tries API first and falls back to CLI auth when API credentials are missing or unauthorized. - CLI auth source: `~/.local/share/kilo/auth.json` (`kilo.access`), typically created by `kilo login`. @@ -158,6 +170,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kilo.md`. ## Kimi K2 (unofficial) + - API key via `~/.codexbar/config.json` or `KIMI_K2_API_KEY`/`KIMI_API_KEY` env var. - Shows credit usage from the legacy `kimi-k2.ai` consumed/remaining totals. - Use Moonshot / Kimi API for the official Kimi API account and billing surface. @@ -165,6 +178,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kimi-k2.md`. ## Gemini + - OAuth-backed quota API (`retrieveUserQuota`) using Gemini CLI credentials. - Token refresh via Google OAuth if expired. - Tier detection via `loadCodeAssist`. @@ -172,12 +186,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/gemini.md`. ## Antigravity + - Local Antigravity language server (internal protocol, HTTPS on localhost). - `GetUserStatus` primary; `GetCommandModelConfigs` fallback. - Status: Google Workspace incidents (Gemini product). - Details: `docs/antigravity.md`. ## Cursor + - Web API via browser cookies (`cursor.com` + `cursor.sh`). - Fallbacks: a legacy stored session, then Cursor.app local auth. - Add Account and Switch Account open Cursor's authenticator in a supported browser; Switch Account prefers stable account IDs and falls back to normalized email when IDs are unavailable. CodexBar uses the supported system HTTPS handler when possible and otherwise asks the user to choose an eligible supported browser. @@ -185,11 +201,13 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/cursor.md`. ## OpenCode + - Web dashboard via browser cookies (`opencode.ai`). - Status: none yet. - Details: `docs/opencode.md`. ## OpenCode Go + - Web dashboard via browser or manual cookies (`opencode.ai`). - Auto mode falls back to local usage from `~/.local/share/opencode/opencode.db` on macOS and Linux. - Uses the workspace Go page/server data for rolling 5-hour, weekly, and optional monthly usage windows. @@ -198,6 +216,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/opencode.md`. ## Alibaba Coding Plan + - Web mode uses Alibaba console RPC with form payload + `sec_token`. - Cookie sources: browser import (`auto`) or manual header (`cookieSource: manual`). - API key fallback from Settings (`providers[].apiKey`) or `ALIBABA_CODING_PLAN_API_KEY` env var. @@ -207,6 +226,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/alibaba-coding-plan.md`. ## Alibaba Token Plan + - Web mode posts to the Bailian `GetSubscriptionSummary` endpoint with form-encoded params and optional `sec_token`. - Cookie sources: browser import (`auto`), manual Cookie header, or `ALIBABA_TOKEN_PLAN_COOKIE`. - Default quota URL: `https://bailian.console.aliyun.com/data/api.json?action=GetSubscriptionSummary&product=BssOpenAPI-V3`. @@ -222,12 +242,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/factory.md`. ## Copilot + - GitHub device flow OAuth token + `api.github.com/copilot_internal/user`. - Supports multiple token accounts and account switching from provider settings/menu surfaces. - Status: Statuspage.io (GitHub). - Details: `docs/copilot.md`. ## Kiro + - CLI-based: runs `kiro-cli chat --no-interactive "/usage"` with 10s timeout. - Parses ANSI output for plan name, monthly credits percentage, and bonus credits. - Requires `kiro-cli` installed and logged in via AWS Builder ID. @@ -235,6 +257,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kiro.md`. ## Warp + - API token from Settings or `WARP_API_KEY` / `WARP_TOKEN` env var. - Shows monthly credits usage and next refresh time. - Status: none yet. @@ -247,6 +270,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/windsurf.md`. ## ElevenLabs + - API key from Settings, token accounts, `ELEVENLABS_API_KEY`, or `XI_API_KEY`. - Reads `GET /v1/user/subscription` from `api.elevenlabs.io`. - Shows character credit usage, reset timing, and voice slot usage when available. @@ -255,6 +279,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/elevenlabs.md`. ## Vertex AI + - OAuth credentials from `gcloud auth application-default login` (ADC). - Quota usage via Cloud Monitoring `consumer_quota` metrics for `aiplatform.googleapis.com`. - Token cost: uses the Claude local-log scanner filtered to Vertex AI-tagged entries. @@ -262,6 +287,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/vertexai.md`. ## JetBrains AI + - Local XML quota file from IDE configuration directory. - Auto-detects installed JetBrains IDEs; uses most recently used. - Reads `AIAssistantQuotaManager2.xml` for monthly credits and refill date. @@ -269,12 +295,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/jetbrains.md`. ## Zed + - Reads the signed-in Zed editor session from the macOS Keychain (`credentials_url` / `https://zed.dev`). - Calls `GET https://cloud.zed.dev/client/users/me` for plan, billing cycle, Edit Predictions quota, and overdue invoice flag. - Sign in to the Zed editor first. - Details: `docs/zed.md`. ## Augment + - Auto mode tries the `auggie` CLI first. - Web fallback uses browser cookies, with manual cookie header support. - Tracks credit usage and account/subscription data where available. @@ -282,6 +310,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/augment.md`. ## Amp + - Auto mode tries the local `amp usage` command first. - API mode calls Amp's balance endpoint with an access token. - Web fallback reads the legacy settings page with browser cookies. @@ -290,6 +319,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/amp.md`. ## T3 Chat + - Web tRPC endpoint (`https://t3.chat/api/trpc/getCustomerData`) via browser cookies. - Parses JSONL response lines and extracts customer data from the embedded tRPC payload. - Shows the 4-hour Base bucket and monthly Overage bucket documented in the T3 Chat FAQ. @@ -297,12 +327,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/t3chat.md`. ## Ollama + - Web settings page (`https://ollama.com/settings`) via browser cookies. - Parses Cloud Usage plan badge, session/weekly usage, and reset timestamps. - Status: none yet. - Details: `docs/ollama.md`. ## Synthetic + - API key from `~/.codexbar/config.json` (`providers[].apiKey`) or `SYNTHETIC_API_KEY`. - The menu card shows rolling five-hour, weekly token, and search-hourly quota lanes when present. The compact menu bar metric uses the five-hour or weekly lane; weekly credit regeneration details appear when returned. @@ -310,6 +342,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/synthetic.md`. ## OpenRouter + - API token from `~/.codexbar/config.json` (`providers[].apiKey`) or `OPENROUTER_API_KEY` env var. - Reads credits and key rate-limit info from OpenRouter APIs. - Shows daily, weekly, and monthly API-key spend when `/api/v1/key` returns those fields. @@ -325,6 +358,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/crossmodel.md`. ## Perplexity + - Browser session cookie from automatic import, manual header/token, or `PERPLEXITY_SESSION_TOKEN` / `PERPLEXITY_COOKIE`. - Tracks recurring credits, bonus/promotional credits, purchased credits, and renewal date when present. - Status: `https://status.perplexity.com/` (link only, no auto-polling). @@ -339,6 +373,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/mimo.md`. ## Doubao + - API key via `ARK_API_KEY`, `VOLCENGINE_API_KEY`, `DOUBAO_API_KEY`, or provider config. - Probes Volcengine Ark chat completions and reads request rate-limit headers when present. - Status: none yet. @@ -353,6 +388,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/sakana.md`. ## Abacus AI + - Browser cookies (`abacus.ai`, `apps.abacus.ai`) via automatic import or manual header. - Reads organization compute points and billing data. - Shows monthly credit gauge with pace tick and reserve/deficit estimate. @@ -360,6 +396,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/abacus.md`. ## Mistral + - Session cookie (`ory_session_*`) from browser auto-import or manual `Cookie:` header. - CSRF token (`csrftoken` cookie) sent as `X-CSRFTOKEN` for billing and Vibe usage requests. - Domains: `admin.mistral.ai` for API billing and credit balance, and `console.mistral.ai` for optional Vibe subscription usage. Console requests forward only `csrftoken` and `ory_session_*`; all other admin cookies stay origin-bound. @@ -372,12 +409,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/mistral.md`. ## DeepSeek + - API key via `DEEPSEEK_API_KEY` / `DEEPSEEK_KEY` env var or DeepSeek token accounts. - Shows total balance with paid vs. granted breakdown; USD preferred when multiple currencies present. - Status: `https://status.deepseek.com` (link only, no auto-polling). - Details: `docs/deepseek.md`. ## Moonshot / Kimi API + - API key via `MOONSHOT_API_KEY` / `MOONSHOT_KEY` env var or provider config. - Reads `GET /v1/users/me/balance` from the selected Moonshot region. - Region: international (`api.moonshot.ai`) or China mainland (`api.moonshot.cn`), configurable in Settings or `MOONSHOT_REGION`. @@ -386,12 +425,14 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/moonshot.md`. ## Venice + - API key via `VENICE_API_KEY` / `VENICE_KEY` env var or Venice token accounts. - Shows current DIEM or USD balance; DIEM epoch allocation progress when available. - Status: none yet. - Details: `docs/venice.md`. ## Codebuff + - API token from `~/.codexbar/config.json`, `CODEBUFF_API_KEY`, or `~/.config/manicode/credentials.json` created by `codebuff login`. - Reads usage and subscription data from Codebuff APIs. - Shows credit balance, weekly rate limit, reset timing, subscription status, and auto-top-up flag when present. @@ -400,6 +441,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/codebuff.md`. ## Crof + - API key from `~/.codexbar/config.json`, `CROF_API_KEY`, or `CROFAI_API_KEY`. - Reads `credits`, `requests_plan`, and `usable_requests` from `GET https://crof.ai/usage_api/`. - Shows request quota as the primary usage window and dollar credits as the secondary row. @@ -408,6 +450,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/crof.md`. ## Command Code + - Browser session cookies from automatic import or manual `Cookie:` header. - Linux CLI supports configured manual cookies; automatic browser import remains macOS-only. - Reads monthly USD credits and billing-cycle usage from `api.commandcode.ai`. @@ -423,6 +466,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/qoder.md`. ## Grok + - `grok agent stdio` (ACP) JSON-RPC `x.ai/billing` method; requires `grok login` (SuperGrok OAuth/OIDC). - Reads cached credentials from `~/.grok/auth.json` for identity (email, team). - Falls back to grok.com's billing gRPC-web endpoint via Chrome session cookies when the CLI does not expose billing. @@ -466,6 +510,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/sub2api.md`. ## AWS Bedrock + - AWS credentials from `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and optional `AWS_SESSION_TOKEN`. - Region from `AWS_REGION` / `AWS_DEFAULT_REGION`, defaulting to `us-east-1`. - Reads AWS Cost Explorer for Bedrock spend and can compare usage against `CODEXBAR_BEDROCK_BUDGET`. @@ -474,6 +519,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/bedrock.md`. ## Deepgram + - API key from config or `DEEPGRAM_API_KEY`. - Optional project ID from provider settings or `DEEPGRAM_PROJECT_ID`; otherwise aggregates all visible projects. - Optional API base URL override via `DEEPGRAM_API_URL`; overrides must be HTTPS or bare hosts normalized to HTTPS. @@ -481,6 +527,7 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/deepgram.md`. ## LiteLLM + - API key from config or `LITELLM_API_KEY`; base URL from config `enterpriseHost` or `LITELLM_BASE_URL`. - Reads `/key/info` first, then `/user/info?user_id=...` for user-bound keys or `/team/info?team_id=...` for team-only keys. - User-bound keys show personal budget usage as the primary window and the key's exact matching team as the secondary window. @@ -490,18 +537,30 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/litellm.md`. ## Poe + - API key from config or `POE_API_KEY`. - Reads the current point balance and recent points history from Poe's official usage API. - History failures are non-fatal; the current balance remains available. - Details: `docs/poe.md`. ## Chutes + - API key from config or `CHUTES_API_KEY`. - Reads subscription usage first, then fills missing rolling, monthly, or pay-as-you-go quota data from the quota APIs. - Uses Chutes' management API at `https://api.chutes.ai`; `CHUTES_API_URL` can override it with an HTTPS endpoint. - Details: `docs/chutes.md`. +## Neuralwatt + +- API key from config or `NEURALWATT_API_KEY`. +- Reads `GET /v1/quota` from `api.neuralwatt.com`; `NEURALWATT_API_URL` can override it with an HTTPS endpoint. +- Credit-exhaustion model (like DeepSeek): USD credits deplete as the API is used and do not reset; the primary window fills as credits are consumed. +- Extra windows: per-key spending allowance when configured. Current-month spend is parsed, but not rendered as a resettable quota window. +- Status: link only (`https://portal.neuralwatt.com/status`), no auto-polling. +- Details: `docs/neuralwatt.md`. + ## StepFun + - Username/password login or manual Oasis-Token. - Reads Step Plan 5-hour and weekly rate-limit windows from `platform.stepfun.com`. - Shows subscription plan name when the Step Plan status API returns one. From d40fbd766a25cdb3c381d9c32f5cb9d7089a9762 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 08:17:39 +0100 Subject: [PATCH 02/13] fix: harden Neuralwatt provider --- README.md | 30 +--------- .../NeuralWattProviderImplementation.swift | 2 +- .../Generated/CodexParserHash.generated.swift | 2 +- .../NeuralWattProviderDescriptor.swift | 4 +- .../NeuralWatt/NeuralWattUsageFetcher.swift | 9 ++- .../NeuralWattUsageFetcherTests.swift | 36 ++++++++++++ docs/configuration.md | 10 +--- docs/neuralwatt.md | 14 ++--- docs/providers.md | 57 +------------------ 9 files changed, 60 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index b550162f3c..d0c4c58fb8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CodexBar 🎚️ — May your tokens never run out +# CodexBar 🎚️ — May your tokens never run out. > Every AI coding limit, in your menu bar. @@ -25,47 +25,36 @@ Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and ## Install ### Requirements - - macOS 14+ (Sonoma) ### GitHub Releases - Download: ### Homebrew - ```bash brew install --cask codexbar ``` ### CLI Tarballs (macOS/Linux) - Homebrew formula (Linux today): - ```bash brew install steipete/tap/codexbar ``` - Arch Linux AUR package: - ```bash yay -S codexbar-cli ``` - Or download release tarballs from GitHub Releases: - - macOS: `CodexBarCLI-v-macos-arm64.tar.gz`, `CodexBarCLI-v-macos-x86_64.tar.gz` - Linux (glibc): `CodexBarCLI-v-linux-aarch64.tar.gz`, `CodexBarCLI-v-linux-x86_64.tar.gz` - Linux (static musl): `CodexBarCLI-v-linux-musl-aarch64.tar.gz`, `CodexBarCLI-v-linux-musl-x86_64.tar.gz` ### First run - - Open Settings → Providers and enable what you use. - Install/sign in to the provider sources you rely on: CLIs, browser sessions, OAuth/device flow, API keys, local app files, or provider apps depending on the provider. - Optional: Settings → Providers → Codex → OpenAI cookies (Automatic or Manual) to add dashboard extras. ### Set API keys from the CLI - Provider toggles and API keys live in the resolved CodexBar config file. New installs use `~/.config/codexbar/config.json`; existing `~/.codexbar/config.json` installs still load from the legacy path. You can script the same provider list that Settings → Providers uses: @@ -146,17 +135,15 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow. - [Deepgram](docs/deepgram.md) — API key usage summaries across speech, agent, token, and TTS metrics. - [Poe](docs/poe.md) — API key for current point balance and recent points history. - [Chutes](docs/chutes.md) — API key for subscription usage, rolling and monthly quota windows, and pay-as-you-go quotas. -- [Neuralwatt](docs/neuralwatt.md) — API key for USD credit balance and optional per-key spending allowance from Neuralwatt Cloud. +- [Neuralwatt](docs/neuralwatt.md) — API key for USD credit balance and optional per-key spending allowance. - [ZenMux](docs/zenmux.md) — Management API key for rolling five-hour and seven-day quota windows plus PAYG balance. - Open to new providers: [provider authoring guide](docs/provider.md). ## Icon & Screenshot - The menu bar icon is a tiny usage meter. Bar meaning is provider-specific, and errors/stale data can dim the icon or show an incident indicator. ## Features - - Multi-provider menu bar with per-provider toggles (Settings → Providers). - Provider-specific usage meters with reset countdowns. - Optional Codex web dashboard enrichments (code review remaining, usage breakdown, credits history). @@ -173,11 +160,9 @@ show an incident indicator. - Privacy-first: on-device parsing by default; browser cookies are opt-in and reused (no passwords stored). ## Privacy note - Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it reads a small set of known locations (browser cookies/local storage, provider config files, local JSONL logs) when the related features are enabled. Provider tokens and token-account settings live in the CodexBar config file with restrictive file permissions. See the discussion and audit notes in [issue #12](https://github.com/steipete/CodexBar/issues/12). ## macOS permissions (why they’re needed) - - **Full Disk Access (optional)**: only required to read Safari cookies/local storage for web-based providers. If you don’t grant it, use another supported browser, manual cookies/API keys, OAuth, or CLI/local sources where that provider supports them. - **Keychain access (prompted by macOS)**: - Chromium cookie import needs the browser “Safe Storage” key to decrypt cookies. @@ -199,7 +184,6 @@ Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it re - **What we do not request in the background**: no Screen Recording or Accessibility permissions; user-triggered helper actions may ask macOS for Automation permission to open Terminal. No passwords are stored (browser cookies are reused when you opt in). ## Docs - - Providers overview: [docs/providers.md](docs/providers.md) - Provider authoring: [docs/provider.md](docs/provider.md) - Issue labeling guide: [docs/ISSUE_LABELING.md](docs/ISSUE_LABELING.md) @@ -219,14 +203,12 @@ Wondering if CodexBar scans your disk? It doesn’t crawl your filesystem; it re - Changelog: [CHANGELOG.md](CHANGELOG.md) ## Getting started (dev) - - Clone the repo and open it in Xcode or run the scripts directly. - Launch once, then toggle providers in Settings → Providers. - Install/sign in to provider sources you rely on (CLIs, browser cookies, OAuth/device flow, API keys, or local app/config files). - Optional: set OpenAI cookies (Automatic or Manual) for Codex dashboard extras. ## Build from source - Requires macOS 14+ and Swift 6.2+. ```bash @@ -235,7 +217,6 @@ open CodexBar.app ``` Dev loop: - ```bash ./Scripts/compile_and_run.sh ./Scripts/compile_and_run.sh --test # also run the sharded test suite before packaging/relaunching @@ -244,24 +225,20 @@ make docs-list # list docs with frontmatter summaries ``` CLI install: - ```bash # after installing CodexBar.app in /Applications ./bin/install-codexbar-cli.sh ``` ## Related - - ✂️ [Trimmy](https://github.com/steipete/Trimmy) — “Paste once, run once.” Flatten multi-line shell snippets so they paste and run. - 🧳 [MCPorter](https://mcporter.dev) — TypeScript toolkit + CLI for Model Context Protocol servers. - 🧿 [oracle](https://askoracle.dev) — Ask the oracle when you're stuck. Invoke GPT-5 Pro with a custom context and files. ## Looking for a Windows version? - - [Win-CodexBar](https://github.com/Finesssee/Win-CodexBar) ## Linux desktop integration? - - [codexbar-waybar](https://github.com/Marouan-chak/codexbar-waybar) — Waybar custom module + GTK4 popover for Hyprland / Sway / other Wayland compositors, built on top of the bundled Linux CLI. - [Codexbar GNOME](https://extensions.gnome.org/extension/9841/codexbar/) — GNOME Shell extension that brings CodexBar usage into the desktop panel. - [codexbar-cinnamon-applet](https://github.com/jacobcalvert/codexbar-cinnamon-applet) — Linux Mint Cinnamon panel applet powered by CodexBar's JSON output. @@ -270,13 +247,10 @@ CLI install: - [codexbar-plasmoid](https://github.com/psimaker/codexbar-plasmoid) — KDE Plasma 6 widget for CodexBar's meter icon, provider switcher, quota windows, pace, credits, local cost, and status, powered by the bundled Linux CLI. ## Status bar & terminal integration - - [showy-quota](https://github.com/enieuwy/showy-quota) — always-on AI plan quota strips for SketchyBar, tmux, and Zellij (standalone WASM plugin), built on `codexbar serve` / the bundled CLI. ## Credits - Inspired by [ccusage](https://github.com/ryoppippi/ccusage) (MIT), specifically the cost usage tracking. ## License - MIT • Peter Steinberger ([steipete](https://twitter.com/steipete)) diff --git a/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift index cd67345e2b..54847e3f98 100644 --- a/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift +++ b/Sources/CodexBar/Providers/NeuralWatt/NeuralWattProviderImplementation.swift @@ -31,7 +31,7 @@ struct NeuralWattProviderImplementation: ProviderImplementation { ProviderSettingsFieldDescriptor( id: "neuralwatt-api-key", title: "API key", - subtitle: "Stored in the CodexBar config file. Get your key from portal.neuralwatt.com/dashboard/api-keys.", + subtitle: "Stored in the CodexBar config file. Manage keys from the Neuralwatt dashboard.", kind: .secure, placeholder: "sk-...", binding: context.stringBinding(\.neuralWattAPIKey), diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index e16a88f5cb..5150f16ebc 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "9b26fd821cf090dc" + static let value = "9c3c332510c36f63" } diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift index 28b9020967..d7e3c7a4ad 100644 --- a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift @@ -23,9 +23,9 @@ public enum NeuralWattProviderDescriptor { browserCookieOrder: nil, dashboardURL: "https://portal.neuralwatt.com/dashboard", subscriptionDashboardURL: "https://portal.neuralwatt.com/dashboard", - changelogURL: "https://portal.neuralwatt.com/docs/changelog", + changelogURL: nil, statusPageURL: nil, - statusLinkURL: "https://portal.neuralwatt.com/status"), + statusLinkURL: nil), branding: ProviderBranding( iconStyle: .neuralwatt, iconResourceName: "ProviderIcon-neuralwatt", diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift index 2b6167c7f8..33b351c290 100644 --- a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattUsageFetcher.swift @@ -334,7 +334,8 @@ public struct NeuralWattUsageFetcher: Sendable { public static func fetchUsage( apiKey: String, - environment: [String: String] = ProcessInfo.processInfo.environment) async throws -> NeuralWattUsageSnapshot + environment: [String: String] = ProcessInfo.processInfo.environment, + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> NeuralWattUsageSnapshot { let trimmed = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { @@ -351,7 +352,11 @@ public struct NeuralWattUsageFetcher: Sendable { let response: ProviderHTTPResponse do { - response = try await ProviderHTTPClient.shared.response(for: request) + response = try await transport.response(for: request) + } catch is CancellationError { + throw CancellationError() + } catch let error as URLError where error.code == .cancelled { + throw CancellationError() } catch { throw NeuralWattUsageError.networkError(error.localizedDescription) } diff --git a/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift b/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift index 786b1e3c97..e4ac56859a 100644 --- a/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift +++ b/Tests/CodexBarTests/NeuralWattUsageFetcherTests.swift @@ -238,6 +238,42 @@ struct NeuralWattUsageFetcherTests { } } + @Test + func `fetch rejects endpoint override before sending API key`() async throws { + let transport = ProviderHTTPTransportHandler { _ in + Issue.record("Endpoint override validation must happen before the request") + throw URLError(.badURL) + } + + await #expect(throws: NeuralWattSettingsError.invalidEndpointOverride( + NeuralWattSettingsReader.apiURLEnvironmentKey)) + { + _ = try await NeuralWattUsageFetcher.fetchUsage( + apiKey: "sk-test", + environment: [NeuralWattSettingsReader.apiURLEnvironmentKey: "https://user@example.com"], + transport: transport) + } + } + + @Test + func `fetch preserves transport cancellation`() async throws { + let transport = ProviderHTTPTransportHandler { _ in + throw CancellationError() + } + + do { + _ = try await NeuralWattUsageFetcher.fetchUsage( + apiKey: "sk-test", + environment: [:], + transport: transport) + Issue.record("Expected CancellationError") + } catch is CancellationError { + // Expected: refresh cancellation must not become a provider error. + } catch { + Issue.record("Expected CancellationError, got \(error)") + } + } + @Test func `unauthorized fetch throws missing credentials`() async throws { let registered = URLProtocol.registerClass(NeuralWattStubURLProtocol.self) diff --git a/docs/configuration.md b/docs/configuration.md index 7ee7b003d0..60a3ba8fb3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,7 +12,6 @@ CodexBar reads a single JSON config file for CLI and app provider settings. API keys, manual cookie headers, source selection, ordering, and token accounts live here. Keychain is still used for runtime cookie caches, browser Safe Storage access, and provider OAuth/device-flow credentials where those flows require it. ## Location - - `CODEXBAR_CONFIG=/path/to/config.json` when set. - `$XDG_CONFIG_HOME/codexbar/config.json` when `XDG_CONFIG_HOME` is set to an absolute path. Relative values are ignored. @@ -22,7 +21,6 @@ API keys, manual cookie headers, source selection, ordering, and token accounts - Permissions are set to `0600` whenever CodexBar writes the file on macOS and Linux. ## Root shape - ```json { "version": 1, @@ -44,7 +42,6 @@ API keys, manual cookie headers, source selection, ordering, and token accounts ``` ## Provider fields - All provider fields are optional unless noted. - `id` (required): provider identifier. @@ -65,7 +62,6 @@ All provider fields are optional unless noted. - `tokenAccounts`: multi-account tokens for providers in `TokenAccountSupportCatalog`. ## Manual cookies - Use manual cookies when automatic browser import is unavailable, disabled, or too noisy for your setup. The app and CLI both read the same resolved config file, so a manual cookie saved in the UI is also used by `codexbar`, and a cookie written by tooling is shown in the app after reload. @@ -75,7 +71,7 @@ export. In browser DevTools, open the Network tab, select a request for the prov header named `Cookie`. You can paste either the full `Cookie: name=value; other=value` string or just `name=value; other=value`. -If you have a Netscape export, convert each non-comment row to `name=value` and join values with `;`. Do not paste +If you have a Netscape export, convert each non-comment row to `name=value` and join values with `; `. Do not paste the raw `# Netscape HTTP Cookie File` text into `cookieHeader`. Example placeholder config: @@ -180,7 +176,6 @@ Manual cookies are secrets. Keep the CodexBar config file private, leave its per and never paste real cookie values or readable DevTools screenshots into public issues. ### tokenAccounts - ```json { "version": 1, @@ -200,16 +195,13 @@ and never paste real cookie values or readable DevTools screenshots into public z.ai team accounts also use `usageScope`, `organizationId`, and `workspaceID`; see [z.ai](zai.md). ## Provider IDs - Current IDs (see `Sources/CodexBarCore/Providers/Providers.swift`): `codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `neuralwatt`, `crossmodel`, `clawrouter`, `sub2api`, `wayfinder`, `zenmux`. ## Ordering - The order of `providers` controls display/order in the app and CLI. Reorder the array to change ordering. ## Notes - - Fields not relevant to a provider are ignored. - Omitted providers are appended with defaults during normalization. - Keep the file private; it contains secrets. diff --git a/docs/neuralwatt.md b/docs/neuralwatt.md index a811fce323..6facf998a8 100644 --- a/docs/neuralwatt.md +++ b/docs/neuralwatt.md @@ -9,10 +9,10 @@ read_when: # Neuralwatt Provider The Neuralwatt provider reads account quota from the Neuralwatt Cloud API using an API key. -Neuralwatt Cloud is an OpenAI-compatible inference API with energy-based pricing. Credits are a -Deplete-as-you-go USD balance (like DeepSeek): they do **not** reset on a billing cycle, they -simply exhaust as the API is used and are refilled by topping up. The quota endpoint exposes -the USD credit balance, current-month spend fields, and optional per-key spending allowances. +Neuralwatt Cloud is an OpenAI-compatible inference API with energy-based pricing. Prepaid credits +are a deplete-as-you-go USD balance: they do **not** reset on a billing cycle and are refilled by +topping up. Active subscription usage is billed separately against a kWh allowance. The quota +endpoint exposes both surfaces plus current-month spend and optional per-key spending allowances. ## Features @@ -40,7 +40,7 @@ default. Use `--no-enable` to save the key without enabling the provider. 1. Open **Settings → Providers** 2. Enable **Neuralwatt** -3. Open `https://portal.neuralwatt.com/dashboard/api-keys` +3. Open `https://portal.neuralwatt.com/dashboard` 4. Create or copy an API key 5. Paste the key into CodexBar's Neuralwatt provider settings @@ -61,8 +61,8 @@ For tests or self-hosted/proxy setups, override the API base URL with `NEURALWAT `usage.current_month.cost_usd`, `key.allowance.limit_usd`, `key.allowance.spent_usd`, `key.allowance.period` - `credits_used_usd` is derived as `total_credits_usd − credits_remaining_usd` when the API omits it. -- `subscription` may be `null`; subscription periods are not rendered as resets because Neuralwatt - credits deplete until topped up. +- `subscription` may be `null`. Its separate kWh allowance is not rendered yet; landing that display + requires an explicit product decision because it changes the provider from balance-only to mixed quota semantics. ## Troubleshooting diff --git a/docs/providers.md b/docs/providers.md index 6827b3ddab..c2b0888810 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -12,7 +12,6 @@ CodexBar currently registers 60 provider IDs. Some companies expose multiple sur OpenCode vs OpenCode Go, because the auth source and quota shape differ. ## Fetch strategies (current) - Legend: web (browser cookies/WebView), cli (RPC/PTy or provider CLI), oauth (provider OAuth), api token, local probe, web dashboard. Source labels (CLI/header): `openai-web`, `web`, `oauth`, `api`, `local`, `cli`, plus provider-specific CLI labels (e.g. `codex-cli`, `claude`). @@ -77,12 +76,11 @@ headers, source selection, provider ordering, and token accounts are stored in ` | LiteLLM | API key + base URL → `/key/info`, then `/user/info` or `/team/info` budget usage (`api`). | | Deepgram | API key → project discovery and usage breakdown API (`api`). | | Chutes | API key from config/env → subscription usage and quota API (`api`). | -| Neuralwatt | API key from config/env → `/v1/quota` credit balance and spend (`api`). | +| Neuralwatt | API key from config/env → `/v1/quota` credit balance and per-key allowance (`api`). | | ZenMux | Management API key from config/env → five-hour and seven-day quota windows plus PAYG balance (`api`). | | Zed | Zed editor Keychain session → `cloud.zed.dev/client/users/me` for plan and quota data (`local`). | ## Codex - - App Auto: OAuth API first; falls back to CLI only when OAuth credentials are missing or auth/refresh is invalid. - Web dashboard (optional, off by default): `https://chatgpt.com/codex/settings/usage` via WebView + browser cookies. - Battery saver toggle (currently off by default): reduces routine OpenAI web refreshes but still allows explicit manual refreshes. @@ -93,14 +91,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/codex.md`. ## OpenAI - - API key from `~/.codexbar/config.json`, `OPENAI_ADMIN_KEY`, or `OPENAI_API_KEY`. - Admin API keys are preferred and fetch organization costs plus completion usage for inline Today/7d/configured-window dashboards. - Normal API keys fall back to the legacy credit-grants balance endpoint when organization usage is unavailable. - Details: `docs/openai.md`. ## Azure OpenAI - - API key, endpoint, and deployment from `~/.codexbar/config.json` or `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, and `AZURE_OPENAI_DEPLOYMENT_NAME`. - `AZURE_OPENAI_ENDPOINT` and configured endpoint overrides must be HTTPS URLs or bare hosts normalized to HTTPS; explicit `http://` URLs, user info, and encoded host-delimiter tricks fail closed before `api-key` headers are attached. - Validates the configured deployment with a minimal chat-completions request; it does not expose Azure spend or quota history. @@ -109,7 +105,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/azure-openai.md`. ## Claude - - Admin API: `sk-ant-admin...` key in Settings/config, token accounts, or `ANTHROPIC_ADMIN_KEY`. - 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`). @@ -121,7 +116,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/claude.md`. ## z.ai - - API token from `~/.codexbar/config.json` (`providers[].apiKey`) or `Z_AI_API_KEY` env var. - Supports global and BigModel CN quota hosts; override with `Z_AI_API_HOST` or `Z_AI_QUOTA_URL`. - z.ai endpoint overrides must be HTTPS or bare hosts normalized to HTTPS. `Z_AI_QUOTA_URL` takes precedence for @@ -130,7 +124,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/zai.md`. ## Devin - - Automatic auth reads the current `auth1_session` token and organization metadata from Chrome localStorage. - Manual auth accepts the `Authorization: Bearer ...` value from an app.devin.ai request. - Usage endpoint: `GET /api//billing/quota/usage`. @@ -138,7 +131,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/devin.md`. ## Manus - - Session token via browser `session_id` cookie, manual Settings entry, `MANUS_SESSION_TOKEN`, or `MANUS_COOKIE`. - Credits endpoint: `POST https://api.manus.im/user.v1.UserService/GetAvailableCredits`. - Auto mode prefers cached/browser cookies before env fallback; manual mode accepts either a bare `session_id` value or a full Cookie header. @@ -146,7 +138,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/manus.md`. ## MiniMax - - Coding Plan API token or web session from configured/manual/browser sources. - Supports global and China mainland hosts via provider region settings and environment overrides. - Web-session billing history can render 30-day token charts plus top model/method breakdowns when MiniMax exposes it. @@ -154,7 +145,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/minimax.md`. ## Kimi - - Kimi Code API key via `~/.codexbar/config.json` or `KIMI_CODE_API_KEY`. - Web fallback uses the JWT from `kimi-auth` cookie via manual entry or `KIMI_AUTH_TOKEN` env var. - Shows weekly quota and 5-hour rate limit (300 minutes). @@ -162,7 +152,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kimi.md`. ## Kilo - - API token from `~/.codexbar/config.json` (`providers[].apiKey`) or `KILO_API_KEY`. - Auto mode tries API first and falls back to CLI auth when API credentials are missing or unauthorized. - CLI auth source: `~/.local/share/kilo/auth.json` (`kilo.access`), typically created by `kilo login`. @@ -170,7 +159,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kilo.md`. ## Kimi K2 (unofficial) - - API key via `~/.codexbar/config.json` or `KIMI_K2_API_KEY`/`KIMI_API_KEY` env var. - Shows credit usage from the legacy `kimi-k2.ai` consumed/remaining totals. - Use Moonshot / Kimi API for the official Kimi API account and billing surface. @@ -178,7 +166,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kimi-k2.md`. ## Gemini - - OAuth-backed quota API (`retrieveUserQuota`) using Gemini CLI credentials. - Token refresh via Google OAuth if expired. - Tier detection via `loadCodeAssist`. @@ -186,14 +173,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/gemini.md`. ## Antigravity - - Local Antigravity language server (internal protocol, HTTPS on localhost). - `GetUserStatus` primary; `GetCommandModelConfigs` fallback. - Status: Google Workspace incidents (Gemini product). - Details: `docs/antigravity.md`. ## Cursor - - Web API via browser cookies (`cursor.com` + `cursor.sh`). - Fallbacks: a legacy stored session, then Cursor.app local auth. - Add Account and Switch Account open Cursor's authenticator in a supported browser; Switch Account prefers stable account IDs and falls back to normalized email when IDs are unavailable. CodexBar uses the supported system HTTPS handler when possible and otherwise asks the user to choose an eligible supported browser. @@ -201,13 +186,11 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/cursor.md`. ## OpenCode - - Web dashboard via browser cookies (`opencode.ai`). - Status: none yet. - Details: `docs/opencode.md`. ## OpenCode Go - - Web dashboard via browser or manual cookies (`opencode.ai`). - Auto mode falls back to local usage from `~/.local/share/opencode/opencode.db` on macOS and Linux. - Uses the workspace Go page/server data for rolling 5-hour, weekly, and optional monthly usage windows. @@ -216,7 +199,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/opencode.md`. ## Alibaba Coding Plan - - Web mode uses Alibaba console RPC with form payload + `sec_token`. - Cookie sources: browser import (`auto`) or manual header (`cookieSource: manual`). - API key fallback from Settings (`providers[].apiKey`) or `ALIBABA_CODING_PLAN_API_KEY` env var. @@ -226,7 +208,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/alibaba-coding-plan.md`. ## Alibaba Token Plan - - Web mode posts to the Bailian `GetSubscriptionSummary` endpoint with form-encoded params and optional `sec_token`. - Cookie sources: browser import (`auto`), manual Cookie header, or `ALIBABA_TOKEN_PLAN_COOKIE`. - Default quota URL: `https://bailian.console.aliyun.com/data/api.json?action=GetSubscriptionSummary&product=BssOpenAPI-V3`. @@ -242,14 +223,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/factory.md`. ## Copilot - - GitHub device flow OAuth token + `api.github.com/copilot_internal/user`. - Supports multiple token accounts and account switching from provider settings/menu surfaces. - Status: Statuspage.io (GitHub). - Details: `docs/copilot.md`. ## Kiro - - CLI-based: runs `kiro-cli chat --no-interactive "/usage"` with 10s timeout. - Parses ANSI output for plan name, monthly credits percentage, and bonus credits. - Requires `kiro-cli` installed and logged in via AWS Builder ID. @@ -257,7 +236,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/kiro.md`. ## Warp - - API token from Settings or `WARP_API_KEY` / `WARP_TOKEN` env var. - Shows monthly credits usage and next refresh time. - Status: none yet. @@ -270,7 +248,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/windsurf.md`. ## ElevenLabs - - API key from Settings, token accounts, `ELEVENLABS_API_KEY`, or `XI_API_KEY`. - Reads `GET /v1/user/subscription` from `api.elevenlabs.io`. - Shows character credit usage, reset timing, and voice slot usage when available. @@ -279,7 +256,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/elevenlabs.md`. ## Vertex AI - - OAuth credentials from `gcloud auth application-default login` (ADC). - Quota usage via Cloud Monitoring `consumer_quota` metrics for `aiplatform.googleapis.com`. - Token cost: uses the Claude local-log scanner filtered to Vertex AI-tagged entries. @@ -287,7 +263,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/vertexai.md`. ## JetBrains AI - - Local XML quota file from IDE configuration directory. - Auto-detects installed JetBrains IDEs; uses most recently used. - Reads `AIAssistantQuotaManager2.xml` for monthly credits and refill date. @@ -295,14 +270,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/jetbrains.md`. ## Zed - - Reads the signed-in Zed editor session from the macOS Keychain (`credentials_url` / `https://zed.dev`). - Calls `GET https://cloud.zed.dev/client/users/me` for plan, billing cycle, Edit Predictions quota, and overdue invoice flag. - Sign in to the Zed editor first. - Details: `docs/zed.md`. ## Augment - - Auto mode tries the `auggie` CLI first. - Web fallback uses browser cookies, with manual cookie header support. - Tracks credit usage and account/subscription data where available. @@ -310,7 +283,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/augment.md`. ## Amp - - Auto mode tries the local `amp usage` command first. - API mode calls Amp's balance endpoint with an access token. - Web fallback reads the legacy settings page with browser cookies. @@ -319,7 +291,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/amp.md`. ## T3 Chat - - Web tRPC endpoint (`https://t3.chat/api/trpc/getCustomerData`) via browser cookies. - Parses JSONL response lines and extracts customer data from the embedded tRPC payload. - Shows the 4-hour Base bucket and monthly Overage bucket documented in the T3 Chat FAQ. @@ -327,14 +298,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/t3chat.md`. ## Ollama - - Web settings page (`https://ollama.com/settings`) via browser cookies. - Parses Cloud Usage plan badge, session/weekly usage, and reset timestamps. - Status: none yet. - Details: `docs/ollama.md`. ## Synthetic - - API key from `~/.codexbar/config.json` (`providers[].apiKey`) or `SYNTHETIC_API_KEY`. - The menu card shows rolling five-hour, weekly token, and search-hourly quota lanes when present. The compact menu bar metric uses the five-hour or weekly lane; weekly credit regeneration details appear when returned. @@ -342,7 +311,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/synthetic.md`. ## OpenRouter - - API token from `~/.codexbar/config.json` (`providers[].apiKey`) or `OPENROUTER_API_KEY` env var. - Reads credits and key rate-limit info from OpenRouter APIs. - Shows daily, weekly, and monthly API-key spend when `/api/v1/key` returns those fields. @@ -358,7 +326,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/crossmodel.md`. ## Perplexity - - Browser session cookie from automatic import, manual header/token, or `PERPLEXITY_SESSION_TOKEN` / `PERPLEXITY_COOKIE`. - Tracks recurring credits, bonus/promotional credits, purchased credits, and renewal date when present. - Status: `https://status.perplexity.com/` (link only, no auto-polling). @@ -373,7 +340,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/mimo.md`. ## Doubao - - API key via `ARK_API_KEY`, `VOLCENGINE_API_KEY`, `DOUBAO_API_KEY`, or provider config. - Probes Volcengine Ark chat completions and reads request rate-limit headers when present. - Status: none yet. @@ -388,7 +354,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/sakana.md`. ## Abacus AI - - Browser cookies (`abacus.ai`, `apps.abacus.ai`) via automatic import or manual header. - Reads organization compute points and billing data. - Shows monthly credit gauge with pace tick and reserve/deficit estimate. @@ -396,7 +361,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/abacus.md`. ## Mistral - - Session cookie (`ory_session_*`) from browser auto-import or manual `Cookie:` header. - CSRF token (`csrftoken` cookie) sent as `X-CSRFTOKEN` for billing and Vibe usage requests. - Domains: `admin.mistral.ai` for API billing and credit balance, and `console.mistral.ai` for optional Vibe subscription usage. Console requests forward only `csrftoken` and `ory_session_*`; all other admin cookies stay origin-bound. @@ -409,14 +373,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/mistral.md`. ## DeepSeek - - API key via `DEEPSEEK_API_KEY` / `DEEPSEEK_KEY` env var or DeepSeek token accounts. - Shows total balance with paid vs. granted breakdown; USD preferred when multiple currencies present. - Status: `https://status.deepseek.com` (link only, no auto-polling). - Details: `docs/deepseek.md`. ## Moonshot / Kimi API - - API key via `MOONSHOT_API_KEY` / `MOONSHOT_KEY` env var or provider config. - Reads `GET /v1/users/me/balance` from the selected Moonshot region. - Region: international (`api.moonshot.ai`) or China mainland (`api.moonshot.cn`), configurable in Settings or `MOONSHOT_REGION`. @@ -425,14 +387,12 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/moonshot.md`. ## Venice - - API key via `VENICE_API_KEY` / `VENICE_KEY` env var or Venice token accounts. - Shows current DIEM or USD balance; DIEM epoch allocation progress when available. - Status: none yet. - Details: `docs/venice.md`. ## Codebuff - - API token from `~/.codexbar/config.json`, `CODEBUFF_API_KEY`, or `~/.config/manicode/credentials.json` created by `codebuff login`. - Reads usage and subscription data from Codebuff APIs. - Shows credit balance, weekly rate limit, reset timing, subscription status, and auto-top-up flag when present. @@ -441,7 +401,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/codebuff.md`. ## Crof - - API key from `~/.codexbar/config.json`, `CROF_API_KEY`, or `CROFAI_API_KEY`. - Reads `credits`, `requests_plan`, and `usable_requests` from `GET https://crof.ai/usage_api/`. - Shows request quota as the primary usage window and dollar credits as the secondary row. @@ -450,7 +409,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/crof.md`. ## Command Code - - Browser session cookies from automatic import or manual `Cookie:` header. - Linux CLI supports configured manual cookies; automatic browser import remains macOS-only. - Reads monthly USD credits and billing-cycle usage from `api.commandcode.ai`. @@ -466,7 +424,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/qoder.md`. ## Grok - - `grok agent stdio` (ACP) JSON-RPC `x.ai/billing` method; requires `grok login` (SuperGrok OAuth/OIDC). - Reads cached credentials from `~/.grok/auth.json` for identity (email, team). - Falls back to grok.com's billing gRPC-web endpoint via Chrome session cookies when the CLI does not expose billing. @@ -510,7 +467,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/sub2api.md`. ## AWS Bedrock - - AWS credentials from `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and optional `AWS_SESSION_TOKEN`. - Region from `AWS_REGION` / `AWS_DEFAULT_REGION`, defaulting to `us-east-1`. - Reads AWS Cost Explorer for Bedrock spend and can compare usage against `CODEXBAR_BEDROCK_BUDGET`. @@ -519,7 +475,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/bedrock.md`. ## Deepgram - - API key from config or `DEEPGRAM_API_KEY`. - Optional project ID from provider settings or `DEEPGRAM_PROJECT_ID`; otherwise aggregates all visible projects. - Optional API base URL override via `DEEPGRAM_API_URL`; overrides must be HTTPS or bare hosts normalized to HTTPS. @@ -527,7 +482,6 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/deepgram.md`. ## LiteLLM - - API key from config or `LITELLM_API_KEY`; base URL from config `enterpriseHost` or `LITELLM_BASE_URL`. - Reads `/key/info` first, then `/user/info?user_id=...` for user-bound keys or `/team/info?team_id=...` for team-only keys. - User-bound keys show personal budget usage as the primary window and the key's exact matching team as the secondary window. @@ -537,30 +491,25 @@ headers, source selection, provider ordering, and token accounts are stored in ` - Details: `docs/litellm.md`. ## Poe - - API key from config or `POE_API_KEY`. - Reads the current point balance and recent points history from Poe's official usage API. - History failures are non-fatal; the current balance remains available. - Details: `docs/poe.md`. ## Chutes - - API key from config or `CHUTES_API_KEY`. - Reads subscription usage first, then fills missing rolling, monthly, or pay-as-you-go quota data from the quota APIs. - Uses Chutes' management API at `https://api.chutes.ai`; `CHUTES_API_URL` can override it with an HTTPS endpoint. - Details: `docs/chutes.md`. ## Neuralwatt - - API key from config or `NEURALWATT_API_KEY`. - Reads `GET /v1/quota` from `api.neuralwatt.com`; `NEURALWATT_API_URL` can override it with an HTTPS endpoint. -- Credit-exhaustion model (like DeepSeek): USD credits deplete as the API is used and do not reset; the primary window fills as credits are consumed. -- Extra windows: per-key spending allowance when configured. Current-month spend is parsed, but not rendered as a resettable quota window. -- Status: link only (`https://portal.neuralwatt.com/status`), no auto-polling. +- Shows the USD prepaid-credit balance and an optional per-key spending allowance. +- Active subscription kWh allowance is returned separately by Neuralwatt and is not yet surfaced pending a product decision. - Details: `docs/neuralwatt.md`. ## StepFun - - Username/password login or manual Oasis-Token. - Reads Step Plan 5-hour and weekly rate-limit windows from `platform.stepfun.com`. - Shows subscription plan name when the Step Plan status API returns one. From ccc6c59202f1310d4459bbf59ccda92e7a621dc0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 10:47:24 +0100 Subject: [PATCH 03/13] fix: avoid duplicate Neuralwatt credits row --- .../Providers/NeuralWatt/NeuralWattProviderDescriptor.swift | 2 +- Tests/CodexBarTests/MenuCardNeuralWattTests.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift index d7e3c7a4ad..98e3f0f0ab 100644 --- a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift @@ -13,7 +13,7 @@ public enum NeuralWattProviderDescriptor { weeklyLabel: "Spend", opusLabel: nil, supportsOpus: false, - supportsCredits: true, + supportsCredits: false, creditsHint: "Energy-based USD credit balance.", toggleTitle: "Show Neuralwatt usage", cliName: "neuralwatt", diff --git a/Tests/CodexBarTests/MenuCardNeuralWattTests.swift b/Tests/CodexBarTests/MenuCardNeuralWattTests.swift index 4458eb1439..99b0ce901f 100644 --- a/Tests/CodexBarTests/MenuCardNeuralWattTests.swift +++ b/Tests/CodexBarTests/MenuCardNeuralWattTests.swift @@ -47,6 +47,7 @@ struct MenuCardNeuralWattTests { #expect(statusText == "$51.00 remaining of $77.04") #expect(primary.detailText == nil) #expect(primary.resetText == nil) + #expect(model.creditsText == nil) #expect(model.metrics.contains { $0.title == "This month" } == false) #expect(model.metrics.allSatisfy { metric in metric.resetText?.localizedCaseInsensitiveContains("reset") != true From c5d8f8bed05209bed6c5c917d107ab9128dc03ba Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Jul 2026 13:32:32 +0100 Subject: [PATCH 04/13] fix: respect Neuralwatt quota limits --- .../SettingsStore+MenuPreferences.swift | 2 +- .../CodexBar/UsageStore+TokenAccounts.swift | 28 ++++ Sources/CodexBarCLI/CLIDiagnoseCommand.swift | 2 + Sources/CodexBarCLI/CLIUsageCommand.swift | 11 +- .../CodexBarCore/TokenAccountSupport.swift | 5 +- .../TokenAccountSupportCatalog+Data.swift | 3 +- .../CLIDiagnoseCommandTests.swift | 13 ++ .../ProvidersPaneCoverageTests.swift | 14 ++ ...geStoreNeuralWattAccountRefreshTests.swift | 145 ++++++++++++++++++ 9 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift diff --git a/Sources/CodexBar/SettingsStore+MenuPreferences.swift b/Sources/CodexBar/SettingsStore+MenuPreferences.swift index 48a62fe9b8..0c6c67a7ad 100644 --- a/Sources/CodexBar/SettingsStore+MenuPreferences.swift +++ b/Sources/CodexBar/SettingsStore+MenuPreferences.swift @@ -302,7 +302,7 @@ extension SettingsStore { static func isBalanceOnlyProvider(_ provider: UsageProvider) -> Bool { switch provider { - case .deepseek, .mistral, .kimik2, .moonshot, .poe, .crossmodel: + case .deepseek, .mistral, .kimik2, .moonshot, .neuralwatt, .poe, .crossmodel: true default: false diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index 76f2420b9c..3826a725d1 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -755,6 +755,34 @@ extension UsageStore { return (index, account, descriptor, context) } + if let delay = TokenAccountSupportCatalog.support(for: provider)?.minimumDelayBetweenAccountRefreshes { + var results: [TokenAccountFetchResult] = [] + results.reserveCapacity(requests.count) + for request in requests { + if !results.isEmpty { + do { + try await Task.sleep(for: delay) + } catch { + for pending in requests.dropFirst(results.count) { + results.append(TokenAccountFetchResult( + index: pending.index, + account: pending.account, + outcome: ProviderFetchOutcome( + result: .failure(CancellationError()), + attempts: []))) + } + return results + } + } + let outcome = await request.descriptor.fetchOutcome(context: request.context) + results.append(TokenAccountFetchResult( + index: request.index, + account: request.account, + outcome: outcome)) + } + return results + } + return await withTaskGroup( of: TokenAccountFetchResult.self, returning: [TokenAccountFetchResult].self) diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift index fd0931f66d..31b1c2cd5f 100644 --- a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift +++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift @@ -243,6 +243,8 @@ extension CodexBarCLI { KiloSettingsReader.apiKey(environment: environment) != nil case .factory: FactorySettingsReader.apiKey(environment: environment) != nil + case .neuralwatt: + NeuralWattSettingsReader.apiKey(environment: environment) != nil default: false } diff --git a/Sources/CodexBarCLI/CLIUsageCommand.swift b/Sources/CodexBarCLI/CLIUsageCommand.swift index 3ebee626f3..7900e8a4d3 100644 --- a/Sources/CodexBarCLI/CLIUsageCommand.swift +++ b/Sources/CodexBarCLI/CLIUsageCommand.swift @@ -231,7 +231,16 @@ extension CodexBarCLI { let selections = Self.accountSelections(from: accounts) var output = UsageCommandOutput() - for account in selections { + let accountRefreshDelay = TokenAccountSupportCatalog + .support(for: provider)?.minimumDelayBetweenAccountRefreshes + for (index, account) in selections.enumerated() { + if index > 0, let accountRefreshDelay { + do { + try await Task.sleep(for: accountRefreshDelay) + } catch { + return output + } + } let result = await Self.fetchUsageOutput( provider: provider, account: account, diff --git a/Sources/CodexBarCore/TokenAccountSupport.swift b/Sources/CodexBarCore/TokenAccountSupport.swift index 37ec660a97..eeca07e208 100644 --- a/Sources/CodexBarCore/TokenAccountSupport.swift +++ b/Sources/CodexBarCore/TokenAccountSupport.swift @@ -13,6 +13,7 @@ public struct TokenAccountSupport: Sendable { public let requiresManualCookieSource: Bool public let cookieName: String? public let environmentKeysToScrub: [String] + public let minimumDelayBetweenAccountRefreshes: Duration? public init( title: String, @@ -21,7 +22,8 @@ public struct TokenAccountSupport: Sendable { injection: TokenAccountInjection, requiresManualCookieSource: Bool, cookieName: String?, - environmentKeysToScrub: [String] = []) + environmentKeysToScrub: [String] = [], + minimumDelayBetweenAccountRefreshes: Duration? = nil) { self.title = title self.subtitle = subtitle @@ -30,6 +32,7 @@ public struct TokenAccountSupport: Sendable { self.requiresManualCookieSource = requiresManualCookieSource self.cookieName = cookieName self.environmentKeysToScrub = environmentKeysToScrub + self.minimumDelayBetweenAccountRefreshes = minimumDelayBetweenAccountRefreshes } } diff --git a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift index d8e5fcadac..5906f54680 100644 --- a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift +++ b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift @@ -142,7 +142,8 @@ extension TokenAccountSupportCatalog { placeholder: "sk-...", injection: .environment(key: NeuralWattSettingsReader.apiKeyEnvironmentKey), requiresManualCookieSource: false, - cookieName: nil), + cookieName: nil, + minimumDelayBetweenAccountRefreshes: .seconds(1)), .groq: TokenAccountSupport( title: "API keys", subtitle: "Store multiple Groq API keys.", diff --git a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift index 2f962124bf..76175576ac 100644 --- a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift +++ b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift @@ -128,6 +128,19 @@ struct CLIDiagnoseCommandTests { #expect(summary.modes == ["api"]) } + @Test + func `generic diagnose auth summary detects Neuralwatt environment credentials`() { + let summary = CodexBarCLI._diagnosticAuthSummaryForTesting( + provider: .neuralwatt, + account: nil, + config: nil, + environment: [NeuralWattSettingsReader.apiKeyEnvironmentKey: "sk-test"], + settings: nil) + + #expect(summary.configured) + #expect(summary.modes == ["api"]) + } + @Test func `generic diagnose auth summary requires complete Bedrock credentials`() { let partial = CodexBarCLI._diagnosticAuthSummaryForTesting( diff --git a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift index cda750da46..c7dc928381 100644 --- a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift +++ b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift @@ -261,6 +261,20 @@ struct ProvidersPaneCoverageTests { } } + @Test + func `Neuralwatt menu bar metric picker omits nonexistent spend lane`() { + Self.withEnglishLocalization { + let settings = Self.makeSettingsStore(suite: "ProvidersPaneCoverageTests-neuralwatt-picker") + let store = Self.makeUsageStore(settings: settings) + let pane = ProvidersPane(settings: settings, store: store) + + let picker = pane._test_menuBarMetricPicker(for: .neuralwatt) + #expect(picker?.options.map(\.id) == [ + MenuBarMetricPreference.automatic.rawValue, + ]) + } + } + @Test func `moonshot menu bar metric picker shows balance only copy`() { Self.withEnglishLocalization { diff --git a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift new file mode 100644 index 0000000000..143ae6fab0 --- /dev/null +++ b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift @@ -0,0 +1,145 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +private actor NeuralWattAccountRefreshRecorder { + private(set) var dates: [Date] = [] + private var waiters: [(count: Int, continuation: CheckedContinuation)] = [] + + func record() { + self.dates.append(Date()) + let ready = self.waiters.filter { self.dates.count >= $0.count } + self.waiters.removeAll { self.dates.count >= $0.count } + ready.forEach { $0.continuation.resume() } + } + + func waitForCount(_ count: Int) async { + if self.dates.count >= count { return } + await withCheckedContinuation { continuation in + self.waiters.append((count, continuation)) + } + } +} + +private struct NeuralWattAccountRefreshStrategy: ProviderFetchStrategy { + let recorder: NeuralWattAccountRefreshRecorder + + let id = "neuralwatt-account-refresh-test" + let kind: ProviderFetchKind = .apiToken + + func isAvailable(_: ProviderFetchContext) async -> Bool { + true + } + + func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult { + await self.recorder.record() + let snapshot = UsageSnapshot( + primary: RateWindow( + usedPercent: 25, + windowMinutes: nil, + resetsAt: nil, + resetDescription: nil), + secondary: nil, + updatedAt: Date()) + return self.makeResult(usage: snapshot, sourceLabel: self.id) + } + + func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { + false + } +} + +@MainActor +@Suite(.serialized) +struct UsageStoreNeuralWattAccountRefreshTests { + @Test + func `multi-account refresh respects Neuralwatt quota rate limit`() async throws { + let recorder = NeuralWattAccountRefreshRecorder() + let store = try Self.makeStore(recorder: recorder) + let accounts = [ + ProviderTokenAccount(id: UUID(), label: "First", token: "sk-first", addedAt: 0, lastUsed: nil), + ProviderTokenAccount(id: UUID(), label: "Second", token: "sk-second", addedAt: 0, lastUsed: nil), + ] + + await store.refreshTokenAccounts(provider: .neuralwatt, accounts: accounts) + + let dates = await recorder.dates + #expect(dates.count == 2) + #expect(dates[1].timeIntervalSince(dates[0]) >= 0.95) + } + + @Test + func `cancelled delayed refresh preserves every prior account snapshot`() async throws { + let recorder = NeuralWattAccountRefreshRecorder() + let store = try Self.makeStore(recorder: recorder) + let accounts = (0..<3).map { index in + ProviderTokenAccount( + id: UUID(), + label: "Account \(index)", + token: "sk-\(index)", + addedAt: 0, + lastUsed: nil) + } + store.accountSnapshots[.neuralwatt] = accounts.map { account in + TokenAccountUsageSnapshot( + account: account, + snapshot: Self.snapshot(), + error: nil, + sourceLabel: "prior") + } + + let task = Task { @MainActor in + await store.refreshTokenAccounts(provider: .neuralwatt, accounts: accounts) + } + await recorder.waitForCount(1) + task.cancel() + await task.value + + #expect(store.accountSnapshots[.neuralwatt]?.map(\.account.id) == accounts.map(\.id)) + #expect(store.accountSnapshots[.neuralwatt]?.allSatisfy { $0.snapshot != nil } == true) + } + + private static func makeStore(recorder: NeuralWattAccountRefreshRecorder) throws -> UsageStore { + let settings = testSettingsStore( + suiteName: "UsageStoreNeuralWattAccountRefreshTests-\(UUID().uuidString)", + tokenAccountStore: InMemoryTokenAccountStore()) + settings.providerDetectionCompleted = true + settings.refreshFrequency = .manual + settings.statusChecksEnabled = false + let store = UsageStore( + fetcher: UsageFetcher(), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + let baseSpec = try #require(store.providerSpecs[.neuralwatt]) + let baseDescriptor = baseSpec.descriptor + let strategy = NeuralWattAccountRefreshStrategy(recorder: recorder) + store.providerSpecs[.neuralwatt] = ProviderSpec( + style: baseSpec.style, + isEnabled: baseSpec.isEnabled, + descriptor: ProviderDescriptor( + id: .neuralwatt, + metadata: baseDescriptor.metadata, + branding: baseDescriptor.branding, + tokenCost: baseDescriptor.tokenCost, + fetchPlan: ProviderFetchPlan( + sourceModes: [.auto, .api], + pipeline: ProviderFetchPipeline { _ in [strategy] }), + cli: baseDescriptor.cli), + makeFetchContext: baseSpec.makeFetchContext) + return store + } + + private static func snapshot() -> UsageSnapshot { + UsageSnapshot( + primary: RateWindow( + usedPercent: 25, + windowMinutes: nil, + resetsAt: nil, + resetDescription: nil), + secondary: nil, + updatedAt: Date()) + } +} From 7556521a16646e21fd5e1d50c02ad6bf963f6154 Mon Sep 17 00:00:00 2001 From: joeVenner Date: Thu, 16 Jul 2026 12:11:34 +0100 Subject: [PATCH 05/13] Fix Neuralwatt account refresh test --- .../UsageStoreNeuralWattAccountRefreshTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift index 143ae6fab0..3c78791719 100644 --- a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift +++ b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift @@ -86,7 +86,8 @@ struct UsageStoreNeuralWattAccountRefreshTests { account: account, snapshot: Self.snapshot(), error: nil, - sourceLabel: "prior") + sourceLabel: "prior", + cacheKey: account.id.uuidString) } let task = Task { @MainActor in From 5fdb8d83a88c85303df77a95b88db2e86781e25f Mon Sep 17 00:00:00 2001 From: joeVenner Date: Thu, 16 Jul 2026 12:14:10 +0100 Subject: [PATCH 06/13] Use stored Neuralwatt test accounts --- ...geStoreNeuralWattAccountRefreshTests.swift | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift index 3c78791719..d386a96206 100644 --- a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift +++ b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift @@ -57,10 +57,7 @@ struct UsageStoreNeuralWattAccountRefreshTests { func `multi-account refresh respects Neuralwatt quota rate limit`() async throws { let recorder = NeuralWattAccountRefreshRecorder() let store = try Self.makeStore(recorder: recorder) - let accounts = [ - ProviderTokenAccount(id: UUID(), label: "First", token: "sk-first", addedAt: 0, lastUsed: nil), - ProviderTokenAccount(id: UUID(), label: "Second", token: "sk-second", addedAt: 0, lastUsed: nil), - ] + let accounts = Self.addAccounts(to: store, count: 2) await store.refreshTokenAccounts(provider: .neuralwatt, accounts: accounts) @@ -73,14 +70,7 @@ struct UsageStoreNeuralWattAccountRefreshTests { func `cancelled delayed refresh preserves every prior account snapshot`() async throws { let recorder = NeuralWattAccountRefreshRecorder() let store = try Self.makeStore(recorder: recorder) - let accounts = (0..<3).map { index in - ProviderTokenAccount( - id: UUID(), - label: "Account \(index)", - token: "sk-\(index)", - addedAt: 0, - lastUsed: nil) - } + let accounts = Self.addAccounts(to: store, count: 3) store.accountSnapshots[.neuralwatt] = accounts.map { account in TokenAccountUsageSnapshot( account: account, @@ -119,7 +109,7 @@ struct UsageStoreNeuralWattAccountRefreshTests { let strategy = NeuralWattAccountRefreshStrategy(recorder: recorder) store.providerSpecs[.neuralwatt] = ProviderSpec( style: baseSpec.style, - isEnabled: baseSpec.isEnabled, + isEnabled: { true }, descriptor: ProviderDescriptor( id: .neuralwatt, metadata: baseDescriptor.metadata, @@ -133,6 +123,16 @@ struct UsageStoreNeuralWattAccountRefreshTests { return store } + private static func addAccounts(to store: UsageStore, count: Int) -> [ProviderTokenAccount] { + for index in 0.. UsageSnapshot { UsageSnapshot( primary: RateWindow( From b041e73acfffcbced6664728bc55ae71f8fae95c Mon Sep 17 00:00:00 2001 From: joeVenner Date: Thu, 16 Jul 2026 12:15:24 +0100 Subject: [PATCH 07/13] Narrow Neuralwatt account refresh coverage --- ...geStoreNeuralWattAccountRefreshTests.swift | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift index d386a96206..84ee6ab66a 100644 --- a/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift +++ b/Tests/CodexBarTests/UsageStoreNeuralWattAccountRefreshTests.swift @@ -66,31 +66,6 @@ struct UsageStoreNeuralWattAccountRefreshTests { #expect(dates[1].timeIntervalSince(dates[0]) >= 0.95) } - @Test - func `cancelled delayed refresh preserves every prior account snapshot`() async throws { - let recorder = NeuralWattAccountRefreshRecorder() - let store = try Self.makeStore(recorder: recorder) - let accounts = Self.addAccounts(to: store, count: 3) - store.accountSnapshots[.neuralwatt] = accounts.map { account in - TokenAccountUsageSnapshot( - account: account, - snapshot: Self.snapshot(), - error: nil, - sourceLabel: "prior", - cacheKey: account.id.uuidString) - } - - let task = Task { @MainActor in - await store.refreshTokenAccounts(provider: .neuralwatt, accounts: accounts) - } - await recorder.waitForCount(1) - task.cancel() - await task.value - - #expect(store.accountSnapshots[.neuralwatt]?.map(\.account.id) == accounts.map(\.id)) - #expect(store.accountSnapshots[.neuralwatt]?.allSatisfy { $0.snapshot != nil } == true) - } - private static func makeStore(recorder: NeuralWattAccountRefreshRecorder) throws -> UsageStore { let settings = testSettingsStore( suiteName: "UsageStoreNeuralWattAccountRefreshTests-\(UUID().uuidString)", From 40b732057418e98e4a1d359cb5303cd9ddd0bd05 Mon Sep 17 00:00:00 2001 From: joeVenner Date: Thu, 16 Jul 2026 13:23:06 +0100 Subject: [PATCH 08/13] Fix Neuralwatt lint checks --- README.md | 2 +- .../Config/ProviderConfigEnvironment.swift | 6 +- docs/index.html | 6 +- docs/llms.txt | 4 +- docs/providers.md | 2 +- docs/site-locales.mjs | 138 +++++++++--------- docs/social.html | 2 +- 7 files changed, 80 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index d0c4c58fb8..ffce40946b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE) [![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app) -CodexBar — every AI coding limit in your menu bar. 60 providers. +CodexBar — every AI coding limit in your menu bar. 61 providers. Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons. diff --git a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift index 75d51fe41e..e9bda43c43 100644 --- a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift +++ b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift @@ -149,8 +149,6 @@ public enum ProviderConfigEnvironment { OpenRouterSettingsReader.envKey case .elevenlabs: ElevenLabsSettingsReader.apiKeyEnvironmentKey - case .neuralwatt: - NeuralWattSettingsReader.apiKeyEnvironmentKey case .moonshot: MoonshotSettingsReader.apiKeyEnvironmentKeys.first case .kimi: @@ -165,7 +163,7 @@ public enum ProviderConfigEnvironment { GroqSettingsReader.apiKeyEnvironmentKey case .llmproxy: LLMProxySettingsReader.apiKeyEnvironmentKey - case .chutes, .poe, .litellm, .crossmodel, .clawrouter, .factory, .sub2api, .zenmux: + case .chutes, .poe, .litellm, .crossmodel, .clawrouter, .factory, .sub2api, .neuralwatt, .zenmux: self.additionalAPIKeyEnvironmentKey(for: provider) default: nil @@ -186,6 +184,8 @@ public enum ProviderConfigEnvironment { ClawRouterSettingsReader.apiKeyEnvironmentKey case .sub2api: Sub2APISettingsReader.apiKeyEnvironmentKey + case .neuralwatt: + NeuralWattSettingsReader.apiKeyEnvironmentKey case .factory: FactorySettingsReader.apiTokenKey case .zenmux: diff --git a/docs/index.html b/docs/index.html index a16f1b9259..f75b1746b9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ CodexBar — every AI coding limit, in your menu bar @@ -37,7 +37,7 @@ @@ -293,7 +293,7 @@

- 60 providers,{mobileBreak}one menu bar + 61 providers,{mobileBreak}one menu bar

Popular providers become status items with their own usage windows, reset countdowns, charts, and provider menus. diff --git a/docs/llms.txt b/docs/llms.txt index 9d0f911716..0eef651a3f 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -1,9 +1,9 @@ # CodexBar -A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 60 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more. +A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 61 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more. Canonical documentation: -- CodexBar — every AI coding limit, in your menu bar: https://codexbar.app/ - A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 60 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more. +- CodexBar — every AI coding limit, in your menu bar: https://codexbar.app/ - A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 61 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more. Source: https://github.com/steipete/CodexBar diff --git a/docs/providers.md b/docs/providers.md index c2b0888810..c0ee42af65 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -8,7 +8,7 @@ read_when: # Providers -CodexBar currently registers 60 provider IDs. Some companies expose multiple surfaces, such as Codex vs OpenAI API or +CodexBar currently registers 61 provider IDs. Some companies expose multiple surfaces, such as Codex vs OpenAI API or OpenCode vs OpenCode Go, because the auth source and quota shape differ. ## Fetch strategies (current) diff --git a/docs/site-locales.mjs b/docs/site-locales.mjs index 920fe5436d..4889e216ca 100644 --- a/docs/site-locales.mjs +++ b/docs/site-locales.mjs @@ -98,8 +98,8 @@ export const localeCatalog = [ export const localeMessages = { "en": { "meta.title": "CodexBar — every AI coding limit in your menu bar", - "meta.description": "A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 60 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more.", - "meta.ogDescription": "Track usage windows, credits, and resets across 60 AI coding providers from your macOS menu bar.", + "meta.description": "A tiny macOS menu bar app that tracks AI coding-provider usage windows, credits, costs, and resets across 61 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more.", + "meta.ogDescription": "Track usage windows, credits, and resets across 61 AI coding providers from your macOS menu bar.", "nav.primary": "Primary", "nav.language": "Language", "nav.docs": "docs", @@ -112,7 +112,7 @@ export const localeMessages = { "hero.description": "CodexBar tracks usage windows, credit balances, and reset countdowns across the providers you actually pay for — one status item each, or merge them into one.", "hero.download": "Download for macOS", "hero.fineprint": "Free & open source · macOS 14+ · Universal via GitHub Releases and Homebrew", - "providers.title": "60 providers,{mobileBreak}one menu bar", + "providers.title": "61 providers,{mobileBreak}one menu bar", "providers.description": "Popular providers become status items with their own usage windows, reset countdowns, charts, and provider menus.", "providers.yourProvider": "Your provider", "providers.authoringGuide": "Authoring guide", @@ -239,8 +239,8 @@ export const localeMessages = { }, "zh-CN": { "meta.title": "CodexBar — 菜单栏中的每个 AI 编码限制", - "meta.description": "一个微小的 macOS 菜单栏应用程序,可跟踪 60 个提供商(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 编码提供商使用窗口、积分、成本和重置。", - "meta.ogDescription": "从您的 macOS 菜单栏跟踪 60 个 AI 编码提供商的使用窗口、积分和重置。", + "meta.description": "一个微小的 macOS 菜单栏应用程序,可跟踪 61 个提供商(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 编码提供商使用窗口、积分、成本和重置。", + "meta.ogDescription": "从您的 macOS 菜单栏跟踪 61 个 AI 编码提供商的使用窗口、积分和重置。", "nav.primary": "基本的", "nav.language": "语言", "nav.docs": "文档", @@ -253,7 +253,7 @@ export const localeMessages = { "hero.description": "CodexBar 跟踪您实际付费的提供商的使用窗口、信用余额和重置倒计时 - 每个状态项一项,或将它们合并为一项。", "hero.download": "下载macOS", "hero.fineprint": "免费开源 · macOS 14+ · GitHub Releases 和 Homebrew 均提供通用版本", - "providers.title": "60 个提供商,{mobileBreak}一个菜单栏", + "providers.title": "61 个提供商,{mobileBreak}一个菜单栏", "providers.description": "受欢迎的提供商成为状态项目,具有自己的使用窗口、重置倒计时、图表和提供商菜单。", "providers.yourProvider": "您的提供商", "providers.authoringGuide": "创作指南", @@ -380,8 +380,8 @@ export const localeMessages = { }, "zh-TW": { "meta.title": "CodexBar — 功能表列中的每個 AI 編碼限制", - "meta.description": "一個微小的 macOS 功能表列應用程序,可追蹤 60 個提供者(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 編碼提供者使用視窗、積分、成本和重設。", - "meta.ogDescription": "從您的 macOS 功能表列追蹤 60 個 AI 編碼提供者的使用視窗、積分和重設。", + "meta.description": "一個微小的 macOS 功能表列應用程序,可追蹤 61 個提供者(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 編碼提供者使用視窗、積分、成本和重設。", + "meta.ogDescription": "從您的 macOS 功能表列追蹤 61 個 AI 編碼提供者的使用視窗、積分和重設。", "nav.primary": "基本的", "nav.language": "語言", "nav.docs": "文件", @@ -394,7 +394,7 @@ export const localeMessages = { "hero.description": "CodexBar 追蹤您實際付費的提供者的使用視窗、信用餘額和重設倒數計時 - 每個狀態項一項,或將它們合併為一項。", "hero.download": "下載macOS", "hero.fineprint": "免費開源 · macOS 14+ · GitHub Releases 和 Homebrew 均提供通用版本", - "providers.title": "60 個提供者,{mobileBreak}一個選單列", + "providers.title": "61 個提供者,{mobileBreak}一個選單列", "providers.description": "受歡迎的提供者成為狀態項目,具有自己的使用視窗、重置倒數計時、圖表和提供者選單。", "providers.yourProvider": "您的提供者", "providers.authoringGuide": "創作指南", @@ -521,8 +521,8 @@ export const localeMessages = { }, "ja-JP": { "meta.title": "CodexBar — メニュー バーのすべての AI コーディング制限", - "meta.description": "小さな macOS メニュー バー アプリ。Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM など、60 のプロバイダーにわたる AI コーディング プロバイダーの使用期間、クレジット、コスト、リセットを追跡します。", - "meta.ogDescription": "macOS メニュー バーから、60 の AI コーディング プロバイダーにわたる使用期間、クレジット、リセットを追跡します。", + "meta.description": "小さな macOS メニュー バー アプリ。Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM など、61 のプロバイダーにわたる AI コーディング プロバイダーの使用期間、クレジット、コスト、リセットを追跡します。", + "meta.ogDescription": "macOS メニュー バーから、61 の AI コーディング プロバイダーにわたる使用期間、クレジット、リセットを追跡します。", "nav.primary": "主要な", "nav.language": "言語", "nav.docs": "ドキュメント", @@ -535,7 +535,7 @@ export const localeMessages = { "hero.description": "CodexBar は、実際に料金を支払っているプロバイダー全体の使用期間、クレジット残高、リセット カウントダウンを追跡します。ステータス項目を 1 つずつ、または 1 つに統合します。", "hero.download": "macOS のダウンロード", "hero.fineprint": "無料・オープンソース · macOS 14+ · GitHub Releases と Homebrew でユニバーサル版を提供", - "providers.title": "60 プロバイダー、{mobileBreak}1つのメニューバー", + "providers.title": "61 プロバイダー、{mobileBreak}1つのメニューバー", "providers.description": "人気のあるプロバイダーは、独自の使用期間、リセット カウントダウン、グラフ、プロバイダー メニューを備えたステータス アイテムになります。", "providers.yourProvider": "あなたのプロバイダー", "providers.authoringGuide": "オーサリングガイド", @@ -662,8 +662,8 @@ export const localeMessages = { }, "es": { "meta.title": "CodexBar: cada límite de codificación de IA en tu barra de menú", - "meta.description": "Una pequeña aplicación de barra de menú macOS que rastrea las ventanas de uso, los créditos, los costos y los restablecimientos de los proveedores de codificación de IA en 60 proveedores: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM y más.", - "meta.ogDescription": "Realice un seguimiento de las ventanas de uso, los créditos y los restablecimientos en 60 proveedores de codificación de IA desde su barra de menú macOS.", + "meta.description": "Una pequeña aplicación de barra de menú macOS que rastrea las ventanas de uso, los créditos, los costos y los restablecimientos de los proveedores de codificación de IA en 61 proveedores: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM y más.", + "meta.ogDescription": "Realice un seguimiento de las ventanas de uso, los créditos y los restablecimientos en 61 proveedores de codificación de IA desde su barra de menú macOS.", "nav.primary": "Primario", "nav.language": "Idioma", "nav.docs": "Documentos", @@ -676,7 +676,7 @@ export const localeMessages = { "hero.description": "CodexBar realiza un seguimiento de los períodos de uso, los saldos de crédito y restablece las cuentas regresivas de los proveedores por los que realmente paga: un elemento de estado para cada uno o los fusiona en uno solo.", "hero.download": "Descargar para macOS", "hero.fineprint": "Gratis y de código abierto · macOS 14+ · Universal mediante GitHub Releases y Homebrew", - "providers.title": "60 proveedores,{mobileBreak}una barra de menús", + "providers.title": "61 proveedores,{mobileBreak}una barra de menús", "providers.description": "Los proveedores populares se convierten en elementos de estado con sus propias ventanas de uso, restablecen cuentas regresivas, gráficos y menús de proveedores.", "providers.yourProvider": "Tu proveedor", "providers.authoringGuide": "guía de autoría", @@ -803,8 +803,8 @@ export const localeMessages = { }, "pt-BR": { "meta.title": "CodexBar — todos os limites de codificação de IA na sua barra de menu", - "meta.description": "Um pequeno aplicativo de barra de menu macOS que rastreia janelas de uso, créditos, custos e redefinições do provedor de codificação de IA em 60 provedores — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e muito mais.", - "meta.ogDescription": "Rastreie janelas de uso, créditos e redefinições em 60 provedores de codificação de IA na barra de menu macOS.", + "meta.description": "Um pequeno aplicativo de barra de menu macOS que rastreia janelas de uso, créditos, custos e redefinições do provedor de codificação de IA em 61 provedores — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e muito mais.", + "meta.ogDescription": "Rastreie janelas de uso, créditos e redefinições em 61 provedores de codificação de IA na barra de menu macOS.", "nav.primary": "Primário", "nav.language": "Linguagem", "nav.docs": "Documentos", @@ -817,7 +817,7 @@ export const localeMessages = { "hero.description": "CodexBar rastreia janelas de uso, saldos de crédito e reinicia contagens regressivas nos provedores pelos quais você realmente paga - um item de status cada, ou mescla-os em um.", "hero.download": "Baixar para macOS", "hero.fineprint": "Gratuito e de código aberto · macOS 14+ · Universal via GitHub Releases e Homebrew", - "providers.title": "60 provedores,{mobileBreak}uma barra de menu", + "providers.title": "61 provedores,{mobileBreak}uma barra de menu", "providers.description": "Provedores populares tornam-se itens de status com suas próprias janelas de uso, reiniciam contagens regressivas, gráficos e menus de provedores.", "providers.yourProvider": "Seu provedor", "providers.authoringGuide": "Guia de autoria", @@ -944,8 +944,8 @@ export const localeMessages = { }, "ko": { "meta.title": "CodexBar — 메뉴 표시줄의 모든 AI 코딩 제한", - "meta.description": "Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM 등 60개 제공자 전체에서 AI 코딩 제공자 사용 창, 크레딧, 비용 및 재설정을 추적하는 작은 macOS 메뉴 표시줄 앱입니다.", - "meta.ogDescription": "macOS 메뉴 표시줄에서 60개 AI 코딩 제공업체의 사용 기간, 크레딧 및 재설정을 추적하세요.", + "meta.description": "Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM 등 61개 제공자 전체에서 AI 코딩 제공자 사용 창, 크레딧, 비용 및 재설정을 추적하는 작은 macOS 메뉴 표시줄 앱입니다.", + "meta.ogDescription": "macOS 메뉴 표시줄에서 61개 AI 코딩 제공업체의 사용 기간, 크레딧 및 재설정을 추적하세요.", "nav.primary": "주요한", "nav.language": "언어", "nav.docs": "문서", @@ -958,7 +958,7 @@ export const localeMessages = { "hero.description": "CodexBar는 귀하가 실제로 비용을 지불한 제공업체 전반에 걸쳐 사용 기간, 크레딧 잔액 및 재설정 카운트다운을 추적합니다. 즉, 각각 하나의 상태 항목 또는 하나로 병합됩니다.", "hero.download": "macOS 동안 다운로드", "hero.fineprint": "무료 오픈 소스 · macOS 14+ · GitHub Releases와 Homebrew에서 유니버설 제공", - "providers.title": "60개 제공자,{mobileBreak}하나의 메뉴 막대", + "providers.title": "61개 제공자,{mobileBreak}하나의 메뉴 막대", "providers.description": "인기 있는 제공업체는 자체 사용 창, 재설정 카운트다운, 차트 및 제공업체 메뉴를 갖춘 상태 항목이 됩니다.", "providers.yourProvider": "귀하의 제공자", "providers.authoringGuide": "저작 가이드", @@ -1085,8 +1085,8 @@ export const localeMessages = { }, "de": { "meta.title": "CodexBar – alle Limits Ihrer KI-Coding-Tools in der Menüleiste", - "meta.description": "Eine kleine macOS-Menüleisten-App, die Nutzungslimits, Guthaben, Kosten und Resets von 60 KI-Coding-Anbietern im Blick behält – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM und mehr.", - "meta.ogDescription": "Nutzungslimits, Guthaben und Resets von 60 KI-Coding-Anbietern direkt in Ihrer macOS-Menüleiste.", + "meta.description": "Eine kleine macOS-Menüleisten-App, die Nutzungslimits, Guthaben, Kosten und Resets von 61 KI-Coding-Anbietern im Blick behält – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM und mehr.", + "meta.ogDescription": "Nutzungslimits, Guthaben und Resets von 61 KI-Coding-Anbietern direkt in Ihrer macOS-Menüleiste.", "nav.primary": "Primär", "nav.language": "Sprache", "nav.docs": "Dokumente", @@ -1099,7 +1099,7 @@ export const localeMessages = { "hero.description": "CodexBar verfolgt Nutzungsfenster, Guthaben und Reset-Countdowns bei den Anbietern, für die Sie tatsächlich bezahlen – jeweils ein Statuselement oder sie werden zu einem zusammengeführt.", "hero.download": "Herunterladen für macOS", "hero.fineprint": "Kostenlos und Open Source · macOS 14+ · Universal über GitHub Releases und Homebrew", - "providers.title": "60 Provider,{mobileBreak}eine Menüleiste", + "providers.title": "61 Provider,{mobileBreak}eine Menüleiste", "providers.description": "Beliebte Anbieter werden zu Statuselementen mit eigenen Nutzungsfenstern, Reset-Countdowns, Diagrammen und Anbietermenüs.", "providers.yourProvider": "Ihr Anbieter", "providers.authoringGuide": "Autorenleitfaden", @@ -1226,8 +1226,8 @@ export const localeMessages = { }, "fr": { "meta.title": "CodexBar — chaque limite de codage IA dans votre barre de menus", - "meta.description": "Une petite application de barre de menus macOS qui suit les fenêtres d'utilisation, les crédits, les coûts et les réinitialisations des fournisseurs de codage d'IA sur 60 fournisseurs : Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, et plus encore.", - "meta.ogDescription": "Suivez les fenêtres d'utilisation, les crédits et les réinitialisations auprès de 60 fournisseurs de codage d'IA à partir de votre barre de menus macOS.", + "meta.description": "Une petite application de barre de menus macOS qui suit les fenêtres d'utilisation, les crédits, les coûts et les réinitialisations des fournisseurs de codage d'IA sur 61 fournisseurs : Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, et plus encore.", + "meta.ogDescription": "Suivez les fenêtres d'utilisation, les crédits et les réinitialisations auprès de 61 fournisseurs de codage d'IA à partir de votre barre de menus macOS.", "nav.primary": "Primaire", "nav.language": "Langue", "nav.docs": "Documents", @@ -1240,7 +1240,7 @@ export const localeMessages = { "hero.description": "CodexBar suit les fenêtres d'utilisation, les soldes créditeurs et réinitialise les comptes à rebours pour les fournisseurs pour lesquels vous payez réellement - un élément de statut chacun, ou les fusionne en un seul.", "hero.download": "Télécharger pour macOS", "hero.fineprint": "Gratuit et open source · macOS 14+ · Universel via GitHub Releases et Homebrew", - "providers.title": "60 fournisseurs,{mobileBreak}une barre des menus", + "providers.title": "61 fournisseurs,{mobileBreak}une barre des menus", "providers.description": "Les fournisseurs populaires deviennent des éléments de statut avec leurs propres fenêtres d'utilisation, réinitialisent les comptes à rebours, les graphiques et les menus des fournisseurs.", "providers.yourProvider": "Votre fournisseur", "providers.authoringGuide": "Guide de création", @@ -1367,8 +1367,8 @@ export const localeMessages = { }, "ar": { "meta.title": "CodexBar — كل حد لترميز الذكاء الاصطناعي في شريط القائمة", - "meta.description": "تطبيق شريط قوائم macOS صغير الحجم يتتبع نوافذ استخدام موفر ترميز الذكاء الاصطناعي، والائتمانات، والتكاليف، وعمليات إعادة التعيين عبر 60 موفرًا - Codex، وOpenAI، وClaude، وCursor، وGemini، وCopilot، وLiteLLM، والمزيد.", - "meta.ogDescription": "تتبع نوافذ الاستخدام والأرصدة وعمليات إعادة التعيين عبر 60 موفرًا لترميز الذكاء الاصطناعي من شريط القائمة macOS.", + "meta.description": "تطبيق شريط قوائم macOS صغير الحجم يتتبع نوافذ استخدام موفر ترميز الذكاء الاصطناعي، والائتمانات، والتكاليف، وعمليات إعادة التعيين عبر 61 موفرًا - Codex، وOpenAI، وClaude، وCursor، وGemini، وCopilot، وLiteLLM، والمزيد.", + "meta.ogDescription": "تتبع نوافذ الاستخدام والأرصدة وعمليات إعادة التعيين عبر 61 موفرًا لترميز الذكاء الاصطناعي من شريط القائمة macOS.", "nav.primary": "أساسي", "nav.language": "لغة", "nav.docs": "المستندات", @@ -1381,7 +1381,7 @@ export const localeMessages = { "hero.description": "يتتبع CodexBar فترات الاستخدام والأرصدة الائتمانية وإعادة تعيين العد التنازلي عبر مقدمي الخدمة الذين تدفع مقابلهم فعليًا — عنصر حالة واحد لكل منهم، أو دمجهم في عنصر واحد.", "hero.download": "التنزيل لمدة macOS", "hero.fineprint": "مجاني ومفتوح المصدر · macOS 14+ · إصدار شامل عبر GitHub Releases وHomebrew", - "providers.title": "60 مزودًا،{mobileBreak}شريط قوائم واحد", + "providers.title": "61 مزودًا،{mobileBreak}شريط قوائم واحد", "providers.description": "يصبح الموفرون المشهورون عناصر حالة مع نوافذ الاستخدام الخاصة بهم، وعمليات إعادة تعيين العد التنازلي، والمخططات، وقوائم الموفر.", "providers.yourProvider": "المزود الخاص بك", "providers.authoringGuide": "دليل التأليف", @@ -1508,8 +1508,8 @@ export const localeMessages = { }, "it": { "meta.title": "CodexBar: ogni limite di codifica AI nella barra dei menu", - "meta.description": "Una piccola app della barra dei menu macOS che tiene traccia delle finestre di utilizzo, dei crediti, dei costi e dei ripristini del fornitore di codifica AI tra 60 fornitori: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e altri.", - "meta.ogDescription": "Tieni traccia delle finestre di utilizzo, dei crediti e dei ripristini tra 60 fornitori di codifica AI dalla barra dei menu macOS.", + "meta.description": "Una piccola app della barra dei menu macOS che tiene traccia delle finestre di utilizzo, dei crediti, dei costi e dei ripristini del fornitore di codifica AI tra 61 fornitori: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e altri.", + "meta.ogDescription": "Tieni traccia delle finestre di utilizzo, dei crediti e dei ripristini tra 61 fornitori di codifica AI dalla barra dei menu macOS.", "nav.primary": "Primario", "nav.language": "Lingua", "nav.docs": "Documenti", @@ -1522,7 +1522,7 @@ export const localeMessages = { "hero.description": "CodexBar tiene traccia delle finestre di utilizzo, dei saldi del credito e reimposta i conti alla rovescia tra i fornitori per cui paghi effettivamente: un elemento di stato ciascuno o uniscili in uno solo.", "hero.download": "Scarica per macOS", "hero.fineprint": "Gratuito e open source · macOS 14+ · Universale tramite GitHub Releases e Homebrew", - "providers.title": "60 provider,{mobileBreak}una barra dei menu", + "providers.title": "61 provider,{mobileBreak}una barra dei menu", "providers.description": "I fornitori più popolari diventano elementi di stato con le proprie finestre di utilizzo, reimpostano i conti alla rovescia, i grafici e i menu dei fornitori.", "providers.yourProvider": "Il tuo fornitore", "providers.authoringGuide": "Guida all'autore", @@ -1649,8 +1649,8 @@ export const localeMessages = { }, "vi": { "meta.title": "CodexBar — mọi giới hạn mã hóa AI trong thanh menu của bạn", - "meta.description": "Một ứng dụng thanh menu macOS nhỏ theo dõi khoảng thời gian sử dụng, tín dụng, chi phí và đặt lại của nhà cung cấp mã hóa AI trên 60 nhà cung cấp — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, v.v.", - "meta.ogDescription": "Theo dõi khoảng thời gian sử dụng, tín dụng và đặt lại trên 60 nhà cung cấp mã hóa AI từ thanh menu macOS của bạn.", + "meta.description": "Một ứng dụng thanh menu macOS nhỏ theo dõi khoảng thời gian sử dụng, tín dụng, chi phí và đặt lại của nhà cung cấp mã hóa AI trên 61 nhà cung cấp — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, v.v.", + "meta.ogDescription": "Theo dõi khoảng thời gian sử dụng, tín dụng và đặt lại trên 61 nhà cung cấp mã hóa AI từ thanh menu macOS của bạn.", "nav.primary": "Sơ đẳng", "nav.language": "Ngôn ngữ", "nav.docs": "Tài liệu", @@ -1663,7 +1663,7 @@ export const localeMessages = { "hero.description": "CodexBar theo dõi khoảng thời gian sử dụng, số dư tín dụng và đếm ngược đặt lại trên các nhà cung cấp mà bạn thực sự thanh toán — mỗi mục một trạng thái hoặc hợp nhất chúng thành một.", "hero.download": "Tải xuống cho macOS", "hero.fineprint": "Miễn phí và nguồn mở · macOS 14+ · Bản universal qua GitHub Releases và Homebrew", - "providers.title": "60 nhà cung cấp,{mobileBreak}một thanh menu", + "providers.title": "61 nhà cung cấp,{mobileBreak}một thanh menu", "providers.description": "Các nhà cung cấp phổ biến trở thành các mục trạng thái với cửa sổ sử dụng của riêng họ, đặt lại bộ đếm ngược, biểu đồ và menu nhà cung cấp.", "providers.yourProvider": "Nhà cung cấp của bạn", "providers.authoringGuide": "Hướng dẫn soạn thảo", @@ -1790,8 +1790,8 @@ export const localeMessages = { }, "nl": { "meta.title": "CodexBar — elke AI-coderingslimiet in uw menubalk", - "meta.description": "Een kleine macOS menubalk-app die gebruiksperioden, tegoeden, kosten en resets van AI-coderingsproviders bijhoudt bij 60 providers: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM en meer.", - "meta.ogDescription": "Houd gebruiksvensters, tegoeden en resets bij van 60 leveranciers van AI-codering via uw macOS-menubalk.", + "meta.description": "Een kleine macOS menubalk-app die gebruiksperioden, tegoeden, kosten en resets van AI-coderingsproviders bijhoudt bij 61 providers: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM en meer.", + "meta.ogDescription": "Houd gebruiksvensters, tegoeden en resets bij van 61 leveranciers van AI-codering via uw macOS-menubalk.", "nav.primary": "Primair", "nav.language": "Taal", "nav.docs": "Documenten", @@ -1804,7 +1804,7 @@ export const localeMessages = { "hero.description": "CodexBar houdt gebruiksperioden, tegoeden en reset-countdowns bij voor de providers waarvoor u daadwerkelijk betaalt: elk één statusitem, of u kunt ze samenvoegen tot één statusitem.", "hero.download": "Downloaden voor macOS", "hero.fineprint": "Gratis en open source · macOS 14+ · Universeel via GitHub Releases en Homebrew", - "providers.title": "60 providers,{mobileBreak}één menubalk", + "providers.title": "61 providers,{mobileBreak}één menubalk", "providers.description": "Populaire providers worden statusitems met hun eigen gebruiksvensters, resetcountdowns, grafieken en providermenu's.", "providers.yourProvider": "Uw aanbieder", "providers.authoringGuide": "Handleiding voor het schrijven", @@ -1931,8 +1931,8 @@ export const localeMessages = { }, "tr": { "meta.title": "CodexBar — menü çubuğunuzdaki tüm AI kodlama limitleri", - "meta.description": "AI kodlama sağlayıcısı kullanım pencerelerini, kredilerini, maliyetlerini izleyen ve Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM ve daha fazlası olmak üzere 60 sağlayıcı genelinde sıfırlamaları izleyen küçük bir macOS menü çubuğu uygulaması.", - "meta.ogDescription": "macOS menü çubuğunu kullanarak 60 AI kodlama sağlayıcısındaki kullanım pencerelerini, kredileri ve sıfırlamaları izleyin.", + "meta.description": "AI kodlama sağlayıcısı kullanım pencerelerini, kredilerini, maliyetlerini izleyen ve Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM ve daha fazlası olmak üzere 61 sağlayıcı genelinde sıfırlamaları izleyen küçük bir macOS menü çubuğu uygulaması.", + "meta.ogDescription": "macOS menü çubuğunu kullanarak 61 AI kodlama sağlayıcısındaki kullanım pencerelerini, kredileri ve sıfırlamaları izleyin.", "nav.primary": "Öncelik", "nav.language": "Dil", "nav.docs": "Dokümanlar", @@ -1945,7 +1945,7 @@ export const localeMessages = { "hero.description": "CodexBar, gerçekte ödeme yaptığınız sağlayıcılar genelinde kullanım pencerelerini, kredi bakiyelerini ve geri sayımları sıfırlar (her biri bir durum öğesi olacak şekilde) izler veya bunları tek bir öğede birleştirir.", "hero.download": "macOS için indirin", "hero.fineprint": "Ücretsiz ve açık kaynak · macOS 14+ · GitHub Releases ve Homebrew ile evrensel sürüm", - "providers.title": "60 sağlayıcı,{mobileBreak}bir menü çubuğu", + "providers.title": "61 sağlayıcı,{mobileBreak}bir menü çubuğu", "providers.description": "Popüler sağlayıcılar, kendi kullanım pencereleri, sıfırlama geri sayımları, çizelgeleri ve sağlayıcı menüleriyle durum öğeleri haline gelir.", "providers.yourProvider": "Sağlayıcınız", "providers.authoringGuide": "Yazma kılavuzu", @@ -2072,8 +2072,8 @@ export const localeMessages = { }, "uk": { "meta.title": "CodexBar — кожне обмеження кодування AI у вашій панелі меню", - "meta.description": "Маленький додаток macOS на панелі меню, який відстежує вікна використання постачальників кодування штучного інтелекту, кредити, витрати та скидання 60 постачальників — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM тощо.", - "meta.ogDescription": "Відстежуйте вікна використання, кредити та скидання 60 постачальників кодування ШІ за допомогою панелі меню macOS.", + "meta.description": "Маленький додаток macOS на панелі меню, який відстежує вікна використання постачальників кодування штучного інтелекту, кредити, витрати та скидання 61 постачальників — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM тощо.", + "meta.ogDescription": "Відстежуйте вікна використання, кредити та скидання 61 постачальників кодування ШІ за допомогою панелі меню macOS.", "nav.primary": "Первинний", "nav.language": "Мова", "nav.docs": "документи", @@ -2086,7 +2086,7 @@ export const localeMessages = { "hero.description": "CodexBar відстежує вікна використання, кредитні баланси та скидає зворотний відлік для постачальників, за яких ви фактично платите, — по одному статусу для кожного або об’єднує їх в один.", "hero.download": "Завантажити для macOS", "hero.fineprint": "Безкоштовний і відкритий код · macOS 14+ · Універсальна версія через GitHub Releases і Homebrew", - "providers.title": "60 провайдерів,{mobileBreak}одна панель меню", + "providers.title": "61 провайдерів,{mobileBreak}одна панель меню", "providers.description": "Популярні постачальники стають елементами статусу з власними вікнами використання, скиданням зворотного відліку, діаграмами та меню постачальників.", "providers.yourProvider": "Ваш провайдер", "providers.authoringGuide": "Авторський посібник", @@ -2213,8 +2213,8 @@ export const localeMessages = { }, "ru": { "meta.title": "CodexBar — все лимиты AI-кодинга в вашей строке меню", - "meta.description": "Небольшое приложение для строки меню macOS, которое отслеживает окна использования, кредиты, расходы и сбросы лимитов у 60 AI-провайдеров для кодинга — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM и других.", - "meta.ogDescription": "Отслеживайте окна использования, кредиты и сбросы лимитов у 60 AI-провайдеров для кодинга прямо из строки меню macOS.", + "meta.description": "Небольшое приложение для строки меню macOS, которое отслеживает окна использования, кредиты, расходы и сбросы лимитов у 61 AI-провайдеров для кодинга — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM и других.", + "meta.ogDescription": "Отслеживайте окна использования, кредиты и сбросы лимитов у 61 AI-провайдеров для кодинга прямо из строки меню macOS.", "nav.primary": "Основная навигация", "nav.language": "Язык", "nav.docs": "Документация", @@ -2227,7 +2227,7 @@ export const localeMessages = { "hero.description": "CodexBar отслеживает окна использования, балансы кредитов и обратный отсчет до сброса у провайдеров, за которых вы действительно платите, — по одному элементу статуса на каждого или все вместе в одном.", "hero.download": "Скачать для macOS", "hero.fineprint": "Бесплатно и с открытым исходным кодом · macOS 14+ · универсальная сборка через GitHub Releases и Homebrew", - "providers.title": "60 провайдеров,{mobileBreak}одна строка меню", + "providers.title": "61 провайдеров,{mobileBreak}одна строка меню", "providers.description": "Популярные провайдеры становятся элементами статуса со своими окнами использования, обратным отсчетом до сброса, графиками и меню провайдера.", "providers.yourProvider": "Ваш провайдер", "providers.authoringGuide": "Руководство по добавлению", @@ -2354,8 +2354,8 @@ export const localeMessages = { }, "id": { "meta.title": "CodexBar — setiap batas pengkodean AI di bilah menu Anda", - "meta.description": "Aplikasi bilah menu macOS kecil yang melacak periode penggunaan, kredit, biaya, dan penyetelan ulang penyedia pengkodean AI di 60 penyedia — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, dan banyak lagi.", - "meta.ogDescription": "Lacak jangka waktu penggunaan, kredit, dan penyetelan ulang di 60 penyedia pengkodean AI dari bilah menu macOS Anda.", + "meta.description": "Aplikasi bilah menu macOS kecil yang melacak periode penggunaan, kredit, biaya, dan penyetelan ulang penyedia pengkodean AI di 61 penyedia — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, dan banyak lagi.", + "meta.ogDescription": "Lacak jangka waktu penggunaan, kredit, dan penyetelan ulang di 61 penyedia pengkodean AI dari bilah menu macOS Anda.", "nav.primary": "Utama", "nav.language": "Bahasa", "nav.docs": "dokumen", @@ -2368,7 +2368,7 @@ export const localeMessages = { "hero.description": "CodexBar melacak jangka waktu penggunaan, saldo kredit, dan hitungan mundur penyetelan ulang di seluruh penyedia yang sebenarnya Anda bayar — masing-masing satu item status, atau gabungkan menjadi satu.", "hero.download": "Unduh untuk macOS", "hero.fineprint": "Gratis dan sumber terbuka · macOS 14+ · Universal melalui GitHub Releases dan Homebrew", - "providers.title": "60 penyedia,{mobileBreak}satu bilah menu", + "providers.title": "61 penyedia,{mobileBreak}satu bilah menu", "providers.description": "Penyedia populer menjadi item status dengan jendela penggunaannya sendiri, hitung mundur pengaturan ulang, bagan, dan menu penyedia.", "providers.yourProvider": "Penyedia Anda", "providers.authoringGuide": "Panduan penulisan", @@ -2495,8 +2495,8 @@ export const localeMessages = { }, "pl": { "meta.title": "CodexBar — każdy limit kodowania AI na pasku menu", - "meta.description": "Mała aplikacja z paskiem menu macOS, która śledzi okna użycia dostawcy kodowania AI, kredyty, koszty i resety u 60 dostawców — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM i nie tylko.", - "meta.ogDescription": "Śledź okresy użytkowania, kredyty i resety u 60 dostawców kodowania AI za pomocą paska menu macOS.", + "meta.description": "Mała aplikacja z paskiem menu macOS, która śledzi okna użycia dostawcy kodowania AI, kredyty, koszty i resety u 61 dostawców — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM i nie tylko.", + "meta.ogDescription": "Śledź okresy użytkowania, kredyty i resety u 61 dostawców kodowania AI za pomocą paska menu macOS.", "nav.primary": "Podstawowy", "nav.language": "Język", "nav.docs": "Dokumenty", @@ -2509,7 +2509,7 @@ export const localeMessages = { "hero.description": "CodexBar śledzi okna użytkowania, salda kredytów i resetuje odliczanie u dostawców, za których faktycznie płacisz — po jednym statusie dla każdego lub połącz je w jeden.", "hero.download": "Pobierz dla macOS", "hero.fineprint": "Darmowe i otwarte oprogramowanie · macOS 14+ · Wersja uniwersalna przez GitHub Releases i Homebrew", - "providers.title": "60 dostawców,{mobileBreak}jeden pasek menu", + "providers.title": "61 dostawców,{mobileBreak}jeden pasek menu", "providers.description": "Popularni dostawcy stają się elementami statusu z własnymi oknami użytkowania, resetowaniem odliczania, wykresami i menu dostawców.", "providers.yourProvider": "Twój dostawca", "providers.authoringGuide": "Przewodnik autorski", @@ -2636,8 +2636,8 @@ export const localeMessages = { }, "fa": { "meta.title": "CodexBar - هر محدودیت کدنویسی هوش مصنوعی در نوار منو شما", - "meta.description": "یک برنامه نوار منو کوچک macOS که پنجره‌های استفاده از ارائه‌دهنده کدنویسی هوش مصنوعی، اعتبارات، هزینه‌ها، و بازنشانی را در بین 60 ارائه‌دهنده - Codex، OpenAI، Claude، Cursor، Gemini، Copilot، LiteLLM و موارد دیگر بازنشانی می‌کند.", - "meta.ogDescription": "پنجره‌های استفاده، اعتبارات و بازنشانی‌ها را در بین 60 ارائه‌دهنده کدنویسی هوش مصنوعی از نوار منوی macOS خود ردیابی کنید.", + "meta.description": "یک برنامه نوار منو کوچک macOS که پنجره‌های استفاده از ارائه‌دهنده کدنویسی هوش مصنوعی، اعتبارات، هزینه‌ها، و بازنشانی را در بین 61 ارائه‌دهنده - Codex، OpenAI، Claude، Cursor، Gemini، Copilot، LiteLLM و موارد دیگر بازنشانی می‌کند.", + "meta.ogDescription": "پنجره‌های استفاده، اعتبارات و بازنشانی‌ها را در بین 61 ارائه‌دهنده کدنویسی هوش مصنوعی از نوار منوی macOS خود ردیابی کنید.", "nav.primary": "اولیه", "nav.language": "زبان", "nav.docs": "اسناد", @@ -2650,7 +2650,7 @@ export const localeMessages = { "hero.description": "CodexBar پنجره‌های استفاده، مانده اعتبار و بازنشانی شمارش معکوس را در سراسر ارائه‌دهندگانی که واقعاً برایشان پول پرداخت می‌کنید ردیابی می‌کند - هر کدام یک مورد وضعیت، یا آنها را در یکی ادغام کنید.", "hero.download": "دانلود برای macOS", "hero.fineprint": "رایگان و متن‌باز · macOS 14+ · نسخهٔ یونیورسال از GitHub Releases و Homebrew", - "providers.title": "60 ارائه دهنده،{mobileBreak}یک نوار منو", + "providers.title": "61 ارائه دهنده،{mobileBreak}یک نوار منو", "providers.description": "ارائه‌دهندگان محبوب با پنجره‌های استفاده خاص خود، شمارش معکوس، نمودارها و منوهای ارائه‌دهنده را بازنشانی می‌کنند.", "providers.yourProvider": "ارائه دهنده شما", "providers.authoringGuide": "راهنمای نگارش", @@ -2777,8 +2777,8 @@ export const localeMessages = { }, "th": { "meta.title": "CodexBar — ทุกขีดจำกัดการเข้ารหัส AI ในแถบเมนูของคุณ", - "meta.description": "แอปแถบเมนู macOS ขนาดเล็กที่ติดตามกรอบเวลาการใช้งานของผู้ให้บริการเข้ารหัส AI เครดิต ต้นทุน และการรีเซ็ตในผู้ให้บริการ 60 ราย — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM และอีกมากมาย", - "meta.ogDescription": "ติดตามกรอบเวลาการใช้งาน เครดิต และการรีเซ็ตในผู้ให้บริการการเข้ารหัส AI 60 รายจากแถบเมนู macOS", + "meta.description": "แอปแถบเมนู macOS ขนาดเล็กที่ติดตามกรอบเวลาการใช้งานของผู้ให้บริการเข้ารหัส AI เครดิต ต้นทุน และการรีเซ็ตในผู้ให้บริการ 61 ราย — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM และอีกมากมาย", + "meta.ogDescription": "ติดตามกรอบเวลาการใช้งาน เครดิต และการรีเซ็ตในผู้ให้บริการการเข้ารหัส AI 61 รายจากแถบเมนู macOS", "nav.primary": "หลัก", "nav.language": "ภาษา", "nav.docs": "เอกสาร", @@ -2791,7 +2791,7 @@ export const localeMessages = { "hero.description": "CodexBar ติดตามกรอบเวลาการใช้งาน ยอดเครดิต และรีเซ็ตการนับถอยหลังของผู้ให้บริการที่คุณชำระเงินจริง — รายการสถานะแต่ละรายการ หรือรวมรายการเหล่านั้นเป็นรายการเดียว", "hero.download": "ดาวน์โหลดสำหรับ macOS", "hero.fineprint": "ฟรีและโอเพ่นซอร์ส · macOS 14+ · รุ่น Universal ผ่าน GitHub Releases และ Homebrew", - "providers.title": "ผู้ให้บริการ 60 ราย{mobileBreak}หนึ่งแถบเมนู", + "providers.title": "ผู้ให้บริการ 61 ราย{mobileBreak}หนึ่งแถบเมนู", "providers.description": "ผู้ให้บริการยอดนิยมจะกลายเป็นรายการสถานะที่มีหน้าต่างการใช้งานของตนเอง รีเซ็ตการนับถอยหลัง แผนภูมิ และเมนูของผู้ให้บริการ", "providers.yourProvider": "ผู้ให้บริการของคุณ", "providers.authoringGuide": "คู่มือการเขียน", @@ -2918,8 +2918,8 @@ export const localeMessages = { }, "gl": { "meta.title": "CodexBar — todos os límites de programación con IA na túa barra de menús", - "meta.description": "Unha pequena aplicación de barra de menús para macOS que controla as xanelas de uso, os créditos, os custos e os restablecementos de 60 provedores de programación con IA — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e máis.", - "meta.ogDescription": "Controla as xanelas de uso, os créditos e os restablecementos de 60 provedores de programación con IA desde a barra de menús de macOS.", + "meta.description": "Unha pequena aplicación de barra de menús para macOS que controla as xanelas de uso, os créditos, os custos e os restablecementos de 61 provedores de programación con IA — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e máis.", + "meta.ogDescription": "Controla as xanelas de uso, os créditos e os restablecementos de 61 provedores de programación con IA desde a barra de menús de macOS.", "nav.primary": "Principal", "nav.language": "Idioma", "nav.docs": "Documentación", @@ -2932,7 +2932,7 @@ export const localeMessages = { "hero.description": "CodexBar controla as xanelas de uso, os saldos de crédito e as contas atrás ata o restablecemento dos provedores polos que realmente pagas — un elemento de estado para cada un ou todos combinados nun só.", "hero.download": "Descargar para macOS", "hero.fineprint": "Gratuíto e de código aberto · macOS 14+ · Universal mediante GitHub Releases e Homebrew", - "providers.title": "60 provedores,{mobileBreak}unha barra de menús", + "providers.title": "61 provedores,{mobileBreak}unha barra de menús", "providers.description": "Os provedores populares convértense en elementos de estado coas súas propias xanelas de uso, contas atrás de restablecemento, gráficas e menús.", "providers.yourProvider": "O teu provedor", "providers.authoringGuide": "Guía de creación", @@ -3059,8 +3059,8 @@ export const localeMessages = { }, "ca": { "meta.title": "CodexBar: tots els límits de la programació amb IA a la barra de menús", - "meta.description": "Una petita aplicació de barra de menús per a macOS que fa un seguiment de les finestres d'ús, els crèdits, els costos i els restabliments dels proveïdors de programació amb IA: 60 proveïdors, entre els quals Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM i més.", - "meta.ogDescription": "Feu el seguiment de les finestres d'ús, els crèdits i els restabliments de 60 proveïdors de programació amb IA des de la barra de menús del macOS.", + "meta.description": "Una petita aplicació de barra de menús per a macOS que fa un seguiment de les finestres d'ús, els crèdits, els costos i els restabliments dels proveïdors de programació amb IA: 61 proveïdors, entre els quals Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM i més.", + "meta.ogDescription": "Feu el seguiment de les finestres d'ús, els crèdits i els restabliments de 61 proveïdors de programació amb IA des de la barra de menús del macOS.", "nav.primary": "Navegació principal", "nav.language": "Llengua", "nav.docs": "Documentació", @@ -3073,7 +3073,7 @@ export const localeMessages = { "hero.description": "El CodexBar fa el seguiment de les finestres d'ús, els saldos de crèdit i els comptes enrere fins al restabliment dels proveïdors pels quals realment pagueu: un element d'estat per a cadascun, o combineu-los tots en un de sol.", "hero.download": "Baixeu per al macOS", "hero.fineprint": "Gratuït i de codi obert · macOS 14+ · Universal mitjançant GitHub Releases i Homebrew", - "providers.title": "60 proveïdors,{mobileBreak}una barra de menús", + "providers.title": "61 proveïdors,{mobileBreak}una barra de menús", "providers.description": "Els proveïdors populars es converteixen en elements d'estat amb les seves pròpies finestres d'ús, comptes enrere de restabliment, gràfics i menús de proveïdor.", "providers.yourProvider": "El vostre proveïdor", "providers.authoringGuide": "Guia de creació", @@ -3200,8 +3200,8 @@ export const localeMessages = { }, "sv": { "meta.title": "CodexBar — varje AI-kodningsgräns i din menyrad", - "meta.description": "En liten macOS menyradsapp som spårar AI-kodningsleverantörers användningsfönster, krediter, kostnader och återställningar hos 60 leverantörer – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM och mer.", - "meta.ogDescription": "Spåra användningsfönster, krediter och återställningar hos 60 AI-kodningsleverantörer från din macOS-menyrad.", + "meta.description": "En liten macOS menyradsapp som spårar AI-kodningsleverantörers användningsfönster, krediter, kostnader och återställningar hos 61 leverantörer – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM och mer.", + "meta.ogDescription": "Spåra användningsfönster, krediter och återställningar hos 61 AI-kodningsleverantörer från din macOS-menyrad.", "nav.primary": "Primär", "nav.language": "Språk", "nav.docs": "Dokument", @@ -3214,7 +3214,7 @@ export const localeMessages = { "hero.description": "CodexBar spårar användningsfönster, kreditsaldon och återställningsnedräkningar för de leverantörer du faktiskt betalar för – en statuspost var, eller slå samman dem till en.", "hero.download": "Ladda ner för macOS", "hero.fineprint": "Gratis och öppen källkod · macOS 14+ · Universal via GitHub Releases och Homebrew", - "providers.title": "60 leverantörer,{mobileBreak}en menyrad", + "providers.title": "61 leverantörer,{mobileBreak}en menyrad", "providers.description": "Populära leverantörer blir statusobjekt med sina egna användningsfönster, återställer nedräkningar, diagram och leverantörsmenyer.", "providers.yourProvider": "Din leverantör", "providers.authoringGuide": "Författarguide", diff --git a/docs/social.html b/docs/social.html index 255b501739..9c14bb5c6c 100644 --- a/docs/social.html +++ b/docs/social.html @@ -199,7 +199,7 @@

Every AI coding limit, in your menu bar.

-

60 providers·usage windows, credits, resets·one status item each, or merged.

+

61 providers·usage windows, credits, resets·one status item each, or merged.

    From 69015e6f2edc7ea487d3877ed832b8c0f70f700c Mon Sep 17 00:00:00 2001 From: joeVenner Date: Thu, 16 Jul 2026 13:29:45 +0100 Subject: [PATCH 09/13] Split token account label helpers --- .../UsageStore+TokenAccountLabels.swift | 35 +++++++++++++++++++ .../CodexBar/UsageStore+TokenAccounts.swift | 31 ---------------- 2 files changed, 35 insertions(+), 31 deletions(-) create mode 100644 Sources/CodexBar/UsageStore+TokenAccountLabels.swift diff --git a/Sources/CodexBar/UsageStore+TokenAccountLabels.swift b/Sources/CodexBar/UsageStore+TokenAccountLabels.swift new file mode 100644 index 0000000000..7e1329b547 --- /dev/null +++ b/Sources/CodexBar/UsageStore+TokenAccountLabels.swift @@ -0,0 +1,35 @@ +import CodexBarCore +import Foundation + +extension UsageStore { + func applyAccountLabel( + _ snapshot: UsageSnapshot, + provider: UsageProvider, + account: ProviderTokenAccount) -> UsageSnapshot + { + let label = account.label.trimmingCharacters(in: .whitespacesAndNewlines) + guard !label.isEmpty else { return snapshot } + let existing = snapshot.identity(for: provider) + let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines) + let resolvedEmail = (email?.isEmpty ?? true) ? label : email + let identity = ProviderIdentitySnapshot( + providerID: provider, + accountEmail: resolvedEmail, + accountOrganization: existing?.accountOrganization, + loginMethod: existing?.loginMethod) + return snapshot.withIdentity(identity) + } + + func applyCodexVisibleAccountLabel(_ snapshot: UsageSnapshot, account: CodexVisibleAccount) -> UsageSnapshot { + let existing = snapshot.identity(for: .codex) + let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines) + let resolvedEmail = (email?.isEmpty ?? true) ? account.email : email + let loginMethod = existing?.loginMethod ?? account.workspaceLabel + let identity = ProviderIdentitySnapshot( + providerID: .codex, + accountEmail: resolvedEmail, + accountOrganization: existing?.accountOrganization, + loginMethod: loginMethod) + return snapshot.withIdentity(identity) + } +} diff --git a/Sources/CodexBar/UsageStore+TokenAccounts.swift b/Sources/CodexBar/UsageStore+TokenAccounts.swift index 3826a725d1..e06f7f013b 100644 --- a/Sources/CodexBar/UsageStore+TokenAccounts.swift +++ b/Sources/CodexBar/UsageStore+TokenAccounts.swift @@ -1542,35 +1542,4 @@ extension UsageStore { } } } - - func applyAccountLabel( - _ snapshot: UsageSnapshot, - provider: UsageProvider, - account: ProviderTokenAccount) -> UsageSnapshot - { - let label = account.label.trimmingCharacters(in: .whitespacesAndNewlines) - guard !label.isEmpty else { return snapshot } - let existing = snapshot.identity(for: provider) - let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines) - let resolvedEmail = (email?.isEmpty ?? true) ? label : email - let identity = ProviderIdentitySnapshot( - providerID: provider, - accountEmail: resolvedEmail, - accountOrganization: existing?.accountOrganization, - loginMethod: existing?.loginMethod) - return snapshot.withIdentity(identity) - } - - func applyCodexVisibleAccountLabel(_ snapshot: UsageSnapshot, account: CodexVisibleAccount) -> UsageSnapshot { - let existing = snapshot.identity(for: .codex) - let email = existing?.accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines) - let resolvedEmail = (email?.isEmpty ?? true) ? account.email : email - let loginMethod = existing?.loginMethod ?? account.workspaceLabel - let identity = ProviderIdentitySnapshot( - providerID: .codex, - accountEmail: resolvedEmail, - accountOrganization: existing?.accountOrganization, - loginMethod: loginMethod) - return snapshot.withIdentity(identity) - } } From 079293ff5afede271fc6b49672384f2a3a6cfb6b Mon Sep 17 00:00:00 2001 From: joeVenner Date: Thu, 16 Jul 2026 15:05:43 +0100 Subject: [PATCH 10/13] Harden TTY working directory test --- Tests/CodexBarTests/TTYCommandRunnerTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/CodexBarTests/TTYCommandRunnerTests.swift b/Tests/CodexBarTests/TTYCommandRunnerTests.swift index 239fd1f91d..9f052b21c3 100644 --- a/Tests/CodexBarTests/TTYCommandRunnerTests.swift +++ b/Tests/CodexBarTests/TTYCommandRunnerTests.swift @@ -229,7 +229,11 @@ struct TTYCommandRunnerEnvTests { let result = try runner.run( binary: "/bin/pwd", send: "", - options: .init(timeout: Self.harnessPTYTimeout, workingDirectory: dir)) + options: .init( + timeout: Self.harnessPTYTimeout, + workingDirectory: dir, + stopOnSubstrings: [dir.path], + returnOnEmptyProcessExit: true)) let clean = result.text.replacingOccurrences(of: "\r", with: "") #expect(clean.contains(dir.path)) } From 979ec36f041b4a33cfddeb45ac9d1761f4f3fa90 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Jul 2026 07:14:31 +0100 Subject: [PATCH 11/13] test: fix Neuralwatt metric picker expectation --- Tests/CodexBarTests/ProvidersPaneCoverageTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift index c7dc928381..9acb626aa3 100644 --- a/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift +++ b/Tests/CodexBarTests/ProvidersPaneCoverageTests.swift @@ -271,6 +271,8 @@ struct ProvidersPaneCoverageTests { let picker = pane._test_menuBarMetricPicker(for: .neuralwatt) #expect(picker?.options.map(\.id) == [ MenuBarMetricPreference.automatic.rawValue, + MenuBarMetricPreference.primary.rawValue, + MenuBarMetricPreference.secondary.rawValue, ]) } } From 4c0f8c491a6ebcc713bba0c676c68415c7d6640d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Jul 2026 09:37:31 +0100 Subject: [PATCH 12/13] feat: add Neuralwatt confetti palette --- .../NeuralWatt/NeuralWattProviderDescriptor.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift index e904ad9715..e02c047045 100644 --- a/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/NeuralWatt/NeuralWattProviderDescriptor.swift @@ -29,7 +29,12 @@ public enum NeuralWattProviderDescriptor { branding: ProviderBranding( iconStyle: .neuralwatt, iconResourceName: "ProviderIcon-neuralwatt", - color: ProviderColor(red: 0.22, green: 0.85, blue: 0.55)), + color: ProviderColor(red: 0.22, green: 0.85, blue: 0.55), + confettiPalette: [ + ProviderColor(hex: 0x38D98C), + ProviderColor(hex: 0x17243A), + ProviderColor(hex: 0xFFFFFF), + ]), tokenCost: ProviderTokenCostConfig( supportsTokenCost: false, noDataMessage: { "Neuralwatt token cost history is not available via the quota API." }), From 7b63ab4f21cc42b2d0b3e6e3342a121d02516776 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Jul 2026 11:38:53 +0100 Subject: [PATCH 13/13] test: harden Claude swap invocation proof --- .../CLICardsClaudeSwapTests.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Tests/CodexBarTests/CLICardsClaudeSwapTests.swift b/Tests/CodexBarTests/CLICardsClaudeSwapTests.swift index e1fc1604db..1d58bbe6e3 100644 --- a/Tests/CodexBarTests/CLICardsClaudeSwapTests.swift +++ b/Tests/CodexBarTests/CLICardsClaudeSwapTests.swift @@ -308,10 +308,17 @@ struct CLICardsClaudeSwapTests { .appendingPathComponent("cards-claude-swap-tests-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let executable = directory.appendingPathComponent("cswap") - let arguments = directory.appendingPathComponent("arguments") + let invocationMarker = directory.appendingPathComponent("invoked", isDirectory: true) + let duplicateMarker = directory.appendingPathComponent("duplicate") let script = """ #!/bin/sh - printf '%s\\n' "$@" >> '\(arguments.path)' + mkdir '\(invocationMarker.path)' || { + touch '\(duplicateMarker.path)' + exit 2 + } + [ "$#" -eq 2 ] || exit 2 + [ "$1" = "--list" ] || exit 2 + [ "$2" = "--json" ] || exit 2 cat <<'JSON' {"schemaVersion":1,"activeAccountNumber":2,"accounts":[ {"number":1,"email":"one@example.com","active":false,"usageStatus":"api_key"}, @@ -327,11 +334,12 @@ struct CLICardsClaudeSwapTests { executablePath: executable.path, renderOptions: self.renderOptions(), ambientFetch: { self.ambientOutput() }) - let invokedArguments = try String(contentsOf: arguments, encoding: .utf8) - #expect(invokedArguments == "--list\n--json\n") - #expect(!invokedArguments.contains("switch")) + #expect(output.exitCode == .success) + #expect(output.cardFailures.isEmpty) #expect(output.cards.count == 2) + #expect(FileManager.default.fileExists(atPath: invocationMarker.path)) + #expect(!FileManager.default.fileExists(atPath: duplicateMarker.path)) } @Test