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
106 changes: 106 additions & 0 deletions Sources/CodexBar/Providers/Mistral/MistralProviderImplementation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct MistralProviderImplementation: ProviderImplementation {
let id: UsageProvider = .mistral

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

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

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
.mistral(context.settings.mistralSettingsSnapshot(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.mistralCookieSource == .manual
}

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

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

let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.mistralCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports browser cookies from admin.mistral.ai.",
manual: "Paste a Cookie header captured from the billing page.",
off: "Mistral cookies are disabled.")
}

return [
ProviderSettingsPickerDescriptor(
id: "mistral-cookie-source",
title: "Cookie source",
subtitle: "Automatic imports browser cookies from admin.mistral.ai.",
dynamicSubtitle: cookieSubtitle,
binding: cookieBinding,
options: cookieOptions,
isVisible: nil,
onChange: nil,
trailingText: {
guard let entry = CookieHeaderCache.load(provider: .mistral) else { return nil }
let when = entry.storedAt.relativeDescription()
return "Cached: \(entry.sourceLabel) • \(when)"
}),
]
}

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "mistral-cookie-header",
title: "Cookie header",
subtitle: "Paste the Cookie header from a request to admin.mistral.ai. "
+ "Must contain an ory_session_* cookie.",
kind: .secure,
placeholder: "ory_session_…=…; csrftoken=…",
binding: context.stringBinding(\.mistralCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "mistral-open-console",
title: "Open Mistral Admin",
style: .link,
isVisible: nil,
perform: {
if let url = URL(string: "https://admin.mistral.ai/organization/usage") {
NSWorkspace.shared.open(url)
}
}),
],
isVisible: { context.settings.mistralCookieSource == .manual },
onActivate: nil),
]
}
}
64 changes: 64 additions & 0 deletions Sources/CodexBar/Providers/Mistral/MistralSettingsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import CodexBarCore
import Foundation

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

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

func ensureMistralCookieLoaded() {}
}

