diff --git a/README.md b/README.md index ebfebf8de2..cd587fcd67 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![License: MIT](https://img.shields.io/badge/license-MIT-6e5aff?style=flat-square)](LICENSE) [![Site](https://img.shields.io/badge/site-codexbar.app-16d3b4?style=flat-square)](https://codexbar.app) -CodexBar — every AI coding limit in your menu bar. 60 providers. +CodexBar — every AI coding limit in your menu bar. 61 providers. Tiny macOS 14+ menu bar app that keeps **AI coding-provider limits visible** and shows when each window resets. Codex, OpenAI, Claude, Cursor, Gemini, Copilot, Grok, GroqCloud, ElevenLabs, Deepgram, z.ai, MiniMax, Kiro, Zed, Vertex AI, Augment, OpenRouter, LiteLLM, LLM Proxy, Codebuff, Command Code, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons. diff --git a/Sources/CodexBar/Providers/LongCat/LongCatProviderImplementation.swift b/Sources/CodexBar/Providers/LongCat/LongCatProviderImplementation.swift new file mode 100644 index 0000000000..d4b1cec73f --- /dev/null +++ b/Sources/CodexBar/Providers/LongCat/LongCatProviderImplementation.swift @@ -0,0 +1,100 @@ +import AppKit +import CodexBarCore +import Foundation +import SwiftUI + +struct LongCatProviderImplementation: ProviderImplementation { + let id: UsageProvider = .longcat + + @MainActor + func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { + ProviderPresentation { context in + context.store.sourceLabel(for: context.provider) + } + } + + @MainActor + func observeSettings(_ settings: SettingsStore) { + _ = settings.longcatUsageDataSource + _ = settings.longcatCookieSource + _ = settings.longcatManualCookieHeader + } + + @MainActor + func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? { + .longcat(context.settings.longcatSettingsSnapshot(tokenOverride: context.tokenOverride)) + } + + @MainActor + func defaultSourceLabel(context: ProviderSourceLabelContext) -> String? { + context.settings.longcatUsageDataSource.rawValue + } + + @MainActor + func sourceMode(context: ProviderSourceModeContext) -> ProviderSourceMode { + switch context.settings.longcatUsageDataSource { + case .web: .web + case .auto, .api, .cli, .oauth: .auto + } + } + + @MainActor + func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] { + let cookieBinding = Binding( + get: { context.settings.longcatCookieSource.rawValue }, + set: { raw in + context.settings.longcatCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto + }) + let options = ProviderCookieSourceUI.options( + allowsOff: true, + keychainDisabled: context.settings.debugDisableKeychainAccess) + + let subtitle: () -> String? = { + ProviderCookieSourceUI.subtitle( + source: context.settings.longcatCookieSource, + keychainDisabled: context.settings.debugDisableKeychainAccess, + auto: "Automatic imports longcat.chat cookies from your browser.", + manual: "Paste a Cookie header copied from longcat.chat.", + off: "LongCat cookies are disabled.") + } + + return [ + ProviderSettingsPickerDescriptor( + id: "longcat-cookie-source", + title: "Cookie source", + subtitle: "Automatic imports longcat.chat cookies from your browser.", + dynamicSubtitle: subtitle, + binding: cookieBinding, + options: options, + isVisible: nil, + onChange: nil), + ] + } + + @MainActor + func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] { + [ + ProviderSettingsFieldDescriptor( + id: "longcat-cookie", + title: "", + subtitle: "", + kind: .secure, + placeholder: "Cookie: \u{2026}", + binding: context.stringBinding(\.longcatManualCookieHeader), + actions: [ + ProviderSettingsActionDescriptor( + id: "longcat-open-console", + title: "Open Console", + style: .link, + isVisible: nil, + perform: { + if let url = URL(string: "https://longcat.chat/platform/") { + NSWorkspace.shared.open(url) + } + }), + ], + isVisible: { context.settings.longcatCookieSource == .manual }, + onActivate: { context.settings.ensureLongCatCookieLoaded() }), + ] + } +} diff --git a/Sources/CodexBar/Providers/LongCat/LongCatSettingsStore.swift b/Sources/CodexBar/Providers/LongCat/LongCatSettingsStore.swift new file mode 100644 index 0000000000..f0747b19df --- /dev/null +++ b/Sources/CodexBar/Providers/LongCat/LongCatSettingsStore.swift @@ -0,0 +1,54 @@ +import CodexBarCore +import Foundation + +extension SettingsStore { + var longcatUsageDataSource: ProviderSourceMode { + get { self.configSnapshot.providerConfig(for: .longcat)?.source ?? .auto } + set { + let source: ProviderSourceMode? = switch newValue { + case .auto: .auto + case .web: .web + case .api, .cli, .oauth: .auto + } + self.updateProviderConfig(provider: .longcat) { entry in + entry.source = source + } + self.logProviderModeChange(provider: .longcat, field: "usageSource", value: newValue.rawValue) + } + } + + var longcatManualCookieHeader: String { + get { self.configSnapshot.providerConfig(for: .longcat)?.sanitizedCookieHeader ?? "" } + set { + self.updateProviderConfig(provider: .longcat) { entry in + entry.cookieHeader = self.normalizedConfigValue(newValue) + } + self.logSecretUpdate(provider: .longcat, field: "cookieHeader", value: newValue) + } + } + + var longcatCookieSource: ProviderCookieSource { + get { self.resolvedCookieSource(provider: .longcat, fallback: .auto) } + set { + self.updateProviderConfig(provider: .longcat) { entry in + entry.cookieSource = newValue + } + self.logProviderModeChange(provider: .longcat, field: "cookieSource", value: newValue.rawValue) + } + } + + func ensureLongCatCookieLoaded() {} +} + +extension SettingsStore { + func longcatSettingsSnapshot(tokenOverride: TokenAccountOverride?) + -> ProviderSettingsSnapshot.LongCatProviderSettings + { + self.ensureLongCatCookieLoaded() + return self.resolvedCookieSettings( + provider: .longcat, + configuredSource: self.longcatCookieSource, + configuredHeader: self.longcatManualCookieHeader, + tokenOverride: tokenOverride) + } +} diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift index 3a62b34e91..93d3a76e02 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift @@ -68,6 +68,7 @@ enum ProviderImplementationRegistry { case .deepgram: DeepgramProviderImplementation() case .poe: PoeProviderImplementation() case .chutes: ChutesProviderImplementation() + case .longcat: LongCatProviderImplementation() case .crossmodel: CrossModelProviderImplementation() case .clawrouter: ClawRouterProviderImplementation() case .sub2api: Sub2APIProviderImplementation() diff --git a/Sources/CodexBar/Resources/ProviderIcon-longcat.svg b/Sources/CodexBar/Resources/ProviderIcon-longcat.svg new file mode 100644 index 0000000000..dd1201c95e --- /dev/null +++ b/Sources/CodexBar/Resources/ProviderIcon-longcat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index efc58a5fe1..d81a77404b 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -1156,7 +1156,7 @@ extension UsageStore { .sakana, .abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes, - .clawrouter, .wayfinder, .sub2api, .zenmux: + .longcat, .clawrouter, .wayfinder, .sub2api, .zenmux: return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented" } } diff --git a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift index f8fda2b523..7001ceafcb 100644 --- a/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift +++ b/Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift @@ -105,6 +105,8 @@ public enum ProviderConfigEnvironment { self.applyDoubaoOverrides(base: base, config: config) case .sakana: self.applySakanaOverrides(base: base, config: config) + case .longcat: + self.applyLongCatOverrides(base: base, config: config) default: nil } @@ -368,6 +370,18 @@ public enum ProviderConfigEnvironment { return env } + private static func applyLongCatOverrides( + base: [String: String], + config: ProviderConfig?) -> [String: String] + { + guard let config else { return base } + var env = base + if let cookieHeader = config.sanitizedCookieHeader { + env[LongCatSettingsReader.cookieHeaderKey] = cookieHeader + } + return env + } + private static func doubaoAccessKeyID(from apiKey: String?) -> String? { guard let apiKey, apiKey.hasPrefix("AKLT") else { return nil } return apiKey diff --git a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift index e16a88f5cb..b9d0f4db7d 100644 --- a/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift +++ b/Sources/CodexBarCore/Generated/CodexParserHash.generated.swift @@ -1,5 +1,5 @@ // Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand. enum CodexParserHash { - static let value = "9b26fd821cf090dc" + static let value = "6ef694bfd3c6067e" } diff --git a/Sources/CodexBarCore/Logging/LogCategories.swift b/Sources/CodexBarCore/Logging/LogCategories.swift index 547df9120e..eb309c049a 100644 --- a/Sources/CodexBarCore/Logging/LogCategories.swift +++ b/Sources/CodexBarCore/Logging/LogCategories.swift @@ -44,6 +44,9 @@ public enum LogCategories { public static let kimiTokenStore = "kimi-token-store" public static let kimiWeb = "kimi-web" public static let kiro = "kiro" + public static let longcatAPI = "longcat-api" + public static let longcatCookie = "longcat-cookie" + public static let longcatWeb = "longcat-web" public static let launchAtLogin = "launch-at-login" public static let login = "login" public static let logging = "logging" diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatAPIError.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatAPIError.swift new file mode 100644 index 0000000000..5f8d741c4d --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatAPIError.swift @@ -0,0 +1,27 @@ +import Foundation + +public enum LongCatAPIError: LocalizedError, Sendable, Equatable { + case missingCookies + case invalidSession + case invalidRequest(String) + case networkError(String) + case apiError(String) + case parseFailed(String) + + public var errorDescription: String? { + switch self { + case .missingCookies: + "LongCat session cookies are missing. Sign in at longcat.chat, or paste a cookie header." + case .invalidSession: + "LongCat session is invalid or expired. Please sign in again at longcat.chat." + case let .invalidRequest(message): + "Invalid request: \(message)" + case let .networkError(message): + "LongCat network error: \(message)" + case let .apiError(message): + "LongCat API error: \(message)" + case let .parseFailed(message): + "Failed to parse LongCat usage data: \(message)" + } + } +} diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatCookieHeader.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatCookieHeader.swift new file mode 100644 index 0000000000..fe3e07006e --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatCookieHeader.swift @@ -0,0 +1,77 @@ +import Foundation + +public struct LongCatCookieOverride: Sendable { + /// Full `Cookie:` header value (e.g. `name=value; name2=value2`). + public let cookieHeader: String + + public init(cookieHeader: String) { + self.cookieHeader = cookieHeader + } +} + +public enum LongCatCookieHeader { + private static let log = CodexBarLog.logger(LogCategories.longcatCookie) + private static let headerPatterns: [String] = [ + #"(?i)-H\s*'Cookie:\s*([^']+)'"#, + #"(?i)-H\s*"Cookie:\s*([^"]+)""#, + #"(?i)\bcookie:\s*'([^']+)'"#, + #"(?i)\bcookie:\s*"([^"]+)""#, + #"(?i)\bcookie:\s*([^\r\n]+)"#, + ] + + public static func resolveCookieOverride(context: ProviderFetchContext) -> LongCatCookieOverride? { + // Off disables LongCat web auth entirely — including a lingering env cookie. + if context.settings?.longcat?.cookieSource == .off { + return nil + } + + if let settings = context.settings?.longcat, settings.cookieSource == .manual { + if let manual = settings.manualCookieHeader, !manual.isEmpty { + return self.override(from: manual) + } + } + + // Route env cookies through the settings reader so the lower-case + // `longcat_manual_cookie` alias and quote-trimming apply on the env path too. + if let envValue = LongCatSettingsReader.cookieHeader(environment: context.env), + let envHeader = self.override(from: envValue) + { + return envHeader + } + + return nil + } + + public static func override(from raw: String?) -> LongCatCookieOverride? { + guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { + return nil + } + + if let header = self.extractHeader(from: raw) { + return LongCatCookieOverride(cookieHeader: header) + } + + // A bare `name=value; ...` string is itself a usable cookie header. + if raw.contains("=") { + return LongCatCookieOverride(cookieHeader: raw) + } + + return nil + } + + private static func extractHeader(from raw: String) -> String? { + for pattern in self.headerPatterns { + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { continue } + let range = NSRange(raw.startIndex..= 2, + let captureRange = Range(match.range(at: 1), in: raw) + else { + continue + } + let captured = String(raw[captureRange]).trimmingCharacters(in: .whitespacesAndNewlines) + if !captured.isEmpty { return captured } + } + return nil + } +} diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatCookieImporter.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatCookieImporter.swift new file mode 100644 index 0000000000..1c011b68dd --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatCookieImporter.swift @@ -0,0 +1,180 @@ +import Foundation + +#if os(macOS) +import SweetCookieKit + +public enum LongCatCookieImporter { + private static let log = CodexBarLog.logger(LogCategories.longcatCookie) + private static let cookieClient = BrowserCookieClient() + private static let cookieDomains = ["longcat.chat", "www.longcat.chat"] + private static let cookieImportOrder: BrowserCookieImportOrder = + ProviderDefaults.metadata[.longcat]?.browserCookieOrder ?? Browser.defaultImportOrder + + public struct SessionInfo: Sendable { + public let cookies: [HTTPCookie] + public let sourceLabel: String + + public init(cookies: [HTTPCookie], sourceLabel: String) { + self.cookies = cookies + self.sourceLabel = sourceLabel + } + + /// Full `Cookie:` header built from every longcat.chat cookie. LongCat's + /// console uses Meituan passport SSO; the exact auth cookie name is not + /// documented, so we forward the whole jar rather than keying on one name. + public var cookieHeader: String? { + guard !self.cookies.isEmpty else { return nil } + let header = HTTPCookie.requestHeaderFields(with: self.cookies)["Cookie"] + if let header, !header.isEmpty { return header } + return nil + } + } + + public static func importSessions( + browserDetection: BrowserDetection = BrowserDetection(), + logger: ((String) -> Void)? = nil) throws -> [SessionInfo] + { + var sessions: [SessionInfo] = [] + let candidates = self.cookieImportOrder.cookieImportCandidates(using: browserDetection) + for browserSource in candidates { + do { + let perSource = try self.importSessions(from: browserSource, logger: logger) + sessions.append(contentsOf: perSource) + } catch { + BrowserCookieAccessGate.recordIfNeeded(error) + self.emit( + "\(browserSource.displayName) cookie import failed: \(error.localizedDescription)", + logger: logger) + } + } + + guard !sessions.isEmpty else { + throw LongCatCookieImportError.noCookies + } + return sessions + } + + public static func importSessions( + from browserSource: Browser, + logger: ((String) -> Void)? = nil) throws -> [SessionInfo] + { + let query = BrowserCookieQuery(domains: self.cookieDomains) + let log: (String) -> Void = { msg in self.emit(msg, logger: logger) } + let sources = try Self.cookieClient.codexBarRecords( + matching: query, + in: browserSource, + logger: log) + + var sessions: [SessionInfo] = [] + let grouped = Dictionary(grouping: sources, by: { $0.store.profile.id }) + let sortedGroups = grouped.values.sorted { lhs, rhs in + self.mergedLabel(for: lhs) < self.mergedLabel(for: rhs) + } + + for group in sortedGroups where !group.isEmpty { + let label = self.mergedLabel(for: group) + let mergedRecords = self.mergeRecords(group) + guard !mergedRecords.isEmpty else { continue } + let httpCookies = BrowserCookieClient.makeHTTPCookies(mergedRecords, origin: query.origin) + guard !httpCookies.isEmpty else { continue } + + log("Found \(httpCookies.count) longcat.chat cookie(s) in \(label)") + sessions.append(SessionInfo(cookies: httpCookies, sourceLabel: label)) + } + return sessions + } + + public static func importSession( + browserDetection: BrowserDetection = BrowserDetection(), + logger: ((String) -> Void)? = nil) throws -> SessionInfo + { + let sessions = try self.importSessions(browserDetection: browserDetection, logger: logger) + guard let first = sessions.first else { + throw LongCatCookieImportError.noCookies + } + return first + } + + public static func hasSession( + browserDetection: BrowserDetection = BrowserDetection(), + logger: ((String) -> Void)? = nil) -> Bool + { + do { + return try !self.importSessions(browserDetection: browserDetection, logger: logger).isEmpty + } catch { + return false + } + } + + private static func emit(_ message: String, logger: ((String) -> Void)?) { + logger?("[longcat-cookie] \(message)") + self.log.debug(message) + } + + private static func mergedLabel(for sources: [BrowserCookieStoreRecords]) -> String { + guard let base = sources.map(\.label).min() else { + return "Unknown" + } + if base.hasSuffix(" (Network)") { + return String(base.dropLast(" (Network)".count)) + } + return base + } + + private static func mergeRecords(_ sources: [BrowserCookieStoreRecords]) -> [BrowserCookieRecord] { + let sortedSources = sources.sorted { lhs, rhs in + self.storePriority(lhs.store.kind) < self.storePriority(rhs.store.kind) + } + var mergedByKey: [String: BrowserCookieRecord] = [:] + for source in sortedSources { + for record in source.records { + let key = self.recordKey(record) + if let existing = mergedByKey[key] { + if self.shouldReplace(existing: existing, candidate: record) { + mergedByKey[key] = record + } + } else { + mergedByKey[key] = record + } + } + } + return Array(mergedByKey.values) + } + + private static func storePriority(_ kind: BrowserCookieStoreKind) -> Int { + switch kind { + case .network: 0 + case .primary: 1 + case .safari: 2 + } + } + + private static func recordKey(_ record: BrowserCookieRecord) -> String { + "\(record.name)|\(record.domain)|\(record.path)" + } + + private static func shouldReplace(existing: BrowserCookieRecord, candidate: BrowserCookieRecord) -> Bool { + switch (existing.expires, candidate.expires) { + case let (lhs?, rhs?): + rhs > lhs + case (nil, .some): + true + case (.some, nil): + false + case (nil, nil): + false + } + } +} + +enum LongCatCookieImportError: LocalizedError { + case noCookies + + var errorDescription: String? { + switch self { + case .noCookies: + "No LongCat session cookies found in browsers." + } + } +} +#endif diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatModels.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatModels.swift new file mode 100644 index 0000000000..7cc1213ee0 --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatModels.swift @@ -0,0 +1,81 @@ +import Foundation + +/// LongCat's web console wraps every response in a Meituan-style envelope: +/// `{ "code": 0, "message": "...", "data": { ... } }`. +/// +/// The exact `data` field names are not documented and cannot be derived from the +/// minified front-end bundle, so extraction is intentionally lenient: we walk the +/// decoded JSON trying a list of candidate keys and log the raw shape once so the +/// mapping can be tightened against a real response. See `LongCatUsageFetcher`. +enum LongCatEnvelope { + /// Returns the `data` payload if the envelope reports success, else throws. + static func unwrap(_ object: Any?) throws -> Any { + guard let dict = object as? [String: Any] else { + throw LongCatAPIError.parseFailed("response was not a JSON object") + } + // Meituan envelopes use code == 0 for success; some surfaces use 200. + if let code = LongCatJSON.int(dict["code"]), code != 0, code != 200 { + let message = LongCatJSON.string(dict["message"]) ?? LongCatJSON.string(dict["msg"]) ?? "code \(code)" + if code == 401 || code == 403 { throw LongCatAPIError.invalidSession } + throw LongCatAPIError.apiError(message) + } + return dict["data"] ?? dict + } +} + +/// Tiny dynamic-JSON helper for lenient extraction by candidate key names. +enum LongCatJSON { + static func int(_ value: Any?) -> Int? { + switch value { + case let v as Int: v + case let v as Double: Int(v) + case let v as String: Int(v) ?? Double(v).map(Int.init) + case let v as NSNumber: v.intValue + default: nil + } + } + + static func double(_ value: Any?) -> Double? { + switch value { + case let v as Double: v + case let v as Int: Double(v) + case let v as String: Double(v) + case let v as NSNumber: v.doubleValue + default: nil + } + } + + static func string(_ value: Any?) -> String? { + switch value { + case let v as String: v + case let v as NSNumber: v.stringValue + default: nil + } + } + + static func object(_ value: Any?) -> [String: Any]? { + value as? [String: Any] + } + + static func array(_ value: Any?) -> [[String: Any]]? { + if let arr = value as? [[String: Any]] { return arr } + if let arr = value as? [Any] { return arr.compactMap { $0 as? [String: Any] } } + return nil + } + + /// First numeric value found under any of `keys`, searched at the top level + /// and one level deep (LongCat nests some figures under `quota`/`detail`). + static func firstNumber(in object: [String: Any], keys: [String]) -> Double? { + for key in keys { + if let value = double(object[key]) { return value } + } + for value in object.values { + if let nested = value as? [String: Any] { + for key in keys { + if let found = double(nested[key]) { return found } + } + } + } + return nil + } +} diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatProviderDescriptor.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatProviderDescriptor.swift new file mode 100644 index 0000000000..6f3068b536 --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatProviderDescriptor.swift @@ -0,0 +1,107 @@ +import Foundation + +public enum LongCatProviderDescriptor { + public static let descriptor: ProviderDescriptor = Self.makeDescriptor() + + static func makeDescriptor() -> ProviderDescriptor { + ProviderDescriptor( + id: .longcat, + metadata: ProviderMetadata( + id: .longcat, + displayName: "LongCat", + sessionLabel: "Quota", + weeklyLabel: "Fuel Pack", + opusLabel: nil, + supportsOpus: false, + supportsCredits: false, + creditsHint: "", + toggleTitle: "Show LongCat usage", + cliName: "longcat", + defaultEnabled: false, + isPrimaryProvider: false, + usesAccountFallback: false, + browserCookieOrder: ProviderBrowserCookieDefaults.longcatCookieImportOrder, + dashboardURL: "https://longcat.chat/platform/", + statusPageURL: nil), + branding: ProviderBranding( + iconStyle: .longcat, + iconResourceName: "ProviderIcon-longcat", + color: ProviderColor(red: 255 / 255, green: 209 / 255, blue: 0 / 255)), + tokenCost: ProviderTokenCostConfig( + supportsTokenCost: false, + noDataMessage: { "LongCat cost summary is not supported." }), + fetchPlan: ProviderFetchPlan( + sourceModes: [.auto, .web], + pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [LongCatWebFetchStrategy()] })), + cli: ProviderCLIConfig( + name: "longcat", + aliases: ["long-cat", "lc"], + versionDetector: nil)) + } +} + +struct LongCatWebFetchStrategy: ProviderFetchStrategy { + let id: String = "longcat.web" + let kind: ProviderFetchKind = .web + private static let log = CodexBarLog.logger(LogCategories.longcatWeb) + + func isAvailable(_ context: ProviderFetchContext) async -> Bool { + if LongCatCookieHeader.resolveCookieOverride(context: context) != nil { + return true + } + + #if os(macOS) + if Self.allowsBrowserImport(context: context) { + return LongCatCookieImporter.hasSession() + } + #endif + + return false + } + + func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult { + guard let cookieHeader = self.resolveCookieHeader(context: context) else { + throw LongCatAPIError.missingCookies + } + + let snapshot = try await LongCatUsageFetcher.fetchUsage(cookieHeader: cookieHeader) + return self.makeResult( + usage: snapshot.toUsageSnapshot(), + sourceLabel: "web") + } + + func shouldFallback(on error: Error, context _: ProviderFetchContext) -> Bool { + if case LongCatAPIError.missingCookies = error { return false } + if case LongCatAPIError.invalidSession = error { return false } + return true + } + + private func resolveCookieHeader(context: ProviderFetchContext) -> String? { + if let override = LongCatCookieHeader.resolveCookieOverride(context: context) { + return override.cookieHeader + } + + #if os(macOS) + if Self.allowsBrowserImport(context: context) { + if let session = try? LongCatCookieImporter.importSession(), + let header = session.cookieHeader + { + return header + } + } + #endif + + return nil + } + + /// Browser cookie/keychain import is only used for user-initiated app + /// refreshes in the Auto source. Manual must use the pasted header and Off + /// disables web auth, so neither should silently fall back to a browser + /// session. + static func allowsBrowserImport(context: ProviderFetchContext) -> Bool { + let source = context.settings?.longcat?.cookieSource + return context.runtime == .app && + ProviderInteractionContext.current == .userInitiated && + (source == nil || source == .auto) + } +} diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatSettingsReader.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatSettingsReader.swift new file mode 100644 index 0000000000..d20a7856f6 --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatSettingsReader.swift @@ -0,0 +1,33 @@ +import Foundation + +public enum LongCatSettingsReader { + public static let cookieHeaderKey = "LONGCAT_MANUAL_COOKIE" + + /// Manual cookie header for the LongCat web console (longcat.chat). + public static func cookieHeader(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { + let raw = environment[self.cookieHeaderKey] ?? environment["longcat_manual_cookie"] + return self.cleaned(raw) + } + + /// LongCat OpenAI/Anthropic-compatible API key. Not used for usage (the public + /// API exposes no usage endpoint) but kept for parity and future signals. + public static func apiKey(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { + let raw = environment["LONGCAT_API_KEY"] ?? environment["longcat_api_key"] + return self.cleaned(raw) + } + + private static func cleaned(_ raw: String?) -> String? { + guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { + return nil + } + + if (value.hasPrefix("\"") && value.hasSuffix("\"")) || + (value.hasPrefix("'") && value.hasSuffix("'")) + { + value = String(value.dropFirst().dropLast()) + } + + value = value.trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } +} diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatUsageFetcher.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatUsageFetcher.swift new file mode 100644 index 0000000000..c50269d637 --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatUsageFetcher.swift @@ -0,0 +1,210 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct LongCatUsageFetcher: Sendable { + private static let log = CodexBarLog.logger(LogCategories.longcatAPI) + private static let host = "https://longcat.chat" + + private static let userCurrentPath = "/api/v1/user-current" + private static let tokenUsagePath = "/api/lc-platform/v1/tokenUsage" + private static let pendingFuelPath = "/api/lc-platform/v1/pending-fuel-packages" + + /// LongCat fetches run on an isolated, ephemeral, cookie-free session so the + /// console's `Set-Cookie` responses never enter the shared provider cookie jar; + /// auth is carried solely by the explicit request `Cookie` header. Mirrors the + /// Sakana provider's isolated transport. + private static let defaultTransport: ProviderHTTPClient = { + let configuration = URLSessionConfiguration.ephemeral + configuration.httpCookieStorage = nil + configuration.httpShouldSetCookies = false + let session = ProviderHTTPClient.redirectGuardedSession(configuration: configuration) + return ProviderHTTPClient(session: session) + }() + + public static func fetchUsage( + cookieHeader: String, + transport transportOverride: (any ProviderHTTPTransport)? = nil, + now: Date = Date()) async throws -> LongCatUsageSnapshot + { + let transport = transportOverride ?? Self.defaultTransport + // Account name. The user-current payload also carries a session token and + // phone number, so its body is never logged. This is the required probe: + // a Meituan envelope with HTTP 200 but code 401/403 surfaces as + // `.invalidSession` here (via unwrap) so expired cookies are reported + // rather than masked by an empty snapshot. + var account: [String: Any]? + if let data = try await self.get( + self.userCurrentPath, + cookieHeader: cookieHeader, + transport: transport, + required: true) + { + account = try LongCatEnvelope.unwrap(self.json(data)) as? [String: Any] + } + + let usage = try await self.optionalPayload( + self.tokenUsagePath, + cookieHeader: cookieHeader, + transport: transport) + + let fuel = try await self.optionalPayload( + self.pendingFuelPath, + cookieHeader: cookieHeader, + transport: transport) + + return self.buildSnapshot(account: account, tokenUsage: usage, pendingFuel: fuel, now: now) + } + + /// Pure extraction over the unwrapped `data` payloads. Field paths are locked + /// against captured live responses; see `LongCatProviderTests`. + static func buildSnapshot( + account: [String: Any]?, + tokenUsage: [String: Any]?, + pendingFuel: [String: Any]?, + now: Date = Date()) -> LongCatUsageSnapshot + { + var snapshot = LongCatUsageSnapshot(updatedAt: now) + + if let account { + snapshot.accountName = LongCatJSON.string(account["name"]) ?? LongCatJSON.string(account["nickName"]) + } + + // Token quota: data.usage is the canonical aggregate; extData holds the + // per-model breakdown (LongCat-Flash-Lite, LongCat-2.0-Preview, ...). + if let tokenUsage { + let usage = LongCatJSON.object(tokenUsage["usage"]) ?? tokenUsage + snapshot.totalQuota = LongCatJSON.double(usage["totalToken"]) + snapshot.usedQuota = LongCatJSON.double(usage["usedToken"]) + snapshot.remainingQuota = LongCatJSON.double(usage["availableToken"]) + } + + if let pendingFuel { + self.applyFuelPackages(pendingFuel, to: &snapshot) + } + + return snapshot + } + + private static func applyFuelPackages(_ dict: [String: Any], to snapshot: inout LongCatUsageSnapshot) { + let total = LongCatJSON.double(dict["totalQuota"]) + let packages = LongCatJSON.array(dict["list"]) ?? [] + + var remaining = 0.0 + var sawRemaining = false + var nearestExpiry: Date? + for package in packages { + // Field names are pinned to the shapes captured from live longcat.chat + // responses (see LongCatProviderTests): a fuel package reports its remaining + // balance under `availableToken` and its expiry under `expireTime`. + if let value = LongCatJSON.double(package["availableToken"]) { + remaining += value + sawRemaining = true + } + if let expiry = self.parseDate(package["expireTime"]) { + if nearestExpiry == nil || expiry < nearestExpiry! { nearestExpiry = expiry } + } + } + + if let total, total > 0 { + snapshot.fuelPackTotal = total + snapshot.fuelPackRemaining = sawRemaining ? remaining : total + } + snapshot.nearestFuelExpiry = nearestExpiry + } + + // MARK: - HTTP + + private static func optionalPayload( + _ path: String, + cookieHeader: String, + transport: any ProviderHTTPTransport) async throws -> [String: Any]? + { + do { + guard let data = try await self.get( + path, + cookieHeader: cookieHeader, + transport: transport, + required: false) + else { + return nil + } + self.logRawShape(path, data) + return try LongCatEnvelope.unwrap(self.json(data)) as? [String: Any] + } catch LongCatAPIError.invalidSession { + throw LongCatAPIError.invalidSession + } catch { + self.log.error("LongCat optional \(path) failed: \(error.localizedDescription)") + return nil + } + } + + private static func get( + _ path: String, + cookieHeader: String, + transport: any ProviderHTTPTransport, + required: Bool) async throws -> Data? + { + guard let url = URL(string: self.host + path) else { + throw LongCatAPIError.invalidRequest("bad URL: \(path)") + } + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue(cookieHeader, forHTTPHeaderField: "Cookie") + request.setValue("application/json, text/plain, */*", forHTTPHeaderField: "Accept") + request.setValue(self.host, forHTTPHeaderField: "Origin") + request.setValue("\(self.host)/platform/usage", forHTTPHeaderField: "Referer") + request.setValue("en-US,en;q=0.9", forHTTPHeaderField: "Accept-Language") + let userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" + request.setValue(userAgent, forHTTPHeaderField: "User-Agent") + + let response = try await transport.response(for: request) + guard response.statusCode == 200 else { + // The shared transport's redirect guard drops cross-origin / non-HTTPS + // hops, so an expired-cookie login redirect surfaces here as the raw 3xx. + // Classify 3xx (and explicit 401/403) as an invalid session rather than a + // generic HTTP error, so users see "sign in again" instead of "HTTP 302". + if response.statusCode == 401 || response.statusCode == 403 + || (300..<400).contains(response.statusCode) + { + throw LongCatAPIError.invalidSession + } + if required { + throw LongCatAPIError.apiError("HTTP \(response.statusCode) for \(path)") + } + Self.log.error("LongCat \(path) returned \(response.statusCode)") + return nil + } + return response.data + } + + private static func json(_ data: Data) -> Any? { + try? JSONSerialization.jsonObject(with: data) + } + + /// Logs the (non-sensitive) response shape to help future debugging. Never + /// called for user-current, whose body carries a session token + phone. + private static func logRawShape(_ path: String, _ data: Data) { + guard let body = String(data: data, encoding: .utf8) else { return } + Self.log.debug("LongCat \(path) raw: \(body.prefix(1200))") + } + + private static func parseDate(_ value: Any?) -> Date? { + if let number = LongCatJSON.double(value) { + let seconds = number > 1_000_000_000_000 ? number / 1000 : number + if seconds > 1_000_000_000 { return Date(timeIntervalSince1970: seconds) } + } + if let string = LongCatJSON.string(value) { + let iso = ISO8601DateFormatter() + if let date = iso.date(from: string) { return date } + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + if let date = formatter.date(from: string) { return date } + } + return nil + } +} diff --git a/Sources/CodexBarCore/Providers/LongCat/LongCatUsageSnapshot.swift b/Sources/CodexBarCore/Providers/LongCat/LongCatUsageSnapshot.swift new file mode 100644 index 0000000000..e2452ed126 --- /dev/null +++ b/Sources/CodexBarCore/Providers/LongCat/LongCatUsageSnapshot.swift @@ -0,0 +1,81 @@ +import Foundation + +/// Parsed, Sendable view of the LongCat console quota model: +/// 总额度 (total token quota) plus 加油包额度 (fuel packs, which expire). +public struct LongCatUsageSnapshot: Sendable { + public var totalQuota: Double? + public var usedQuota: Double? + public var remainingQuota: Double? + public var fuelPackTotal: Double? + public var fuelPackRemaining: Double? + public var nearestFuelExpiry: Date? + public var accountName: String? + public var updatedAt: Date + + public init( + totalQuota: Double? = nil, + usedQuota: Double? = nil, + remainingQuota: Double? = nil, + fuelPackTotal: Double? = nil, + fuelPackRemaining: Double? = nil, + nearestFuelExpiry: Date? = nil, + accountName: String? = nil, + updatedAt: Date = Date()) + { + self.totalQuota = totalQuota + self.usedQuota = usedQuota + self.remainingQuota = remainingQuota + self.fuelPackTotal = fuelPackTotal + self.fuelPackRemaining = fuelPackRemaining + self.nearestFuelExpiry = nearestFuelExpiry + self.accountName = accountName + self.updatedAt = updatedAt + } +} + +extension LongCatUsageSnapshot { + private func resolvedUsed(total: Double) -> Double { + if let used = usedQuota { return max(0, used) } + if let remaining = remainingQuota { return max(0, total - remaining) } + return 0 + } + + public func toUsageSnapshot() -> UsageSnapshot { + // Primary: overall token quota consumption (总额度). + var primary: RateWindow? + if let total = totalQuota, total > 0 { + let used = self.resolvedUsed(total: total) + primary = RateWindow( + usedPercent: min(100, used / total * 100), + windowMinutes: nil, + resetsAt: nil, + resetDescription: "\(Int(used))/\(Int(total))") + } + + // Secondary: fuel-pack balance (加油包额度), with nearest expiry as reset. + var secondary: RateWindow? + if let total = fuelPackTotal, total > 0 { + let remaining = self.fuelPackRemaining ?? total + let used = max(0, total - remaining) + secondary = RateWindow( + usedPercent: min(100, used / total * 100), + windowMinutes: nil, + resetsAt: self.nearestFuelExpiry, + resetDescription: "Fuel pack: \(Int(remaining))/\(Int(total))") + } + + let identity = ProviderIdentitySnapshot( + providerID: .longcat, + accountEmail: nil, + accountOrganization: self.accountName, + loginMethod: nil) + + return UsageSnapshot( + primary: primary, + secondary: secondary, + tertiary: nil, + providerCost: nil, + updatedAt: self.updatedAt, + identity: identity) + } +} diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift index 9f16b60743..c4491b8e2b 100644 --- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift @@ -108,6 +108,7 @@ public enum ProviderDescriptorRegistry { .deepgram: DeepgramProviderDescriptor.descriptor, .poe: PoeProviderDescriptor.descriptor, .chutes: ChutesProviderDescriptor.descriptor, + .longcat: LongCatProviderDescriptor.descriptor, .crossmodel: CrossModelProviderDescriptor.descriptor, .clawrouter: ClawRouterProviderDescriptor.descriptor, .sub2api: Sub2APIProviderDescriptor.descriptor, diff --git a/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift b/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift index 09a53275de..9c744f589e 100644 --- a/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift +++ b/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift @@ -25,6 +25,7 @@ public struct ProviderSettingsSnapshot: Sendable { copilot: CopilotProviderSettings? = nil, kilo: KiloProviderSettings? = nil, kimi: KimiProviderSettings? = nil, + longcat: LongCatProviderSettings? = nil, augment: AugmentProviderSettings? = nil, moonshot: MoonshotProviderSettings? = nil, amp: AmpProviderSettings? = nil, @@ -58,6 +59,7 @@ public struct ProviderSettingsSnapshot: Sendable { copilot: copilot, kilo: kilo, kimi: kimi, + longcat: longcat, augment: augment, moonshot: moonshot, amp: amp, @@ -293,6 +295,16 @@ public struct ProviderSettingsSnapshot: Sendable { } } + public struct LongCatProviderSettings: ProviderCookieSettings { + public let cookieSource: ProviderCookieSource + public let manualCookieHeader: String? + + public init(cookieSource: ProviderCookieSource, manualCookieHeader: String?) { + self.cookieSource = cookieSource + self.manualCookieHeader = manualCookieHeader + } + } + public struct AugmentProviderSettings: ProviderCookieSettings { public let cookieSource: ProviderCookieSource public let manualCookieHeader: String? @@ -472,6 +484,7 @@ public struct ProviderSettingsSnapshot: Sendable { public let copilot: CopilotProviderSettings? public let kilo: KiloProviderSettings? public let kimi: KimiProviderSettings? + public let longcat: LongCatProviderSettings? public let augment: AugmentProviderSettings? public let moonshot: MoonshotProviderSettings? public let amp: AmpProviderSettings? @@ -509,6 +522,7 @@ public struct ProviderSettingsSnapshot: Sendable { copilot: CopilotProviderSettings?, kilo: KiloProviderSettings?, kimi: KimiProviderSettings?, + longcat: LongCatProviderSettings? = nil, augment: AugmentProviderSettings?, moonshot: MoonshotProviderSettings? = nil, amp: AmpProviderSettings?, @@ -541,6 +555,7 @@ public struct ProviderSettingsSnapshot: Sendable { self.copilot = copilot self.kilo = kilo self.kimi = kimi + self.longcat = longcat self.augment = augment self.moonshot = moonshot self.amp = amp @@ -574,6 +589,7 @@ public enum ProviderSettingsSnapshotContribution: Sendable { case copilot(ProviderSettingsSnapshot.CopilotProviderSettings) case kilo(ProviderSettingsSnapshot.KiloProviderSettings) case kimi(ProviderSettingsSnapshot.KimiProviderSettings) + case longcat(ProviderSettingsSnapshot.LongCatProviderSettings) case augment(ProviderSettingsSnapshot.AugmentProviderSettings) case moonshot(ProviderSettingsSnapshot.MoonshotProviderSettings) case amp(ProviderSettingsSnapshot.AmpProviderSettings) @@ -608,6 +624,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable { public var copilot: ProviderSettingsSnapshot.CopilotProviderSettings? public var kilo: ProviderSettingsSnapshot.KiloProviderSettings? public var kimi: ProviderSettingsSnapshot.KimiProviderSettings? + public var longcat: ProviderSettingsSnapshot.LongCatProviderSettings? public var augment: ProviderSettingsSnapshot.AugmentProviderSettings? public var moonshot: ProviderSettingsSnapshot.MoonshotProviderSettings? public var amp: ProviderSettingsSnapshot.AmpProviderSettings? @@ -646,6 +663,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable { case let .copilot(value): self.copilot = value case let .kilo(value): self.kilo = value case let .kimi(value): self.kimi = value + case let .longcat(value): self.longcat = value case let .augment(value): self.augment = value case let .moonshot(value): self.moonshot = value case let .amp(value): self.amp = value @@ -682,6 +700,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable { copilot: self.copilot, kilo: self.kilo, kimi: self.kimi, + longcat: self.longcat, augment: self.augment, moonshot: self.moonshot, amp: self.amp, diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift index bef804bfc0..d9d54c013d 100644 --- a/Sources/CodexBarCore/Providers/Providers.swift +++ b/Sources/CodexBarCore/Providers/Providers.swift @@ -58,6 +58,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable { case deepgram case poe case chutes + case longcat case crossmodel case clawrouter case sub2api @@ -121,6 +122,7 @@ public enum IconStyle: String, Sendable, CaseIterable { case deepgram case poe case chutes + case longcat case crossmodel case clawrouter case sub2api @@ -294,4 +296,13 @@ public enum ProviderBrowserCookieDefaults { nil #endif } + + /// LongCat Auto imports only from Chrome by default to avoid prompting unrelated browser keychains. + public static var longcatCookieImportOrder: BrowserCookieImportOrder? { + #if os(macOS) + [.chrome] + #else + nil + #endif + } } diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift index de337ed815..0d3be1bf9a 100644 --- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift +++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift @@ -825,7 +825,7 @@ enum CostUsageScanner { .copilot, .devin, .minimax, .manus, .kilo, .kiro, .kimi, .kimik2, .moonshot, .augment, .jetbrains, .amp, .ollama, .t3chat, .synthetic, .openrouter, .elevenlabs, .warp, .perplexity, .mimo, .doubao, .sakana, .abacus, .mistral, .deepseek, .codebuff, .crof, .windsurf, .zed, .venice, .commandcode, .qoder, .stepfun, - .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .crossmodel, .clawrouter, + .bedrock, .grok, .groq, .llmproxy, .litellm, .deepgram, .poe, .chutes, .longcat, .crossmodel, .clawrouter, .sub2api, .wayfinder, .zenmux: return emptyReport } diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift index 52ae23da1f..5337668669 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift @@ -124,6 +124,7 @@ enum ProviderChoice: String, AppEnum { case .deepgram: return nil // Deepgram not yet supported in widgets case .poe: return nil // Poe not yet supported in widgets case .chutes: return nil // Chutes not yet supported in widgets + case .longcat: return nil // LongCat not yet supported in widgets case .zed: return nil // Zed not yet supported in widgets case .zenmux: return nil // ZenMux not yet supported in widgets } diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 4457269111..0e1784e4bc 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -345,6 +345,7 @@ private struct ProviderSwitchChip: View { case .deepgram: "Deepgram" case .poe: "Poe" case .chutes: "Chutes" + case .longcat: "LongCat" case .zed: "Zed" case .zenmux: "ZenMux" } @@ -1082,6 +1083,8 @@ enum WidgetColors { Color(red: 93 / 255, green: 92 / 255, blue: 222 / 255) // Poe purple case .chutes: Color(red: 24 / 255, green: 160 / 255, blue: 88 / 255) + case .longcat: + Color(red: 255 / 255, green: 209 / 255, blue: 0 / 255) case .zed: Color(red: 64 / 255, green: 156 / 255, blue: 255 / 255) case .zenmux: diff --git a/Tests/CodexBarTests/BrowserCookieOrderLabelTests.swift b/Tests/CodexBarTests/BrowserCookieOrderLabelTests.swift index 8824497bb2..ca9912275c 100644 --- a/Tests/CodexBarTests/BrowserCookieOrderLabelTests.swift +++ b/Tests/CodexBarTests/BrowserCookieOrderLabelTests.swift @@ -86,5 +86,11 @@ struct BrowserCookieOrderStatusStringTests { #expect(ProviderDefaults.metadata[.copilot]?.browserCookieOrder == [.chrome]) #expect(ProviderBrowserCookieDefaults.copilotCookieImportOrder == [.chrome]) } + + @Test + func `longcat cookie imports default to chrome only`() { + #expect(ProviderDefaults.metadata[.longcat]?.browserCookieOrder == [.chrome]) + #expect(ProviderBrowserCookieDefaults.longcatCookieImportOrder == [.chrome]) + } #endif } diff --git a/Tests/CodexBarTests/LongCatProviderTests.swift b/Tests/CodexBarTests/LongCatProviderTests.swift new file mode 100644 index 0000000000..8af68ea839 --- /dev/null +++ b/Tests/CodexBarTests/LongCatProviderTests.swift @@ -0,0 +1,299 @@ +import Foundation +import Testing +@testable import CodexBarCore + +struct LongCatProviderTests { + // MARK: - Settings reader + + @Test + func `reads LONGCAT_MANUAL_COOKIE`() { + let env = ["LONGCAT_MANUAL_COOKIE": "passport_token=abc; uid=42"] + #expect(LongCatSettingsReader.cookieHeader(environment: env) == "passport_token=abc; uid=42") + } + + @Test + func `reads LONGCAT_API_KEY and trims quotes`() { + #expect(LongCatSettingsReader.apiKey(environment: ["LONGCAT_API_KEY": " \"ak_x\" "]) == "ak_x") + } + + @Test + func `missing env returns nil`() { + #expect(LongCatSettingsReader.cookieHeader(environment: [:]) == nil) + #expect(LongCatSettingsReader.apiKey(environment: [:]) == nil) + } + + @Test + func `cookieHeader reads lowercase alias and trims quotes`() { + // The env path routes through this reader, so the lower-case alias and + // quote-trimming must apply (regression for the env-bypass fix). + #expect(LongCatSettingsReader.cookieHeader(environment: ["longcat_manual_cookie": "'a=b; c=d'"]) == "a=b; c=d") + } + + // MARK: - Cookie header override + + @Test + func `override accepts bare cookie pair string`() { + let override = LongCatCookieHeader.override(from: "passport_token=abc; uid=42") + #expect(override?.cookieHeader == "passport_token=abc; uid=42") + } + + @Test + func `override extracts from a curl Cookie header`() { + let raw = "curl 'https://longcat.chat/api/v1/user-current' -H 'Cookie: passport_token=abc; uid=42'" + let override = LongCatCookieHeader.override(from: raw) + #expect(override?.cookieHeader == "passport_token=abc; uid=42") + } + + @Test + func `override rejects a token-less string`() { + #expect(LongCatCookieHeader.override(from: "not a cookie") == nil) + #expect(LongCatCookieHeader.override(from: " ") == nil) + } + + // MARK: - Snapshot mapping + + @Test + func `total quota maps to primary used percent`() { + let snapshot = LongCatUsageSnapshot(totalQuota: 1000, usedQuota: 250) + let usage = snapshot.toUsageSnapshot() + #expect(usage.identity?.providerID == .longcat) + #expect(abs((usage.primary?.usedPercent ?? 0) - 25) < 0.001) + } + + @Test + func `remaining quota infers used when used is absent`() { + let snapshot = LongCatUsageSnapshot(totalQuota: 1000, remainingQuota: 400) + #expect(abs((snapshot.toUsageSnapshot().primary?.usedPercent ?? 0) - 60) < 0.001) + } + + @Test + func `missing quota data omits primary window`() { + let usage = LongCatUsageSnapshot(fuelPackTotal: 500, fuelPackRemaining: 200).toUsageSnapshot() + #expect(usage.primary == nil) + #expect(usage.secondary != nil) + } + + @Test + func `fuel pack populates secondary window`() { + let snapshot = LongCatUsageSnapshot(fuelPackTotal: 500, fuelPackRemaining: 200) + let usage = snapshot.toUsageSnapshot() + #expect(usage.secondary != nil) + #expect(abs((usage.secondary?.usedPercent ?? 0) - 60) < 0.001) + } + + // MARK: - buildSnapshot against captured live response shapes + + private func object(_ json: String) throws -> [String: Any] { + let parsed = try JSONSerialization.jsonObject(with: Data(json.utf8)) + return try #require(parsed as? [String: Any]) + } + + @Test + func `buildSnapshot maps live tokenUsage and account fields`() throws { + // Shapes captured from longcat.chat console (values neutralised). + let account = try self.object(#"{"userId":1,"name":"LongCat User","phone":"x","token":"secret"}"#) + let tokenUsage = try self.object(#""" + {"usage":{"totalToken":500000,"usedToken":120000,"availableToken":380000,"freeAvailableToken":380000}, + "extData":{"LongCat-Flash-Lite":{"totalToken":50000000,"usedToken":0}}} + """#) + let fuel = try self.object(#"{"totalQuota":0,"list":[]}"#) + + let snapshot = LongCatUsageFetcher.buildSnapshot(account: account, tokenUsage: tokenUsage, pendingFuel: fuel) + #expect(snapshot.accountName == "LongCat User") + #expect(snapshot.totalQuota == 500_000) + #expect(snapshot.usedQuota == 120_000) + #expect(snapshot.remainingQuota == 380_000) + #expect(snapshot.fuelPackTotal == nil) // empty fuel list + + let usage = snapshot.toUsageSnapshot() + #expect(abs((usage.primary?.usedPercent ?? 0) - 24) < 0.001) + #expect(usage.secondary == nil) + } + + @Test + func `buildSnapshot sums active fuel packages`() throws { + let fuel = try self.object(#""" + {"totalQuota":1000,"list":[{"availableToken":600,"expireTime":1750000000000}, + {"availableToken":150,"expireTime":1760000000000}]} + """#) + let snapshot = LongCatUsageFetcher.buildSnapshot(account: nil, tokenUsage: nil, pendingFuel: fuel) + #expect(snapshot.fuelPackTotal == 1000) + #expect(snapshot.fuelPackRemaining == 750) + #expect(snapshot.nearestFuelExpiry != nil) + #expect(snapshot.toUsageSnapshot().primary == nil) + } + + // MARK: - Envelope + + @Test + func `envelope surfaces invalid session on auth code`() { + #expect(throws: LongCatAPIError.invalidSession) { + try LongCatEnvelope.unwrap(["code": 401, "message": "unauthorized"]) + } + } + + @Test + func `envelope unwraps data on success`() throws { + let data = try LongCatEnvelope.unwrap(["code": 0, "data": ["x": 1]]) as? [String: Any] + #expect(data?["x"] as? Int == 1) + } + + // MARK: - Cookie source semantics + + private func context( + env: [String: String], + cookieSource: ProviderCookieSource, + runtime: ProviderRuntime = .app) -> ProviderFetchContext + { + let browserDetection = BrowserDetection(cacheTTL: 0) + return ProviderFetchContext( + runtime: runtime, + sourceMode: .web, + includeCredits: false, + webTimeout: 1, + webDebugDumpHTML: false, + verbose: false, + env: env, + settings: ProviderSettingsSnapshot.make( + longcat: .init(cookieSource: cookieSource, manualCookieHeader: nil)), + fetcher: UsageFetcher(environment: [:]), + claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection), + browserDetection: browserDetection) + } + + @Test + func `off source disables env cookie override`() { + let ctx = self.context(env: ["LONGCAT_MANUAL_COOKIE": "a=b"], cookieSource: .off) + #expect(LongCatCookieHeader.resolveCookieOverride(context: ctx) == nil) + } + + @Test + func `auto source allows env cookie override`() { + let ctx = self.context(env: ["LONGCAT_MANUAL_COOKIE": "a=b"], cookieSource: .auto) + #expect(LongCatCookieHeader.resolveCookieOverride(context: ctx)?.cookieHeader == "a=b") + } + + @Test + func `browser import is user initiated app auto only`() { + let appAuto = self.context(env: [:], cookieSource: .auto) + let cliAuto = self.context(env: [:], cookieSource: .auto, runtime: .cli) + let appManual = self.context(env: [:], cookieSource: .manual) + let appOff = self.context(env: [:], cookieSource: .off) + + #expect(LongCatWebFetchStrategy.allowsBrowserImport(context: appAuto) == false) + #expect(LongCatWebFetchStrategy.allowsBrowserImport(context: cliAuto) == false) + + ProviderInteractionContext.$current.withValue(.userInitiated) { + #expect(LongCatWebFetchStrategy.allowsBrowserImport(context: appAuto)) + #expect(LongCatWebFetchStrategy.allowsBrowserImport(context: cliAuto) == false) + #expect(LongCatWebFetchStrategy.allowsBrowserImport(context: appManual) == false) + #expect(LongCatWebFetchStrategy.allowsBrowserImport(context: appOff) == false) + } + } + + // MARK: - HTTP status handling (fetchUsage over an injected transport) + + @Test + func `fetch surfaces invalid session on 401`() async { + let transport = LongCatScriptedTransport(results: [.status(401)]) + await #expect(throws: LongCatAPIError.invalidSession) { + _ = try await LongCatUsageFetcher.fetchUsage(cookieHeader: "session=x", transport: transport) + } + } + + @Test + func `fetch surfaces invalid session on 403`() async { + let transport = LongCatScriptedTransport(results: [.status(403)]) + await #expect(throws: LongCatAPIError.invalidSession) { + _ = try await LongCatUsageFetcher.fetchUsage(cookieHeader: "session=x", transport: transport) + } + } + + @Test + func `fetch treats a blocked login redirect as invalid session`() async { + // The shared transport's redirect guard drops the cross-origin login hop, so an + // expired cookie surfaces here as a raw 3xx; it must still read as invalid-session. + let transport = LongCatScriptedTransport(results: [.status(302)]) + await #expect(throws: LongCatAPIError.invalidSession) { + _ = try await LongCatUsageFetcher.fetchUsage(cookieHeader: "session=x", transport: transport) + } + } + + @Test + func `fetch surfaces invalid session from optional quota envelopes`() async { + let transport = LongCatScriptedTransport(results: [ + .body(#"{"code":0,"data":{"name":"Leo"}}"#), + .body(#"{"code":401,"message":"unauthorized"}"#), + ]) + + await #expect(throws: LongCatAPIError.invalidSession) { + _ = try await LongCatUsageFetcher.fetchUsage(cookieHeader: "session=x", transport: transport) + } + } + + @Test + func `fetch keeps optional non auth quota failures contained`() async throws { + let transport = LongCatScriptedTransport(results: [ + .body(#"{"code":0,"data":{"name":"Leo"}}"#), + .body(#"{"code":500,"message":"temporarily unavailable"}"#), + .body(#"{"code":0,"data":{"totalQuota":1000,"list":[{"availableToken":600}]}}"#), + ]) + + let snapshot = try await LongCatUsageFetcher.fetchUsage(cookieHeader: "session=x", transport: transport) + #expect(snapshot.accountName == "Leo") + #expect(snapshot.totalQuota == nil) + #expect(snapshot.fuelPackTotal == 1000) + #expect(snapshot.fuelPackRemaining == 600) + } + + @Test + func `fetch maps a full live response over the transport`() async throws { + let transport = LongCatScriptedTransport(results: [ + .body(#"{"code":0,"data":{"name":"Leo"}}"#), + .body(#"{"code":0,"data":{"usage":{"totalToken":500000,"usedToken":120000,"availableToken":380000}}}"#), + .body(#"{"code":0,"data":{"totalQuota":1000,"list":[{"availableToken":600,"expireTime":1750000000000}]}}"#), + ]) + let snapshot = try await LongCatUsageFetcher.fetchUsage(cookieHeader: "session=x", transport: transport) + #expect(snapshot.accountName == "Leo") + #expect(snapshot.totalQuota == 500_000) + #expect(snapshot.usedQuota == 120_000) + #expect(snapshot.fuelPackTotal == 1000) + #expect(snapshot.fuelPackRemaining == 600) + } +} + +/// Scripted transport for exercising `LongCatUsageFetcher.fetchUsage` HTTP paths +/// without a network. Returns the given results in order; an exhausted script +/// yields an empty 200 so best-effort follow-up probes decode to nil. +private actor LongCatScriptedTransport: ProviderHTTPTransport { + enum Result { + case status(Int) + case body(String) + } + + private var results: [Result] + + init(results: [Result]) { + self.results = results + } + + func data(for request: URLRequest) throws -> (Data, URLResponse) { + let result = self.results.isEmpty ? .status(200) : self.results.removeFirst() + let statusCode: Int + let body: String + switch result { + case let .status(code): + statusCode = code + body = "" + case let .body(text): + statusCode = 200 + body = text + } + let response = HTTPURLResponse( + url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil)! + return (Data(body.utf8), response) + } +} diff --git a/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift b/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift index 77b4302d01..dfdd4ab88c 100644 --- a/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift +++ b/Tests/CodexBarTests/ProviderConfigEnvironmentTests.swift @@ -266,6 +266,18 @@ struct ProviderConfigEnvironmentTests { #expect(SakanaSettingsReader.cookieHeader(environment: env) == "session=abc") } + @Test + func `applies cookie header override for longcat`() { + let config = ProviderConfig(id: .longcat, cookieHeader: "Cookie: passport_token=abc; uid=42") + let env = ProviderConfigEnvironment.applyProviderConfigOverrides( + base: [:], + provider: .longcat, + config: config) + + #expect(env[LongCatSettingsReader.cookieHeaderKey] == "Cookie: passport_token=abc; uid=42") + #expect(LongCatSettingsReader.cookieHeader(environment: env) == "Cookie: passport_token=abc; uid=42") + } + @Test func `applies API key override for moonshot`() { let config = ProviderConfig(id: .moonshot, apiKey: "moon-token") diff --git a/Tests/CodexBarTests/ProviderIconResourcesTests.swift b/Tests/CodexBarTests/ProviderIconResourcesTests.swift index 069f461800..68ee72e78b 100644 --- a/Tests/CodexBarTests/ProviderIconResourcesTests.swift +++ b/Tests/CodexBarTests/ProviderIconResourcesTests.swift @@ -29,6 +29,7 @@ struct ProviderIconResourcesTests { "commandcode", "t3chat", "kimi", + "longcat", "bedrock", "elevenlabs", "groq", diff --git a/docs/configuration.md b/docs/configuration.md index c4e58b2a36..dd1d60ae5e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -196,7 +196,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`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `crossmodel`, `clawrouter`, `sub2api`, `wayfinder`, `zenmux`. +`codex`, `openai`, `azureopenai`, `claude`, `cursor`, `opencode`, `opencodego`, `alibaba`, `alibabatokenplan`, `factory`, `gemini`, `antigravity`, `copilot`, `devin`, `zai`, `minimax`, `manus`, `kimi`, `kilo`, `kiro`, `vertexai`, `augment`, `jetbrains`, `kimik2`, `moonshot`, `amp`, `t3chat`, `ollama`, `synthetic`, `warp`, `openrouter`, `elevenlabs`, `windsurf`, `zed`, `perplexity`, `mimo`, `doubao`, `sakana`, `abacus`, `mistral`, `deepseek`, `codebuff`, `crof`, `venice`, `commandcode`, `qoder`, `stepfun`, `bedrock`, `grok`, `groq`, `llmproxy`, `litellm`, `deepgram`, `poe`, `chutes`, `longcat`, `crossmodel`, `clawrouter`, `sub2api`, `wayfinder`, `zenmux`. ## Ordering The order of `providers` controls display/order in the app and CLI. Reorder the array to change ordering. diff --git a/docs/index.html b/docs/index.html index a16f1b9259..f75b1746b9 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,7 +6,7 @@ CodexBar — every AI coding limit, in your menu bar @@ -37,7 +37,7 @@ @@ -293,7 +293,7 @@

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

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

Every AI coding limit, in your menu bar.

-

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

+

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