Skip to content
Merged
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. 61 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 62 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, ClinePass, AWS Bedrock, and many newer coding providers. One status item per provider, or Merge Icons mode with a provider switcher. No Dock icon, minimal UI, dynamic bar icons.

Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@ extension UsageMenuCardView.Model {
percentLine: nil)
}

if provider == .openai || provider == .claude || provider == .litellm, cost.limit <= 0 {
if provider == .openai || provider == .claude || provider == .litellm || provider == .aiand,
cost.limit <= 0
{
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days")
return ProviderCostSection(
Expand Down
52 changes: 52 additions & 0 deletions Sources/CodexBar/Providers/AiAnd/AiAndProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import AppKit
import CodexBarCore
import Foundation

struct AiAndProviderImplementation: ProviderImplementation {
let id: UsageProvider = .aiand

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { _ in "api" }
}

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

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if AiAndSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
return !context.settings.aiAndAPIKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "aiand-api-key",
title: "API key",
subtitle: "Stored in CodexBar's config file. Create a key in the ai& console (shown once).",
kind: .secure,
placeholder: "sk-…",
binding: context.stringBinding(\.aiAndAPIKey),
actions: [
ProviderSettingsActionDescriptor(
id: "aiand-open-console",
title: "Open ai& Console",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://console.aiand.com") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/AiAnd/AiAndSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var aiAndAPIKey: String {
get { self.configSnapshot.providerConfig(for: .aiand)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .aiand) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .aiand, field: "apiKey", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ enum ProviderImplementationRegistry {
case .sub2api: Sub2APIProviderImplementation()
case .wayfinder: WayfinderProviderImplementation()
case .zenmux: ZenMuxProviderImplementation()
case .aiand: AiAndProviderImplementation()
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-aiand.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ extension UsageStore {
.wayfinder: "Wayfinder debug log not yet implemented",
.sub2api: "sub2api debug log not yet implemented",
.zenmux: "ZenMux debug log not yet implemented",
.aiand: "ai& debug log not yet implemented",
]
let buildText = {
switch provider {
Expand Down Expand Up @@ -1046,7 +1047,8 @@ extension UsageStore {
.copilot, .devin, .vertexai, .kilo, .kiro, .kimi, .moonshot, .jetbrains, .perplexity,
.mimo, .doubao, .sakana, .abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus,
.commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy, .litellm, .zed,
.deepgram, .poe, .chutes, .neuralwatt, .clawrouter, .longcat, .wayfinder, .sub2api, .zenmux:
.deepgram, .poe, .chutes, .neuralwatt, .clawrouter, .longcat, .wayfinder, .sub2api, .zenmux,
.aiand:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ extension CodexBarCLI {
ChutesSettingsReader.apiKey(environment: environment) != nil
case .zenmux:
ZenMuxSettingsReader.managementAPIKey(environment: environment) != nil
case .aiand:
AiAndSettingsReader.apiKey(environment: environment) != nil
case .crof:
CrofSettingsReader.apiKey(environment: environment) != nil
case .deepgram:
Expand Down
4 changes: 3 additions & 1 deletion Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public enum ProviderConfigEnvironment {
GroqSettingsReader.apiKeyEnvironmentKey
case .llmproxy:
LLMProxySettingsReader.apiKeyEnvironmentKey
case .chutes, .poe, .litellm, .clawrouter, .factory, .sub2api, .neuralwatt, .zenmux:
case .chutes, .poe, .litellm, .clawrouter, .factory, .sub2api, .neuralwatt, .zenmux, .aiand:
self.additionalAPIKeyEnvironmentKey(for: provider)
default:
nil
Expand All @@ -210,6 +210,8 @@ public enum ProviderConfigEnvironment {
FactorySettingsReader.apiTokenKey
case .zenmux:
ZenMuxSettingsReader.managementAPIKeyEnvironmentKey
case .aiand:
AiAndSettingsReader.apiKeyEnvironmentKey
default:
nil
}
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 = "2d056ae5a24d5157"
static let value = "011e4792f1671ebe"
}
66 changes: 66 additions & 0 deletions Sources/CodexBarCore/Providers/AiAnd/AiAndProviderDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation

public enum AiAndProviderDescriptor {
public static let descriptor: ProviderDescriptor = Self.makeDescriptor()

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .aiand,
metadata: ProviderMetadata(
id: .aiand,
displayName: "ai&",
sessionLabel: "Spend",
weeklyLabel: "Spend",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show ai& usage",
cliName: "aiand",
defaultEnabled: false,
dashboardURL: "https://console.aiand.com",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .aiand,
iconResourceName: "ProviderIcon-aiand",
color: ProviderColor(red: 226 / 255, green: 92 / 255, blue: 43 / 255),
confettiPalette: [
ProviderColor(hex: 0xE25C2B),
ProviderColor(hex: 0xF2A17E),
ProviderColor(hex: 0x33231C),
]),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "ai& spend is summed from the request logs API." }),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [AiAndAPIFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "aiand",
aliases: ["ai&", "ai-and"],
versionDetector: nil))
}
}

struct AiAndAPIFetchStrategy: ProviderFetchStrategy {
let id = "aiand.api"
let kind: ProviderFetchKind = .apiToken

func isAvailable(_ context: ProviderFetchContext) async -> Bool {
AiAndSettingsReader.apiKey(environment: context.env) != nil
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let credential = AiAndSettingsReader.apiKey(environment: context.env) else {
throw AiAndUsageError.notConfigured
}
let usage = try await AiAndUsageFetcher.fetchUsage(credential)
return self.makeResult(
usage: usage.toUsageSnapshot(),
sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}
}
24 changes: 24 additions & 0 deletions Sources/CodexBarCore/Providers/AiAnd/AiAndSettingsReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Foundation

public enum AiAndSettingsReader {
public static let apiKeyEnvironmentKey = "AIAND_API_KEY"

public static func apiKey(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
self.cleaned(environment[self.apiKeyEnvironmentKey])
}

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
}
}
Loading