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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 60 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 61 providers." width="100%" /></a>

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.

Expand Down
100 changes: 100 additions & 0 deletions Sources/CodexBar/Providers/LongCat/LongCatProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wire LongCat cookies into CLI snapshots

This app-side snapshot wiring does not cover the CLI path: TokenAccountCLIContext.makeCookieBackedSnapshot/makeSnapshot has no LongCat case, and the environment resolver does not project a config cookieHeader into LONGCAT_MANUAL_COOKIE. In the scenario where a user runs codexbar usage --provider longcat with LongCat credentials saved in the config, the fetch context is built with settings == nil, so LongCatCookieHeader only sees process env and reports missing cookies even though the config contains them.

Useful? React with 👍 / 👎.

}

@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() }),
]
}
}
54 changes: 54 additions & 0 deletions Sources/CodexBar/Providers/LongCat/LongCatSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-longcat.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Expand Down
14 changes: 14 additions & 0 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
3 changes: 3 additions & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions Sources/CodexBarCore/Providers/LongCat/LongCatAPIError.swift
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
77 changes: 77 additions & 0 deletions Sources/CodexBarCore/Providers/LongCat/LongCatCookieHeader.swift
Original file line number Diff line number Diff line change
@@ -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..<raw.endIndex, in: raw)
guard let match = regex.firstMatch(in: raw, options: [], range: range),
match.numberOfRanges >= 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
}
}
Loading