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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ See [CLI configuration](docs/cli-configuration.md) for the full flow.
- [ZenMux](docs/zenmux.md) — Management API key for rolling five-hour and seven-day quota windows plus PAYG balance.
- [xAI](docs/xai.md) — Management API key + team ID for prepaid credit balance and daily platform spend.
- [IBM Bob](docs/ibm-bob.md) — API key for monthly Bobcoin budget and usage across subscription teams.
- [Muse](docs/muse.md) — local session logs for daily token usage; `muse login` supplies account identity. Meta publishes no usage endpoint, so no quota is shown.
- Open to new providers: [provider authoring guide](docs/provider.md).

## Icon & Screenshot
Expand Down
65 changes: 65 additions & 0 deletions Sources/CodexBar/Providers/Muse/MuseProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct MuseProviderImplementation: ProviderImplementation {
let id: UsageProvider = .muse

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

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

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if MuseSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
if !context.settings.museAPIToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return true
}
// A rejected MUSE_BASE_URL must still reach the fetch path so the override error is visible.
if MuseSettingsReader.hasBaseURLOverride(environment: context.environment) {
return true
}
return MuseLocalAuthReader.read() != nil
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "muse-api-key",
title: "API key",
subtitle: "Stored in ~/.codexbar/config.json. Create a key at https://dev.meta.ai, or run `muse login`.",
kind: .secure,
placeholder: "Paste META_API_KEY…",
binding: context.stringBinding(\.museAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "muse-open-dev",
title: "Open dev.meta.ai",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://dev.meta.ai") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: nil,
onActivate: nil),
]
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
[]
}
}
14 changes: 14 additions & 0 deletions Sources/CodexBar/Providers/Muse/MuseSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var museAPIToken: String {
get { self.configSnapshot.providerConfig(for: .muse)?.sanitizedAPIKey ?? "" }
set {
self.updateProviderConfig(provider: .muse) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .muse, field: "apiKey", value: newValue)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ enum ProviderImplementationManifest {
{ XAIProviderImplementation() },
{ NotionProviderImplementation() },
{ IBMBobProviderImplementation() },
{ MuseProviderImplementation() },
]
}
6 changes: 6 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-muse.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 120 additions & 14 deletions Sources/CodexBarCore/CostUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -451,25 +451,23 @@ public struct CostUsageFetcher: Sendable {
calendar: fallbackCalendar,
historyCoverageIsEstablished: false)
}
// Provider-specific by design: Antigravity uses recognized local stores without generic pricing or cache scans.
if provider == .antigravity {
if let local = try await self.loadAntigravityLocalSnapshot(
context: AntigravityLocalReader.Context(environment: environment),
// Provider-specific by design: Muse records durable session logs and publishes no usage endpoint.
if provider == .muse {
return try await self.museLocalSnapshotOrEmpty(
environment: environment,
now: now,
historyDays: clampedHistoryDays,
calendar: fallbackCalendar)
{
return local
}
if let remoteError {
throw remoteError
}
return Self.tokenSnapshot(
from: CostUsageDailyReport(data: [], summary: nil),
calendar: fallbackCalendar,
remoteError: remoteError)
}
// Provider-specific by design: Antigravity uses recognized local stores without generic pricing or cache scans.
if provider == .antigravity {
return try await self.antigravityLocalSnapshotOrEmpty(
environment: environment,
now: now,
historyDays: clampedHistoryDays,
calendar: fallbackCalendar,
historyCoverageIsEstablished: false)
remoteError: remoteError)
}
if let remoteError {
throw remoteError
Expand Down Expand Up @@ -1267,6 +1265,114 @@ public struct CostUsageFetcher: Sendable {
costProvenance: .unknown)
}

/// Mirrors ``museLocalSnapshotOrEmpty`` for Antigravity's local stores.
private static func antigravityLocalSnapshotOrEmpty(
environment: [String: String],
now: Date,
historyDays: Int,
calendar: Calendar,
remoteError: (any Error)?) async throws -> CostUsageTokenSnapshot
{
if let local = try await self.loadAntigravityLocalSnapshot(
context: AntigravityLocalReader.Context(environment: environment),
now: now,
historyDays: historyDays,
calendar: calendar)
{
return local
}
if let remoteError {
throw remoteError
}
return Self.tokenSnapshot(
from: CostUsageDailyReport(data: [], summary: nil),
now: now,
historyDays: historyDays,
calendar: calendar,
historyCoverageIsEstablished: false)
}

/// Muse's only usage source is local, so a missing read falls through to an empty snapshot rather
/// than to a remote retry.
private static func museLocalSnapshotOrEmpty(
environment: [String: String],
now: Date,
historyDays: Int,
calendar: Calendar,
remoteError: (any Error)?) async throws -> CostUsageTokenSnapshot
{
if let local = try await self.loadMuseLocalSnapshot(
context: MuseLocalUsageReader.Context(environment: environment),
now: now,
historyDays: historyDays,
calendar: calendar)
{
return local
}
if let remoteError {
throw remoteError
}
return Self.tokenSnapshot(
from: CostUsageDailyReport(data: [], summary: nil),
now: now,
historyDays: historyDays,
calendar: calendar,
historyCoverageIsEstablished: false)
}

/// Builds a Muse token snapshot from the CLI's local session logs.
///
/// Muse exposes no usage endpoint, so this is the provider's only quota-free data source. Costs
/// stay `nil`: the logs record tokens, not billed amounts, and Meta prices per tier.
private static func loadMuseLocalSnapshot(
context: MuseLocalUsageReader.Context,
now: Date,
historyDays: Int,
cacheRoot: URL? = nil,
calendar: Calendar = .current) async throws -> CostUsageTokenSnapshot?
{
let cal = calendar
let windowStart = cal.date(byAdding: .day, value: -(historyDays - 1), to: cal.startOfDay(for: now)) ?? now
let sinceDayKey = CostUsageLocalDay.key(from: windowStart, calendar: cal)
let reportResult = try await CostUsageScanExecutor.run { checkCancellation in
try MuseLocalUsageReader.makeDailyReportWithStatus(
context: context,
calendar: cal,
sinceDayKey: sinceDayKey,
cacheRoot: cacheRoot,
checkCancellation: checkCancellation)
}
guard reportResult.isAvailable else { return nil }
let report = reportResult.report
if report.data.isEmpty {
guard reportResult.isComplete else { return nil }
return Self.tokenSnapshot(
from: CostUsageDailyReport(data: [], summary: nil),
now: now,
historyDays: historyDays,
useCurrentLocalDayForSession: true,
calendar: cal,
historyCoverageIsEstablished: true,
costProvenance: .unknown)
}
let nowKey = CostUsageLocalDay.key(from: now, calendar: cal)
let filtered = report.data.filter { $0.date >= sinceDayKey && $0.date <= nowKey }
let totalTokens = MuseLocalUsageReader.checkedSum(filtered.compactMap(\.totalTokens))
let filteredSummary: CostUsageDailyReport.Summary? = filtered.isEmpty ? nil : .init(
totalInputTokens: MuseLocalUsageReader.checkedSum(filtered.compactMap(\.inputTokens)),
totalOutputTokens: MuseLocalUsageReader.checkedSum(filtered.compactMap(\.outputTokens)),
totalTokens: totalTokens,
totalCostUSD: nil)
return Self.tokenSnapshot(
from: CostUsageDailyReport(data: filtered, summary: filteredSummary),
now: now,
historyDays: historyDays,
useCurrentLocalDayForSession: true,
calendar: cal,
historyCoverageIsEstablished: reportResult.isComplete,
costProvenance: .unknown)
}

static func tokenSnapshot(
from daily: CostUsageDailyReport,
now: Date,
Expand Down
75 changes: 75 additions & 0 deletions Sources/CodexBarCore/Providers/Muse/MuseLocalAuthReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import Foundation

/// Account metadata `muse login` writes beside its credential.
///
/// The secret itself lives in the macOS Keychain (`storage: "keychain"`); this reader deliberately
/// only parses the plaintext metadata file, so resolving a Muse identity never issues a SecItem read
/// and never raises a Keychain prompt.
public struct MuseLocalAuth: Sendable, Equatable {
public let accountEmail: String?
public let accountName: String?
/// `oauth` for `muse login`, `api_key` for `muse auth set`.
public let mechanism: String?
public let apiBaseURL: URL?

public init(accountEmail: String?, accountName: String?, mechanism: String?, apiBaseURL: URL?) {
self.accountEmail = accountEmail
self.accountName = accountName
self.mechanism = mechanism
self.apiBaseURL = apiBaseURL
}

/// Human-readable login source for the identity card.
public var loginMethod: String {
switch self.mechanism {
case "oauth": "Meta account"
case "api_key": "API key"
default: "muse CLI"
}
}
}

public enum MuseLocalAuthReader {
/// `~/.config/muse/auth.json`, written by `muse login` / `muse auth set`.
public static func defaultPath(home: String = NSHomeDirectory()) -> String {
"\(home)/.config/muse/auth.json"
}

public static func read(
path: String? = nil,
home: String = NSHomeDirectory(),
fileManager: FileManager = .default) -> MuseLocalAuth?
{
let resolved = path ?? self.defaultPath(home: home)
guard fileManager.fileExists(atPath: resolved),
let data = fileManager.contents(atPath: resolved)
else {
return nil
}
return self.parse(data: data)
}

static func parse(data: Data) -> MuseLocalAuth? {
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let providers = root["providers"] as? [String: Any],
let meta = providers["meta"] as? [String: Any]
else {
return nil
}

let baseURL = (meta["api_base_url"] as? String)
.flatMap { ProviderEndpointOverrideValidator().validatedURLAllowingPrivateNetworkHTTP($0) }

let auth = MuseLocalAuth(
accountEmail: MuseSettingsReader.cleaned(meta["user_email"] as? String),
accountName: MuseSettingsReader.cleaned(meta["user_full_name"] as? String),
mechanism: MuseSettingsReader.cleaned(meta["mechanism"] as? String),
apiBaseURL: baseURL)

// An entry with no usable field at all is the same as having no login.
if auth.accountEmail == nil, auth.accountName == nil, auth.mechanism == nil, auth.apiBaseURL == nil {
return nil
}
return auth
}
}
Loading