Skip to content
Merged
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
11 changes: 6 additions & 5 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ extension UsageMenuCardView.Model {
}
if labels.showsTertiary, let opus = snapshot.tertiary {
var tertiaryDetailText: String?
if input.provider == .alibaba,
if input.provider == .alibaba || input.provider == .alibabatokenplan,
let detail = opus.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
Expand Down Expand Up @@ -1206,9 +1206,10 @@ extension UsageMenuCardView.Model {
let total = UsageFormatter.kiroCreditNumber(kiroUsage.creditsTotal)
primaryDetailLeft = "\(remaining) of \(total) credits left"
}
if input.provider == .alibaba || input.provider == .mistral || input.provider == .manus,
let detail = primary.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
if input.provider == .alibaba || input.provider == .alibabatokenplan || input.provider == .mistral || input
.provider == .manus,
let detail = primary.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
primaryDetailText = detail
if input.provider == .manus { primaryResetText = nil }
Expand Down Expand Up @@ -1330,7 +1331,7 @@ extension UsageMenuCardView.Model {
pacePercent: nil,
paceOnTop: true)
}
if input.provider == .alibaba,
if input.provider == .alibaba || input.provider == .alibabatokenplan,
let detail = weekly.resetDescription,
!detail.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import AppKit
import CodexBarCore
import CodexBarMacroSupport
import Foundation
import SwiftUI

@ProviderImplementationRegistration
struct AlibabaTokenPlanProviderImplementation: ProviderImplementation {
let id: UsageProvider = .alibabatokenplan

@MainActor
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
ProviderPresentation { context in
context.store.sourceLabel(for: context.provider)
}
}

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

@MainActor
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
_ = context
return .alibabaTokenPlan(context.settings.alibabaTokenPlanSettingsSnapshot())
}

@MainActor
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
let cookieBinding = Binding(
get: { context.settings.alibabaTokenPlanCookieSource.rawValue },
set: { raw in
context.settings.alibabaTokenPlanCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
})
let cookieOptions = ProviderCookieSourceUI.options(
allowsOff: false,
keychainDisabled: context.settings.debugDisableKeychainAccess)
let cookieSubtitle: () -> String? = {
ProviderCookieSourceUI.subtitle(
source: context.settings.alibabaTokenPlanCookieSource,
keychainDisabled: context.settings.debugDisableKeychainAccess,
auto: "Automatic imports browser cookies from Bailian.",
manual: "Paste a Cookie header from bailian.console.aliyun.com.",
off: "Alibaba Token Plan cookies are disabled.")
}

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