extension SettingsStore {
func mistralSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot
.MistralProviderSettings
{
ProviderSettingsSnapshot.MistralProviderSettings(
cookieSource: self.mistralSnapshotCookieSource(tokenOverride: tokenOverride),
manualCookieHeader: self.mistralSnapshotCookieHeader(tokenOverride: tokenOverride))
}

private func mistralSnapshotCookieHeader(tokenOverride: TokenAccountOverride?) -> String {
let fallback = self.mistralCookieHeader
guard let support = TokenAccountSupportCatalog.support(for: .mistral),
case .cookieHeader = support.injection
else {
return fallback
}
guard let account = ProviderTokenAccountSelection.selectedAccount(
provider: .mistral,
settings: self,
override: tokenOverride)
else {
return fallback
}
return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
}

private func mistralSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
let fallback = self.mistralCookieSource
guard let support = TokenAccountSupportCatalog.support(for: .mistral),
support.requiresManualCookieSource
else {
return fallback
}
if self.tokenAccounts(for: .mistral).isEmpty { return fallback }
return .manual
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ enum ProviderImplementationRegistry {
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
case .mistral: MistralProviderImplementation()
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/Resources/ProviderIcon-mistral.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,7 @@ extension UsageStore {
let source = resolution?.source.rawValue ?? "none"
return "WARP_API_KEY=\(hasAny ? "present" : "missing") source=\(source)"
case .gemini, .antigravity, .opencode, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .jetbrains:
.kimik2, .jetbrains, .mistral:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
13 changes: 11 additions & 2 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ struct TokenAccountCLIContext {
return self.makeSnapshot(
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings(
ideBasePath: nil))
case .mistral:
let cookieHeader = self.manualCookieHeader(provider: provider, account: account, config: config)
let cookieSource = self.cookieSource(provider: provider, account: account, config: config)
return self.makeSnapshot(
mistral: ProviderSettingsSnapshot.MistralProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp:
return nil
}
Expand All @@ -196,7 +203,8 @@ struct TokenAccountCLIContext {
augment: ProviderSettingsSnapshot.AugmentProviderSettings? = nil,
amp: ProviderSettingsSnapshot.AmpProviderSettings? = nil,
ollama: ProviderSettingsSnapshot.OllamaProviderSettings? = nil,
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil) -> ProviderSettingsSnapshot
jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings? = nil,
mistral: ProviderSettingsSnapshot.MistralProviderSettings? = nil) -> ProviderSettingsSnapshot
{
ProviderSettingsSnapshot.make(
codex: codex,
Expand All @@ -212,7 +220,8 @@ struct TokenAccountCLIContext {
augment: augment,
amp: amp,
ollama: ollama,
jetbrains: jetbrains)
jetbrains: jetbrains,
mistral: mistral)
}

func environment(
Expand Down
101 changes: 101 additions & 0 deletions Sources/CodexBarCore/Providers/Mistral/MistralCookieImporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Foundation

#if os(macOS)
import SweetCookieKit

private let mistralCookieImportOrder: BrowserCookieImportOrder =
ProviderDefaults.metadata[.mistral]?.browserCookieOrder ?? Browser.defaultImportOrder

public enum MistralCookieImporter {
private static let cookieClient = BrowserCookieClient()
private static let cookieDomains = ["mistral.ai", "admin.mistral.ai", "auth.mistral.ai"]

public struct SessionInfo: Sendable {
public let cookies: [HTTPCookie]
public let sourceLabel: String

public init(cookies: [HTTPCookie], sourceLabel: String) {
self.cookies = cookies
self.sourceLabel = sourceLabel
}

public var cookieHeader: String {
self.cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; ")
}

/// Extracts the CSRF token from the `csrftoken` cookie for the `X-CSRFTOKEN` header.
public var csrfToken: String? {
self.cookies.first { $0.name == "csrftoken" }?.value
}
}

/// Returns `true` if any cookie name starts with `ory_session_` (the Ory Kratos session cookie).
private static func hasSessionCookie(_ cookies: [HTTPCookie]) -> Bool {
cookies.contains { $0.name.hasPrefix("ory_session_") }
}

public static func importSession(
browserDetection: BrowserDetection,
preferredBrowsers: [Browser] = [.chrome],
logger: ((String) -> Void)? = nil) throws -> SessionInfo
{
let log: (String) -> Void = { msg in logger?("[mistral-cookie] \(msg)") }
let installedBrowsers = preferredBrowsers.isEmpty
? mistralCookieImportOrder.cookieImportCandidates(using: browserDetection)
: preferredBrowsers.cookieImportCandidates(using: browserDetection)

for browserSource in installedBrowsers {
do {
let query = BrowserCookieQuery(domains: self.cookieDomains)
let sources = try Self.cookieClient.records(
matching: query,
in: browserSource,
logger: log)
for source in sources where !source.records.isEmpty {
let httpCookies = BrowserCookieClient.makeHTTPCookies(source.records, origin: query.origin)
if !httpCookies.isEmpty {
guard Self.hasSessionCookie(httpCookies) else {
log("Skipping \(source.label) cookies: missing ory_session_* cookie")
continue
}
log("Found \(httpCookies.count) Mistral cookies in \(source.label)")
return SessionInfo(cookies: httpCookies, sourceLabel: source.label)
}
}
} catch {
BrowserCookieAccessGate.recordIfNeeded(error)
log("\(browserSource.displayName) cookie import failed: \(error.localizedDescription)")
}
}

throw MistralCookieImportError.noCookies
}

public static func hasSession(
browserDetection: BrowserDetection,
preferredBrowsers: [Browser] = [.chrome],
logger: ((String) -> Void)? = nil) -> Bool
{
do {
_ = try self.importSession(
browserDetection: browserDetection,
preferredBrowsers: preferredBrowsers,
logger: logger)
return true
} catch {
return false
}
}
}

enum MistralCookieImportError: LocalizedError {
case noCookies

var errorDescription: String? {
switch self {
case .noCookies:
"No Mistral session cookies found in browsers."
}
}
}
#endif
35 changes: 35 additions & 0 deletions Sources/CodexBarCore/Providers/Mistral/MistralErrors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

public enum MistralUsageError: LocalizedError, Sendable {
case missingCookie
case invalidCredentials
case apiError(String)
case parseFailed(String)

public var errorDescription: String? {
switch self {
case .missingCookie:
"No Mistral session cookies found in browsers."
case .invalidCredentials:
"Mistral session expired or invalid (HTTP 401/403)."
case let .apiError(detail):
"Mistral API error: \(detail)"
case let .parseFailed(detail):
"Failed to parse Mistral billing response: \(detail)"
}
}
}

enum MistralSettingsError: LocalizedError {
case missingCookie
case invalidCookie

var errorDescription: String? {
switch self {
case .missingCookie:
"No Mistral session cookies found in browsers."
case .invalidCookie:
"Mistral cookie header is invalid or missing ory_session cookie."
}
}
}
Loading