Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 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
70 changes: 70 additions & 0 deletions Sources/CodexBar/Providers/Muse/MuseProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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
_ = settings.museBaseURL
}

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

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
if MuseSettingsReader.apiKey(environment: context.environment) != nil {
return true
}
context.settings.ensureMuseAPITokenLoaded()
return context.settings.hasMuseAPIToken
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "muse-api-key",
title: "Meta Muse API key",
subtitle: "API key from ai.developer.meta.com. Also accepts META_API_KEY.",
kind: .secure,
placeholder: "sk-...",
binding: context.stringBinding(\.museAPIToken),
actions: [
ProviderSettingsActionDescriptor(
id: "muse-open-dashboard",
title: "Open Meta developer console",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(URL(string: "https://ai.developer.meta.com/")!)
}),
],
isVisible: nil,
onActivate: { context.settings.ensureMuseAPITokenLoaded() }),
ProviderSettingsFieldDescriptor(
id: "muse-base-url",
title: "API base URL (optional)",
subtitle: "Override for self-hosted or proxy. Default: https://api.meta.ai",
kind: .plain,
placeholder: "https://api.meta.ai",
binding: context.stringBinding(\.museBaseURL),
actions: [],
isVisible: nil,
onActivate: nil),
]
}
}
52 changes: 52 additions & 0 deletions Sources/CodexBar/Providers/Muse/MuseSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import CodexBarCore
import Foundation

extension SettingsStore {
var museAPIToken: String {
get {
guard let config = self.configSnapshot.providerConfig(for: .muse) else { return "" }
return config.sanitizedAPIKey ?? ""
}
set {
self.updateProviderConfig(provider: .muse) { entry in
entry.apiKey = self.normalizedConfigValue(newValue)
}
self.logSecretUpdate(provider: .muse, field: "apiKey", value: newValue)
}
}

var museBaseURL: String {
get {
guard let config = self.configSnapshot.providerConfig(for: .muse) else { return "" }
return config.sanitizedBaseURL ?? ""
}
set {
self.updateProviderConfig(provider: .muse) { entry in
entry.baseURL = self.normalizedConfigValue(newValue)
}
}
}

func ensureMuseAPITokenLoaded() {}

var hasMuseAPIToken: Bool {
guard let config = self.configSnapshot.providerConfig(for: .muse) else { return false }
return config.sanitizedAPIKey != nil
}

var configuredMuseBaseURL: String? {
guard let raw = self.configSnapshot.providerConfig(for: .muse)?.baseURL?
.trimmingCharacters(in: .whitespacesAndNewlines),
!raw.isEmpty
else {
return nil
}
return raw
}
}

extension SettingsStore {
func museSettingsSnapshot() -> ProviderSettingsSnapshot.MuseProviderSettings {
ProviderSettingsSnapshot.MuseProviderSettings(baseURL: self.configuredMuseBaseURL)
}
}
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.
119 changes: 119 additions & 0 deletions Sources/CodexBarCore/Providers/Muse/MuseProviderDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Foundation

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

