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
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. 69 providers." width="100%" /></a>
<a href="https://codexbar.app"><img src="docs/social.png" alt="CodexBar — every AI coding limit in your menu bar. 70 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 @@ -116,6 +116,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [Sakana AI](docs/sakana.md) — Manual Cookie header for 5-hour and weekly quota windows.
- [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.
- [Replicate](docs/replicate.md) — Browser cookies for monthly spend and prepaid credit balance.
- [DeepSeek](docs/deepseek.md) — API key for credit balance tracking (paid vs. granted breakdown).
- [Fireworks](docs/fireworks.md) — API key + account slug for 30-day spend from the billing summary API.
- [DeepInfra](docs/deepinfra.md) — API key for prepaid balance, current-month spend, and spending-limit tracking.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct ReplicateProviderImplementation: ProviderImplementation {
let id: UsageProvider = .replicate

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

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

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.replicate(context.settings.replicateSettingsSnapshot(tokenOverride: context.tokenOverride))
}

@MainActor
func tokenAccountsVisibility(context: ProviderSettingsContext, support: TokenAccountSupport) -> Bool {
guard support.requiresManualCookieSource else { return true }
if !context.settings.tokenAccounts(for: context.provider).isEmpty { return true }
return context.settings.replicateCookieSource == .manual
}

@MainActor
func applyTokenAccountCookieSource(settings: SettingsStore) {
if settings.replicateCookieSource != .manual {
settings.replicateCookieSource = .manual
}
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.replicateCookieSource.rawValue },
set: { raw in
context.settings.replicateCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: false,
keychainDisabled: context.settings.debugDisableKeychainAccess)

let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.replicateCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports Chrome cookies from replicate.com.",
manual: "Paste a Cookie header captured from the billing page.",
off: "Replicate cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "replicate-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports Chrome cookies from replicate.com.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: nil,
onChange: nil,
trailingText: {
ProviderCookieSourceUI.cachedTrailingText(provider: .replicate)
}),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "replicate-cookie-header",
title: "Cookie header",
subtitle: "Paste the Cookie header from a request to replicate.com/account/billing. "
+ "Must contain a sessionid cookie.",
kind: .secure,
placeholder: "sessionid=…; csrftoken=…",
binding: context.stringBinding(\.replicateCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "replicate-open-billing",
title: "Open Replicate Billing",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://replicate.com/account/billing") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: { context.settings.replicateCookieSource == .manual },
onActivate: nil),
]
}
}
38 changes: 38 additions & 0 deletions Sources/CodexBar/Providers/Replicate/ReplicateSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import CodexBarCore
import Foundation

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

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

func ensureReplicateCookieLoaded() {}
}

