Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CodexBar 🎚️ - May your tokens never run out.

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

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

Expand Down Expand Up @@ -48,6 +48,7 @@ Linux support via Omarchy: community Waybar module and TUI, driven by the `codex
- [JetBrains AI](docs/jetbrains.md) — Local XML-based quota from JetBrains IDE configuration; monthly credits tracking.
- [OpenRouter](docs/openrouter.md) — API token for credit-based usage tracking across multiple AI providers.
- [Abacus AI](docs/abacus.md) — Browser cookie auth for ChatLLM/RouteLLM compute credit tracking.
- [Codebuff](docs/codebuff.md) — API token (or `~/.config/manicode/credentials.json`) for credit balance + weekly rate limit.
- Open to new providers: [provider authoring guide](docs/provider.md).

## Icon & Screenshot
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation

@ProviderImplementationRegistration
struct CodebuffProviderImplementation: ProviderImplementation {
let id: UsageProvider = .codebuff

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.codebuffAPIToken
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "codebuff-api-key",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let " +
"CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`).",
kind: .secure,
placeholder: "cb_...",
binding: context.stringBinding(\.codebuffAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "codebuff-open-dashboard",
title: "Open Codebuff Dashboard",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://www.codebuff.com/usage") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Codebuff/CodebuffSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var codebuffAPIToken: String {
get { self.configSnapshot.providerConfig(for: .codebuff)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .codebuff) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .codebuff, field: "apiKey", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ enum ProviderImplementationRegistry {
case .warp: WarpProviderImplementation()
case .perplexity: PerplexityProviderImplementation()
case .abacus: AbacusProviderImplementation()
case .codebuff: CodebuffProviderImplementation()
}
}

Expand Down
4 changes: 4 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-codebuff.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 @@ -880,7 +880,7 @@ extension UsageStore {
let source = resolution?.source.rawValue ?? "none"
return "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .opencodego, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains, .perplexity, .abacus:
.kimik2, .jetbrains, .perplexity, .abacus, .codebuff:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ struct TokenAccountCLIContext {
abacus: ProviderSettingsSnapshot.AbacusProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp:
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp,
.codebuff:
return nil
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public enum ProviderConfigEnvironment {
}
case .openrouter:
env[OpenRouterSettingsReader.envKey] = apiKey
case .codebuff:
env[CodebuffSettingsReader.apiTokenKey] = apiKey
default:
break
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import CodexBarMacroSupport
import Foundation

@ProviderDescriptorRegistration
@ProviderDescriptorDefinition
public enum CodebuffProviderDescriptor {
static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .codebuff,
metadata: ProviderMetadata(
id: .codebuff,
displayName: "Codebuff",
sessionLabel: "Credits",
weeklyLabel: "Weekly",
opusLabel: nil,
supportsOpus: false,
supportsCredits: true,
creditsHint: "Credit balance from the Codebuff API",
toggleTitle: "Show Codebuff usage",
cliName: "codebuff",
defaultEnabled: false,
isPrimaryProvider: false,
usesAccountFallback: false,
browserCookieOrder: nil,
dashboardURL: "https://www.codebuff.com/usage",
statusPageURL: nil,
statusLinkURL: nil),
branding: ProviderBranding(
iconStyle: .codebuff,
iconResourceName: "ProviderIcon-codebuff",
color: ProviderColor(red: 68 / 255, green: 255 / 255, blue: 0 / 255)),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Codebuff cost summary is not yet supported." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [CodebuffAPIFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "codebuff",
aliases: ["manicode"],
versionDetector: nil))
}
}

struct CodebuffAPIFetchStrategy: ProviderFetchStrategy {
let id: String = "codebuff.api"
let kind: ProviderFetchKind = .apiToken

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
_ = context
// Keep the strategy available so missing-token surfaces as a user-friendly error
// instead of a generic "no strategy" outcome.
return true
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = Self.resolveToken(environment: context.env) else {
throw CodebuffUsageError.missingCredentials
}
let usage = try await CodebuffUsageFetcher.fetchUsage(apiKey: apiKey, environment: context.env)
return self.makeResult(
usage: usage.toUsageSnapshot(),
sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}

private static func resolveToken(environment: [String: String]) -> String? {
ProviderTokenResolver.codebuffToken(environment: environment)
}
}

/// Errors related to Codebuff settings.
public enum CodebuffSettingsError: LocalizedError, Sendable {
case missingToken

public var errorDescription: String? {
switch self {
case .missingToken:
"Codebuff API token not configured. Set CODEBUFF_API_KEY or run `codebuff login` to " +
"populate ~/.config/manicode/credentials.json."
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

/// Reads Codebuff settings from the environment or the local credentials file
/// that the `codebuff` CLI (formerly `manicode`) writes when the user logs in.
public enum CodebuffSettingsReader {
/// Environment variable key for the Codebuff API token.
public static let apiTokenKey = "CODEBUFF_API_KEY"

/// Returns the API token from environment if present and non-empty.
public static func apiKey(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? {
self.cleaned(environment[self.apiTokenKey])
}

/// Returns the API base URL, defaulting to the production endpoint.
public static func apiURL(environment: [String: String] = ProcessInfo.processInfo.environment) -> URL {
if let override = environment["CODEBUFF_API_URL"],
let url = URL(string: cleaned(override) ?? "")
{
return url
}
return URL(string: "https://www.codebuff.com")!
}

/// Returns the auth token from the local credentials file if present.
public static func authToken(
authFileURL: URL? = nil,
homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser) -> String?
{
let fileURL = authFileURL ?? self.defaultAuthFileURL(homeDirectory: homeDirectory)
guard let data = try? Data(contentsOf: fileURL) else { return nil }
return self.parseAuthToken(data: data)
}

/// Default on-disk credentials path: `~/.config/manicode/credentials.json`.
static func defaultAuthFileURL(homeDirectory: URL) -> URL {
homeDirectory
.appendingPathComponent(".config", isDirectory: true)
.appendingPathComponent("manicode", isDirectory: true)
.appendingPathComponent("credentials.json", isDirectory: false)
}

static func parseAuthToken(data: Data) -> String? {
guard let payload = try? JSONDecoder().decode(CredentialsFile.self, from: data) else {
return nil
}
return self.cleaned(payload.authToken)
}

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.removeFirst()
value.removeLast()
}

value = value.trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}

private struct CredentialsFile: Decodable {
let authToken: String?
let fingerprintId: String?
let email: String?
let name: String?
}
40 changes: 40 additions & 0 deletions Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

public enum CodebuffUsageError: LocalizedError, Sendable, Equatable {
case missingCredentials
case unauthorized
case endpointNotFound
case serviceUnavailable(Int)
case apiError(Int)
case networkError(String)
case parseFailed(String)

public static let missingToken: CodebuffUsageError = .missingCredentials

public var errorDescription: String? {
switch self {
case .missingCredentials:
"Codebuff API token not configured. Set CODEBUFF_API_KEY or run `codebuff login` to " +
"populate ~/.config/manicode/credentials.json."
case .unauthorized:
"Unauthorized. Please sign in to Codebuff again."
case .endpointNotFound:
"Codebuff usage endpoint not found."
case let .serviceUnavailable(status):
"Codebuff API is temporarily unavailable (status \(status))."
case let .apiError(status):
"Codebuff API returned an unexpected status (\(status))."
case let .networkError(message):
"Codebuff API error: \(message)"
case let .parseFailed(message):
"Could not parse Codebuff usage: \(message)"
}
}

public var isAuthRelated: Bool {
switch self {
case .unauthorized, .missingCredentials: true
default: false
}
}
}
Loading