diff --git a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
index 6fb94b479..500821ec0 100644
--- a/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
+++ b/Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift
@@ -36,6 +36,7 @@ enum ProviderImplementationRegistry {
case .synthetic: SyntheticProviderImplementation()
case .openrouter: OpenRouterProviderImplementation()
case .warp: WarpProviderImplementation()
+ case .windsurf: WindsurfProviderImplementation()
case .perplexity: PerplexityProviderImplementation()
}
}
diff --git a/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift b/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift
new file mode 100644
index 000000000..04070618d
--- /dev/null
+++ b/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift
@@ -0,0 +1,111 @@
+import CodexBarCore
+import CodexBarMacroSupport
+import Foundation
+import SwiftUI
+
+@ProviderImplementationRegistration
+struct WindsurfProviderImplementation: ProviderImplementation {
+ let id: UsageProvider = .windsurf
+
+ @MainActor
+ func observeSettings(_ settings: SettingsStore) {
+ _ = settings.windsurfUsageDataSource
+ _ = settings.windsurfCookieSource
+ _ = settings.windsurfCookieHeader
+ }
+
+ @MainActor
+ func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
+ .windsurf(context.settings.windsurfSettingsSnapshot(tokenOverride: context.tokenOverride))
+ }
+
+ @MainActor
+ func sourceMode(context: ProviderSourceModeContext) -> ProviderSourceMode {
+ switch context.settings.windsurfUsageDataSource {
+ case .auto: .auto
+ case .web: .web
+ case .cli: .cli
+ }
+ }
+
+ @MainActor
+ func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
+ // Usage source picker
+ let usageBinding = Binding(
+ get: { context.settings.windsurfUsageDataSource.rawValue },
+ set: { raw in
+ context.settings.windsurfUsageDataSource = WindsurfUsageDataSource(rawValue: raw) ?? .auto
+ })
+ let usageOptions = WindsurfUsageDataSource.allCases.map {
+ ProviderSettingsPickerOption(id: $0.rawValue, title: $0.displayName)
+ }
+
+ // Cookie source picker
+ let cookieBinding = Binding(
+ get: { context.settings.windsurfCookieSource.rawValue },
+ set: { raw in
+ context.settings.windsurfCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
+ })
+ let cookieOptions = ProviderCookieSourceUI.options(
+ allowsOff: true,
+ keychainDisabled: context.settings.debugDisableKeychainAccess)
+
+ let cookieSubtitle: () -> String? = {
+ ProviderCookieSourceUI.subtitle(
+ source: context.settings.windsurfCookieSource,
+ keychainDisabled: context.settings.debugDisableKeychainAccess,
+ auto: "Automatic imports Firebase tokens from browser IndexedDB.",
+ manual: "Paste a Firebase access token for windsurf.com.",
+ off: "Windsurf web API access is disabled.")
+ }
+
+ return [
+ ProviderSettingsPickerDescriptor(
+ id: "windsurf-usage-source",
+ title: "Usage source",
+ subtitle: "Auto falls back to the next source if the preferred one fails.",
+ binding: usageBinding,
+ options: usageOptions,
+ isVisible: nil,
+ onChange: nil,
+ trailingText: {
+ guard context.settings.windsurfUsageDataSource == .auto else { return nil }
+ let label = context.store.sourceLabel(for: .windsurf)
+ return label == "auto" ? nil : label
+ }),
+ ProviderSettingsPickerDescriptor(
+ id: "windsurf-cookie-source",
+ title: "Cookie source",
+ subtitle: "Automatic imports Firebase tokens from browser IndexedDB.",
+ dynamicSubtitle: cookieSubtitle,
+ binding: cookieBinding,
+ options: cookieOptions,
+ isVisible: nil,
+ onChange: nil,
+ trailingText: {
+ guard !context.settings.debugDisableKeychainAccess else { return nil }
+ guard let entry = CookieHeaderCache.load(provider: .windsurf) else { return nil }
+ let when = entry.storedAt.relativeDescription()
+ return "Cached: \(entry.sourceLabel) • \(when)"
+ }),
+ ]
+ }
+
+ @MainActor
+ func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
+ [
+ ProviderSettingsFieldDescriptor(
+ id: "windsurf-cookie-header",
+ title: "",
+ subtitle: "",
+ kind: .secure,
+ placeholder: "Firebase refresh token (AMf-vB…) or access token (eyJ…)",
+ binding: context.stringBinding(\.windsurfCookieHeader),
+ actions: [],
+ isVisible: {
+ context.settings.windsurfCookieSource == .manual
+ },
+ onActivate: nil),
+ ]
+ }
+}
diff --git a/Sources/CodexBar/Providers/Windsurf/WindsurfSettingsStore.swift b/Sources/CodexBar/Providers/Windsurf/WindsurfSettingsStore.swift
new file mode 100644
index 000000000..683e97c7e
--- /dev/null
+++ b/Sources/CodexBar/Providers/Windsurf/WindsurfSettingsStore.swift
@@ -0,0 +1,93 @@
+import CodexBarCore
+import Foundation
+
+extension SettingsStore {
+ var windsurfUsageDataSource: WindsurfUsageDataSource {
+ get {
+ let source = self.configSnapshot.providerConfig(for: .windsurf)?.source
+ return Self.windsurfUsageDataSource(from: source)
+ }
+ set {
+ let source: ProviderSourceMode? = switch newValue {
+ case .auto: .auto
+ case .web: .web
+ case .cli: .cli
+ }
+ self.updateProviderConfig(provider: .windsurf) { entry in
+ entry.source = source
+ }
+ self.logProviderModeChange(provider: .windsurf, field: "usageSource", value: newValue.rawValue)
+ }
+ }
+
+ var windsurfCookieSource: ProviderCookieSource {
+ get { self.resolvedCookieSource(provider: .windsurf, fallback: .auto) }
+ set {
+ self.updateProviderConfig(provider: .windsurf) { entry in
+ entry.cookieSource = newValue
+ }
+ self.logProviderModeChange(provider: .windsurf, field: "cookieSource", value: newValue.rawValue)
+ }
+ }
+
+ var windsurfCookieHeader: String {
+ get { self.configSnapshot.providerConfig(for: .windsurf)?.sanitizedCookieHeader ?? "" }
+ set {
+ self.updateProviderConfig(provider: .windsurf) { entry in
+ entry.cookieHeader = self.normalizedConfigValue(newValue)
+ }
+ self.logSecretUpdate(provider: .windsurf, field: "cookieHeader", value: newValue)
+ }
+ }
+}
+
+extension SettingsStore {
+ func windsurfSettingsSnapshot(
+ tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot.WindsurfProviderSettings
+ {
+ ProviderSettingsSnapshot.WindsurfProviderSettings(
+ usageDataSource: self.windsurfUsageDataSource,
+ cookieSource: self.windsurfSnapshotCookieSource(tokenOverride: tokenOverride),
+ manualCookieHeader: self.windsurfSnapshotCookieHeader(tokenOverride: tokenOverride))
+ }
+
+ private static func windsurfUsageDataSource(from source: ProviderSourceMode?) -> WindsurfUsageDataSource {
+ guard let source else { return .auto }
+ switch source {
+ case .auto, .oauth, .api:
+ return .auto
+ case .web:
+ return .web
+ case .cli:
+ return .cli
+ }
+ }
+
+ private func windsurfSnapshotCookieHeader(tokenOverride: TokenAccountOverride?) -> String {
+ let fallback = self.windsurfCookieHeader
+ guard let support = TokenAccountSupportCatalog.support(for: .windsurf),
+ case .cookieHeader = support.injection
+ else {
+ return fallback
+ }
+ guard let account = ProviderTokenAccountSelection.selectedAccount(
+ provider: .windsurf,
+ settings: self,
+ override: tokenOverride)
+ else {
+ return fallback
+ }
+ return TokenAccountSupportCatalog.normalizedCookieHeader(account.token, support: support)
+ }
+
+ private func windsurfSnapshotCookieSource(tokenOverride: TokenAccountOverride?) -> ProviderCookieSource {
+ let fallback = self.windsurfCookieSource
+ guard let support = TokenAccountSupportCatalog.support(for: .windsurf),
+ support.requiresManualCookieSource
+ else {
+ return fallback
+ }
+ if self.tokenAccounts(for: .windsurf).isEmpty { return fallback }
+ return .manual
+ }
+}
diff --git a/Sources/CodexBar/Resources/ProviderIcon-windsurf.svg b/Sources/CodexBar/Resources/ProviderIcon-windsurf.svg
new file mode 100644
index 000000000..3bc424679
--- /dev/null
+++ b/Sources/CodexBar/Resources/ProviderIcon-windsurf.svg
@@ -0,0 +1,3 @@
+
diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift
index 9bb258a14..d2945fa9e 100644
--- a/Sources/CodexBar/UsageStore.swift
+++ b/Sources/CodexBar/UsageStore.swift
@@ -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, .perplexity:
+ .kimik2, .jetbrains, .windsurf, .perplexity:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
diff --git a/Sources/CodexBarCLI/TokenAccountCLI.swift b/Sources/CodexBarCLI/TokenAccountCLI.swift
index d52302847..7afd78b4c 100644
--- a/Sources/CodexBarCLI/TokenAccountCLI.swift
+++ b/Sources/CodexBarCLI/TokenAccountCLI.swift
@@ -184,7 +184,8 @@ struct TokenAccountCLIContext {
perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
- case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp:
+ case .gemini, .antigravity, .copilot, .kiro, .vertexai, .kimik2, .synthetic, .openrouter, .warp,
+ .windsurf:
return nil
}
}
diff --git a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift
index 236af4bd3..b6469077f 100644
--- a/Sources/CodexBarCore/Providers/ProviderDescriptor.swift
+++ b/Sources/CodexBarCore/Providers/ProviderDescriptor.swift
@@ -76,6 +76,7 @@ public enum ProviderDescriptorRegistry {
.synthetic: SyntheticProviderDescriptor.descriptor,
.openrouter: OpenRouterProviderDescriptor.descriptor,
.warp: WarpProviderDescriptor.descriptor,
+ .windsurf: WindsurfProviderDescriptor.descriptor,
.perplexity: PerplexityProviderDescriptor.descriptor,
]
private static let bootstrap: Void = {
diff --git a/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift b/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift
index 7c1d1f786..b3101c366 100644
--- a/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift
+++ b/Sources/CodexBarCore/Providers/ProviderSettingsSnapshot.swift
@@ -19,6 +19,7 @@ public struct ProviderSettingsSnapshot: Sendable {
amp: AmpProviderSettings? = nil,
ollama: OllamaProviderSettings? = nil,
jetbrains: JetBrainsProviderSettings? = nil,
+ windsurf: WindsurfProviderSettings? = nil,
perplexity: PerplexityProviderSettings? = nil) -> ProviderSettingsSnapshot
{
ProviderSettingsSnapshot(
@@ -39,6 +40,7 @@ public struct ProviderSettingsSnapshot: Sendable {
amp: amp,
ollama: ollama,
jetbrains: jetbrains,
+ windsurf: windsurf,
perplexity: perplexity)
}
@@ -211,6 +213,22 @@ public struct ProviderSettingsSnapshot: Sendable {
}
}
+ public struct WindsurfProviderSettings: Sendable {
+ public let usageDataSource: WindsurfUsageDataSource
+ public let cookieSource: ProviderCookieSource
+ public let manualCookieHeader: String?
+
+ public init(
+ usageDataSource: WindsurfUsageDataSource,
+ cookieSource: ProviderCookieSource,
+ manualCookieHeader: String?)
+ {
+ self.usageDataSource = usageDataSource
+ self.cookieSource = cookieSource
+ self.manualCookieHeader = manualCookieHeader
+ }
+ }
+
public struct PerplexityProviderSettings: Sendable {
public let cookieSource: ProviderCookieSource
public let manualCookieHeader: String?
@@ -238,6 +256,7 @@ public struct ProviderSettingsSnapshot: Sendable {
public let amp: AmpProviderSettings?
public let ollama: OllamaProviderSettings?
public let jetbrains: JetBrainsProviderSettings?
+ public let windsurf: WindsurfProviderSettings?
public let perplexity: PerplexityProviderSettings?
public var jetbrainsIDEBasePath: String? {
@@ -262,6 +281,7 @@ public struct ProviderSettingsSnapshot: Sendable {
amp: AmpProviderSettings?,
ollama: OllamaProviderSettings?,
jetbrains: JetBrainsProviderSettings? = nil,
+ windsurf: WindsurfProviderSettings? = nil,
perplexity: PerplexityProviderSettings? = nil)
{
self.debugMenuEnabled = debugMenuEnabled
@@ -281,6 +301,7 @@ public struct ProviderSettingsSnapshot: Sendable {
self.amp = amp
self.ollama = ollama
self.jetbrains = jetbrains
+ self.windsurf = windsurf
self.perplexity = perplexity
}
}
@@ -301,6 +322,7 @@ public enum ProviderSettingsSnapshotContribution: Sendable {
case amp(ProviderSettingsSnapshot.AmpProviderSettings)
case ollama(ProviderSettingsSnapshot.OllamaProviderSettings)
case jetbrains(ProviderSettingsSnapshot.JetBrainsProviderSettings)
+ case windsurf(ProviderSettingsSnapshot.WindsurfProviderSettings)
case perplexity(ProviderSettingsSnapshot.PerplexityProviderSettings)
}
@@ -322,6 +344,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable {
public var amp: ProviderSettingsSnapshot.AmpProviderSettings?
public var ollama: ProviderSettingsSnapshot.OllamaProviderSettings?
public var jetbrains: ProviderSettingsSnapshot.JetBrainsProviderSettings?
+ public var windsurf: ProviderSettingsSnapshot.WindsurfProviderSettings?
public var perplexity: ProviderSettingsSnapshot.PerplexityProviderSettings?
public init(debugMenuEnabled: Bool = false, debugKeepCLISessionsAlive: Bool = false) {
@@ -346,6 +369,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable {
case let .amp(value): self.amp = value
case let .ollama(value): self.ollama = value
case let .jetbrains(value): self.jetbrains = value
+ case let .windsurf(value): self.windsurf = value
case let .perplexity(value): self.perplexity = value
}
}
@@ -369,6 +393,7 @@ public struct ProviderSettingsSnapshotBuilder: Sendable {
amp: self.amp,
ollama: self.ollama,
jetbrains: self.jetbrains,
+ windsurf: self.windsurf,
perplexity: self.perplexity)
}
}
diff --git a/Sources/CodexBarCore/Providers/Providers.swift b/Sources/CodexBarCore/Providers/Providers.swift
index ff0f8eeb4..f52ea21a7 100644
--- a/Sources/CodexBarCore/Providers/Providers.swift
+++ b/Sources/CodexBarCore/Providers/Providers.swift
@@ -26,6 +26,7 @@ public enum UsageProvider: String, CaseIterable, Sendable, Codable {
case synthetic
case warp
case openrouter
+ case windsurf
case perplexity
}
@@ -55,6 +56,7 @@ public enum IconStyle: Sendable, CaseIterable {
case synthetic
case warp
case openrouter
+ case windsurf
case perplexity
case combined
}
diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfFirebaseTokenImporter.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfFirebaseTokenImporter.swift
new file mode 100644
index 000000000..73c09f144
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfFirebaseTokenImporter.swift
@@ -0,0 +1,237 @@
+import Foundation
+#if os(macOS)
+import SweetCookieKit
+#endif
+
+#if os(macOS)
+enum WindsurfFirebaseTokenImporter {
+ struct TokenInfo {
+ let refreshToken: String
+ let accessToken: String?
+ let sourceLabel: String
+ }
+
+ static func importFirebaseTokens(
+ browserDetection: BrowserDetection,
+ logger: ((String) -> Void)? = nil) -> [TokenInfo]
+ {
+ let log: (String) -> Void = { msg in logger?("[windsurf-firebase] \(msg)") }
+ var tokens: [TokenInfo] = []
+
+ let candidates = self.chromeIndexedDBCandidates(browserDetection: browserDetection)
+ if !candidates.isEmpty {
+ log("IndexedDB candidates: \(candidates.count)")
+ }
+
+ for candidate in candidates {
+ let extracted = self.readFirebaseTokens(from: candidate.url, logger: log)
+ for token in extracted {
+ log("Found Firebase refresh token in \(candidate.label)")
+ tokens.append(TokenInfo(
+ refreshToken: token.refreshToken,
+ accessToken: token.accessToken,
+ sourceLabel: candidate.label))
+ }
+ }
+
+ if tokens.isEmpty {
+ log("No Firebase refresh token found in browser IndexedDB")
+ }
+
+ return tokens
+ }
+
+ // MARK: - IndexedDB discovery (follows MiniMax chromeProfileIndexedDBDirs pattern)
+
+ private struct IndexedDBCandidate {
+ let label: String
+ let url: URL
+ }
+
+ private static func chromeIndexedDBCandidates(browserDetection: BrowserDetection) -> [IndexedDBCandidate] {
+ let browsers: [Browser] = [
+ .chrome,
+ .chromeBeta,
+ .chromeCanary,
+ .edge,
+ .edgeBeta,
+ .edgeCanary,
+ .brave,
+ .braveBeta,
+ .braveNightly,
+ .vivaldi,
+ .arc,
+ .arcBeta,
+ .arcCanary,
+ .dia,
+ .chatgptAtlas,
+ .chromium,
+ .helium,
+ ]
+
+ let installedBrowsers = browsers.browsersWithProfileData(using: browserDetection)
+
+ let roots = ChromiumProfileLocator
+ .roots(for: installedBrowsers, homeDirectories: BrowserCookieClient.defaultHomeDirectories())
+ .map { (url: $0.url, labelPrefix: $0.labelPrefix) }
+
+ var candidates: [IndexedDBCandidate] = []
+ for root in roots {
+ candidates.append(contentsOf: self.chromeProfileIndexedDBDirs(
+ root: root.url,
+ labelPrefix: root.labelPrefix))
+ }
+ return candidates
+ }
+
+ private static let indexedDBPrefix = "https_windsurf.com_"
+
+ private static func chromeProfileIndexedDBDirs(root: URL, labelPrefix: String) -> [IndexedDBCandidate] {
+ guard let entries = try? FileManager.default.contentsOfDirectory(
+ at: root,
+ includingPropertiesForKeys: [.isDirectoryKey],
+ options: [.skipsHiddenFiles])
+ else { return [] }
+
+ let profileDirs = entries.filter { url in
+ guard let isDir = (try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory), isDir else {
+ return false
+ }
+ let name = url.lastPathComponent
+ return name == "Default" || name.hasPrefix("Profile ") || name.hasPrefix("user-")
+ }
+ .sorted { $0.lastPathComponent < $1.lastPathComponent }
+
+ var candidates: [IndexedDBCandidate] = []
+ for dir in profileDirs {
+ let indexedDBRoot = dir.appendingPathComponent("IndexedDB")
+ guard let dbEntries = try? FileManager.default.contentsOfDirectory(
+ at: indexedDBRoot,
+ includingPropertiesForKeys: [.isDirectoryKey],
+ options: [.skipsHiddenFiles])
+ else { continue }
+ for entry in dbEntries {
+ guard let isDir = (try? entry.resourceValues(forKeys: [.isDirectoryKey]).isDirectory), isDir else {
+ continue
+ }
+ let name = entry.lastPathComponent
+ guard name.hasPrefix(self.indexedDBPrefix),
+ name.hasSuffix(".indexeddb.leveldb")
+ else { continue }
+ let label = "\(labelPrefix) \(dir.lastPathComponent)"
+ candidates.append(IndexedDBCandidate(label: label, url: entry))
+ }
+ }
+ return candidates
+ }
+
+ // MARK: - Token extraction (follows Factory readWorkOSToken pattern)
+
+ private static func readFirebaseTokens(
+ from levelDBURL: URL,
+ logger: ((String) -> Void)? = nil) -> [TokenInfo]
+ {
+ // Try structured reading first via SweetCookieKit
+ let textEntries = SweetCookieKit.ChromiumLocalStorageReader.readTextEntries(
+ in: levelDBURL,
+ logger: logger)
+ var tokens: [TokenInfo] = []
+ var seenRefresh = Set()
+
+ for entry in textEntries {
+ if let token = self.extractFirebaseTokens(from: entry.value), !seenRefresh.contains(token.refreshToken) {
+ seenRefresh.insert(token.refreshToken)
+ tokens.append(token)
+ }
+ }
+
+ if tokens.isEmpty {
+ let rawCandidates = SweetCookieKit.ChromiumLocalStorageReader.readTokenCandidates(
+ in: levelDBURL,
+ minimumLength: 40,
+ logger: logger)
+ for candidate in rawCandidates {
+ if let token = self.extractFirebaseTokens(from: candidate),
+ !seenRefresh.contains(token.refreshToken)
+ {
+ seenRefresh.insert(token.refreshToken)
+ tokens.append(token)
+ }
+ }
+ }
+
+ // Fallback: scan raw .ldb/.log files (Factory readWorkOSToken pattern)
+ if tokens.isEmpty {
+ if let token = self.scanLevelDBFiles(at: levelDBURL) {
+ tokens.append(token)
+ }
+ }
+
+ return tokens
+ }
+
+ private static func scanLevelDBFiles(at levelDBURL: URL) -> TokenInfo? {
+ guard let entries = try? FileManager.default.contentsOfDirectory(
+ at: levelDBURL,
+ includingPropertiesForKeys: [.contentModificationDateKey],
+ options: [.skipsHiddenFiles])
+ else { return nil }
+
+ let files = entries.filter { url in
+ let ext = url.pathExtension.lowercased()
+ return ext == "ldb" || ext == "log"
+ }
+ .sorted { lhs, rhs in
+ let left = (try? lhs.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate)
+ let right = (try? rhs.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate)
+ return (left ?? .distantPast) > (right ?? .distantPast)
+ }
+
+ for file in files {
+ guard let data = try? Data(contentsOf: file, options: [.mappedIfSafe]) else { continue }
+ guard let contents = String(data: data, encoding: .utf8) ??
+ String(data: data, encoding: .isoLatin1)
+ else { continue }
+ if let token = self.extractFirebaseTokens(from: contents) {
+ return token
+ }
+ }
+ return nil
+ }
+
+ private static func extractFirebaseTokens(from value: String) -> TokenInfo? {
+ // Firebase refresh tokens start with AMf-vB (Google Identity Toolkit)
+ let refreshToken = self.matchToken(
+ in: value,
+ pattern: #"refreshToken.{1,20}(AMf-vB[A-Za-z0-9_-]{20,})"#)
+ ?? self.matchToken(
+ in: value,
+ pattern: #"refresh_token.{1,20}(AMf-vB[A-Za-z0-9_-]{20,})"#)
+ ?? self.matchToken(
+ in: value,
+ pattern: #"(AMf-vB[A-Za-z0-9_-]{40,})"#)
+
+ guard let refreshToken else { return nil }
+
+ // Firebase access tokens are JWTs (eyJ...)
+ let accessToken = self.matchToken(
+ in: value,
+ pattern: #"accessToken.{1,20}(eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)"#)
+ ?? self.matchToken(
+ in: value,
+ pattern: #"access_token.{1,20}(eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)"#)
+
+ return TokenInfo(refreshToken: refreshToken, accessToken: accessToken, sourceLabel: "browser")
+ }
+
+ private static func matchToken(in contents: String, pattern: String) -> String? {
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return nil }
+ let range = NSRange(contents.startIndex.. 1,
+ let tokenRange = Range(match.range(at: 1), in: contents)
+ else { return nil }
+ return String(contents[tokenRange])
+ }
+}
+#endif
diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfProviderDescriptor.swift
new file mode 100644
index 000000000..c236e9e50
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfProviderDescriptor.swift
@@ -0,0 +1,100 @@
+import CodexBarMacroSupport
+import Foundation
+
+@ProviderDescriptorRegistration
+@ProviderDescriptorDefinition
+public enum WindsurfProviderDescriptor {
+ static func makeDescriptor() -> ProviderDescriptor {
+ ProviderDescriptor(
+ id: .windsurf,
+ metadata: ProviderMetadata(
+ id: .windsurf,
+ displayName: "Windsurf",
+ sessionLabel: "Daily",
+ weeklyLabel: "Weekly",
+ opusLabel: nil,
+ supportsOpus: false,
+ supportsCredits: false,
+ creditsHint: "",
+ toggleTitle: "Show Windsurf usage",
+ cliName: "windsurf",
+ defaultEnabled: false,
+ isPrimaryProvider: false,
+ usesAccountFallback: false,
+ dashboardURL: "https://windsurf.com/subscription/usage",
+ statusPageURL: nil),
+ branding: ProviderBranding(
+ iconStyle: .windsurf,
+ iconResourceName: "ProviderIcon-windsurf",
+ color: ProviderColor(red: 52 / 255, green: 232 / 255, blue: 187 / 255)),
+ tokenCost: ProviderTokenCostConfig(
+ supportsTokenCost: false,
+ noDataMessage: { "Windsurf cost summary is not supported." }),
+ fetchPlan: ProviderFetchPlan(
+ sourceModes: [.auto, .web, .cli],
+ pipeline: ProviderFetchPipeline(resolveStrategies: { _ in
+ [WindsurfWebFetchStrategy(), WindsurfLocalFetchStrategy()]
+ })),
+ cli: ProviderCLIConfig(
+ name: "windsurf",
+ versionDetector: nil))
+ }
+}
+
+struct WindsurfWebFetchStrategy: ProviderFetchStrategy {
+ let id: String = "windsurf.web"
+ let kind: ProviderFetchKind = .web
+
+ func isAvailable(_ context: ProviderFetchContext) async -> Bool {
+ guard context.sourceMode.usesWeb else { return false }
+ guard context.settings?.windsurf?.cookieSource != .off else { return false }
+ return true
+ }
+
+ func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
+ #if os(macOS)
+ let cookieSource = context.settings?.windsurf?.cookieSource ?? .auto
+ let manualToken = Self.manualToken(from: context)
+ let usage = try await WindsurfWebFetcher.fetchUsage(
+ browserDetection: context.browserDetection,
+ cookieSource: cookieSource,
+ manualAccessToken: manualToken,
+ logger: context.verbose ? { print($0) } : nil)
+ return self.makeResult(usage: usage, sourceLabel: "windsurf-web")
+ #else
+ throw WindsurfStatusProbeError.notSupported
+ #endif
+ }
+
+ func shouldFallback(on _: Error, context: ProviderFetchContext) -> Bool {
+ context.sourceMode == .auto
+ }
+
+ private static func manualToken(from context: ProviderFetchContext) -> String? {
+ guard context.settings?.windsurf?.cookieSource == .manual else { return nil }
+ let header = context.settings?.windsurf?.manualCookieHeader ?? ""
+ return header.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : header
+ }
+}
+
+struct WindsurfLocalFetchStrategy: ProviderFetchStrategy {
+ let id: String = "windsurf.local"
+ let kind: ProviderFetchKind = .localProbe
+
+ func isAvailable(_: ProviderFetchContext) async -> Bool {
+ true
+ }
+
+ func fetch(_ context: ProviderFetchContext) async throws -> ProviderFetchResult {
+ let probe = WindsurfStatusProbe()
+ let planInfo = try probe.fetch()
+ let usage = planInfo.toUsageSnapshot()
+ return self.makeResult(
+ usage: usage,
+ sourceLabel: "local")
+ }
+
+ func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool {
+ false
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift
new file mode 100644
index 000000000..9061243db
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift
@@ -0,0 +1,236 @@
+import Foundation
+
+// MARK: - Cached Plan Info (Codable)
+
+public struct WindsurfCachedPlanInfo: Codable, Sendable {
+ public let planName: String?
+ public let startTimestamp: Int64?
+ public let endTimestamp: Int64?
+ public let usage: Usage?
+ public let quotaUsage: QuotaUsage?
+
+ public struct Usage: Codable, Sendable {
+ public let messages: Int?
+ public let usedMessages: Int?
+ public let remainingMessages: Int?
+ public let flowActions: Int?
+ public let usedFlowActions: Int?
+ public let remainingFlowActions: Int?
+ }
+
+ public struct QuotaUsage: Codable, Sendable {
+ public let dailyRemainingPercent: Double?
+ public let weeklyRemainingPercent: Double?
+ public let dailyResetAtUnix: Int64?
+ public let weeklyResetAtUnix: Int64?
+ }
+}
+
+// MARK: - Errors & Probe
+
+#if os(macOS)
+
+import SQLite3
+
+public enum WindsurfStatusProbeError: LocalizedError, Sendable, Equatable {
+ case dbNotFound(String)
+ case sqliteFailed(String)
+ case noData
+ case parseFailed(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case let .dbNotFound(path):
+ "Windsurf database not found at \(path). Ensure Windsurf is installed and has been launched at least once."
+ case let .sqliteFailed(message):
+ "SQLite error reading Windsurf data: \(message)"
+ case .noData:
+ "No plan data found in Windsurf database. Sign in to Windsurf first."
+ case let .parseFailed(message):
+ "Could not parse Windsurf plan data: \(message)"
+ }
+ }
+}
+
+// MARK: - Probe
+
+public struct WindsurfStatusProbe: Sendable {
+ private static let defaultDBPath: String = {
+ let home = NSHomeDirectory()
+ return "\(home)/Library/Application Support/Windsurf/User/globalStorage/state.vscdb"
+ }()
+
+ private static let query = "SELECT value FROM ItemTable WHERE key = 'windsurf.settings.cachedPlanInfo' LIMIT 1;"
+
+ private let dbPath: String
+
+ public init(dbPath: String? = nil) {
+ self.dbPath = dbPath ?? Self.defaultDBPath
+ }
+
+ public func fetch() throws -> WindsurfCachedPlanInfo {
+ guard FileManager.default.fileExists(atPath: self.dbPath) else {
+ throw WindsurfStatusProbeError.dbNotFound(self.dbPath)
+ }
+
+ var db: OpaquePointer?
+ guard sqlite3_open_v2(self.dbPath, &db, SQLITE_OPEN_READONLY, nil) == SQLITE_OK else {
+ let message = db.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "unknown error"
+ sqlite3_close(db)
+ throw WindsurfStatusProbeError.sqliteFailed(message)
+ }
+ defer { sqlite3_close(db) }
+
+ sqlite3_busy_timeout(db, 250)
+
+ var stmt: OpaquePointer?
+ guard sqlite3_prepare_v2(db, Self.query, -1, &stmt, nil) == SQLITE_OK else {
+ let message = db.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "unknown error"
+ throw WindsurfStatusProbeError.sqliteFailed(message)
+ }
+ defer { sqlite3_finalize(stmt) }
+
+ let stepResult = sqlite3_step(stmt)
+ guard stepResult == SQLITE_ROW else {
+ if stepResult == SQLITE_DONE {
+ throw WindsurfStatusProbeError.noData
+ }
+ let message = db.flatMap { String(cString: sqlite3_errmsg($0)) } ?? "unknown error"
+ throw WindsurfStatusProbeError.sqliteFailed(message)
+ }
+
+ guard let jsonString = Self.decodeSQLiteValue(stmt: stmt, index: 0) else {
+ throw WindsurfStatusProbeError.noData
+ }
+ guard let jsonData = jsonString.data(using: .utf8) else {
+ throw WindsurfStatusProbeError.parseFailed("Invalid UTF-8 encoding")
+ }
+
+ do {
+ return try JSONDecoder().decode(WindsurfCachedPlanInfo.self, from: jsonData)
+ } catch {
+ throw WindsurfStatusProbeError.parseFailed(error.localizedDescription)
+ }
+ }
+
+ private static func decodeSQLiteValue(stmt: OpaquePointer?, index: Int32) -> String? {
+ switch sqlite3_column_type(stmt, index) {
+ case SQLITE_TEXT:
+ guard let c = sqlite3_column_text(stmt, index) else { return nil }
+ return String(cString: c)
+ case SQLITE_BLOB:
+ guard let bytes = sqlite3_column_blob(stmt, index) else { return nil }
+ let data = Data(bytes: bytes, count: Int(sqlite3_column_bytes(stmt, index)))
+ // VSCode/Windsurf state.vscdb schema declares value as BLOB;
+ // try UTF-16LE first (common for VSCode derivatives), then UTF-8.
+ if let decoded = String(data: data, encoding: .utf16LittleEndian) {
+ return decoded.trimmingCharacters(in: .controlCharacters)
+ }
+ if let decoded = String(data: data, encoding: .utf8) {
+ return decoded.trimmingCharacters(in: .controlCharacters)
+ }
+ return nil
+ default:
+ return nil
+ }
+ }
+}
+
+#else
+
+// MARK: - Windsurf (Unsupported)
+
+public enum WindsurfStatusProbeError: LocalizedError, Sendable, Equatable {
+ case notSupported
+
+ public var errorDescription: String? {
+ "Windsurf is only supported on macOS."
+ }
+}
+
+public struct WindsurfStatusProbe: Sendable {
+ public init(dbPath _: String? = nil) {}
+
+ public func fetch() throws -> WindsurfCachedPlanInfo {
+ throw WindsurfStatusProbeError.notSupported
+ }
+}
+
+#endif
+
+// MARK: - Conversion to UsageSnapshot
+
+extension WindsurfCachedPlanInfo {
+ public func toUsageSnapshot() -> UsageSnapshot {
+ var primary: RateWindow?
+ var secondary: RateWindow?
+
+ if let quota = self.quotaUsage {
+ // Primary: daily usage (usedPercent = 100 - dailyRemainingPercent)
+ if let daily = quota.dailyRemainingPercent {
+ let resetDate = quota.dailyResetAtUnix.map {
+ Date(timeIntervalSince1970: TimeInterval($0))
+ }
+ primary = RateWindow(
+ usedPercent: max(0, min(100, 100 - daily)),
+ windowMinutes: nil,
+ resetsAt: resetDate,
+ resetDescription: Self.formatResetDescription(resetDate))
+ }
+
+ // Secondary: weekly usage
+ if let weekly = quota.weeklyRemainingPercent {
+ let resetDate = quota.weeklyResetAtUnix.map {
+ Date(timeIntervalSince1970: TimeInterval($0))
+ }
+ secondary = RateWindow(
+ usedPercent: max(0, min(100, 100 - weekly)),
+ windowMinutes: nil,
+ resetsAt: resetDate,
+ resetDescription: Self.formatResetDescription(resetDate))
+ }
+ }
+
+ // Identity
+ var orgDescription: String?
+ if let endTimestamp = self.endTimestamp {
+ let endDate = Date(timeIntervalSince1970: TimeInterval(endTimestamp) / 1000)
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .none
+ orgDescription = "Expires \(formatter.string(from: endDate))"
+ }
+
+ let identity = ProviderIdentitySnapshot(
+ providerID: .windsurf,
+ accountEmail: nil,
+ accountOrganization: orgDescription,
+ loginMethod: self.planName)
+
+ return UsageSnapshot(
+ primary: primary,
+ secondary: secondary,
+ updatedAt: Date(),
+ identity: identity)
+ }
+
+ private static func formatResetDescription(_ date: Date?) -> String? {
+ guard let date else { return nil }
+ let now = Date()
+ let interval = date.timeIntervalSince(now)
+ guard interval > 0 else { return "Expired" }
+
+ let hours = Int(interval / 3600)
+ let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60)
+
+ if hours > 24 {
+ let days = hours / 24
+ let remainingHours = hours % 24
+ return "Resets in \(days)d \(remainingHours)h"
+ } else if hours > 0 {
+ return "Resets in \(hours)h \(minutes)m"
+ } else {
+ return "Resets in \(minutes)m"
+ }
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfUsageDataSource.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfUsageDataSource.swift
new file mode 100644
index 000000000..1330be598
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfUsageDataSource.swift
@@ -0,0 +1,27 @@
+import Foundation
+
+public enum WindsurfUsageDataSource: String, CaseIterable, Identifiable, Sendable {
+ case auto
+ case web
+ case cli
+
+ public var id: String {
+ self.rawValue
+ }
+
+ public var displayName: String {
+ switch self {
+ case .auto: "Auto"
+ case .web: "Web API (IndexedDB)"
+ case .cli: "Local (SQLite cache)"
+ }
+ }
+
+ public var sourceLabel: String {
+ switch self {
+ case .auto: "auto"
+ case .web: "web"
+ case .cli: "cli"
+ }
+ }
+}
diff --git a/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift b/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift
new file mode 100644
index 000000000..ed57a2ad1
--- /dev/null
+++ b/Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift
@@ -0,0 +1,308 @@
+import Foundation
+
+// MARK: - API Response Model
+
+public struct WindsurfGetPlanStatusResponse: Codable, Sendable {
+ public let planStatus: PlanStatus?
+
+ public struct PlanStatus: Codable, Sendable {
+ public let planInfo: PlanInfo?
+ public let planStart: String?
+ public let planEnd: String?
+ public let availablePromptCredits: Int?
+ public let availableFlowCredits: Int?
+ public let dailyQuotaRemainingPercent: Double?
+ public let weeklyQuotaRemainingPercent: Double?
+ public let dailyQuotaResetAtUnix: String?
+ public let weeklyQuotaResetAtUnix: String?
+ public let topUpStatus: TopUpStatus?
+ public let gracePeriodStatus: String?
+
+ public struct PlanInfo: Codable, Sendable {
+ public let planName: String?
+ public let teamsTier: String?
+ }
+
+ public struct TopUpStatus: Codable, Sendable {
+ public let topUpTransactionStatus: String?
+ }
+ }
+}
+
+// MARK: - Conversion to UsageSnapshot
+
+extension WindsurfGetPlanStatusResponse {
+ public func toUsageSnapshot() -> UsageSnapshot {
+ var primary: RateWindow?
+ var secondary: RateWindow?
+
+ if let status = self.planStatus {
+ if let daily = status.dailyQuotaRemainingPercent {
+ let resetDate = status.dailyQuotaResetAtUnix.flatMap { Int64($0) }.map {
+ Date(timeIntervalSince1970: TimeInterval($0))
+ }
+ primary = RateWindow(
+ usedPercent: max(0, min(100, 100 - daily)),
+ windowMinutes: nil,
+ resetsAt: resetDate,
+ resetDescription: Self.formatResetDescription(resetDate))
+ }
+
+ if let weekly = status.weeklyQuotaRemainingPercent {
+ let resetDate = status.weeklyQuotaResetAtUnix.flatMap { Int64($0) }.map {
+ Date(timeIntervalSince1970: TimeInterval($0))
+ }
+ secondary = RateWindow(
+ usedPercent: max(0, min(100, 100 - weekly)),
+ windowMinutes: nil,
+ resetsAt: resetDate,
+ resetDescription: Self.formatResetDescription(resetDate))
+ }
+ }
+
+ var orgDescription: String?
+ if let planEnd = self.planStatus?.planEnd {
+ let isoFormatter = ISO8601DateFormatter()
+ isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ let endDate = isoFormatter.date(from: planEnd)
+ ?? ISO8601DateFormatter().date(from: planEnd)
+ if let endDate {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .medium
+ formatter.timeStyle = .none
+ orgDescription = "Expires \(formatter.string(from: endDate))"
+ }
+ }
+
+ let identity = ProviderIdentitySnapshot(
+ providerID: .windsurf,
+ accountEmail: nil,
+ accountOrganization: orgDescription,
+ loginMethod: self.planStatus?.planInfo?.planName)
+
+ return UsageSnapshot(
+ primary: primary,
+ secondary: secondary,
+ updatedAt: Date(),
+ identity: identity)
+ }
+
+ private static func formatResetDescription(_ date: Date?) -> String? {
+ guard let date else { return nil }
+ let now = Date()
+ let interval = date.timeIntervalSince(now)
+ guard interval > 0 else { return "Expired" }
+
+ let hours = Int(interval / 3600)
+ let minutes = Int((interval.truncatingRemainder(dividingBy: 3600)) / 60)
+
+ if hours > 24 {
+ let days = hours / 24
+ let remainingHours = hours % 24
+ return "Resets in \(days)d \(remainingHours)h"
+ } else if hours > 0 {
+ return "Resets in \(hours)h \(minutes)m"
+ } else {
+ return "Resets in \(minutes)m"
+ }
+ }
+}
+
+// MARK: - Web Fetcher
+
+#if os(macOS)
+
+public enum WindsurfWebFetcherError: LocalizedError, Sendable {
+ case noFirebaseToken
+ case tokenRefreshFailed(String)
+ case apiCallFailed(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .noFirebaseToken:
+ "No Firebase token found in browser IndexedDB. Sign in to windsurf.com in Chrome or Edge first."
+ case let .tokenRefreshFailed(message):
+ "Firebase token refresh failed: \(message)"
+ case let .apiCallFailed(message):
+ "Windsurf API call failed: \(message)"
+ }
+ }
+}
+
+public enum WindsurfWebFetcher {
+ // Public Firebase API key (embedded in windsurf.com frontend)
+ private static let firebaseAPIKey = "AIzaSyDsOl-1XpT5err0Tcnx8FFod1H8gVGIycY"
+ private static let getPlanStatusURL = "https://windsurf.com/_backend/exa.seat_management_pb.SeatManagementService/GetPlanStatus"
+
+ public static func fetchUsage(
+ browserDetection: BrowserDetection,
+ cookieSource: ProviderCookieSource = .auto,
+ manualAccessToken: String? = nil,
+ timeout: TimeInterval = 15,
+ logger: ((String) -> Void)? = nil) async throws -> UsageSnapshot
+ {
+ let log: (String) -> Void = { msg in logger?("[windsurf-web] \(msg)") }
+ let useKeychain = cookieSource == .auto
+
+ // 0. Manual token override (cookie source = manual)
+ // Accepts either a refresh token (AMf-vB prefix, long-lived) or access token (eyJ prefix, ~1h)
+ if let manualAccessToken, !manualAccessToken.isEmpty {
+ let token = manualAccessToken.trimmingCharacters(in: .whitespacesAndNewlines)
+ if token.hasPrefix("AMf-vB") {
+ log("Using manual refresh token → exchanging for access token")
+ let accessToken = try await self.refreshFirebaseToken(token, timeout: timeout)
+ let response = try await self.fetchPlanStatus(accessToken: accessToken, timeout: timeout)
+ return response.toUsageSnapshot()
+ } else {
+ log("Using manual access token")
+ let response = try await self.fetchPlanStatus(accessToken: token, timeout: timeout)
+ return response.toUsageSnapshot()
+ }
+ }
+
+ // 1. Try cached token from CookieHeaderCache (only when auto / Keychain allowed)
+ if useKeychain, let cached = CookieHeaderCache.load(provider: .windsurf) {
+ log("Trying cached Firebase access token")
+ do {
+ let response = try await self.fetchPlanStatus(accessToken: cached.cookieHeader, timeout: timeout)
+ return response.toUsageSnapshot()
+ } catch {
+ log("Cached token failed: \(error.localizedDescription)")
+ CookieHeaderCache.clear(provider: .windsurf)
+ }
+ }
+
+ // 2. Import Firebase tokens from browser IndexedDB
+ let tokenInfos = WindsurfFirebaseTokenImporter.importFirebaseTokens(
+ browserDetection: browserDetection,
+ logger: logger)
+ guard !tokenInfos.isEmpty else {
+ throw WindsurfWebFetcherError.noFirebaseToken
+ }
+
+ var lastError: Error?
+
+ for tokenInfo in tokenInfos {
+ // 2a. Try existing access token first (if not expired)
+ if let accessToken = tokenInfo.accessToken {
+ log("Trying access token from \(tokenInfo.sourceLabel)")
+ do {
+ let response = try await self.fetchPlanStatus(accessToken: accessToken, timeout: timeout)
+ if useKeychain {
+ CookieHeaderCache.store(
+ provider: .windsurf,
+ cookieHeader: accessToken,
+ sourceLabel: tokenInfo.sourceLabel)
+ }
+ return response.toUsageSnapshot()
+ } catch {
+ log("Access token failed: \(error.localizedDescription)")
+ lastError = error
+ }
+ }
+
+ // 2b. Refresh token to get new access token
+ log("Refreshing Firebase token from \(tokenInfo.sourceLabel)")
+ do {
+ let accessToken = try await self.refreshFirebaseToken(
+ tokenInfo.refreshToken,
+ timeout: timeout)
+ let response = try await self.fetchPlanStatus(accessToken: accessToken, timeout: timeout)
+ if useKeychain {
+ CookieHeaderCache.store(
+ provider: .windsurf,
+ cookieHeader: accessToken,
+ sourceLabel: tokenInfo.sourceLabel)
+ }
+ return response.toUsageSnapshot()
+ } catch {
+ log("Token refresh/API call failed: \(error.localizedDescription)")
+ lastError = error
+ }
+ }
+
+ throw lastError ?? WindsurfWebFetcherError.noFirebaseToken
+ }
+
+ // MARK: - Firebase Token Refresh
+
+ private static func refreshFirebaseToken(
+ _ refreshToken: String,
+ timeout: TimeInterval) async throws -> String
+ {
+ guard let url = URL(string: "https://securetoken.googleapis.com/v1/token?key=\(self.firebaseAPIKey)") else {
+ throw WindsurfWebFetcherError.tokenRefreshFailed("Invalid Firebase token URL")
+ }
+
+ var request = URLRequest(url: url)
+ request.timeoutInterval = timeout
+ request.httpMethod = "POST"
+ request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
+
+ let body = "grant_type=refresh_token&refresh_token=\(refreshToken)"
+ request.httpBody = body.data(using: .utf8)
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw WindsurfWebFetcherError.tokenRefreshFailed("Invalid response")
+ }
+
+ guard httpResponse.statusCode == 200 else {
+ let body = String(data: data, encoding: .utf8)?
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ let snippet = body.isEmpty ? "" : ": \(body.prefix(200))"
+ throw WindsurfWebFetcherError.tokenRefreshFailed("HTTP \(httpResponse.statusCode)\(snippet)")
+ }
+
+ guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ let accessToken = json["access_token"] as? String
+ else {
+ throw WindsurfWebFetcherError.tokenRefreshFailed("Missing access_token in response")
+ }
+
+ return accessToken
+ }
+
+ // MARK: - GetPlanStatus API
+
+ private static func fetchPlanStatus(
+ accessToken: String,
+ timeout: TimeInterval) async throws -> WindsurfGetPlanStatusResponse
+ {
+ guard let url = URL(string: self.getPlanStatusURL) else {
+ throw WindsurfWebFetcherError.apiCallFailed("Invalid GetPlanStatus URL")
+ }
+
+ var request = URLRequest(url: url)
+ request.timeoutInterval = timeout
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("1", forHTTPHeaderField: "Connect-Protocol-Version")
+
+ let body: [String: Any] = [
+ "authToken": accessToken,
+ "includeTopUpStatus": true,
+ ]
+ request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
+
+ let (data, response) = try await URLSession.shared.data(for: request)
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw WindsurfWebFetcherError.apiCallFailed("Invalid response")
+ }
+
+ guard httpResponse.statusCode == 200 else {
+ let body = String(data: data, encoding: .utf8)?
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
+ let snippet = body.isEmpty ? "" : ": \(body.prefix(200))"
+ throw WindsurfWebFetcherError.apiCallFailed("HTTP \(httpResponse.statusCode)\(snippet)")
+ }
+
+ do {
+ return try JSONDecoder().decode(WindsurfGetPlanStatusResponse.self, from: data)
+ } catch {
+ throw WindsurfWebFetcherError.apiCallFailed("Parse error: \(error.localizedDescription)")
+ }
+ }
+}
+
+#endif
diff --git a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
index f4a3ba8bb..d74c86b12 100644
--- a/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
+++ b/Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift
@@ -72,7 +72,7 @@ enum CostUsageScanner {
return self.loadClaudeDaily(provider: .vertexai, range: range, now: now, options: filtered)
case .zai, .gemini, .antigravity, .cursor, .opencode, .alibaba, .factory, .copilot, .minimax, .kilo,
.kiro, .kimi,
- .kimik2, .augment, .jetbrains, .amp, .ollama, .synthetic, .openrouter, .warp, .perplexity:
+ .kimik2, .augment, .jetbrains, .amp, .ollama, .synthetic, .openrouter, .warp, .windsurf, .perplexity:
return emptyReport
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
index c0e600fa9..84ad8a6e3 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
@@ -71,6 +71,7 @@ enum ProviderChoice: String, AppEnum {
case .synthetic: return nil // Synthetic not yet supported in widgets
case .openrouter: return nil // OpenRouter not yet supported in widgets
case .warp: return nil // Warp not yet supported in widgets
+ case .windsurf: return nil // Windsurf not yet supported in widgets
case .perplexity: return nil // Perplexity not yet supported in widgets
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift
index e08b23367..8f19c0471 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift
@@ -280,6 +280,7 @@ private struct ProviderSwitchChip: View {
case .synthetic: "Synthetic"
case .openrouter: "OpenRouter"
case .warp: "Warp"
+ case .windsurf: "Windsurf"
case .perplexity: "Pplx"
}
}
@@ -622,6 +623,8 @@ enum WidgetColors {
Color(red: 111 / 255, green: 66 / 255, blue: 193 / 255) // OpenRouter purple
case .warp:
Color(red: 147 / 255, green: 139 / 255, blue: 180 / 255)
+ case .windsurf:
+ Color(red: 52 / 255, green: 232 / 255, blue: 187 / 255) // Windsurf #34e8bb
case .perplexity:
Color(red: 32 / 255, green: 178 / 255, blue: 170 / 255) // Perplexity teal
}
diff --git a/Tests/CodexBarTests/WindsurfStatusProbeTests.swift b/Tests/CodexBarTests/WindsurfStatusProbeTests.swift
new file mode 100644
index 000000000..bdb37a3d7
--- /dev/null
+++ b/Tests/CodexBarTests/WindsurfStatusProbeTests.swift
@@ -0,0 +1,195 @@
+import CodexBarCore
+import Foundation
+import Testing
+
+struct WindsurfStatusProbeTests {
+ // MARK: - Helper
+
+ private static func decode(_ json: String) throws -> WindsurfCachedPlanInfo {
+ try JSONDecoder().decode(WindsurfCachedPlanInfo.self, from: Data(json.utf8))
+ }
+
+ // MARK: - JSON Decoding
+
+ @Test
+ func `decodes full plan info`() throws {
+ let info = try Self.decode("""
+ {
+ "planName": "Pro",
+ "startTimestamp": 1771610750000,
+ "endTimestamp": 1774029950000,
+ "usage": {
+ "messages": 50000,
+ "usedMessages": 35650,
+ "remainingMessages": 14350,
+ "flowActions": 150000,
+ "usedFlowActions": 0,
+ "remainingFlowActions": 150000
+ },
+ "quotaUsage": {
+ "dailyRemainingPercent": 9,
+ "weeklyRemainingPercent": 54,
+ "dailyResetAtUnix": 1774080000,
+ "weeklyResetAtUnix": 1774166400
+ }
+ }
+ """)
+
+ #expect(info.planName == "Pro")
+ #expect(info.startTimestamp == 1_771_610_750_000)
+ #expect(info.endTimestamp == 1_774_029_950_000)
+ #expect(info.usage?.messages == 50000)
+ #expect(info.usage?.usedMessages == 35650)
+ #expect(info.usage?.remainingMessages == 14350)
+ #expect(info.usage?.flowActions == 150_000)
+ #expect(info.usage?.usedFlowActions == 0)
+ #expect(info.usage?.remainingFlowActions == 150_000)
+ #expect(info.quotaUsage?.dailyRemainingPercent == 9)
+ #expect(info.quotaUsage?.weeklyRemainingPercent == 54)
+ #expect(info.quotaUsage?.dailyResetAtUnix == 1_774_080_000)
+ #expect(info.quotaUsage?.weeklyResetAtUnix == 1_774_166_400)
+ }
+
+ @Test
+ func `decodes minimal plan info`() throws {
+ let info = try Self.decode("""
+ {"planName": "Free"}
+ """)
+
+ #expect(info.planName == "Free")
+ #expect(info.usage == nil)
+ #expect(info.quotaUsage == nil)
+ #expect(info.endTimestamp == nil)
+ }
+
+ @Test
+ func `decodes empty object`() throws {
+ let info = try Self.decode("{}")
+
+ #expect(info.planName == nil)
+ #expect(info.usage == nil)
+ #expect(info.quotaUsage == nil)
+ }
+
+ // MARK: - toUsageSnapshot Conversion
+
+ @Test
+ func `converts full plan to usage snapshot`() throws {
+ let info = try Self.decode("""
+ {
+ "planName": "Pro",
+ "startTimestamp": 1771610750000,
+ "endTimestamp": 1774029950000,
+ "usage": {
+ "messages": 50000, "usedMessages": 35650, "remainingMessages": 14350,
+ "flowActions": 150000, "usedFlowActions": 0, "remainingFlowActions": 150000
+ },
+ "quotaUsage": {
+ "dailyRemainingPercent": 9, "weeklyRemainingPercent": 54,
+ "dailyResetAtUnix": 1774080000, "weeklyResetAtUnix": 1774166400
+ }
+ }
+ """)
+
+ let snapshot = info.toUsageSnapshot()
+
+ // Primary = daily: usedPercent = 100 - 9 = 91
+ #expect(snapshot.primary?.usedPercent == 91)
+ #expect(snapshot.primary?.resetsAt != nil)
+
+ // Secondary = weekly: usedPercent = 100 - 54 = 46
+ #expect(snapshot.secondary?.usedPercent == 46)
+ #expect(snapshot.secondary?.resetsAt != nil)
+
+ // Identity
+ #expect(snapshot.identity?.providerID == .windsurf)
+ #expect(snapshot.identity?.loginMethod == "Pro")
+ #expect(snapshot.identity?.accountOrganization != nil)
+ }
+
+ @Test
+ func `converts minimal plan to usage snapshot`() throws {
+ let info = try Self.decode("""
+ {"planName": "Free"}
+ """)
+
+ let snapshot = info.toUsageSnapshot()
+
+ // Without quotaUsage, primary and secondary should be nil
+ #expect(snapshot.primary == nil)
+ #expect(snapshot.secondary == nil)
+ #expect(snapshot.identity?.loginMethod == "Free")
+ #expect(snapshot.identity?.accountOrganization == nil)
+ }
+
+ @Test
+ func `daily at zero remaining shows 100 percent used`() throws {
+ let info = try Self.decode("""
+ {
+ "planName": "Pro",
+ "quotaUsage": {"dailyRemainingPercent": 0, "weeklyRemainingPercent": 100}
+ }
+ """)
+
+ let snapshot = info.toUsageSnapshot()
+
+ #expect(snapshot.primary?.usedPercent == 100)
+ #expect(snapshot.secondary?.usedPercent == 0)
+ }
+
+ @Test
+ func `weekly at full remaining shows 0 percent used`() throws {
+ let info = try Self.decode("""
+ {
+ "planName": "Pro",
+ "quotaUsage": {"dailyRemainingPercent": 100, "weeklyRemainingPercent": 100}
+ }
+ """)
+
+ let snapshot = info.toUsageSnapshot()
+
+ #expect(snapshot.primary?.usedPercent == 0)
+ #expect(snapshot.secondary?.usedPercent == 0)
+ }
+
+ @Test
+ func `reset dates are correctly converted from unix timestamps`() throws {
+ let info = try Self.decode("""
+ {
+ "planName": "Pro",
+ "quotaUsage": {
+ "dailyRemainingPercent": 50, "weeklyRemainingPercent": 50,
+ "dailyResetAtUnix": 1774080000, "weeklyResetAtUnix": 1774166400
+ }
+ }
+ """)
+
+ let snapshot = info.toUsageSnapshot()
+
+ #expect(snapshot.primary?.resetsAt == Date(timeIntervalSince1970: 1_774_080_000))
+ #expect(snapshot.secondary?.resetsAt == Date(timeIntervalSince1970: 1_774_166_400))
+ }
+
+ @Test
+ func `end timestamp converts to expiry description`() throws {
+ let futureMs = Int64(Date().addingTimeInterval(86400 * 30).timeIntervalSince1970 * 1000)
+ let info = try Self.decode("""
+ {"planName": "Pro", "endTimestamp": \(futureMs)}
+ """)
+
+ let snapshot = info.toUsageSnapshot()
+
+ #expect(snapshot.identity?.accountOrganization?.hasPrefix("Expires ") == true)
+ }
+
+ // MARK: - Probe Error Cases
+
+ @Test
+ func `probe throws dbNotFound for missing file`() {
+ let probe = WindsurfStatusProbe(dbPath: "/nonexistent/path/state.vscdb")
+
+ #expect(throws: WindsurfStatusProbeError.self) {
+ _ = try probe.fetch()
+ }
+ }
+}
diff --git a/docs/windsurf.md b/docs/windsurf.md
new file mode 100644
index 000000000..c30369a6f
--- /dev/null
+++ b/docs/windsurf.md
@@ -0,0 +1,165 @@
+---
+summary: "Windsurf provider data sources: Firebase IndexedDB tokens, local SQLite cache, and GetPlanStatus API."
+read_when:
+ - Debugging Windsurf usage fetch
+ - Updating Windsurf Firebase token import or API handling
+ - Adjusting Windsurf provider UI/menu behavior
+---
+
+# Windsurf provider
+
+Windsurf supports two data sources: a web API (via Firebase tokens from browser IndexedDB) and a local SQLite cache.
+
+## Data sources + fallback order
+
+Usage source picker:
+- Preferences → Providers → Windsurf → Usage source (Auto / Web API / Local).
+
+### Auto mode (default)
+1) **Web API** (preferred) — real-time data from windsurf.com API.
+2) **Local SQLite cache** (fallback) — reads from Windsurf's `state.vscdb`.
+
+### Web API fetch order
+1) **Manual token** (when Cookie source = Manual).
+2) **Cached Firebase access token** (Keychain cache `com.steipete.codexbar.cache`, account `cookie.windsurf`).
+3) **Browser IndexedDB import** — extracts Firebase tokens from Chromium browsers.
+
+### Local SQLite cache
+- File: `~/Library/Application Support/Windsurf/User/globalStorage/state.vscdb`.
+- Key: `windsurf.settings.cachedPlanInfo` in `ItemTable`.
+- Limitation: only updates when Windsurf is launched; can be significantly stale.
+
+## Cookie source settings
+
+Preferences → Providers → Windsurf → Cookie source:
+
+- **Automatic** (default): imports Firebase tokens from browser IndexedDB, caches access tokens in Keychain.
+- **Manual**: paste a Firebase refresh token or access token directly (see below).
+- **Off**: disables web API access entirely; only local SQLite cache is used.
+
+### How to get a manual token
+
+1. Open `https://windsurf.com/subscription/usage` in Chrome or Edge and sign in.
+2. Open Developer Tools (F12 or Cmd+Option+I).
+3. Go to the **Console** tab.
+4. Paste the following JavaScript and press Enter:
+
+```javascript
+(async () => {
+ const dbs = await indexedDB.databases();
+ const fbDb = dbs.find(d => d.name === 'firebaseLocalStorageDb');
+ if (!fbDb) { console.log('Not signed in'); return; }
+ const db = await new Promise((r, e) => {
+ const req = indexedDB.open(fbDb.name);
+ req.onsuccess = () => r(req.result);
+ req.onerror = () => e(req.error);
+ });
+ const tx = db.transaction('firebaseLocalStorage', 'readonly');
+ const store = tx.objectStore('firebaseLocalStorage');
+ const all = await new Promise(r => {
+ const req = store.getAll();
+ req.onsuccess = () => r(req.result);
+ });
+ const entry = all.find(e => e.value?.stsTokenManager);
+ if (entry) {
+ const mgr = entry.value.stsTokenManager;
+ console.log('=== Refresh token (long-lived, recommended) ===');
+ console.log(mgr.refreshToken);
+ console.log('=== Access token (expires ~1h) ===');
+ console.log(mgr.accessToken);
+ } else {
+ console.log('No Firebase token found. Sign in to windsurf.com first.');
+ }
+})();
+```
+
+5. Copy the **refresh token** (starts with `AMf-vB`, long-lived) from the console output.
+6. In CodexBar: Providers → Windsurf → Cookie source → Manual → paste the token.
+
+**Note**: Both token types are accepted. Refresh tokens (`AMf-vB…`) are recommended — they persist across sessions and CodexBar will automatically exchange them for short-lived access tokens. Access tokens (`eyJ…`) expire after ~1 hour.
+
+## Authentication flow (Automatic mode)
+
+```
+Browser IndexedDB (LevelDB on disk)
+ ↓ extract Firebase refreshToken (prefix: AMf-vB...)
+POST https://securetoken.googleapis.com/v1/token
+ ↓ exchange for accessToken (JWT, ~1h expiry)
+POST https://windsurf.com/_backend/.../GetPlanStatus
+ ↓ body: { authToken, includeTopUpStatus: true }
+UsageSnapshot (daily/weekly quota %)
+```
+
+## Firebase token extraction
+
+- **Browsers scanned**: Chrome, Edge, Brave, Arc, Vivaldi, Chromium, and other Chromium forks.
+- **IndexedDB path**: `~/Library/Application Support///IndexedDB/https_windsurf.com_*.indexeddb.leveldb/`
+- **Token patterns**:
+ - Refresh token: `AMf-vB` prefix (Google Identity Toolkit format).
+ - Access token: `eyJ` prefix (JWT).
+- **Extraction methods** (in order):
+ 1. `ChromiumLocalStorageReader.readTextEntries()` (structured LevelDB read).
+ 2. `ChromiumLocalStorageReader.readTokenCandidates()` (raw token scan).
+ 3. Direct `.ldb`/`.log` file scan with regex (fallback).
+
+## API endpoints
+
+### Firebase token refresh
+- `POST https://securetoken.googleapis.com/v1/token?key=AIzaSyDsOl-1XpT5err0Tcnx8FFod1H8gVGIycY`
+- Content-Type: `application/x-www-form-urlencoded`
+- Body: `grant_type=refresh_token&refresh_token=`
+- Returns: `{ "access_token": "...", "expires_in": "3600", ... }`
+
+### GetPlanStatus (ConnectRPC over JSON)
+- `POST https://windsurf.com/_backend/exa.seat_management_pb.SeatManagementService/GetPlanStatus`
+- Headers:
+ - `Content-Type: application/json`
+ - `Connect-Protocol-Version: 1`
+- Body: `{ "authToken": "", "includeTopUpStatus": true }`
+- Response:
+```json
+{
+ "planStatus": {
+ "planInfo": { "planName": "Pro", "teamsTier": "TEAMS_TIER_PRO" },
+ "planStart": "2026-03-20T18:05:50Z",
+ "planEnd": "2026-04-20T18:05:50Z",
+ "dailyQuotaRemainingPercent": 68,
+ "weeklyQuotaRemainingPercent": 84,
+ "dailyQuotaResetAtUnix": "1774166400",
+ "weeklyQuotaResetAtUnix": "1774166400"
+ }
+}
+```
+
+## Snapshot mapping
+- Primary: daily usage percent (100 - dailyQuotaRemainingPercent).
+- Secondary: weekly usage percent (100 - weeklyQuotaRemainingPercent).
+- Reset: daily/weekly reset timestamps (Unix seconds as string).
+- Plan: planName from planInfo.
+- Expiry: planEnd date.
+
+## Troubleshooting
+
+### "No Firebase token found in browser IndexedDB"
+- Sign in to `https://windsurf.com` in Chrome, Edge, or another Chromium browser.
+- Grant Full Disk Access to CodexBar (System Settings → Privacy & Security → Full Disk Access).
+- Try Manual mode and paste the token directly.
+
+### "Firebase token refresh failed"
+- Your refresh token may have expired. Sign in to windsurf.com again in your browser.
+- Check your internet connection.
+
+### "Windsurf API call failed: HTTP 401"
+- The access token has expired. CodexBar will automatically refresh it on the next fetch.
+- If using Manual mode, paste a fresh access token.
+
+### Stale data with Local mode
+- The local SQLite cache only updates when Windsurf is launched. Switch to Auto or Web API mode for real-time data.
+
+## Key files
+- `Sources/CodexBarCore/Providers/Windsurf/WindsurfStatusProbe.swift` (local SQLite)
+- `Sources/CodexBarCore/Providers/Windsurf/WindsurfFirebaseTokenImporter.swift` (IndexedDB extraction)
+- `Sources/CodexBarCore/Providers/Windsurf/WindsurfWebFetcher.swift` (token refresh + API)
+- `Sources/CodexBarCore/Providers/Windsurf/WindsurfProviderDescriptor.swift` (fetch strategies)
+- `Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift` (settings UI)
+- `Sources/CodexBar/Providers/Windsurf/WindsurfSettingsStore.swift` (settings persistence)