Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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. 65 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 66 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 Expand Up @@ -117,6 +117,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Abacus AI](docs/abacus.md) — Browser cookie auth for ChatLLM/RouteLLM compute credit tracking.
- [Mistral](docs/mistral.md) — Browser cookies for API spend, credit balance, and monthly-plan usage.
- [DeepSeek](docs/deepseek.md) — API key for credit balance tracking (paid vs. granted breakdown).
- [Charm Hyper](docs/hyper.md) — Signed-in session or API key for remaining Hypercredit balance.
- [DeepInfra](docs/deepinfra.md) — API key for prepaid balance, current-month spend, and spending-limit tracking.
- [Moonshot / Kimi API](docs/moonshot.md) — API key for Moonshot/Kimi API account balance tracking.
- [Venice](docs/venice.md) — API key for DIEM or USD balance tracking.
Expand Down
14 changes: 14 additions & 0 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,20 @@ extension UsageMenuCardView.Model {
percentLine: nil)
}

if provider == .hyper,
cost.period == "Hypercredits balance",
let value = cost.balance
{
let balance = value.rounded() == value
? String(format: "%.0f", value)
: String(format: "%.2f", value)
return ProviderCostSection(
title: "Hypercredits",
percentUsed: nil,
spendLine: "\(L("Balance")): \(balance) HC",
percentLine: nil)
}

if provider == .zenmux || provider == .neuralwatt {
let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
return ProviderCostSection(
Expand Down
109 changes: 109 additions & 0 deletions Sources/CodexBar/Providers/Hyper/HyperProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct HyperProviderImplementation: ProviderImplementation {
let id: UsageProvider = .hyper

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { context in
context.store.sourceLabel(for: context.provider)
}
}

@MainActor
func observeSettings(_ settings: SettingsStore) {
_ = settings.hyperAPIKey
_ = settings.hyperCookieSource
_ = settings.hyperCookieHeader
_ = settings.tokenAccountsData(for: .hyper)
}

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.hyper(context.settings.hyperSettingsSnapshot())
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let binding = Binding(
get: { context.settings.hyperCookieSource.rawValue },
set: { raw in
context.settings.hyperCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let options = ProviderCookieSourceUI.options(
allowsOff: true,
keychainDisabled: context.settings.debugDisableKeychainAccess)
let subtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.hyperCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Prefer a signed-in Hyper session from Chrome, then fall back to an API key.",
manual: "Paste a Cookie header from hyper.charm.land.",
off: "Use only the configured API key.")
}

return [
ProviderSettingsPickerDescriptor(
id: "hyper-cookie-source",
title: "Session source",
subtitle: "Prefer a signed-in Hyper session, then fall back to an API key.",
dynamicSubtitle: subtitle,
binding: binding,
options: options,
isVisible: nil,
onChange: nil,
trailingText: {
ProviderCookieRefreshAction.trailingText(
provider: .hyper,
cookieSource: context.settings.hyperCookieSource,
context: context)
},
trailingActions: [
ProviderCookieRefreshAction.descriptor(
provider: .hyper,
cookieSource: { context.settings.hyperCookieSource },
context: context),
]),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "hyper-cookie",
title: "Hyper cookie",
subtitle: "Paste a Cookie header copied from a signed-in hyper.charm.land request.",
kind: .secure,
placeholder: "Cookie: ...",
binding: context.stringBinding(\.hyperCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "hyper-open-dashboard",
title: "Open Charm Hyper",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://hyper.charm.land") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: { context.settings.hyperCookieSource == .manual },
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "hyper-api-key",
title: "API key",
subtitle: "Fallback when no signed-in Hyper session is available. Stored in the CodexBar config file.",
kind: .secure,
placeholder: "Paste API key…",
binding: context.stringBinding(\.hyperAPIKey),
actions: [],
isVisible: nil,
onActivate: nil),
]
}
}
40 changes: 40 additions & 0 deletions Sources/CodexBar/Providers/Hyper/HyperSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var hyperCookieSource: ProviderCookieSource {
get { self.resolvedCookieSource(provider: .hyper, fallback: .auto) }
set {
self.updateProviderConfig(provider: .hyper) { entry in
entry.cookieSource = newValue
}
self.logProviderModeChange(provider: .hyper, field: "cookieSource", value: newValue.rawValue)
}
}

var hyperCookieHeader: String {
get { self.configSnapshot.providerConfig(for: .hyper)?.sanitizedCookieHeader ?? "" }
set {
self.updateProviderConfig(provider: .hyper) { entry in
entry.cookieHeader = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .hyper, field: "cookieHeader", value: newValue)
}
}

var hyperAPIKey: String {
get { self.configSnapshot.providerConfig(for: .hyper)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .hyper) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .hyper, field: "apiKey", value: newValue)
}
}

