Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.36.2 — Unreleased

### Added
- Rovo Dev: add experimental monthly credit tracking with Atlassian email and API-token authentication (#1275). Thanks @sameemcodes!
- Cursor: show personal on-demand spend alongside the shared team pool. Thanks @yashiels!
- Documentation: link the community KDE Plasma panel integration. Thanks @tylxr59!
- Codex: show available manual rate-limit reset credits and their next expiry for signed-in OAuth accounts. Thanks @rogdex24!
Expand Down
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. 53 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 54 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 Expand Up @@ -127,6 +127,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Deepgram](docs/deepgram.md) — API key usage summaries across speech, agent, token, and TTS metrics.
- [Poe](docs/poe.md) — API key for current point balance and recent points history.
- [Chutes](docs/chutes.md) — API key for subscription usage, rolling and monthly quota windows, and pay-as-you-go quotas.
- [Rovo Dev](docs/rovodev.md) — Experimental Atlassian email + scoped API token for monthly credit usage.
- Open to new providers: [provider authoring guide](docs/provider.md).

## Icon & Screenshot
Expand Down
6 changes: 3 additions & 3 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1331,8 +1331,8 @@ extension UsageMenuCardView.Model {
{
primaryDetailLeft = detail
}
if input.provider == .warp || input.provider == .kilo || input.provider == .mimo || input.provider == .deepseek
|| input.provider == .litellm,
if input.provider == .warp || input.provider == .kilo || input.provider == .mimo || input.provider == .rovodev
|| input.provider == .deepseek || input.provider == .litellm,
let detail = primary.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
Expand All @@ -1357,7 +1357,7 @@ extension UsageMenuCardView.Model {
primaryDetailText = detail
if input.provider == .manus { primaryResetText = nil }
}
if [.warp, .kilo, .mimo, .deepseek, .litellm].contains(input.provider), primary.resetsAt == nil {
if [.warp, .kilo, .mimo, .rovodev, .deepseek, .litellm].contains(input.provider), primary.resetsAt == nil {
primaryResetText = nil
}
// Abacus: show credits as detail, compute pace on the primary monthly window
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ struct MenuDescriptor {
if let primary = snap.primary {
let primaryDetail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines)
let primaryDescriptionIsDetail = provider == .warp || provider == .kilo || provider == .abacus ||
provider == .deepseek || provider == .azureopenai || provider == .mimo
provider == .deepseek || provider == .azureopenai || provider == .mimo || provider == .rovodev
let primaryWindow = if primaryDescriptionIsDetail {
// Some providers use resetDescription for non-reset detail
// (e.g., "Unlimited", "X/Y credits"). Avoid rendering it as a "Resets ..." line.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import AppKit
import CodexBarCore
import Foundation

struct RovoDevProviderImplementation: ProviderImplementation {
let id: UsageProvider = .rovodev

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

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

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
// Available when both email and API token are configured.
if RovoDevSettingsReader.apiToken(environment: context.environment) != nil,
RovoDevSettingsReader.email(environment: context.environment) != nil
{
return true
}
return !context.settings.rovoDevAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
!context.settings.rovoDevEmail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "rovodev-email",
title: "Atlassian email",
subtitle: "Your Atlassian account email address.",
kind: .plain,
placeholder: "you@example.com",
binding: context.stringBinding(\.rovoDevEmail),
actions: [],
isVisible: nil,
onActivate: nil),
ProviderSettingsFieldDescriptor(
id: "rovodev-api-token",
title: "API token",
subtitle: "Create a Rovo Dev token with only the read:rovodev:limits scope. " +
"Stored in ~/.codexbar/config.json or set ROVODEV_API_TOKEN in your environment.",
kind: .secure,
placeholder: "ATATT3x...",
binding: context.stringBinding(\.rovoDevAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "rovodev-open-token-page",
title: "Get API token",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://id.atlassian.com/manage-profile/security/api-tokens") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}
}
25 changes: 25 additions & 0 deletions Sources/CodexBar/Providers/RovoDev/RovoDevSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import CodexBarCore
import Foundation

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

/// Stores the Atlassian email used for Basic auth.
/// Mapped to ``ProviderConfig.workspaceID`` since there is no dedicated email field.
var rovoDevEmail: String {
get { self.configSnapshot.providerConfig(for: .rovodev)?.sanitizedWorkspaceID ?? "" }
set {
self.updateProviderConfig(provider: .rovodev) { entry in
entry.workspaceID = self.normalizedConfigValue(newValue)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ enum ProviderImplementationRegistry {
case .deepgram: DeepgramProviderImplementation()
case .poe: PoeProviderImplementation()
case .chutes: ChutesProviderImplementation()
case .rovodev: RovoDevProviderImplementation()
}
}

Expand Down
19 changes: 19 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-rovodev.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,7 @@ extension UsageStore {
.litellm: "LiteLLM debug log not yet implemented",
.deepgram: "Deepgram debug log not yet implemented",
.chutes: "Chutes debug log not yet implemented",
.rovodev: "Rovo Dev debug log not yet implemented",
]
let buildText = {
switch provider {
Expand Down Expand Up @@ -1063,7 +1064,7 @@ extension UsageStore {
case .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .factory, .copilot, .devin,
.vertexai, .kilo, .kiro, .kimi, .kimik2, .moonshot, .jetbrains, .perplexity, .mimo, .doubao,
.abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .stepfun, .bedrock,
.grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes:
.grok, .groq, .t3chat, .llmproxy, .litellm, .zed, .deepgram, .poe, .chutes, .rovodev:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/CodexBarCLI/CLIDiagnoseCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,13 @@ extension CodexBarCLI {
}

var modes: [String] = []
if account != nil {
if account != nil, provider != .rovodev {
modes.append("tokenAccount")
}
let hasConfigAPIAuth = if provider == .bedrock {
config?.sanitizedAPIKey != nil && config?.sanitizedSecretKey != nil
} else if provider == .rovodev {
config?.sanitizedAPIKey != nil && config?.sanitizedWorkspaceID != nil
} else {
config?.sanitizedAPIKey != nil || config?.sanitizedSecretKey != nil
}
Expand Down Expand Up @@ -244,6 +246,9 @@ extension CodexBarCLI {
OpenAIAPISettingsReader.apiKey(environment: environment) != nil
case .openrouter:
OpenRouterSettingsReader.apiToken(environment: environment) != nil
case .rovodev:
RovoDevSettingsReader.apiToken(environment: environment) != nil &&
RovoDevSettingsReader.email(environment: environment) != nil
case .stepfun:
StepFunSettingsReader.token(environment: environment) != nil
case .synthetic:
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBarCLI/CLIRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ enum CLIRenderer {
lines: inout [String])
{
if provider == .warp || provider == .kilo || provider == .mistral || provider == .deepseek ||
provider == .crof
provider == .crof || provider == .rovodev
{
if let reset = self.resetLineForDetailBackedWindow(window: window, style: context.resetStyle, now: now) {
lines.append(self.subtleLine(reset, useColor: context.useColor))
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Config/CodexBarConfigValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public enum CodexBarConfigValidator {
.opencodego,
.devin,
.deepgram,
.rovodev,
]

public static func validate(_ config: CodexBarConfig) -> [CodexBarConfigIssue] {
Expand Down
71 changes: 45 additions & 26 deletions Sources/CodexBarCore/Config/ProviderConfigEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ public enum ProviderConfigEnvironment {
provider: UsageProvider,
config: ProviderConfig?) -> [String: String]
{
if let env = self.applyDedicatedProviderOverrides(base: base, provider: provider, config: config) {
return env
if let overrides = self.applyProviderSpecificOverrides(base: base, provider: provider, config: config) {
return overrides
}
guard let apiKey = config?.sanitizedAPIKey, !apiKey.isEmpty else { return base }
var env = base
Expand Down Expand Up @@ -50,6 +50,31 @@ public enum ProviderConfigEnvironment {
return env
}

private static func applyProviderSpecificOverrides(
base: [String: String],
provider: UsageProvider,
config: ProviderConfig?) -> [String: String]?
{
switch provider {
case .openai:
self.applyOpenAIOverrides(base: base, config: config)
case .bedrock:
self.applyBedrockOverrides(base: base, config: config)
case .deepgram:
self.applyDeepgramOverrides(base: base, config: config)
case .rovodev:
self.applyRovoDevOverrides(base: base, config: config)
case .llmproxy, .litellm:
self.applyAPIKeyAndBaseURLOverrides(base: base, provider: provider, config: config)
case .azureopenai:
self.applyAzureOpenAIOverrides(base: base, config: config)
case .kimi:
self.applyKimiOverrides(base: base, config: config)
default:
nil
}
}

public static func supportsAPIKeyOverride(for provider: UsageProvider) -> Bool {
if self.directAPIKeyEnvironmentKey(for: provider) != nil { return true }
switch provider {
Expand Down Expand Up @@ -77,29 +102,6 @@ public enum ProviderConfigEnvironment {
self.baseURLEnvironmentKey(for: provider) != nil
}

private static func applyDedicatedProviderOverrides(
base: [String: String],
provider: UsageProvider,
config: ProviderConfig?) -> [String: String]?
{
switch provider {
case .openai:
self.applyOpenAIOverrides(base: base, config: config)
case .bedrock:
self.applyBedrockOverrides(base: base, config: config)
case .deepgram:
self.applyDeepgramOverrides(base: base, config: config)
case .llmproxy, .litellm:
self.applyAPIKeyAndBaseURLOverrides(base: base, provider: provider, config: config)
case .azureopenai:
self.applyAzureOpenAIOverrides(base: base, config: config)
case .kimi:
self.applyKimiOverrides(base: base, config: config)
default:
nil
}
}

private static func directAPIKeyEnvironmentKey(for provider: UsageProvider) -> String? {
switch provider {
case .amp:
Expand Down Expand Up @@ -138,7 +140,7 @@ public enum ProviderConfigEnvironment {
GroqSettingsReader.apiKeyEnvironmentKey
case .llmproxy:
LLMProxySettingsReader.apiKeyEnvironmentKey
case .chutes, .poe, .litellm:
case .chutes, .poe, .litellm, .rovodev:
self.additionalAPIKeyEnvironmentKey(for: provider)
default:
nil
Expand All @@ -153,11 +155,28 @@ public enum ProviderConfigEnvironment {
PoeSettingsReader.apiKeyEnvironmentKey
case .litellm:
LiteLLMSettingsReader.apiKeyEnvironmentKey
case .rovodev:
RovoDevSettingsReader.apiTokenEnvironmentKey
default:
nil
}
}

private static func applyRovoDevOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
{
guard let config else { return base }
var env = base
if let apiKey = config.sanitizedAPIKey {
env[RovoDevSettingsReader.apiTokenEnvironmentKey] = apiKey
}
if let email = config.sanitizedWorkspaceID {
env[RovoDevSettingsReader.emailEnvironmentKey] = email
}
return env
}

private static func applyOpenAIOverrides(
base: [String: String],
config: ProviderConfig?) -> [String: String]
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 = "800a06dead603ea7"
static let value = "1fe2da2eb08f87bb"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Logging/LogCategories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,5 @@ public enum LogCategories {
public static let zaiTokenStore = "zai-token-store"
public static let zaiUsage = "zai-usage"
public static let stepfunUsage = "stepfun-usage"
public static let rovoDevUsage = "rovodev-usage"
}
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/ProviderDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public enum ProviderDescriptorRegistry {
.deepgram: DeepgramProviderDescriptor.descriptor,
.poe: PoeProviderDescriptor.descriptor,
.chutes: ChutesProviderDescriptor.descriptor,
.rovodev: RovoDevProviderDescriptor.descriptor,
]
private static let bootstrap: Void = {
for provider in UsageProvider.allCases {
Expand Down
12 changes: 12 additions & 0 deletions Sources/CodexBarCore/Providers/ProviderTokenResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ public enum ProviderTokenResolver {
self.elevenLabsResolution(environment: environment)?.token
}

public static func rovoDevToken(
environment: [String: String] = ProcessInfo.processInfo.environment) -> String?
{
self.rovoDevResolution(environment: environment)?.token
}

public static func groqToken(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? {
self.groqResolution(environment: environment)?.token
}
Expand Down Expand Up @@ -346,6 +352,12 @@ public enum ProviderTokenResolver {
self.resolveEnv(ElevenLabsSettingsReader.apiKey(environment: environment))
}

public static func rovoDevResolution(
environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution?
{
self.resolveEnv(RovoDevSettingsReader.apiToken(environment: environment))
}

public static func groqResolution(
environment: [String: String] = ProcessInfo.processInfo.environment) -> ProviderTokenResolution?
{
Expand Down
Loading