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: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Added
- Nous Portal: new provider showing monthly subscription credits, cycle reset, plan, and purchased credit balance by reusing the Hermes Agent OAuth login from `~/.hermes/auth.json` (#1367).

### Fixed
- Settings: disable iCloud sync sub-options when the main sync switch is off, preserving their saved choices for the next time sync is enabled (#3406). Thanks @elijahfriedman!
- Antigravity: match local token-history timestamps by per-turn IDs when auxiliary or reordered steps would otherwise assign usage to the wrong day, while withholding conflicting evidence and preserving legacy timestamp recovery (#3403). Thanks @WeGoToMars!
Expand Down
42 changes: 42 additions & 0 deletions Sources/CodexBar/Providers/Nous/NousProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CodexBarCore
import Foundation
import SwiftUI

struct NousProviderImplementation: ProviderImplementation {
let id: UsageProvider = .nous

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

@MainActor
func isAvailable(context: ProviderAvailabilityContext) -> Bool {
NousSettingsReader.credential(environment: context.environment) != nil
}

@MainActor
func settingsActions(context _: ProviderSettingsContext) -> [ProviderSettingsActionsDescriptor] {
let status = NousSettingsReader.unavailableMessage(environment: ProcessInfo.processInfo.environment)
?? "Reading the Nous Portal login Hermes Agent stored in ~/.hermes/auth.json."
return [
ProviderSettingsActionsDescriptor(
id: "nous-hermes-login",
title: "Hermes Agent login",
subtitle: status + " CodexBar never refreshes the token; run `hermes` to renew it. "
+ "Set NOUS_PORTAL_ACCESS_TOKEN or HERMES_HOME to override.",
actions: [
ProviderSettingsActionDescriptor(
id: "nous-open-portal",
title: "Open Nous Portal",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(NousSettingsReader.defaultPortalBaseURL)
}),
],
isVisible: nil),
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,6 @@ enum ProviderImplementationManifest {
{ XAIProviderImplementation() },
{ NotionProviderImplementation() },
{ IBMBobProviderImplementation() },
{ NousProviderImplementation() },
]
}
4 changes: 4 additions & 0 deletions Sources/CodexBar/Resources/ProviderIcon-nous.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions Sources/CodexBarCore/Providers/Nous/NousProviderDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Foundation

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

private static let credentials = ProviderCredentialAdapter(
supportsAPIKeyOverride: false,
requiresAPIKeyForAPISource: false,
tokenResolver: { kind, environment, _ in
guard kind == .primary, let credential = NousSettingsReader.credential(environment: environment) else {
return nil
}
let source: ProviderTokenSource = credential.source == .environment ? .environment : .authFile
return ProviderTokenResolution(token: credential.token, source: source)
},
authDetector: { environment, _ in
NousSettingsReader.credential(environment: environment) == nil ? [] : ["api"]
},
missingCredentialMessage: { environment in
NousSettingsReader.unavailableMessage(environment: environment)
})

static func makeDescriptor() -> ProviderDescriptor {
ProviderDescriptor(
id: .nous,
menuBarMetrics: ProviderMenuBarMetricCapabilities(supported: [.automatic, .primary]),
credentials: self.credentials,
metadata: ProviderMetadata(
id: .nous,
displayName: "Nous Portal",
shortDisplayName: "Nous",
sessionLabel: "Monthly credits",
weeklyLabel: "Weekly",
opusLabel: nil,
supportsOpus: false,
supportsCredits: true,
creditsHint: "Purchased credit balance from Nous Portal",
toggleTitle: "Show Nous Portal usage",
cliName: "nous",
defaultEnabled: false,
widgetSelectable: false,
isPrimaryProvider: false,
usesAccountFallback: false,
dashboardURL: "https://portal.nousresearch.com/usage",
subscriptionDashboardURL: "https://portal.nousresearch.com/manage-subscription",
statusPageURL: nil),
branding: ProviderBranding(
iconStyle: .init(provider: .nous),
iconResourceName: "ProviderIcon-nous",
color: ProviderColor(red: 214 / 255, green: 165 / 255, blue: 92 / 255),
confettiPalette: [
ProviderColor(hex: 0xD6A55C),
ProviderColor(hex: 0x1C1B1A),
ProviderColor(hex: 0xF3EADB),
]),
tokenCost: ProviderTokenCostConfig(
supportsTokenCost: false,
noDataMessage: { "Nous Portal cost summary is not available." }),
presentation: ProviderUsagePresentation(
planRow: ProviderPlanRowPresentation(label: "Plan")),
fetchPlan: ProviderFetchPlan(
sourceModes: [.auto, .api],
pipeline: ProviderFetchPipeline(resolveStrategies: { _ in [NousAPIFetchStrategy()] })),
cli: ProviderCLIConfig(
name: "nous",
aliases: ["nous-portal", "hermes"],
versionDetector: nil))
}
}

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

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

/// Available whenever some Nous credential exists, even an expired one, so the fetch surfaces the specific
/// expiry or trust error instead of a bare "unavailable".
func isAvailable(_ context: ProviderFetchContext) async -> Bool {
do {
_ = try NousSettingsReader.resolveCredential(environment: context.env)
return true
} catch NousUsageError.missingCredentials {
return false
} catch {
return true
}
}

func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
// Credits ride along in the same account response, so attach them regardless of `includeCredits`
// (the app only sets that flag for Codex's separate credits request).
let credential = try NousSettingsReader.resolveCredential(environment: context.env)
let account = try await NousUsageFetcher.fetchAccount(credential: credential, transport: self.transport)
// The app renders `diagnostic` as a warning line, so only emit it when there is something to warn about
// (an ignored stored host) or when the caller asked for a verbose trace.
var diagnostic: String?
if credential.rejectedPortalHost != nil || context.verbose {
var note = "portal=\(credential.portalBaseURL.host ?? "?") credential=\(credential.source.label)"
if let rejected = credential.rejectedPortalHost {
note += " rejectedStoredHost=\(rejected)"
}
diagnostic = note
}
return self.makeResult(
usage: account.toUsageSnapshot(),
credits: account.toCreditsSnapshot(),
sourceLabel: "api",
diagnostic: diagnostic)
}

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