private static let credentials = ProviderCredentialAdapter.apiKey(
environmentKey: MuseSettingsReader.apiKeyEnvironmentKeys[0],
precedence: .environment,
environmentHasValue: { MuseSettingsReader.apiKey(environment: $0) != nil },
resolve: { env in MuseSettingsReader.apiKey(environment: env) },
missingCredentialMessage: { _ in MuseUsageError.missingCredentials.errorDescription ?? "Missing Muse API key" }
)

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .muse,
settingsSection: .init(MuseProviderSettingsKey.self, credentialSettings: { context in
MuseProviderSettings(baseURL: context.config?.sanitizedBaseURL)
}),
credentials: self.credentials,
metadata: ProviderMetadata(
id: .muse,
displayName: "Muse",
shortDisplayName: "Muse",
sessionLabel: "Balance",
weeklyLabel: "Balance",
opusLabel: nil,
supportsOpus: false,
supportsCredits: false,
creditsHint: "",
toggleTitle: "Show Muse (Meta) usage",
cliName: "muse",
defaultEnabled: false,
widgetSelectable: false,
isPrimaryProvider: false,
usesAccountFallback: false,
balanceOnly: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the balance-only gatekeeper set

Setting balanceOnly here adds .muse to descriptors.filter(\.metadata.balanceOnly), but ProviderArchitectureGatekeeperTests.swift:196-198 still asserts the exact set without Muse, so the full test suite will deterministically fail. Update that fixture and add focused Muse coverage alongside the new provider.

AGENTS.md reference: AGENTS.md:L20-L24

Useful? React with 👍 / 👎.

browserCookieOrder: nil,
dashboardURL: "https://ai.developer.meta.com/",
statusPageURL: nil,
statusLinkURL: nil
),
branding: ProviderBranding(
iconStyle: .init(provider: .muse),
iconResourceName: "ProviderIcon-muse",
color: ProviderColor(red: 0.0 / 255, green: 100 / 255, blue: 224 / 255),
confettiPalette: [
ProviderColor(hex: 0x0064E0),
ProviderColor(hex: 0x0469FF),
ProviderColor(hex: 0x7B61FF),
],
widgetColor: ProviderColor(red: 0.0 / 255, green: 100 / 255, blue: 224 / 255)
),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Muse cost history is not available via API. Billing is pay-as-you-go at $1.25 / $4.25 per 1M tokens." }
),
presentation: ProviderUsagePresentation(
planRow: ProviderPlanRowPresentation(label: "Balance", stripsBalancePrefix: true)
),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [MuseAPIFetchStrategy()] })
),
cli: ProviderCLIConfig(
name: "muse",
aliases: ["meta", "metamuse"],
versionDetector: nil
)
)
}
}

struct MuseAPIFetchStrategy: ProviderFetchStrategy {
let id = "muse.api"
let kind: ProviderFetchKind = .apiToken
private let transport: any ProviderHTTPTransport

init(transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) {
self.transport = transport
}

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

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
guard let apiKey = MuseSettingsReader.apiKey(environment: context.env) else {
throw MuseUsageError.missingCredentials
}

// Prefer config baseURL (via settings), then env, then default
let baseURL: String? = context.settings?.muse?.baseURL ?? MuseSettingsReader.baseURL(environment: context.env)

let usage = try await MuseUsageFetcher.fetchUsage(
apiKey: apiKey,
baseURLString: baseURL,
session: self.transport
)
return self.makeResult(usage: usage.toUsageSnapshot(), sourceLabel: "api")
}

func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
false
}
}

// MARK: - ProviderConfig extension for baseURL

extension ProviderConfig {
public var baseURL: String? {
get { self.extensionValue(forKey: "baseURL") }
set { self.setExtensionValue(newValue, forKey: "baseURL") }
}

public var sanitizedBaseURL: String? {
Self.clean(self.baseURL)
}
}
31 changes: 31 additions & 0 deletions Sources/CodexBarCore/Providers/Muse/MuseProviderSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

public struct MuseProviderSettings: Sendable {
public let baseURL: String?

public init(baseURL: String? = nil) {
self.baseURL = baseURL
}
}

public enum MuseProviderSettingsKey: ProviderSettingsSectionKey {
public static let providerID = ProviderInstanceID.muse
public typealias Section = MuseProviderSettings
}

extension ProviderSettingsSnapshot {
public typealias MuseProviderSettings = CodexBarCore.MuseProviderSettings
public var muse: MuseProviderSettings? {
self[MuseProviderSettingsKey.self]
}

public static func make(muse: MuseProviderSettings?) -> Self {
self.make(muse, for: MuseProviderSettingsKey.self)
}
}

extension ProviderSettingsSnapshotContribution {
public static func muse(_ section: MuseProviderSettings) -> Self {
Self(section, for: MuseProviderSettingsKey.self)
}
}
30 changes: 30 additions & 0 deletions Sources/CodexBarCore/Providers/Muse/MuseSettingsReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

public enum MuseSettingsReader: Sendable {
public static let apiKeyEnvironmentKeys = ["MUSE_API_KEY", "META_API_KEY", "META_MUSE_API_KEY"]
public static let baseURLEnvironmentKey = "MUSE_API_URL"

public static func apiKey(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? {
for key in apiKeyEnvironmentKeys {
if let value = cleaned(environment[key]) {
return value
}
}
return nil
}

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

private 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