diff --git a/README.md b/README.md index 3ef5206fd9..a244c605b4 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. 66 providers. +CodexBar — every AI coding limit in your menu bar. 67 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, ClinePass, 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. @@ -117,6 +117,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow. - [Abacus AI](docs/abacus.md) — Browser cookie auth for ChatLLM/RouteLLM compute credit tracking. - [Mistral](docs/mistral.md) — Browser cookies for API spend, credit balance, and monthly-plan usage. - [DeepSeek](docs/deepseek.md) — API key for credit balance tracking (paid vs. granted breakdown). +- [Charm Hyper](docs/hyper.md) — Signed-in session or API key for remaining Hypercredit balance. - [DeepInfra](docs/deepinfra.md) — API key for prepaid balance, current-month spend, and spending-limit tracking. - [Moonshot / Kimi API](docs/moonshot.md) — API key for Moonshot/Kimi API account balance tracking. - [Venice](docs/venice.md) — API key for DIEM or USD balance tracking. diff --git a/Sources/CodexBar/MenuCardView+Costs.swift b/Sources/CodexBar/MenuCardView+Costs.swift index 33921e7856..894e1228dd 100644 --- a/Sources/CodexBar/MenuCardView+Costs.swift +++ b/Sources/CodexBar/MenuCardView+Costs.swift @@ -127,7 +127,9 @@ extension UsageMenuCardView.Model { preferredCurrencyCode: String = "auto") -> String? { guard metadata.supportsCredits else { return nil } - if metadata.id == .codex, credits == nil, error == nil { return nil } + if metadata.id == .codex, credits == nil, error == nil { + return nil + } if metadata.id == .amp, let ampUsage = snapshot?.ampUsage, let ampCredits = self.ampCreditsLine(ampUsage, preferredCurrencyCode: preferredCurrencyCode) @@ -338,13 +340,19 @@ extension UsageMenuCardView.Model { return (entry, dayKey) } .max { lhs, rhs in - if lhs.dayKey != rhs.dayKey { return lhs.dayKey < rhs.dayKey } + if lhs.dayKey != rhs.dayKey { + return lhs.dayKey < rhs.dayKey + } let lCost = lhs.entry.costUSD ?? -1 let rCost = rhs.entry.costUSD ?? -1 - if lCost != rCost { return lCost < rCost } + if lCost != rCost { + return lCost < rCost + } let lTokens = lhs.entry.totalTokens ?? -1 let rTokens = rhs.entry.totalTokens ?? -1 - if lTokens != rTokens { return lTokens < rTokens } + if lTokens != rTokens { + return lTokens < rTokens + } return lhs.entry.date < rhs.entry.date }?.entry } @@ -396,8 +404,12 @@ extension UsageMenuCardView.Model { private static func daysInBedrockBillingMonth(_ month: Int, year: Int) -> Int { switch month { case 2: - if year.isMultiple(of: 400) { return 29 } - if year.isMultiple(of: 100) { return 28 } + if year.isMultiple(of: 400) { + return 29 + } + if year.isMultiple(of: 100) { + return 28 + } return year.isMultiple(of: 4) ? 29 : 28 case 4, 6, 9, 11: return 30 @@ -406,6 +418,54 @@ extension UsageMenuCardView.Model { } } + /// Providers whose cost snapshot is a plain prepaid balance with no limit or period to chart. + private static func balanceOnlyCostSection( + provider: UsageProvider, + cost: ProviderCostSnapshot, + formatCost: (Double) -> String) -> ProviderCostSection? + { + func balanceSection(title: String, balance: String) -> ProviderCostSection { + ProviderCostSection( + title: title, + percentUsed: nil, + spendLine: "\(L("Balance")): \(balance)", + percentLine: nil) + } + + if provider == .factory || provider == .devin, cost.period == "Extra usage balance" { + return balanceSection(title: L("Extra usage"), balance: formatCost(cost.used)) + } + + if provider == .opencodego, cost.period == "Zen balance" { + return balanceSection(title: L("Zen balance"), balance: formatCost(cost.used)) + } + + if provider == .minimax, cost.period == "MiniMax points balance" { + return balanceSection(title: L("Credits"), balance: String(format: "%.0f", cost.used)) + } + + if provider == .hyper, + cost.period == "Hypercredits balance", + let value = cost.balance + { + let balance = value.rounded() == value + ? String(format: "%.0f", value) + : String(format: "%.2f", value) + return balanceSection(title: "Hypercredits", balance: "\(balance) HC") + } + + if provider == .xai, cost.period == "Prepaid credits" { + let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) + return balanceSection(title: L("Credits"), balance: balance) + } + + if provider == .zenmux || provider == .neuralwatt { + return balanceSection(title: L("metric_mistral_payg"), balance: formatCost(cost.used)) + } + + return nil + } + static func providerCostSection( provider: UsageProvider, cost: ProviderCostSnapshot?, @@ -426,49 +486,12 @@ extension UsageMenuCardView.Model { providerCurrency: providerCurrency ?? cost.currencyCode) } - if provider == .factory || provider == .devin, cost.period == "Extra usage balance" { - let balance = formatCost(cost.used) - return ProviderCostSection( - title: L("Extra usage"), - percentUsed: nil, - spendLine: "\(L("Balance")): \(balance)", - percentLine: nil) - } - - if provider == .opencodego, cost.period == "Zen balance" { - let balance = formatCost(cost.used) - return ProviderCostSection( - title: L("Zen balance"), - percentUsed: nil, - spendLine: "\(L("Balance")): \(balance)", - percentLine: nil) - } - - if provider == .minimax, cost.period == "MiniMax points balance" { - let balance = String(format: "%.0f", cost.used) - return ProviderCostSection( - title: L("Credits"), - percentUsed: nil, - spendLine: "\(L("Balance")): \(balance)", - percentLine: nil) - } - - if provider == .xai, cost.period == "Prepaid credits" { - let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) - return ProviderCostSection( - title: L("Credits"), - percentUsed: nil, - spendLine: "\(L("Balance")): \(balance)", - percentLine: nil) - } - - if provider == .zenmux || provider == .neuralwatt { - let balance = formatCost(cost.used) - return ProviderCostSection( - title: L("metric_mistral_payg"), - percentUsed: nil, - spendLine: "\(L("Balance")): \(balance)", - percentLine: nil) + if let section = Self.balanceOnlyCostSection( + provider: provider, + cost: cost, + formatCost: { formatCost($0) }) + { + return section } if provider == .claude { diff --git a/Sources/CodexBar/Providers/Hyper/HyperProviderImplementation.swift b/Sources/CodexBar/Providers/Hyper/HyperProviderImplementation.swift new file mode 100644 index 0000000000..c17eb745cc --- /dev/null +++ b/Sources/CodexBar/Providers/Hyper/HyperProviderImplementation.swift @@ -0,0 +1,109 @@ +import AppKit +import CodexBarCore +import Foundation +import SwiftUI + +struct HyperProviderImplementation: ProviderImplementation { + let id: UsageProvider = .hyper + + @MainActor + func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { + ProviderPresentation { context in + context.store.sourceLabel(for: context.provider) + } + } + + @MainActor + func observeSettings(_ settings: SettingsStore) { + _ = settings.hyperAPIKey + _ = settings.hyperCookieSource + _ = settings.hyperCookieHeader + _ = settings.tokenAccountsData(for: .hyper) + } + + @MainActor + func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? { + .hyper(context.settings.hyperSettingsSnapshot()) + } + + @MainActor + func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] { + let binding = Binding( + get: { context.settings.hyperCookieSource.rawValue }, + set: { raw in + context.settings.hyperCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto + }) + let options = ProviderCookieSourceUI.options( + allowsOff: true, + keychainDisabled: context.settings.debugDisableKeychainAccess) + let subtitle: () -> String? = { + ProviderCookieSourceUI.subtitle( + source: context.settings.hyperCookieSource, + keychainDisabled: context.settings.debugDisableKeychainAccess, + auto: "Prefer a signed-in Hyper session from Chrome, then fall back to an API key.", + manual: "Paste a Cookie header from hyper.charm.land.", + off: "Use only the configured API key.") + } + + return [ + ProviderSettingsPickerDescriptor( + id: "hyper-cookie-source", + title: "Session source", + subtitle: "Prefer a signed-in Hyper session, then fall back to an API key.", + dynamicSubtitle: subtitle, + binding: binding, + options: options, + isVisible: nil, + onChange: nil, + trailingText: { + ProviderCookieRefreshAction.trailingText( + provider: .hyper, + cookieSource: context.settings.hyperCookieSource, + context: context) + }, + trailingActions: [ + ProviderCookieRefreshAction.descriptor( + provider: .hyper, + cookieSource: { context.settings.hyperCookieSource }, + context: context), + ]), + ] + } + + @MainActor + func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] { + [ + ProviderSettingsFieldDescriptor( + id: "hyper-cookie", + title: "Hyper cookie", + subtitle: "Paste a Cookie header copied from a signed-in hyper.charm.land request.", + kind: .secure, + placeholder: "Cookie: ...", + binding: context.stringBinding(\.hyperCookieHeader), + actions: [ + ProviderSettingsActionDescriptor( + id: "hyper-open-dashboard", + title: "Open Charm Hyper", + style: .link, + isVisible: nil, + perform: { + if let url = URL(string: "https://hyper.charm.land") { + NSWorkspace.shared.open(url) + } + }), + ], + isVisible: { context.settings.hyperCookieSource == .manual }, + onActivate: nil), + ProviderSettingsFieldDescriptor( + id: "hyper-api-key", + title: "API key", + subtitle: "Fallback when no signed-in Hyper session is available. Stored in the CodexBar config file.", + kind: .secure, + placeholder: "Paste API key…", + binding: context.stringBinding(\.hyperAPIKey), + actions: [], + isVisible: nil, + onActivate: nil), + ] + } +} diff --git a/Sources/CodexBar/Providers/Hyper/HyperSettingsStore.swift b/Sources/CodexBar/Providers/Hyper/HyperSettingsStore.swift new file mode 100644 index 0000000000..f5d141ceb7 --- /dev/null +++ b/Sources/CodexBar/Providers/Hyper/HyperSettingsStore.swift @@ -0,0 +1,40 @@ +import CodexBarCore +import Foundation + +extension SettingsStore { + var hyperCookieSource: ProviderCookieSource { + get { self.resolvedCookieSource(provider: .hyper, fallback: .auto) } + set { + self.updateProviderConfig(provider: .hyper) { entry in + entry.cookieSource = newValue + } + self.logProviderModeChange(provider: .hyper, field: "cookieSource", value: newValue.rawValue) + } + } + + var hyperCookieHeader: String { + get { self.configSnapshot.providerConfig(for: .hyper)?.sanitizedCookieHeader ?? "" } + set { + self.updateProviderConfig(provider: .hyper) { entry in + entry.cookieHeader = self.normalizedConfigValue(newValue) + } + self.logSecretUpdate(provider: .hyper, field: "cookieHeader", value: newValue) + } + } + + var hyperAPIKey: String { + get { self.configSnapshot.providerConfig(for: .hyper)?.sanitizedAPIKey ?? "" } + set { + self.updateProviderConfig(provider: .hyper) { entry in + entry.apiKey = self.normalizedConfigValue(newValue) + } + self.logSecretUpdate(provider: .hyper, field: "apiKey", value: newValue) + } + } + + func hyperSettingsSnapshot() -> ProviderSettingsSnapshot.CookieProviderSettings { + ProviderSettingsSnapshot.CookieProviderSettings( + cookieSource: self.hyperCookieSource, + manualCookieHeader: self.hyperCookieHeader) + } +} diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift index c811590992..47be002242 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift @@ -56,6 +56,7 @@ enum ProviderImplementationRegistry { case .mistral: MistralProviderImplementation() case .deepseek: DeepSeekProviderImplementation() case .deepinfra: DeepInfraProviderImplementation() + case .hyper: HyperProviderImplementation() case .codebuff: CodebuffProviderImplementation() case .crof: CrofProviderImplementation() case .venice: VeniceProviderImplementation() diff --git a/Sources/CodexBar/Resources/ProviderIcon-hyper.svg b/Sources/CodexBar/Resources/ProviderIcon-hyper.svg new file mode 100644 index 0000000000..25ea99e682 --- /dev/null +++ b/Sources/CodexBar/Resources/ProviderIcon-hyper.svg @@ -0,0 +1,3 @@ + + + diff --git a/Sources/CodexBar/SettingsStore+MenuPreferences.swift b/Sources/CodexBar/SettingsStore+MenuPreferences.swift index 7f28ffa86f..668bd9779c 100644 --- a/Sources/CodexBar/SettingsStore+MenuPreferences.swift +++ b/Sources/CodexBar/SettingsStore+MenuPreferences.swift @@ -342,7 +342,7 @@ extension SettingsStore { static func isBalanceOnlyProvider(_ provider: UsageProvider) -> Bool { switch provider { - case .deepseek, .deepinfra, .mistral, .moonshot, .poe: + case .deepseek, .deepinfra, .hyper, .mistral, .moonshot, .poe: true default: false diff --git a/Sources/CodexBar/StatusItemController+Animation.swift b/Sources/CodexBar/StatusItemController+Animation.swift index 209a4b2626..ee70542027 100644 --- a/Sources/CodexBar/StatusItemController+Animation.swift +++ b/Sources/CodexBar/StatusItemController+Animation.swift @@ -872,6 +872,11 @@ extension StatusItemController { { return balance } + if provider == .hyper, + let balance = Self.hyperBalanceDisplayText(snapshot: snapshot) + { + return balance + } if provider == .mimo, let balance = Self.miMoBalanceDisplayText( snapshot: snapshot, @@ -1015,6 +1020,23 @@ extension StatusItemController { return prefix + String(value) } + nonisolated static func hyperBalanceDisplayText(snapshot: UsageSnapshot?) -> String? { + guard snapshot?.primary == nil, + snapshot?.secondary == nil, + let cost = snapshot?.providerCost, + cost.period == "Hypercredits balance", + let value = cost.balance, + value.isFinite, + value >= 0 + else { + return nil + } + let balance = value.rounded() == value + ? String(format: "%.0f", value) + : String(format: "%.2f", value) + return "\(balance) HC" + } + nonisolated static func miMoBalanceDisplayText( snapshot: UsageSnapshot?, preference: MenuBarMetricPreference) -> String? diff --git a/Sources/CodexBar/UsageStore+Accessors.swift b/Sources/CodexBar/UsageStore+Accessors.swift index e013db3189..9456b315d0 100644 --- a/Sources/CodexBar/UsageStore+Accessors.swift +++ b/Sources/CodexBar/UsageStore+Accessors.swift @@ -155,6 +155,8 @@ extension UsageStore { return DeepSeekUsageError.missingCredentials.errorDescription case .deepinfra: return DeepInfraUsageError.missingCredentials.errorDescription + case .hyper: + return HyperUsageError.missingCredentials.errorDescription case .perplexity: return PerplexityAPIError.missingToken.errorDescription case .minimax: diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index 19c9488717..17de187f52 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -985,6 +985,7 @@ extension UsageStore { .sakana: "Sakana AI debug log not yet implemented", .venice: "Venice debug log not yet implemented", .deepinfra: "DeepInfra debug log not yet implemented", + .hyper: "Charm Hyper debug log not yet implemented", .commandcode: "Command Code debug log not yet implemented", .qoder: "Qoder debug log not yet implemented", .stepfun: "StepFun debug log not yet implemented", @@ -1079,7 +1080,7 @@ extension UsageStore { hasTokenAccount: deepSeekHasTokenAccount) case .clinepass, .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .qwencloud, .factory, .copilot, .devin, .vertexai, .kilo, .kiro, .kimi, .moonshot, .jetbrains, .perplexity, - .mimo, .doubao, .sakana, .abacus, .mistral, .deepinfra, .codebuff, .crof, .windsurf, + .mimo, .doubao, .sakana, .abacus, .mistral, .deepinfra, .hyper, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes, .neuralwatt, .clawrouter, .longcat, .wayfinder, .sub2api, .zenmux, .aiand, .zoommate, .xai: diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift index 26a0e60984..2b5b28327e 100644 --- a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift +++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift @@ -261,6 +261,8 @@ extension CodexBarCLI { switch provider { case .kimi: KimiSettingsReader.apiKey(environment: environment) != nil + case .hyper: + HyperSettingsReader.apiKey(environment: environment) != nil case .llmproxy: LLMProxySettingsReader.apiKey(environment: environment) != nil case .clawrouter: diff --git a/Sources/CodexBarCLI/CLIRenderer.swift b/Sources/CodexBarCLI/CLIRenderer.swift index 6048c748f8..c094f8a368 100644 --- a/Sources/CodexBarCLI/CLIRenderer.swift +++ b/Sources/CodexBarCLI/CLIRenderer.swift @@ -621,6 +621,14 @@ enum CLIRenderer { // pair; the dedicated balance line renders it instead. !(provider == .xai && cost.period == "Prepaid credits") else { return } + if provider == .hyper, + cost.period == "Hypercredits balance", + let value = cost.balance + { + let balance = Self.hypercreditsString(value) + lines.append(self.labelValueLine("Balance", value: "\(balance) HC", useColor: context.useColor)) + return + } // Fallback to cost/quota display if no primary rate window. let label = cost.currencyCode == "Quota" ? "Quota" : "Cost" let value = "\(String(format: "%.1f", cost.used)) / \(String(format: "%.1f", cost.limit))" @@ -1043,6 +1051,11 @@ enum CLIRenderer { } } + private static func hypercreditsString(_ value: Double) -> String { + if value.rounded() == value { return String(format: "%.0f", value) } + return String(format: "%.2f", value) + } + private static func resetLineForDetailBackedWindow( window: RateWindow, style: ResetTimeDisplayStyle, diff --git a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift index 0fbe433390..c099240648 100644 --- a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift +++ b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift @@ -188,7 +188,7 @@ public enum ProviderConfigEnvironment { case .llmproxy: LLMProxySettingsReader.apiKeyEnvironmentKey case .chutes, .poe, .litellm, .clawrouter, .factory, .sub2api, .neuralwatt, .zenmux, .deepinfra, .aiand, - .xai: + .hyper, .xai: self.additionalAPIKeyEnvironmentKey(for: provider) default: nil @@ -215,6 +215,8 @@ public enum ProviderConfigEnvironment { ZenMuxSettingsReader.managementAPIKeyEnvironmentKey case .deepinfra: DeepInfraSettingsReader.apiKeyEnvironmentKey + case .hyper: + HyperSettingsReader.apiKeyEnvironmentKey case .aiand: AiAndSettingsReader.apiKeyEnvironmentKey case .xai: diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index ca353e24bb..65e5e753a0 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 = "3aa49b47f4b78e13" + static let value = "87d9368d3ae2c687" } diff --git a/Sources/CodexBarCore/Providers/Hyper/HyperCookieImporter.swift b/Sources/CodexBarCore/Providers/Hyper/HyperCookieImporter.swift new file mode 100644 index 0000000000..4776d285fd --- /dev/null +++ b/Sources/CodexBarCore/Providers/Hyper/HyperCookieImporter.swift @@ -0,0 +1,112 @@ +import Foundation + +#if os(macOS) +import SweetCookieKit + +public enum HyperCookieImporter { + private static let cookieClient = BrowserCookieClient() + private static let cookieDomains = ["hyper.charm.land"] + private static let cookieImportOrder: BrowserCookieImportOrder = + ProviderDefaults.metadata[.hyper]?.browserCookieOrder ?? [.chrome] + + public struct SessionInfo: Sendable { + public let cookies: [HTTPCookie] + public let sourceLabel: String + + public var cookieHeader: String { + self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ") + } + } + + public static func importSession( + browserDetection: BrowserDetection = BrowserDetection(), + logger: ((String) -> Void)? = nil) throws -> SessionInfo + { + let candidates = self.cookieImportOrder.cookieImportCandidates(using: browserDetection) + for browser in candidates { + do { + if let session = try self.importSessions(from: browser, logger: logger).first { + return session + } + } catch { + BrowserCookieAccessGate.recordIfNeeded(error) + logger?("[hyper-cookie] \(browser.displayName) cookie import failed: \(error.localizedDescription)") + } + } + throw HyperCookieImportError.noCookies + } + + public static func hasSession( + browserDetection: BrowserDetection = BrowserDetection(), + logger: ((String) -> Void)? = nil) -> Bool + { + (try? self.importSession(browserDetection: browserDetection, logger: logger)) != nil + } + + private static func importSessions( + from browser: Browser, + logger: ((String) -> Void)?) throws -> [SessionInfo] + { + let query = BrowserCookieQuery(domains: self.cookieDomains) + let sources = try self.cookieClient.codexBarRecords( + matching: query, + in: browser, + logger: logger) + let groups = Dictionary(grouping: sources, by: { $0.store.profile.id }) + + return groups.values.compactMap { group in + let records = self.mergeRecords(group) + let cookies = BrowserCookieClient.makeHTTPCookies(records, origin: query.origin) + guard !cookies.isEmpty else { return nil } + let sourceLabel = group.map(\.label).min() ?? browser.displayName + logger?("[hyper-cookie] Found \(cookies.count) cookie(s) in \(sourceLabel)") + return SessionInfo(cookies: cookies, sourceLabel: sourceLabel) + } + .sorted { $0.sourceLabel < $1.sourceLabel } + } + + private static func mergeRecords(_ sources: [BrowserCookieStoreRecords]) -> [BrowserCookieRecord] { + let sortedSources = sources.sorted { self.priority($0.store.kind) < self.priority($1.store.kind) } + var recordsByKey: [String: BrowserCookieRecord] = [:] + for source in sortedSources { + for record in source.records { + let key = "\(record.name)|\(record.domain)|\(record.path)" + if let current = recordsByKey[key] { + if self.shouldReplace(current, with: record) { + recordsByKey[key] = record + } + } else { + recordsByKey[key] = record + } + } + } + return recordsByKey.values.sorted { lhs, rhs in + (lhs.name, lhs.domain, lhs.path) < (rhs.name, rhs.domain, rhs.path) + } + } + + private static func priority(_ kind: BrowserCookieStoreKind) -> Int { + switch kind { + case .network: 0 + case .primary: 1 + case .safari: 2 + } + } + + private static func shouldReplace(_ current: BrowserCookieRecord, with candidate: BrowserCookieRecord) -> Bool { + switch (current.expires, candidate.expires) { + case let (lhs?, rhs?): rhs > lhs + case (nil, .some): true + case (.some, nil), (nil, nil): false + } + } +} + +enum HyperCookieImportError: LocalizedError { + case noCookies + + var errorDescription: String? { + "No Charm Hyper browser session found. Sign in to hyper.charm.land or configure an API key." + } +} +#endif diff --git a/Sources/CodexBarCore/Providers/Hyper/HyperProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Hyper/HyperProviderDescriptor.swift new file mode 100644 index 0000000000..b686cc5f67 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Hyper/HyperProviderDescriptor.swift @@ -0,0 +1,153 @@ +import Foundation + +public enum HyperProviderDescriptor { + public static let descriptor: ProviderDescriptor = Self.makeDescriptor() + + static func makeDescriptor() -> ProviderDescriptor { + ProviderDescriptor( + id: .hyper, + metadata: ProviderMetadata( + id: .hyper, + displayName: "Charm Hyper", + sessionLabel: "Balance", + weeklyLabel: "Balance", + opusLabel: nil, + supportsOpus: false, + supportsCredits: false, + creditsHint: "", + toggleTitle: "Show Charm Hyper usage", + cliName: "hyper", + defaultEnabled: false, + isPrimaryProvider: false, + usesAccountFallback: false, + browserCookieOrder: ProviderBrowserCookieDefaults.chromeOnlyImportOrder, + dashboardURL: "https://hyper.charm.land", + statusPageURL: nil), + branding: ProviderBranding( + iconStyle: .hyper, + iconResourceName: "ProviderIcon-hyper", + color: ProviderColor(red: 1, green: 96 / 255, blue: 1), + confettiPalette: [ + ProviderColor(red: 1, green: 96 / 255, blue: 1), + ProviderColor(red: 1, green: 1, blue: 1), + ]), + tokenCost: ProviderTokenCostConfig( + supportsTokenCost: false, + noDataMessage: { "Charm Hyper cost history is not available via API." }), + fetchPlan: ProviderFetchPlan( + sourceModes: [.auto, .web, .api], + pipeline: ProviderFetchPipeline(resolveStrategies: self.resolveStrategies)), + cli: ProviderCLIConfig(name: "hyper", aliases: [], versionDetector: nil)) + } + + private static func resolveStrategies(context: ProviderFetchContext) async -> [any ProviderFetchStrategy] { + switch context.sourceMode { + case .web: + [HyperSessionFetchStrategy()] + case .api: + [HyperAPIFetchStrategy()] + case .auto: + if context.settings?.hyper?.cookieSource == .off { + [HyperAPIFetchStrategy()] + } else { + [HyperSessionFetchStrategy(), HyperAPIFetchStrategy()] + } + case .cli, .oauth: + [] + } + } +} + +struct HyperSessionFetchStrategy: ProviderFetchStrategy { + let id = "hyper.web" + let kind: ProviderFetchKind = .web + + func isAvailable(_ context: ProviderFetchContext) async -> Bool { + let cookieSource = context.settings?.hyper?.cookieSource ?? .auto + guard cookieSource != .off else { return false } + if cookieSource == .manual { + return CookieHeaderNormalizer.normalize(context.settings?.hyper?.manualCookieHeader) != nil + } + #if os(macOS) + if let cached = CookieHeaderCache.load(provider: .hyper), + !cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { + return true + } + return HyperCookieImporter.hasSession(browserDetection: context.browserDetection) + #else + return false + #endif + } + + func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { + let cookieHeader = try Self.resolveCookieHeader(context: context) + do { + let usage = try await HyperUsageFetcher.fetchUsage(cookieHeader: cookieHeader) + return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "web") + } catch HyperUsageError.missingCredentials { + if context.settings?.hyper?.cookieSource != .manual { + CookieHeaderCache.clear(provider: .hyper) + } + throw HyperUsageError.missingCredentials + } + } + + func shouldFallback(on error: Error, context: ProviderFetchContext) -> Bool { + guard context.sourceMode == .auto else { return false } + if error is CancellationError || (error as? URLError)?.code == .cancelled { + return false + } + guard let error = error as? HyperUsageError else { return true } + switch error { + case .missingCredentials, .networkError: + return true + case .apiError, .parseFailed: + return false + } + } + + private static func resolveCookieHeader(context: ProviderFetchContext) throws -> String { + if context.settings?.hyper?.cookieSource == .manual { + guard let header = CookieHeaderNormalizer.normalize(context.settings?.hyper?.manualCookieHeader) else { + throw HyperUsageError.missingCredentials + } + return header + } + + #if os(macOS) + if let cached = CookieHeaderCache.load(provider: .hyper), + !cached.cookieHeader.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { + return cached.cookieHeader + } + let session = try HyperCookieImporter.importSession(browserDetection: context.browserDetection) + CookieHeaderCache.store(provider: .hyper, cookieHeader: session.cookieHeader, sourceLabel: session.sourceLabel) + return session.cookieHeader + #else + throw HyperUsageError.missingCredentials + #endif + } +} + +struct HyperAPIFetchStrategy: ProviderFetchStrategy { + let id = "hyper.api" + let kind: ProviderFetchKind = .apiToken + + func isAvailable(_: ProviderFetchContext) async -> Bool { + // Keep this strategy available so a missing key yields provider-specific setup guidance. + true + } + + func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { + guard let apiKey = ProviderTokenResolver.hyperToken(environment: context.env) else { + throw HyperUsageError.missingCredentials + } + let usage = try await HyperUsageFetcher.fetchUsage(apiKey: apiKey) + return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api") + } + + func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { + false + } +} diff --git a/Sources/CodexBarCore/Providers/Hyper/HyperSettingsReader.swift b/Sources/CodexBarCore/Providers/Hyper/HyperSettingsReader.swift new file mode 100644 index 0000000000..27843460a9 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Hyper/HyperSettingsReader.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct HyperSettingsReader: Sendable { + public static let apiKeyEnvironmentKey = "HYPER_API_KEY" + + public static func apiKey( + environment: [String: String] = ProcessInfo.processInfo.environment) -> String? + { + guard var value = environment[apiKeyEnvironmentKey]?.trimmingCharacters(in: .whitespacesAndNewlines), + !value.isEmpty + else { return nil } + if (value.hasPrefix("\"") && value.hasSuffix("\"")) || + (value.hasPrefix("'") && value.hasSuffix("'")) + { + value = String(value.dropFirst().dropLast()).trimmingCharacters(in: .whitespacesAndNewlines) + } + return value.isEmpty ? nil : value + } +} diff --git a/Sources/CodexBarCore/Providers/Hyper/HyperUsageFetcher.swift b/Sources/CodexBarCore/Providers/Hyper/HyperUsageFetcher.swift new file mode 100644 index 0000000000..7b7f86eb53 --- /dev/null +++ b/Sources/CodexBarCore/Providers/Hyper/HyperUsageFetcher.swift @@ -0,0 +1,184 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct HyperCreditsResponse: Decodable, Sendable { + public let balance: Double +} + +public struct HyperUsageSnapshot: Sendable { + public let balance: Double + public let updatedAt: Date + + public init(balance: Double, updatedAt: Date) { + self.balance = balance + self.updatedAt = updatedAt + } + + public func toUsageSnapshot() -> UsageSnapshot { + UsageSnapshot( + primary: nil, + secondary: nil, + providerCost: ProviderCostSnapshot( + used: 0, + limit: 0, + currencyCode: "HC", + period: "Hypercredits balance", + balance: self.balance, + updatedAt: self.updatedAt), + updatedAt: self.updatedAt, + identity: ProviderIdentitySnapshot( + providerID: .hyper, + accountEmail: nil, + accountOrganization: nil, + loginMethod: nil), + dataConfidence: .exact) + } +} + +public enum HyperUsageError: LocalizedError, Sendable, Equatable { + case missingCredentials + case networkError(String) + case apiError(String) + case parseFailed(String) + + public var errorDescription: String? { + switch self { + case .missingCredentials: + "Sign in to hyper.charm.land or configure a Charm Hyper API key." + case let .networkError(message): "Charm Hyper network error: \(message)" + case let .apiError(message): "Charm Hyper API error: \(message)" + case let .parseFailed(message): "Failed to parse Charm Hyper response: \(message)" + } + } +} + +public struct HyperUsageFetcher: Sendable { + private enum Authentication: Equatable { + case apiKey + case session + } + + private static let creditsURL = URL(string: "https://hyper.charm.land/v1/credits")! + + public static func fetchUsage(apiKey: String) async throws -> HyperUsageSnapshot { + let token = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) + guard !token.isEmpty else { throw HyperUsageError.missingCredentials } + return try await self.fetchUsage( + request: self.request(authorization: "Bearer \(token)"), + authentication: .apiKey, + transport: ProviderHTTPClient.shared, + now: Date()) + } + + public static func fetchUsage(cookieHeader: String) async throws -> HyperUsageSnapshot { + guard let header = CookieHeaderNormalizer.normalize(cookieHeader) else { + throw HyperUsageError.missingCredentials + } + return try await self.fetchUsage( + request: self.request(cookieHeader: header), + authentication: .session, + transport: ProviderHTTPClient.shared, + now: Date()) + } + + static func _fetchUsageForTesting( + apiKey: String, + transport: any ProviderHTTPTransport, + now: Date = Date()) async throws -> HyperUsageSnapshot + { + let token = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) + guard !token.isEmpty else { throw HyperUsageError.missingCredentials } + return try await self.fetchUsage( + request: self.request(authorization: "Bearer \(token)"), + authentication: .apiKey, + transport: transport, + now: now) + } + + static func _fetchSessionForTesting( + cookieHeader: String, + transport: any ProviderHTTPTransport, + now: Date = Date()) async throws -> HyperUsageSnapshot + { + guard let header = CookieHeaderNormalizer.normalize(cookieHeader) else { + throw HyperUsageError.missingCredentials + } + return try await self.fetchUsage( + request: self.request(cookieHeader: header), + authentication: .session, + transport: transport, + now: now) + } + + static func _parseSnapshotForTesting(_ data: Data, now: Date = Date()) throws -> HyperUsageSnapshot { + try self.parseSnapshot(data: data, now: now) + } + + private static func fetchUsage( + request: URLRequest, + authentication: Authentication, + transport: any ProviderHTTPTransport, + now: Date) async throws -> HyperUsageSnapshot + { + do { + let response = try await transport.response( + for: request, + retryPolicy: .transientIdempotent) + try self.validate(response, authentication: authentication) + return try self.parseSnapshot(data: response.data, now: now) + } catch is CancellationError { + throw CancellationError() + } catch let error as HyperUsageError { + throw error + } catch { + throw HyperUsageError.networkError(error.localizedDescription) + } + } + + private static func request(authorization: String? = nil, cookieHeader: String? = nil) -> URLRequest { + var request = URLRequest(url: self.creditsURL) + request.httpMethod = "GET" + request.setValue(authorization, forHTTPHeaderField: "Authorization") + request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.timeoutInterval = 30 + return request + } + + private static func validate(_ response: ProviderHTTPResponse, authentication: Authentication) throws { + switch response.statusCode { + case 200: + if authentication == .session { + let contentType = response.response.value(forHTTPHeaderField: "Content-Type")?.lowercased() ?? "" + let path = response.response.url?.path.lowercased() ?? "" + if contentType.contains("text/html") || path.hasPrefix("/auth") { + throw HyperUsageError.missingCredentials + } + } + return + case 401: + if authentication == .session { throw HyperUsageError.missingCredentials } + throw HyperUsageError.apiError("API key rejected (HTTP 401).") + case 403: + if authentication == .session { throw HyperUsageError.missingCredentials } + throw HyperUsageError.apiError("API key cannot access credits (HTTP 403).") + default: throw HyperUsageError.apiError("HTTP \(response.statusCode)") + } + } + + private static func parseSnapshot(data: Data, now: Date) throws -> HyperUsageSnapshot { + do { + let response = try JSONDecoder().decode(HyperCreditsResponse.self, from: data) + guard response.balance.isFinite, response.balance >= 0 else { + throw HyperUsageError.parseFailed("Balance must be a non-negative number.") + } + return HyperUsageSnapshot(balance: response.balance, updatedAt: now) + } catch let error as HyperUsageError { + throw error + } catch { + throw HyperUsageError.parseFailed(error.localizedDescription) + } + } +} diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift index 6e24825e55..49ed1c5652 100644 --- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift @@ -166,6 +166,7 @@ public enum ProviderDescriptorRegistry { .mistral: MistralProviderDescriptor.descriptor, .deepseek: DeepSeekProviderDescriptor.descriptor, .deepinfra: DeepInfraProviderDescriptor.descriptor, + .hyper: HyperProviderDescriptor.descriptor, .codebuff: CodebuffProviderDescriptor.descriptor, .crof: CrofProviderDescriptor.descriptor, .venice: VeniceProviderDescriptor.descriptor, diff --git a/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift b/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift index 1f14ab28c5..ffbeef1506 100644 --- a/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift +++ b/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift @@ -41,6 +41,7 @@ public struct ProviderSettingsSnapshot: Sendable { mimo: MiMoProviderSettings? = nil, abacus: AbacusProviderSettings? = nil, mistral: MistralProviderSettings? = nil, + hyper: CookieProviderSettings? = nil, qoder: QoderProviderSettings? = nil, stepfun: StepFunProviderSettings? = nil) -> ProviderSettingsSnapshot { @@ -77,6 +78,7 @@ public struct ProviderSettingsSnapshot: Sendable { mimo: mimo, abacus: abacus, mistral: mistral, + hyper: hyper, qoder: qoder, stepfun: stepfun) } @@ -524,6 +526,7 @@ public struct ProviderSettingsSnapshot: Sendable { public let mimo: MiMoProviderSettings? public let abacus: AbacusProviderSettings? public let mistral: MistralProviderSettings? + public let hyper: CookieProviderSettings? public let qoder: QoderProviderSettings? public let stepfun: StepFunProviderSettings? @@ -564,6 +567,7 @@ public struct ProviderSettingsSnapshot: Sendable { mimo: MiMoProviderSettings? = nil, abacus: AbacusProviderSettings? = nil, mistral: MistralProviderSettings? = nil, + hyper: CookieProviderSettings? = nil, qoder: QoderProviderSettings? = nil, stepfun: StepFunProviderSettings? = nil) { @@ -599,6 +603,7 @@ public struct ProviderSettingsSnapshot: Sendable { self.mimo = mimo self.abacus = abacus self.mistral = mistral + self.hyper = hyper self.qoder = qoder self.stepfun = stepfun } @@ -635,6 +640,7 @@ public enum ProviderSettingsSnapshotContribution: Sendable { case mimo(ProviderSettingsSnapshot.MiMoProviderSettings) case abacus(ProviderSettingsSnapshot.AbacusProviderSettings) case mistral(ProviderSettingsSnapshot.MistralProviderSettings) + case hyper(ProviderSettingsSnapshot.CookieProviderSettings) case qoder(ProviderSettingsSnapshot.QoderProviderSettings) case stepfun(ProviderSettingsSnapshot.StepFunProviderSettings) } @@ -672,6 +678,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable { public var mimo: ProviderSettingsSnapshot.MiMoProviderSettings? public var abacus: ProviderSettingsSnapshot.AbacusProviderSettings? public var mistral: ProviderSettingsSnapshot.MistralProviderSettings? + public var hyper: ProviderSettingsSnapshot.CookieProviderSettings? public var qoder: ProviderSettingsSnapshot.QoderProviderSettings? public var stepfun: ProviderSettingsSnapshot.StepFunProviderSettings? @@ -713,6 +720,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable { case let .mimo(value): self.mimo = value case let .abacus(value): self.abacus = value case let .mistral(value): self.mistral = value + case let .hyper(value): self.hyper = value case let .qoder(value): self.qoder = value case let .stepfun(value): self.stepfun = value } @@ -752,6 +760,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable { mimo: self.mimo, abacus: self.abacus, mistral: self.mistral, + hyper: self.hyper, qoder: self.qoder, stepfun: self.stepfun) } diff --git a/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift b/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift index b21c177ffc..27a78ac1a1 100644 --- a/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift +++ b/Sources/CodexBarCore/Providers/ProviderTokenResolver.swift @@ -167,6 +167,12 @@ public enum ProviderTokenResolver { self.deepInfraResolution(environment: environment)?.token } + public static func hyperToken( + environment: [String: String] = ProcessInfo.processInfo.environment) -> String? + { + self.hyperResolution(environment: environment)?.token + } + public static func stepfunToken( environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { @@ -207,6 +213,12 @@ public enum ProviderTokenResolver { self.resolveEnv(DeepInfraSettingsReader.apiKey(environment: environment)) } + public static func hyperResolution( + environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution? + { + self.resolveEnv(HyperSettingsReader.apiKey(environment: environment)) + } + public static func poeResolution( environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution? { diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift index 7033a5ff29..8044ad9c04 100644 --- a/Sources/CodexBarCore/Providers/Providers.swift +++ b/Sources/CodexBarCore/Providers/Providers.swift @@ -46,6 +46,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable { case mistral case deepseek case deepinfra + case hyper case codebuff case crof case venice @@ -115,6 +116,7 @@ public enum IconStyle: String, Sendable, CaseIterable { case mistral case deepseek case deepinfra + case hyper case codebuff case crof case venice diff --git a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift index ae2807a2fe..bdab83b4db 100644 --- a/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift +++ b/Sources/CodexBarCore/TokenAccountSupportCatalog+Data.swift @@ -38,6 +38,13 @@ extension TokenAccountSupportCatalog { injection: .environment(key: DeepInfraSettingsReader.apiKeyEnvironmentKey), requiresManualCookieSource: false, cookieName: nil), + .hyper: TokenAccountSupport( + title: "API keys", + subtitle: "Store multiple Charm Hyper API keys.", + placeholder: "Paste API key…", + injection: .environment(key: HyperSettingsReader.apiKeyEnvironmentKey), + requiresManualCookieSource: false, + cookieName: nil), .antigravity: TokenAccountSupport( title: "Google accounts", subtitle: "Store multiple Antigravity Google OAuth accounts for quick switching.", diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index fe2677734a..22fbffdfeb 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -1043,7 +1043,7 @@ enum CostUsageScanner { .alibabatokenplan, .qwencloud, .factory, .copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .moonshot, .augment, .jetbrains, .amp, .ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana, - .abacus, .mistral, .deepseek, .deepinfra, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, + .abacus, .mistral, .deepseek, .deepinfra, .hyper, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .neuralwatt, .clawrouter, .longcat, .sub2api, .wayfinder, .zenmux, .aiand, .zoommate, .xai: return emptyReport diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift index 86acf1caea..af09df87d4 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift @@ -115,6 +115,7 @@ enum ProviderChoice: String, AppEnum { case .mistral: self = .mistral case .deepseek: return nil // DeepSeek not yet supported in widgets case .deepinfra: return nil // DeepInfra not yet supported in widgets + case .hyper: return nil // Charm Hyper not yet supported in widgets case .codebuff: return nil // Codebuff not yet supported in widgets case .crof: return nil // Crof not yet supported in widgets case .venice: return nil // Venice not yet supported in widgets diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 6cfaee31ee..6b6a91d783 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -345,6 +345,7 @@ private struct ProviderSwitchChip: View { case .mistral: "Mistral" case .deepseek: "DeepSeek" case .deepinfra: "DeepInfra" + case .hyper: "Hyper" case .codebuff: "Codebuff" case .crof: "Crof" case .venice: "Venice" @@ -1087,6 +1088,8 @@ enum WidgetColors { Color(red: 82 / 255, green: 125 / 255, blue: 240 / 255) case .deepinfra: Color(red: 42 / 255, green: 50 / 255, blue: 117 / 255) + case .hyper: + Color(red: 1, green: 96 / 255, blue: 1) case .codebuff: Color(red: 68 / 255, green: 255 / 255, blue: 0 / 255) // Codebuff lime case .crof: diff --git a/Tests/CodexBarTests/HyperPresentationTests.swift b/Tests/CodexBarTests/HyperPresentationTests.swift new file mode 100644 index 0000000000..e8190331ab --- /dev/null +++ b/Tests/CodexBarTests/HyperPresentationTests.swift @@ -0,0 +1,68 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCLI + +struct HyperPresentationTests { + @Test + func `CLI renders Hypercredits as a balance instead of a zero-limit cost`() { + let snapshot = HyperUsageSnapshot(balance: 42.5, updatedAt: Date(timeIntervalSince1970: 1)) + .toUsageSnapshot() + + let output = CLIRenderer.renderText( + provider: .hyper, + snapshot: snapshot, + credits: nil, + context: RenderContext( + header: "hyper", + status: nil, + useColor: false, + resetStyle: .absolute)) + + #expect(output.contains("Balance: 42.50 HC")) + #expect(!output.contains("Cost:")) + #expect(!output.contains("/ 0")) + } + + @Test + func `menu bar renders the Hypercredit balance`() { + let snapshot = HyperUsageSnapshot(balance: 42.5, updatedAt: Date(timeIntervalSince1970: 1)) + .toUsageSnapshot() + + #expect(StatusItemController.hyperBalanceDisplayText(snapshot: snapshot) == "42.50 HC") + } + + @Test + @MainActor + func `menu card renders Hypercredits as a balance without a percentage`() throws { + let now = Date(timeIntervalSince1970: 1) + let snapshot = HyperUsageSnapshot(balance: 42.5, updatedAt: now).toUsageSnapshot() + let metadata = try #require(ProviderDefaults.metadata[.hyper]) + + let model = UsageMenuCardView.Model.make(.init( + provider: .hyper, + 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: true, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + + #expect(model.providerCost?.title == "Hypercredits") + #expect(model.providerCost?.spendLine == "Balance: 42.50 HC") + #expect(model.providerCost?.percentUsed == nil) + #expect(model.providerCost?.percentLine == nil) + } +} diff --git a/Tests/CodexBarTests/HyperSettingsReaderTests.swift b/Tests/CodexBarTests/HyperSettingsReaderTests.swift new file mode 100644 index 0000000000..ba7d3ef1c0 --- /dev/null +++ b/Tests/CodexBarTests/HyperSettingsReaderTests.swift @@ -0,0 +1,71 @@ +import Testing +@testable import CodexBarCore + +struct HyperSettingsReaderTests { + @Test + func `reads and cleans Hyper API key from environment`() { + #expect(HyperSettingsReader.apiKey(environment: ["HYPER_API_KEY": " 'hyper-key' "]) == "hyper-key") + } + + @Test + func `API key only disables the session strategy`() async { + let descriptor = ProviderDescriptorRegistry.descriptor(for: .hyper) + let environment = ["HYPER_API_KEY": "hyper-key"] + let settings = ProviderSettingsSnapshot.make( + hyper: .init(cookieSource: .off, manualCookieHeader: nil)) + let context = Self.context(environment: environment, settings: settings) + let strategies = await descriptor.fetchPlan.pipeline.resolveStrategies(context) + + #expect(descriptor.metadata.displayName == "Charm Hyper") + #expect(descriptor.metadata.cliName == "hyper") + #expect(descriptor.metadata.dashboardURL == "https://hyper.charm.land") + #expect(strategies.map(\.id) == ["hyper.api"]) + } + + @Test + func `signed in session is preferred over an API key`() async { + let settings = ProviderSettingsSnapshot.make( + hyper: .init(cookieSource: .manual, manualCookieHeader: "session=fixture")) + let context = Self.context( + environment: ["HYPER_API_KEY": "hyper-key"], + settings: settings) + let strategies = await HyperProviderDescriptor.descriptor.fetchPlan.pipeline.resolveStrategies(context) + + #expect(strategies.map(\.id) == ["hyper.web", "hyper.api"]) + #expect(await strategies[0].isAvailable(context)) + } + + @Test + func `missing session and API key return setup guidance`() async { + let settings = ProviderSettingsSnapshot.make( + hyper: .init(cookieSource: .off, manualCookieHeader: nil)) + let context = Self.context(environment: [:], settings: settings) + let strategies = await HyperProviderDescriptor.descriptor.fetchPlan.pipeline.resolveStrategies(context) + + #expect(strategies.map(\.id) == ["hyper.api"]) + await #expect(throws: HyperUsageError.missingCredentials) { + try await strategies[0].fetch(context) + } + #expect(HyperUsageError.missingCredentials.errorDescription?.contains("Sign in") == true) + #expect(HyperUsageError.missingCredentials.errorDescription?.contains("API key") == true) + } + + private static func context( + environment: [String: String], + settings: ProviderSettingsSnapshot) -> ProviderFetchContext + { + let browserDetection = BrowserDetection(cacheTTL: 0) + return ProviderFetchContext( + runtime: .app, + sourceMode: .auto, + includeCredits: false, + webTimeout: 1, + webDebugDumpHTML: false, + verbose: false, + env: environment, + settings: settings, + fetcher: UsageFetcher(environment: environment), + claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection), + browserDetection: browserDetection) + } +} diff --git a/Tests/CodexBarTests/HyperUsageFetcherTests.swift b/Tests/CodexBarTests/HyperUsageFetcherTests.swift new file mode 100644 index 0000000000..64c379ea7b --- /dev/null +++ b/Tests/CodexBarTests/HyperUsageFetcherTests.swift @@ -0,0 +1,137 @@ +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Testing +@testable import CodexBarCore + +struct HyperUsageFetcherTests { + @Test + func `parses a non-negative Hypercredit balance`() throws { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let snapshot = try HyperUsageFetcher._parseSnapshotForTesting(Data(#"{"balance":42.5}"#.utf8), now: now) + + #expect(snapshot.balance == 42.5) + #expect(snapshot.updatedAt == now) + #expect(snapshot.toUsageSnapshot().providerCost?.balance == 42.5) + #expect(snapshot.toUsageSnapshot().providerCost?.used == 0) + #expect(snapshot.toUsageSnapshot().providerCost?.limit == 0) + #expect(snapshot.toUsageSnapshot().providerCost?.period == "Hypercredits balance") + #expect(snapshot.toUsageSnapshot().identity?.providerID == .hyper) + } + + @Test + func `fetches credits with a signed in session cookie`() async throws { + let recorder = HyperRequestRecorder() + let transport = ProviderHTTPTransportHandler { request in + await recorder.append(request) + let response = try HTTPURLResponse( + url: #require(request.url), + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + return (Data(#"{"balance":18.5}"#.utf8), response) + } + + let snapshot = try await HyperUsageFetcher._fetchSessionForTesting( + cookieHeader: "Cookie: session=fixture-session", + transport: transport) + let request = try #require(await recorder.values.first) + + #expect(snapshot.balance == 18.5) + #expect(request.url?.absoluteString == "https://hyper.charm.land/v1/credits") + #expect(request.value(forHTTPHeaderField: "Cookie") == "session=fixture-session") + #expect(request.value(forHTTPHeaderField: "Authorization") == nil) + } + + @Test + func `session redirect to login is treated as missing credentials`() async { + let transport = ProviderHTTPTransportHandler { _ in + let response = try HTTPURLResponse( + url: #require(URL(string: "https://hyper.charm.land/auth")), + statusCode: 200, + httpVersion: nil, + headerFields: ["Content-Type": "text/html; charset=utf-8"])! + return (Data("Log in".utf8), response) + } + + await #expect(throws: HyperUsageError.missingCredentials) { + try await HyperUsageFetcher._fetchSessionForTesting( + cookieHeader: "session=expired", + transport: transport) + } + } + + @Test + func `fetches documented credits endpoint with a bearer API key`() async throws { + let recorder = HyperRequestRecorder() + let transport = ProviderHTTPTransportHandler { request in + await recorder.append(request) + let response = try HTTPURLResponse( + url: #require(request.url), + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + return (Data(#"{"balance":12}"#.utf8), response) + } + + let snapshot = try await HyperUsageFetcher._fetchUsageForTesting(apiKey: "fixture-token", transport: transport) + let request = try #require(await recorder.values.first) + + #expect(snapshot.balance == 12) + #expect(request.url?.scheme == "https") + #expect(request.url?.host == "hyper.charm.land") + #expect(request.url?.path == "/v1/credits") + #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer fixture-token") + #expect(request.value(forHTTPHeaderField: "Accept") == "application/json") + } + + @Test(arguments: [ + "", + "{}", + #"{"balance":"#, + #"{"balance":-1}"#, + #"{"balance":"invalid"}"#, + ]) + func `rejects invalid balances`(payload: String) { + #expect(throws: HyperUsageError.self) { + try HyperUsageFetcher._parseSnapshotForTesting(Data(payload.utf8)) + } + } + + @Test + func `rejects empty credentials before making a request`() async { + let transport = ProviderHTTPTransportHandler { _ in + Issue.record("Transport should not be called for empty credentials") + throw HyperUsageError.networkError("unexpected request") + } + + await #expect(throws: HyperUsageError.missingCredentials) { + try await HyperUsageFetcher._fetchUsageForTesting(apiKey: " ", transport: transport) + } + } + + @Test + func `reports rejected API keys without exposing the key`() async { + let transport = ProviderHTTPTransportHandler { request in + let response = try HTTPURLResponse( + url: #require(request.url), + statusCode: 401, + httpVersion: nil, + headerFields: nil)! + return (Data(), response) + } + + await #expect(throws: HyperUsageError.apiError("API key rejected (HTTP 401).")) { + try await HyperUsageFetcher._fetchUsageForTesting(apiKey: "secret-fixture", transport: transport) + } + } +} + +private actor HyperRequestRecorder { + private(set) var values: [URLRequest] = [] + + func append(_ request: URLRequest) { + self.values.append(request) + } +} diff --git a/Tests/CodexBarTests/ProviderIconResourcesTests.swift b/Tests/CodexBarTests/ProviderIconResourcesTests.swift index 064e4c33a9..2d5cd637a7 100644 --- a/Tests/CodexBarTests/ProviderIconResourcesTests.swift +++ b/Tests/CodexBarTests/ProviderIconResourcesTests.swift @@ -44,6 +44,7 @@ struct ProviderIconResourcesTests { "zenmux", "aiand", "zoommate", + "hyper", "xai", ] for slug in slugs { diff --git a/docs/configuration.md b/docs/configuration.md index 0c72cb3ff7..2e80755e80 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -270,7 +270,7 @@ z.ai team accounts also use `usageScope`, `organizationId`, and `workspaceID`; s ## Provider IDs Current IDs (see `Sources/CodexBarCore/Providers/Providers.swift`): -`codex`, `openai`, `azureopenai`, `claude`, `clinepass`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `qwencloud`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `deepinfra`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `neuralwatt`, `clawrouter`, `longcat`, `sub2api`, `wayfinder`, `zenmux`, `aiand`, `zoommate`, `xai`. +`codex`, `openai`, `azureopenai`, `claude`, `clinepass`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `qwencloud`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `deepinfra`, `hyper`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `neuralwatt`, `clawrouter`, `longcat`, `sub2api`, `wayfinder`, `zenmux`, `aiand`, `zoommate`, `xai`. ## Ordering The order of `providers` controls display/order in the app and CLI. Reorder the array to change ordering. diff --git a/docs/hyper.md b/docs/hyper.md new file mode 100644 index 0000000000..f717979ce6 --- /dev/null +++ b/docs/hyper.md @@ -0,0 +1,21 @@ +--- +summary: "Charm Hyper provider setup and Hypercredit balance display." +--- + +# Charm Hyper provider + +CodexBar reads the remaining Charm Hypercredit balance through Charm Hyper's maintained credits endpoint. + +## Authentication + +In **Auto** mode, CodexBar prefers a signed-in `hyper.charm.land` browser session and falls back to an API key. Automatic session import is Chrome-only; choose **Manual** to paste a Cookie header or **Off** to use only an API key. + +For API-key access, create a Charm Hyper API key and add it under **Settings > Providers > Charm Hyper > API key**, or set `HYPER_API_KEY` in the environment used to launch CodexBar. Token accounts are also supported. + +Both strategies request `GET https://hyper.charm.land/v1/credits`. Session cookies are sent only to `hyper.charm.land`; API keys are sent only as Bearer tokens to the same host. + +## Display + +CodexBar displays the returned `balance` in native HC units. The credits response does not establish a plan limit or reset timestamp, so CodexBar does not infer a usage percentage, quota, or reset countdown. + +If neither a usable browser session nor an API key is available, CodexBar reports setup guidance instead of showing an empty balance. diff --git a/docs/index.html b/docs/index.html index 2bb7f382b3..c2d73f5bb0 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 @@

- 66 providers,{mobileBreak}one menu bar + 67 providers,{mobileBreak}one menu bar

Popular providers become status items with their own usage windows, reset countdowns, charts, and provider menus. @@ -332,6 +332,7 @@

MistralCookies

  • DeepSeekAPI key

  • DeepInfraAPI key

  • +
  • Charm HyperAPI key + Cookies

  • T3 ChatCookies

  • CodebuffAPI key

  • PoeAPI key

  • diff --git a/docs/llms.txt b/docs/llms.txt index 28cce05624..eade804c20 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 66 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 67 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 66 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 67 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 49255e4e75..32bfe826d1 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -8,7 +8,7 @@ read_when: # Providers -CodexBar currently registers 66 provider IDs. Some companies expose multiple surfaces, such as Codex vs OpenAI API or +CodexBar currently registers 67 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) @@ -75,6 +75,7 @@ scan fails, while provider/account configuration changes replace obsolete result | Mistral | Console billing, credit balance, and Vibe subscription usage via browser cookies (`web`). | | DeepSeek | API key from env or token accounts → balance endpoint (`api`). | | DeepInfra | API key from env or token accounts → billing checklist + monthly usage endpoints (`api`). | +| Charm Hyper | Signed-in browser session (`web`) → API key from `HYPER_API_KEY` or token accounts (`api`) → Hypercredit balance endpoint. | | Moonshot | API key from config/env → balance endpoint (`api`). | | Codebuff | API token from config/env or `codebuff login` credentials → usage API (`api`). | | Crof | API key from config/env → credit balance + optional request quota API (`api`). | diff --git a/docs/site-locales.mjs b/docs/site-locales.mjs index c99368fe05..17616e9e4f 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 66 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more.", - "meta.ogDescription": "Track usage windows, credits, and resets across 66 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 67 providers — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, and more.", + "meta.ogDescription": "Track usage windows, credits, and resets across 67 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": "66 providers,{mobileBreak}one menu bar", + "providers.title": "67 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", @@ -238,8 +238,8 @@ export const localeMessages = { }, "zh-CN": { "meta.title": "CodexBar — 菜单栏中的每个 AI 编码限制", - "meta.description": "一个微小的 macOS 菜单栏应用程序,可跟踪 66 个提供商(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 编码提供商使用窗口、积分、成本和重置。", - "meta.ogDescription": "从您的 macOS 菜单栏跟踪 66 个 AI 编码提供商的使用窗口、积分和重置。", + "meta.description": "一个微小的 macOS 菜单栏应用程序,可跟踪 67 个提供商(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 编码提供商使用窗口、积分、成本和重置。", + "meta.ogDescription": "从您的 macOS 菜单栏跟踪 67 个 AI 编码提供商的使用窗口、积分和重置。", "nav.primary": "基本的", "nav.language": "语言", "nav.docs": "文档", @@ -252,7 +252,7 @@ export const localeMessages = { "hero.description": "CodexBar 跟踪您实际付费的提供商的使用窗口、信用余额和重置倒计时 - 每个状态项一项,或将它们合并为一项。", "hero.download": "下载macOS", "hero.fineprint": "免费开源 · macOS 14+ · GitHub Releases 和 Homebrew 均提供通用版本", - "providers.title": "66 个提供商,{mobileBreak}一个菜单栏", + "providers.title": "67 个提供商,{mobileBreak}一个菜单栏", "providers.description": "受欢迎的提供商成为状态项目,具有自己的使用窗口、重置倒计时、图表和提供商菜单。", "providers.yourProvider": "您的提供商", "providers.authoringGuide": "创作指南", @@ -378,8 +378,8 @@ export const localeMessages = { }, "zh-TW": { "meta.title": "CodexBar — 功能表列中的每個 AI 編碼限制", - "meta.description": "一個微小的 macOS 功能表列應用程序,可追蹤 66 個提供者(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 編碼提供者使用視窗、積分、成本和重設。", - "meta.ogDescription": "從您的 macOS 功能表列追蹤 66 個 AI 編碼提供者的使用視窗、積分和重設。", + "meta.description": "一個微小的 macOS 功能表列應用程序,可追蹤 67 個提供者(Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM 等)的 AI 編碼提供者使用視窗、積分、成本和重設。", + "meta.ogDescription": "從您的 macOS 功能表列追蹤 67 個 AI 編碼提供者的使用視窗、積分和重設。", "nav.primary": "基本的", "nav.language": "語言", "nav.docs": "文件", @@ -392,7 +392,7 @@ export const localeMessages = { "hero.description": "CodexBar 追蹤您實際付費的提供者的使用視窗、信用餘額和重設倒數計時 - 每個狀態項一項,或將它們合併為一項。", "hero.download": "下載macOS", "hero.fineprint": "免費開源 · macOS 14+ · GitHub Releases 和 Homebrew 均提供通用版本", - "providers.title": "66 個提供者,{mobileBreak}一個選單列", + "providers.title": "67 個提供者,{mobileBreak}一個選單列", "providers.description": "受歡迎的提供者成為狀態項目,具有自己的使用視窗、重置倒數計時、圖表和提供者選單。", "providers.yourProvider": "您的提供者", "providers.authoringGuide": "創作指南", @@ -518,8 +518,8 @@ export const localeMessages = { }, "ja-JP": { "meta.title": "CodexBar — メニュー バーのすべての AI コーディング制限", - "meta.description": "小さな macOS メニュー バー アプリ。Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM など、66 のプロバイダーにわたる AI コーディング プロバイダーの使用期間、クレジット、コスト、リセットを追跡します。", - "meta.ogDescription": "macOS メニュー バーから、66 の AI コーディング プロバイダーにわたる使用期間、クレジット、リセットを追跡します。", + "meta.description": "小さな macOS メニュー バー アプリ。Codex、OpenAI、Claude、Cursor、Gemini、Copilot、LiteLLM など、67 のプロバイダーにわたる AI コーディング プロバイダーの使用期間、クレジット、コスト、リセットを追跡します。", + "meta.ogDescription": "macOS メニュー バーから、67 の AI コーディング プロバイダーにわたる使用期間、クレジット、リセットを追跡します。", "nav.primary": "主要な", "nav.language": "言語", "nav.docs": "ドキュメント", @@ -532,7 +532,7 @@ export const localeMessages = { "hero.description": "CodexBar は、実際に料金を支払っているプロバイダー全体の使用期間、クレジット残高、リセット カウントダウンを追跡します。ステータス項目を 1 つずつ、または 1 つに統合します。", "hero.download": "macOS のダウンロード", "hero.fineprint": "無料・オープンソース · macOS 14+ · GitHub Releases と Homebrew でユニバーサル版を提供", - "providers.title": "66 プロバイダー、{mobileBreak}1つのメニューバー", + "providers.title": "67 プロバイダー、{mobileBreak}1つのメニューバー", "providers.description": "人気のあるプロバイダーは、独自の使用期間、リセット カウントダウン、グラフ、プロバイダー メニューを備えたステータス アイテムになります。", "providers.yourProvider": "あなたのプロバイダー", "providers.authoringGuide": "オーサリングガイド", @@ -658,8 +658,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 66 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 66 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 67 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 67 proveedores de codificación de IA desde su barra de menú macOS.", "nav.primary": "Primario", "nav.language": "Idioma", "nav.docs": "Documentos", @@ -672,7 +672,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": "66 proveedores,{mobileBreak}una barra de menús", + "providers.title": "67 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", @@ -798,8 +798,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 66 provedores — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e muito mais.", - "meta.ogDescription": "Rastreie janelas de uso, créditos e redefinições em 66 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 67 provedores — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e muito mais.", + "meta.ogDescription": "Rastreie janelas de uso, créditos e redefinições em 67 provedores de codificação de IA na barra de menu macOS.", "nav.primary": "Primário", "nav.language": "Linguagem", "nav.docs": "Documentos", @@ -812,7 +812,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": "66 provedores,{mobileBreak}uma barra de menu", + "providers.title": "67 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", @@ -938,8 +938,8 @@ export const localeMessages = { }, "ko": { "meta.title": "CodexBar — 메뉴 표시줄의 모든 AI 코딩 제한", - "meta.description": "Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM 등 66개 제공자 전체에서 AI 코딩 제공자 사용 창, 크레딧, 비용 및 재설정을 추적하는 작은 macOS 메뉴 표시줄 앱입니다.", - "meta.ogDescription": "macOS 메뉴 표시줄에서 66개 AI 코딩 제공업체의 사용 기간, 크레딧 및 재설정을 추적하세요.", + "meta.description": "Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM 등 67개 제공자 전체에서 AI 코딩 제공자 사용 창, 크레딧, 비용 및 재설정을 추적하는 작은 macOS 메뉴 표시줄 앱입니다.", + "meta.ogDescription": "macOS 메뉴 표시줄에서 67개 AI 코딩 제공업체의 사용 기간, 크레딧 및 재설정을 추적하세요.", "nav.primary": "주요한", "nav.language": "언어", "nav.docs": "문서", @@ -952,7 +952,7 @@ export const localeMessages = { "hero.description": "CodexBar는 귀하가 실제로 비용을 지불한 제공업체 전반에 걸쳐 사용 기간, 크레딧 잔액 및 재설정 카운트다운을 추적합니다. 즉, 각각 하나의 상태 항목 또는 하나로 병합됩니다.", "hero.download": "macOS 동안 다운로드", "hero.fineprint": "무료 오픈 소스 · macOS 14+ · GitHub Releases와 Homebrew에서 유니버설 제공", - "providers.title": "66개 제공자,{mobileBreak}하나의 메뉴 막대", + "providers.title": "67개 제공자,{mobileBreak}하나의 메뉴 막대", "providers.description": "인기 있는 제공업체는 자체 사용 창, 재설정 카운트다운, 차트 및 제공업체 메뉴를 갖춘 상태 항목이 됩니다.", "providers.yourProvider": "귀하의 제공자", "providers.authoringGuide": "저작 가이드", @@ -1078,8 +1078,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 66 KI-Coding-Anbietern im Blick behält – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM und mehr.", - "meta.ogDescription": "Nutzungslimits, Guthaben und Resets von 66 KI-Coding-Anbietern direkt in Ihrer macOS-Menüleiste.", + "meta.description": "Eine kleine macOS-Menüleisten-App, die Nutzungslimits, Guthaben, Kosten und Resets von 67 KI-Coding-Anbietern im Blick behält – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM und mehr.", + "meta.ogDescription": "Nutzungslimits, Guthaben und Resets von 67 KI-Coding-Anbietern direkt in Ihrer macOS-Menüleiste.", "nav.primary": "Primär", "nav.language": "Sprache", "nav.docs": "Dokumente", @@ -1092,7 +1092,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": "66 Provider,{mobileBreak}eine Menüleiste", + "providers.title": "67 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", @@ -1218,8 +1218,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 66 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 66 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 67 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 67 fournisseurs de codage d'IA à partir de votre barre de menus macOS.", "nav.primary": "Primaire", "nav.language": "Langue", "nav.docs": "Documents", @@ -1232,7 +1232,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": "66 fournisseurs,{mobileBreak}une barre des menus", + "providers.title": "67 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", @@ -1358,8 +1358,8 @@ export const localeMessages = { }, "ar": { "meta.title": "CodexBar — كل حد لترميز الذكاء الاصطناعي في شريط القائمة", - "meta.description": "تطبيق شريط قوائم macOS صغير الحجم يتتبع نوافذ استخدام موفر ترميز الذكاء الاصطناعي، والائتمانات، والتكاليف، وعمليات إعادة التعيين عبر 66 موفرًا - Codex، وOpenAI، وClaude، وCursor، وGemini، وCopilot، وLiteLLM، والمزيد.", - "meta.ogDescription": "تتبع نوافذ الاستخدام والأرصدة وعمليات إعادة التعيين عبر 66 موفرًا لترميز الذكاء الاصطناعي من شريط القائمة macOS.", + "meta.description": "تطبيق شريط قوائم macOS صغير الحجم يتتبع نوافذ استخدام موفر ترميز الذكاء الاصطناعي، والائتمانات، والتكاليف، وعمليات إعادة التعيين عبر 67 موفرًا - Codex، وOpenAI، وClaude، وCursor، وGemini، وCopilot، وLiteLLM، والمزيد.", + "meta.ogDescription": "تتبع نوافذ الاستخدام والأرصدة وعمليات إعادة التعيين عبر 67 موفرًا لترميز الذكاء الاصطناعي من شريط القائمة macOS.", "nav.primary": "أساسي", "nav.language": "لغة", "nav.docs": "المستندات", @@ -1372,7 +1372,7 @@ export const localeMessages = { "hero.description": "يتتبع CodexBar فترات الاستخدام والأرصدة الائتمانية وإعادة تعيين العد التنازلي عبر مقدمي الخدمة الذين تدفع مقابلهم فعليًا — عنصر حالة واحد لكل منهم، أو دمجهم في عنصر واحد.", "hero.download": "التنزيل لمدة macOS", "hero.fineprint": "مجاني ومفتوح المصدر · macOS 14+ · إصدار شامل عبر GitHub Releases وHomebrew", - "providers.title": "66 مزودًا،{mobileBreak}شريط قوائم واحد", + "providers.title": "67 مزودًا،{mobileBreak}شريط قوائم واحد", "providers.description": "يصبح الموفرون المشهورون عناصر حالة مع نوافذ الاستخدام الخاصة بهم، وعمليات إعادة تعيين العد التنازلي، والمخططات، وقوائم الموفر.", "providers.yourProvider": "المزود الخاص بك", "providers.authoringGuide": "دليل التأليف", @@ -1498,8 +1498,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 66 fornitori: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e altri.", - "meta.ogDescription": "Tieni traccia delle finestre di utilizzo, dei crediti e dei ripristini tra 66 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 67 fornitori: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM e altri.", + "meta.ogDescription": "Tieni traccia delle finestre di utilizzo, dei crediti e dei ripristini tra 67 fornitori di codifica AI dalla barra dei menu macOS.", "nav.primary": "Primario", "nav.language": "Lingua", "nav.docs": "Documenti", @@ -1512,7 +1512,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": "66 provider,{mobileBreak}una barra dei menu", + "providers.title": "67 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", @@ -1638,8 +1638,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 66 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 66 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 67 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 67 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", @@ -1652,7 +1652,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": "66 nhà cung cấp,{mobileBreak}một thanh menu", + "providers.title": "67 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", @@ -1778,8 +1778,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 66 providers: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM en meer.", - "meta.ogDescription": "Houd gebruiksvensters, tegoeden en resets bij van 66 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 67 providers: Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM en meer.", + "meta.ogDescription": "Houd gebruiksvensters, tegoeden en resets bij van 67 leveranciers van AI-codering via uw macOS-menubalk.", "nav.primary": "Primair", "nav.language": "Taal", "nav.docs": "Documenten", @@ -1792,7 +1792,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": "66 providers,{mobileBreak}één menubalk", + "providers.title": "67 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", @@ -1918,8 +1918,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 66 sağlayıcı genelinde sıfırlamaları izleyen küçük bir macOS menü çubuğu uygulaması.", - "meta.ogDescription": "macOS menü çubuğunu kullanarak 66 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 67 sağlayıcı genelinde sıfırlamaları izleyen küçük bir macOS menü çubuğu uygulaması.", + "meta.ogDescription": "macOS menü çubuğunu kullanarak 67 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", @@ -1932,7 +1932,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": "66 sağlayıcı,{mobileBreak}bir menü çubuğu", + "providers.title": "67 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", @@ -2058,8 +2058,8 @@ export const localeMessages = { }, "uk": { "meta.title": "CodexBar — кожне обмеження кодування AI у вашій панелі меню", - "meta.description": "Маленький додаток macOS на панелі меню, який відстежує вікна використання постачальників кодування штучного інтелекту, кредити, витрати та скидання 66 постачальників — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM тощо.", - "meta.ogDescription": "Відстежуйте вікна використання, кредити та скидання 66 постачальників кодування ШІ за допомогою панелі меню macOS.", + "meta.description": "Маленький додаток macOS на панелі меню, який відстежує вікна використання постачальників кодування штучного інтелекту, кредити, витрати та скидання 67 постачальників — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM тощо.", + "meta.ogDescription": "Відстежуйте вікна використання, кредити та скидання 67 постачальників кодування ШІ за допомогою панелі меню macOS.", "nav.primary": "Первинний", "nav.language": "Мова", "nav.docs": "документи", @@ -2072,7 +2072,7 @@ export const localeMessages = { "hero.description": "CodexBar відстежує вікна використання, кредитні баланси та скидає зворотний відлік для постачальників, за яких ви фактично платите, — по одному статусу для кожного або об’єднує їх в один.", "hero.download": "Завантажити для macOS", "hero.fineprint": "Безкоштовний і відкритий код · macOS 14+ · Універсальна версія через GitHub Releases і Homebrew", - "providers.title": "66 провайдерів,{mobileBreak}одна панель меню", + "providers.title": "67 провайдерів,{mobileBreak}одна панель меню", "providers.description": "Популярні постачальники стають елементами статусу з власними вікнами використання, скиданням зворотного відліку, діаграмами та меню постачальників.", "providers.yourProvider": "Ваш провайдер", "providers.authoringGuide": "Авторський посібник", @@ -2198,8 +2198,8 @@ export const localeMessages = { }, "ru": { "meta.title": "CodexBar — все лимиты AI-кодинга в вашей строке меню", - "meta.description": "Небольшое приложение для строки меню macOS, которое отслеживает окна использования, кредиты, расходы и сбросы лимитов у 66 AI-провайдеров для кодинга — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM и других.", - "meta.ogDescription": "Отслеживайте окна использования, кредиты и сбросы лимитов у 66 AI-провайдеров для кодинга прямо из строки меню macOS.", + "meta.description": "Небольшое приложение для строки меню macOS, которое отслеживает окна использования, кредиты, расходы и сбросы лимитов у 67 AI-провайдеров для кодинга — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM и других.", + "meta.ogDescription": "Отслеживайте окна использования, кредиты и сбросы лимитов у 67 AI-провайдеров для кодинга прямо из строки меню macOS.", "nav.primary": "Основная навигация", "nav.language": "Язык", "nav.docs": "Документация", @@ -2212,7 +2212,7 @@ export const localeMessages = { "hero.description": "CodexBar отслеживает окна использования, балансы кредитов и обратный отсчет до сброса у провайдеров, за которых вы действительно платите, — по одному элементу статуса на каждого или все вместе в одном.", "hero.download": "Скачать для macOS", "hero.fineprint": "Бесплатно и с открытым исходным кодом · macOS 14+ · универсальная сборка через GitHub Releases и Homebrew", - "providers.title": "66 провайдеров,{mobileBreak}одна строка меню", + "providers.title": "67 провайдеров,{mobileBreak}одна строка меню", "providers.description": "Популярные провайдеры становятся элементами статуса со своими окнами использования, обратным отсчетом до сброса, графиками и меню провайдера.", "providers.yourProvider": "Ваш провайдер", "providers.authoringGuide": "Руководство по добавлению", @@ -2338,8 +2338,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 66 penyedia — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, dan banyak lagi.", - "meta.ogDescription": "Lacak jangka waktu penggunaan, kredit, dan penyetelan ulang di 66 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 67 penyedia — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM, dan banyak lagi.", + "meta.ogDescription": "Lacak jangka waktu penggunaan, kredit, dan penyetelan ulang di 67 penyedia pengkodean AI dari bilah menu macOS Anda.", "nav.primary": "Utama", "nav.language": "Bahasa", "nav.docs": "dokumen", @@ -2352,7 +2352,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": "66 penyedia,{mobileBreak}satu bilah menu", + "providers.title": "67 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", @@ -2478,8 +2478,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 66 dostawców — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM i nie tylko.", - "meta.ogDescription": "Śledź okresy użytkowania, kredyty i resety u 66 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 67 dostawców — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM i nie tylko.", + "meta.ogDescription": "Śledź okresy użytkowania, kredyty i resety u 67 dostawców kodowania AI za pomocą paska menu macOS.", "nav.primary": "Podstawowy", "nav.language": "Język", "nav.docs": "Dokumenty", @@ -2492,7 +2492,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": "66 dostawców,{mobileBreak}jeden pasek menu", + "providers.title": "67 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", @@ -2618,8 +2618,8 @@ export const localeMessages = { }, "fa": { "meta.title": "CodexBar - هر محدودیت کدنویسی هوش مصنوعی در نوار منو شما", - "meta.description": "یک برنامه نوار منو کوچک macOS که پنجره‌های استفاده از ارائه‌دهنده کدنویسی هوش مصنوعی، اعتبارات، هزینه‌ها، و بازنشانی را در بین 66 ارائه‌دهنده - Codex، OpenAI، Claude، Cursor، Gemini، Copilot، LiteLLM و موارد دیگر بازنشانی می‌کند.", - "meta.ogDescription": "پنجره‌های استفاده، اعتبارات و بازنشانی‌ها را در بین 66 ارائه‌دهنده کدنویسی هوش مصنوعی از نوار منوی macOS خود ردیابی کنید.", + "meta.description": "یک برنامه نوار منو کوچک macOS که پنجره‌های استفاده از ارائه‌دهنده کدنویسی هوش مصنوعی، اعتبارات، هزینه‌ها، و بازنشانی را در بین 67 ارائه‌دهنده - Codex، OpenAI، Claude، Cursor، Gemini، Copilot، LiteLLM و موارد دیگر بازنشانی می‌کند.", + "meta.ogDescription": "پنجره‌های استفاده، اعتبارات و بازنشانی‌ها را در بین 67 ارائه‌دهنده کدنویسی هوش مصنوعی از نوار منوی macOS خود ردیابی کنید.", "nav.primary": "اولیه", "nav.language": "زبان", "nav.docs": "اسناد", @@ -2632,7 +2632,7 @@ export const localeMessages = { "hero.description": "CodexBar پنجره‌های استفاده، مانده اعتبار و بازنشانی شمارش معکوس را در سراسر ارائه‌دهندگانی که واقعاً برایشان پول پرداخت می‌کنید ردیابی می‌کند - هر کدام یک مورد وضعیت، یا آنها را در یکی ادغام کنید.", "hero.download": "دانلود برای macOS", "hero.fineprint": "رایگان و متن‌باز · macOS 14+ · نسخهٔ یونیورسال از GitHub Releases و Homebrew", - "providers.title": "66 ارائه دهنده،{mobileBreak}یک نوار منو", + "providers.title": "67 ارائه دهنده،{mobileBreak}یک نوار منو", "providers.description": "ارائه‌دهندگان محبوب با پنجره‌های استفاده خاص خود، شمارش معکوس، نمودارها و منوهای ارائه‌دهنده را بازنشانی می‌کنند.", "providers.yourProvider": "ارائه دهنده شما", "providers.authoringGuide": "راهنمای نگارش", @@ -2758,8 +2758,8 @@ export const localeMessages = { }, "th": { "meta.title": "CodexBar — ทุกขีดจำกัดการเข้ารหัส AI ในแถบเมนูของคุณ", - "meta.description": "แอปแถบเมนู macOS ขนาดเล็กที่ติดตามกรอบเวลาการใช้งานของผู้ให้บริการเข้ารหัส AI เครดิต ต้นทุน และการรีเซ็ตในผู้ให้บริการ 66 ราย — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM และอีกมากมาย", - "meta.ogDescription": "ติดตามกรอบเวลาการใช้งาน เครดิต และการรีเซ็ตในผู้ให้บริการการเข้ารหัส AI 66 รายจากแถบเมนู macOS", + "meta.description": "แอปแถบเมนู macOS ขนาดเล็กที่ติดตามกรอบเวลาการใช้งานของผู้ให้บริการเข้ารหัส AI เครดิต ต้นทุน และการรีเซ็ตในผู้ให้บริการ 67 ราย — Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM และอีกมากมาย", + "meta.ogDescription": "ติดตามกรอบเวลาการใช้งาน เครดิต และการรีเซ็ตในผู้ให้บริการการเข้ารหัส AI 67 รายจากแถบเมนู macOS", "nav.primary": "หลัก", "nav.language": "ภาษา", "nav.docs": "เอกสาร", @@ -2772,7 +2772,7 @@ export const localeMessages = { "hero.description": "CodexBar ติดตามกรอบเวลาการใช้งาน ยอดเครดิต และรีเซ็ตการนับถอยหลังของผู้ให้บริการที่คุณชำระเงินจริง — รายการสถานะแต่ละรายการ หรือรวมรายการเหล่านั้นเป็นรายการเดียว", "hero.download": "ดาวน์โหลดสำหรับ macOS", "hero.fineprint": "ฟรีและโอเพ่นซอร์ส · macOS 14+ · รุ่น Universal ผ่าน GitHub Releases และ Homebrew", - "providers.title": "ผู้ให้บริการ 66 ราย{mobileBreak}หนึ่งแถบเมนู", + "providers.title": "ผู้ให้บริการ 67 ราย{mobileBreak}หนึ่งแถบเมนู", "providers.description": "ผู้ให้บริการยอดนิยมจะกลายเป็นรายการสถานะที่มีหน้าต่างการใช้งานของตนเอง รีเซ็ตการนับถอยหลัง แผนภูมิ และเมนูของผู้ให้บริการ", "providers.yourProvider": "ผู้ให้บริการของคุณ", "providers.authoringGuide": "คู่มือการเขียน", @@ -2898,8 +2898,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 66 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 66 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 67 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 67 provedores de programación con IA desde a barra de menús de macOS.", "nav.primary": "Principal", "nav.language": "Idioma", "nav.docs": "Documentación", @@ -2912,7 +2912,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": "66 provedores,{mobileBreak}unha barra de menús", + "providers.title": "67 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", @@ -3038,8 +3038,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: 66 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 66 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: 67 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 67 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ó", @@ -3052,7 +3052,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": "66 proveïdors,{mobileBreak}una barra de menús", + "providers.title": "67 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ó", @@ -3178,8 +3178,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 66 leverantörer – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM och mer.", - "meta.ogDescription": "Spåra användningsfönster, krediter och återställningar hos 66 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 67 leverantörer – Codex, OpenAI, Claude, Cursor, Gemini, Copilot, LiteLLM och mer.", + "meta.ogDescription": "Spåra användningsfönster, krediter och återställningar hos 67 AI-kodningsleverantörer från din macOS-menyrad.", "nav.primary": "Primär", "nav.language": "Språk", "nav.docs": "Dokument", @@ -3192,7 +3192,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": "66 leverantörer,{mobileBreak}en menyrad", + "providers.title": "67 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 b5fa85a9aa..59bf7124c0 100644 --- a/docs/social.html +++ b/docs/social.html @@ -199,7 +199,7 @@

    Every AI coding limit, in your menu bar.

    -

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

    +

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