func hyperSettingsSnapshot() -> ProviderSettingsSnapshot.CookieProviderSettings {
ProviderSettingsSnapshot.CookieProviderSettings(
cookieSource: self.hyperCookieSource,
manualCookieHeader: self.hyperCookieHeader)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ enum ProviderImplementationRegistry {
case .mistral: MistralProviderImplementation()
case .deepseek: DeepSeekProviderImplementation()
case .deepinfra: DeepInfraProviderImplementation()
case .hyper: HyperProviderImplementation()
case .codebuff: CodebuffProviderImplementation()
case .crof: CrofProviderImplementation()
case .venice: VeniceProviderImplementation()
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-hyper.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/SettingsStore+MenuPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ extension SettingsStore {

static func isBalanceOnlyProvider(_ provider: UsageProvider) -> Bool {
switch provider {
case .deepseek, .deepinfra, .mistral, .moonshot, .poe:
case .deepseek, .deepinfra, .hyper, .mistral, .moonshot, .poe:
true
default:
false
Expand Down
22 changes: 22 additions & 0 deletions Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,11 @@ extension StatusItemController {
{
return balance
}
if provider == .hyper,
let balance = Self.hyperBalanceDisplayText(snapshot: snapshot)
{
return balance
}
if provider == .mimo,
let balance = Self.miMoBalanceDisplayText(
snapshot: snapshot,
Expand Down Expand Up @@ -1012,6 +1017,23 @@ extension StatusItemController {
return prefix + String(value)
}

nonisolated static func hyperBalanceDisplayText(snapshot: UsageSnapshot?) -> String? {
guard snapshot?.primary == nil,
snapshot?.secondary == nil,
let cost = snapshot?.providerCost,
cost.period == "Hypercredits balance",
let value = cost.balance,
value.isFinite,
value >= 0
else {
return nil
}
let balance = value.rounded() == value
? String(format: "%.0f", value)
: String(format: "%.2f", value)
return "\(balance) HC"
}

nonisolated static func miMoBalanceDisplayText(
snapshot: UsageSnapshot?,
preference: MenuBarMetricPreference) -> String?
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/UsageStore+Accessors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ extension UsageStore {
return DeepSeekUsageError.missingCredentials.errorDescription
case .deepinfra:
return DeepInfraUsageError.missingCredentials.errorDescription
case .hyper:
return HyperUsageError.missingCredentials.errorDescription
case .perplexity:
return PerplexityAPIError.missingToken.errorDescription
case .minimax:
Expand Down
3 changes: 2 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ extension UsageStore {
.sakana: "Sakana AI debug log not yet implemented",
.venice: "Venice debug log not yet implemented",
.deepinfra: "DeepInfra debug log not yet implemented",
.hyper: "Charm Hyper debug log not yet implemented",
.commandcode: "Command Code debug log not yet implemented",
.qoder: "Qoder debug log not yet implemented",
.stepfun: "StepFun debug log not yet implemented",
Expand Down Expand Up @@ -1076,7 +1077,7 @@ extension UsageStore {
hasTokenAccount: deepSeekHasTokenAccount)
case .clinepass, .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .qwencloud, .factory,
.copilot, .devin, .vertexai, .kilo, .kiro, .kimi, .moonshot, .jetbrains, .perplexity,
.mimo, .doubao, .sakana, .abacus, .mistral, .deepinfra, .codebuff, .crof, .windsurf,
.mimo, .doubao, .sakana, .abacus, .mistral, .deepinfra, .hyper, .codebuff, .crof, .windsurf,
.venice, .manus, .commandcode, .qoder, .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy,
.litellm, .zed, .deepgram, .poe, .chutes, .neuralwatt, .clawrouter, .longcat, .wayfinder,
.sub2api, .zenmux, .aiand, .zoommate:
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ extension CodexBarCLI {
switch provider {
case .kimi:
KimiSettingsReader.apiKey(environment: environment) != nil
case .hyper:
HyperSettingsReader.apiKey(environment: environment) != nil
case .llmproxy:
LLMProxySettingsReader.apiKey(environment: environment) != nil
case .clawrouter:
Expand Down
13 changes: 13 additions & 0 deletions Sources/CodexBarCLI/CLIRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,14 @@ enum CLIRenderer {
!(provider == .devin && cost.period == "Extra usage balance"),
!(provider == .claude && cost.used == 0 && cost.limit == 0 && cost.balance != nil)
else { return }
if provider == .hyper,
cost.period == "Hypercredits balance",
let value = cost.balance
{
let balance = Self.hypercreditsString(value)
lines.append(self.labelValueLine("Balance", value: "\(balance) HC", useColor: context.useColor))
return
}
// Fallback to cost/quota display if no primary rate window.
let label = cost.currencyCode == "Quota" ? "Quota" : "Cost"
let value = "\(String(format: "%.1f", cost.used)) / \(String(format: "%.1f", cost.limit))"
Expand Down Expand Up @@ -1020,6 +1028,11 @@ enum CLIRenderer {
}
}

private static func hypercreditsString(_ value: Double) -> String {
if value.rounded() == value { return String(format: "%.0f", value) }
return String(format: "%.2f", value)
}

private static func resetLineForDetailBackedWindow(
window: RateWindow,
style: ResetTimeDisplayStyle,
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, .deepinfra, .aiand:
case .chutes, .poe, .litellm, .clawrouter, .factory, .sub2api, .neuralwatt, .zenmux, .deepinfra, .hyper, .aiand:
self.additionalAPIKeyEnvironmentKey(for: provider)
default:
nil
Expand All @@ -212,6 +212,8 @@ public enum ProviderConfigEnvironment {
ZenMuxSettingsReader.managementAPIKeyEnvironmentKey
case .deepinfra:
DeepInfraSettingsReader.apiKeyEnvironmentKey
case .hyper:
HyperSettingsReader.apiKeyEnvironmentKey
case .aiand:
AiAndSettingsReader.apiKeyEnvironmentKey
default:
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 = "6f689d90f8eedcbd"
static let value = "1f58928572064c7b"
}
Loading