extension SettingsStore {
func replicateSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot
.ReplicateProviderSettings
{
self.resolvedCookieSettings(
provider: .replicate,
configuredSource: self.replicateCookieSource,
configuredHeader: self.replicateCookieHeader,
tokenOverride: tokenOverride)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ enum ProviderImplementationManifest {
{ VeniceProviderImplementation() },
{ CommandCodeProviderImplementation() },
{ QoderProviderImplementation() },
{ ReplicateProviderImplementation() },
{ StepFunProviderImplementation() },
{ BedrockProviderImplementation() },
{ GrokProviderImplementation() },
Expand Down
23 changes: 23 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-replicate.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,11 @@ extension StatusItemController {
return spend
}
}
if provider == .replicate,
let spend = Self.replicateSpendDisplayText(snapshot: snapshot)
{
return spend
}
if provider == .kiro {
return Self.kiroDisplayText(
snapshot: snapshot,
Expand Down Expand Up @@ -1055,6 +1060,20 @@ extension StatusItemController {
removingSuffix: " this month")
}

nonisolated static func replicateSpendDisplayText(snapshot: UsageSnapshot?) -> String? {
guard
let detail = snapshot?.primary?.resetDescription?
.trimmingCharacters(in: .whitespacesAndNewlines),
let spendDetail = detail.components(separatedBy: " · ").first?
.trimmingCharacters(in: .whitespacesAndNewlines),
spendDetail.hasPrefix("$"),
let value = spendDetail.split(separator: " ", maxSplits: 1).first
else {
return nil
}
return String(value)
}

nonisolated static func extraUsageSpendDisplayText(snapshot: UsageSnapshot?) -> String? {
guard let cost = snapshot?.providerCost,
cost.limit > 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ extension ProviderInstanceID {
public static let venice = UsageProvider.venice.instanceID
public static let commandcode = UsageProvider.commandcode.instanceID
public static let qoder = UsageProvider.qoder.instanceID
public static let replicate = UsageProvider.replicate.instanceID
public static let stepfun = UsageProvider.stepfun.instanceID
public static let bedrock = UsageProvider.bedrock.instanceID
public static let grok = UsageProvider.grok.instanceID
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/ProviderManifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public enum ProviderManifest {
VeniceProviderDescriptor.descriptor,
CommandCodeProviderDescriptor.descriptor,
QoderProviderDescriptor.descriptor,
ReplicateProviderDescriptor.descriptor,
StepFunProviderDescriptor.descriptor,
BedrockProviderDescriptor.descriptor,
GrokProviderDescriptor.descriptor,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBarCore/Providers/Providers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case venice
case commandcode
case qoder
case replicate
case stepfun
case bedrock
case grok
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Foundation

/// Constants locked from Replicate dashboard frontend route table + billing UI field usage.
/// Source: public frontend bundle `index-BJr3klVG.js` route table (verified in task-2 discovery).
/// Update only when Replicate changes the dashboard network surface.
public enum ReplicateBillingEndpoints: Sendable {
private static let baseURLString = "https://replicate.com"

/// Domains passed to SweetCookieKit / BrowserCookieClient for Automatic import.
/// Session cookie: Django-style `sessionid`; also import `csrftoken` for completeness.
public static let cookieDomains = ["replicate.com"]

public static let dashboardURLString = "https://replicate.com/account/billing"
public static let timeoutSeconds: TimeInterval = 30

// MARK: - Invoices (current-month spend)

/// GET — returns invoices including the current `monthly-usage` row.
///
/// JSON field mapping (menu-bar spend):
/// - Filter `invoices[]` where `type == "monthly-usage"`.
/// - Current invoice = first where `ended_before` is null or parses to a future date.
/// - **Usage this month** (`currentMonthSpend`): `Number(invoice.total_cost_before_adjustments ?? "0")`.
/// - Outstanding balance (optional): `Number(invoice.total_cost ?? "0")` — not the menu-bar metric.
/// - `currencyCode`: USD implied when absent (amounts are string decimals).
/// - Period: calendar month via `started_on` / `ended_before` on the draft monthly-usage invoice.
public static func userInvoicesURL(username: String) -> URL {
self.apiURL(pathComponents: ["api", "users", username, "invoices"])
}

/// GET — org-scoped invoices (same response shape as user invoices).
public static func organizationInvoicesURL(organizationName: String) -> URL {
self.apiURL(pathComponents: ["api", "organizations", organizationName, "invoices"])
}

// MARK: - Unused credit (prepaid balance)

/// GET — prepaid unused credit.
///
/// JSON field mapping:
/// - **Credit balance** (`creditBalance`): `Number(unused_credit ?? "0")` (string number).
/// - `link_to_add_credit` (optional URL string).
public static func userUnusedCreditURL(username: String) -> URL {
self.apiURL(pathComponents: ["api", "users", username, "unused-credit"])
}

/// GET — org-scoped unused credit.
public static func organizationUnusedCreditURL(organizationName: String) -> URL {
self.apiURL(pathComponents: ["api", "organizations", organizationName, "unused-credit"])
}

// MARK: - Account bootstrap (later tasks)

// Invoices/credit URLs require `{username}` and account kind (`user` vs `organization`).
// Bootstrap strategy: with session cookies, GET `dashboardURLString` and parse
// `<script type="application/json" id="react-component-props-...">` for
// `account: { kind, username }` from signed-in page props.
//
// Spend limit: no JSON read API found in the frontend bundle — only POST/form routes
// (`/users/{username}/settings/set-spend-limit`, `/orgs/{organization_name}/settings/set-spend-limit`).
// Omit `spendLimit` for v1 unless a live capture proves a readable field.

private static func apiURL(pathComponents: [String]) -> URL {
var url = URL(string: Self.baseURLString)!
for component in pathComponents {
url = url.appendingPathComponent(component)
}
return url
}
}
Loading