@MainActor
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
[
ProviderSettingsFieldDescriptor(
id: "alibaba-token-plan-cookie",
title: "Cookie header",
subtitle: "",
kind: .secure,
placeholder: "Cookie: ...",
binding: context.stringBinding(\.alibabaTokenPlanCookieHeader),
actions: [
ProviderSettingsActionDescriptor(
id: "alibaba-token-plan-open-dashboard",
title: "Open Token Plan",
style: .link,
isVisible: nil,
perform: {
NSWorkspace.shared.open(AlibabaTokenPlanUsageFetcher.dashboardURL)
}),
],
isVisible: {
context.settings.alibabaTokenPlanCookieSource == .manual
},
onActivate: nil),
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import CodexBarCore
import Foundation

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

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

func alibabaTokenPlanSettingsSnapshot() -> ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings {
ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings(
cookieSource: self.alibabaTokenPlanCookieSource,
manualCookieHeader: self.alibabaTokenPlanCookieHeader)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum ProviderImplementationRegistry {
case .opencode: OpenCodeProviderImplementation()
case .opencodego: OpenCodeGoProviderImplementation()
case .alibaba: AlibabaCodingPlanProviderImplementation()
case .alibabatokenplan: AlibabaTokenPlanProviderImplementation()
case .factory: FactoryProviderImplementation()
case .gemini: GeminiProviderImplementation()
case .antigravity: AntigravityProviderImplementation()
Expand Down
10 changes: 6 additions & 4 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,7 @@ extension UsageStore {
await AugmentStatusProbe.latestDumps()
}

// swiftlint:disable:next function_body_length
func debugLog(for provider: UsageProvider) async -> String {
if let cached = self.probeLogs[provider], !cached.isEmpty {
return cached
Expand Down Expand Up @@ -977,6 +978,7 @@ extension UsageStore {
.antigravity: "Antigravity debug log not yet implemented",
.opencode: "OpenCode debug log not yet implemented",
.alibaba: "Alibaba Coding Plan debug log not yet implemented",
.alibabatokenplan: "Alibaba Token Plan debug log not yet implemented",
.factory: "Droid debug log not yet implemented",
.copilot: "Copilot debug log not yet implemented",
.manus: "Manus debug log not yet implemented",
Expand Down Expand Up @@ -1071,10 +1073,10 @@ extension UsageStore {
configToken: nil,
hasEnvToken: deepSeekHasEnvToken,
hasTokenAccount: deepSeekHasTokenAccount)
case .gemini, .antigravity, .opencode, .opencodego, .factory, .copilot, .vertexai, .kilo, .kiro, .kimi,
.kimik2, .moonshot, .jetbrains, .perplexity, .mimo, .doubao, .abacus, .mistral, .codebuff, .crof,
.windsurf, .venice, .manus, .commandcode, .stepfun, .bedrock, .grok, .groq, .t3chat, .llmproxy,
.deepgram:
case .gemini, .antigravity, .opencode, .opencodego, .alibabatokenplan, .factory, .copilot,
.vertexai, .kilo, .kiro, .kimi, .kimik2, .moonshot, .jetbrains, .perplexity, .mimo, .doubao,
.abacus, .mistral, .codebuff, .crof, .windsurf, .venice, .manus, .commandcode, .stepfun, .bedrock,
.grok, .groq, .t3chat, .llmproxy, .deepgram:
return unimplementedDebugLogMessages[provider] ?? "Debug log not yet implemented"
}
}
Expand Down
7 changes: 7 additions & 0 deletions Sources/CodexBarCLI/TokenAccountCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ struct TokenAccountCLIContext {
cookieSource: cookieSource,
manualCookieHeader: cookieHeader,
apiRegion: self.resolveAlibabaCodingPlanRegion(config)))
case .alibabatokenplan:
return self.makeSnapshot(
alibabaTokenPlan: ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings(
cookieSource: cookieSource,
manualCookieHeader: cookieHeader))
case .factory:
return self.makeSnapshot(
factory: ProviderSettingsSnapshot.FactoryProviderSettings(
Expand Down Expand Up @@ -249,6 +254,7 @@ struct TokenAccountCLIContext {
opencode: ProviderSettingsSnapshot.OpenCodeProviderSettings? = nil,
opencodego: ProviderSettingsSnapshot.OpenCodeProviderSettings? = nil,
alibaba: ProviderSettingsSnapshot.AlibabaCodingPlanProviderSettings? = nil,
alibabaTokenPlan: ProviderSettingsSnapshot.AlibabaTokenPlanProviderSettings? = nil,
factory: ProviderSettingsSnapshot.FactoryProviderSettings? = nil,
minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings? = nil,
manus: ProviderSettingsSnapshot.ManusProviderSettings? = nil,
Expand All @@ -273,6 +279,7 @@ struct TokenAccountCLIContext {
opencode: opencode,
opencodego: opencodego,
alibaba: alibaba,
alibabaTokenPlan: alibabaTokenPlan,
factory: factory,
minimax: minimax,
manus: manus,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Generated by Scripts/regenerate-codex-parser-hash.sh. Do not edit by hand.

enum CodexParserHash {
static let value = "881a341768657996"
static let value = "e563303df766b16f"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import Foundation

struct AlibabaTokenPlanCookieHeaders: Sendable {
private static let cachedAPIHeaderName = "__codexbar_alibaba_token_plan_api"
private static let cachedDashboardHeaderName = "__codexbar_alibaba_token_plan_dashboard"

let apiCookieHeader: String
let dashboardCookieHeader: String

init(apiCookieHeader: String, dashboardCookieHeader: String) {
self.apiCookieHeader = apiCookieHeader
self.dashboardCookieHeader = dashboardCookieHeader
}

init?(singleHeader raw: String?) {
guard let normalized = CookieHeaderNormalizer.normalize(raw) else { return nil }
self.apiCookieHeader = normalized
self.dashboardCookieHeader = normalized
}

init?(cachedHeader raw: String?) {
var valuesByName: [String: String] = [:]
for pair in CookieHeaderNormalizer.pairs(from: raw ?? "") {
valuesByName[pair.name] = pair.value
}
if let encodedAPI = valuesByName[Self.cachedAPIHeaderName],
let encodedDashboard = valuesByName[Self.cachedDashboardHeaderName],
let apiHeader = Self.decodeCachedHeader(encodedAPI),
let dashboardHeader = Self.decodeCachedHeader(encodedDashboard),
let normalizedAPI = CookieHeaderNormalizer.normalize(apiHeader),
let normalizedDashboard = CookieHeaderNormalizer.normalize(dashboardHeader)
{
self.init(apiCookieHeader: normalizedAPI, dashboardCookieHeader: normalizedDashboard)
return
}

self.init(singleHeader: raw)
}

var cacheCookieHeader: String {
[
"\(Self.cachedAPIHeaderName)=\(Self.encodeCachedHeader(self.apiCookieHeader))",
"\(Self.cachedDashboardHeaderName)=\(Self.encodeCachedHeader(self.dashboardCookieHeader))",
].joined(separator: "; ")
}

var apiCookieNames: [String] {
Self.cookieNames(from: self.apiCookieHeader)
}

var dashboardCookieNames: [String] {
Self.cookieNames(from: self.dashboardCookieHeader)
}

func hasCookie(named name: String) -> Bool {
Self.cookieNames(from: self.apiCookieHeader).contains(name) ||
Self.cookieNames(from: self.dashboardCookieHeader).contains(name)
}

private static func cookieNames(from header: String) -> [String] {
CookieHeaderNormalizer.pairs(from: header)
.map(\.name)
.filter { !$0.isEmpty }
.uniquedSorted()
}

private static func encodeCachedHeader(_ header: String) -> String {
Data(header.utf8).base64EncodedString()
}

private static func decodeCachedHeader(_ encoded: String) -> String? {
guard let data = Data(base64Encoded: encoded) else { return nil }
return String(data: data, encoding: .utf8)
}
}

enum AlibabaTokenPlanCookieHeader {
static func headers(
from cookies: [HTTPCookie],
environment: [String: String] = ProcessInfo.processInfo.environment) -> AlibabaTokenPlanCookieHeaders?
{
guard let apiHeader = self.header(
from: cookies,
targetURL: AlibabaTokenPlanUsageFetcher.resolveQuotaURL(environment: environment)),
let dashboardHeader = self.header(
from: cookies,
targetURL: AlibabaTokenPlanUsageFetcher.dashboardURL(environment: environment))
else {
return nil
}
return AlibabaTokenPlanCookieHeaders(apiCookieHeader: apiHeader, dashboardCookieHeader: dashboardHeader)
}

static func header(from cookies: [HTTPCookie], targetURL: URL) -> String? {
var byName: [String: HTTPCookie] = [:]
for cookie in cookies {
guard !cookie.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { continue }
guard !cookie.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { continue }
if let expiry = cookie.expiresDate, expiry < Date() { continue }
guard self.matchesRequestURL(cookie: cookie, url: targetURL) else { continue }

if let existing = byName[cookie.name] {
if self.cookieSortKey(for: cookie) >= self.cookieSortKey(for: existing) {
byName[cookie.name] = cookie
}
} else {
byName[cookie.name] = cookie
}
}

guard !byName.isEmpty else { return nil }
return byName.keys.sorted().compactMap { name in
guard let cookie = byName[name] else { return nil }
return "\(cookie.name)=\(cookie.value)"
}.joined(separator: "; ")
}

private static func matchesRequestURL(cookie: HTTPCookie, url: URL) -> Bool {
guard let host = url.host?.lowercased() else { return false }
let normalizedDomain = cookie.domain.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: "."))
guard !normalizedDomain.isEmpty else { return false }
guard host == normalizedDomain || host.hasSuffix(".\(normalizedDomain)") else { return false }

let cookiePath = cookie.path.isEmpty ? "/" : cookie.path
let requestPath = url.path.isEmpty ? "/" : url.path
if requestPath == cookiePath {
return true
}
guard requestPath.hasPrefix(cookiePath) else { return false }
guard cookiePath != "/" else { return true }
if cookiePath.hasSuffix("/") {
return true
}
guard
let boundaryIndex = requestPath.index(
requestPath.startIndex,
offsetBy: cookiePath.count,
limitedBy: requestPath.endIndex),
boundaryIndex < requestPath.endIndex
else {
return true
}
return requestPath[boundaryIndex] == "/"
}

private static func cookieSortKey(for cookie: HTTPCookie) -> (Int, Int, Date) {
let pathLength = cookie.path.count
let normalizedDomain = cookie.domain.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: "."))
let domainLength = normalizedDomain.count
let expiry = cookie.expiresDate ?? .distantPast
return (pathLength, domainLength, expiry)
}
}

extension [String] {
fileprivate func uniquedSorted() -> [String] {
Array(Set(self)).sorted()
}
}
Loading