mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-19 18:23:41 +00:00
fix(commandcode): show the monthly window (#1734)
Command Code's credits endpoint only reports monthly credits left, so the monthly window was dropped. For an active subscription on a known plan the plan price now comes from a small table (the API has no monthly cap field): pool = max(plan, left), used = pool - left, reset = currentPeriodEnd. Unknown plans and purchased/free credits stay out. CLI and menubar mirrored.
This commit is contained in:
parent
48773459ab
commit
4c779ef301
4 changed files with 133 additions and 23 deletions
|
|
@ -4,12 +4,23 @@ import Foundation
|
|||
/// Command Code CLI keeps in ~/.commandcode/auth.json (read-only, never logged).
|
||||
/// GET /alpha/billing/credits carries the 5-hour and weekly windows as USD
|
||||
/// credits used of a cap (resetAt epoch ms, 0 while no window is open) plus
|
||||
/// the remaining credits; GET /alpha/billing/subscriptions carries the plan id.
|
||||
/// Mirrors src/quota/commandcode.ts.
|
||||
/// the remaining credits; GET /alpha/billing/subscriptions carries the plan id,
|
||||
/// status and period end. Mirrors src/quota/commandcode.ts.
|
||||
enum CommandCodeSubscriptionService {
|
||||
static let creditsURL = URL(string: "https://api.commandcode.ai/alpha/billing/credits")!
|
||||
static let subscriptionsURL = URL(string: "https://api.commandcode.ai/alpha/billing/subscriptions")!
|
||||
private static let timeoutSeconds: TimeInterval = 15
|
||||
// Command Code's API gives no monthly cap, so the plan price comes from this table.
|
||||
static let planMonthlyUSD: [String: Double] = [
|
||||
"individual-go": 10, "individual-go-v1": 10, "individual-goat": 70, "individual-pro": 30, "individual-pro-v1": 80,
|
||||
"individual-provider": 15, "individual-max": 150, "individual-ultra": 300, "teams-pro": 40,
|
||||
]
|
||||
|
||||
struct Subscription: Equatable, Sendable {
|
||||
var planID: String?
|
||||
var status: String?
|
||||
var currentPeriodEnd: String?
|
||||
}
|
||||
|
||||
enum FetchError: Error, Equatable, LocalizedError, Sendable {
|
||||
case noCredentials
|
||||
|
|
@ -117,29 +128,34 @@ enum CommandCodeSubscriptionService {
|
|||
default: throw FetchError.parseFailure
|
||||
}
|
||||
|
||||
var planID: String?
|
||||
var plan = Subscription()
|
||||
if let (body, http) = await subscription, http.statusCode == 200,
|
||||
let root = try? JSONSerialization.jsonObject(with: body) as? [String: Any] {
|
||||
planID = (root["data"] as? [String: Any])?["planId"] as? String
|
||||
let root = try? JSONSerialization.jsonObject(with: body) as? [String: Any],
|
||||
let row = root["data"] as? [String: Any] {
|
||||
plan = Subscription(
|
||||
planID: row["planId"] as? String,
|
||||
status: row["status"] as? String,
|
||||
currentPeriodEnd: row["currentPeriodEnd"] as? String
|
||||
)
|
||||
}
|
||||
return try decode(data, planID: planID)
|
||||
return try decode(data, subscription: plan)
|
||||
}
|
||||
|
||||
static func decode(_ data: Data, planID: String? = nil) throws -> QuotaSummary {
|
||||
static func decode(_ data: Data, subscription: Subscription = Subscription()) throws -> QuotaSummary {
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw FetchError.parseFailure
|
||||
}
|
||||
let limits = root["windowLimits"] as? [String: Any] ?? [:]
|
||||
let fiveHour = window("5-hour", limits["fiveHour"])
|
||||
let weekly = window("Weekly", limits["weekly"])
|
||||
let details = [fiveHour, weekly].compactMap { $0 }
|
||||
guard !details.isEmpty else { throw FetchError.parseFailure }
|
||||
guard fiveHour != nil || weekly != nil else { throw FetchError.parseFailure }
|
||||
let details = [fiveHour, weekly, monthlyWindow(root["credits"], subscription)].compactMap { $0 }
|
||||
return QuotaSummary(
|
||||
providerFilter: .all,
|
||||
connection: .connected,
|
||||
primary: weekly ?? fiveHour,
|
||||
details: details,
|
||||
planLabel: planLabel(planID),
|
||||
planLabel: planLabel(subscription.planID),
|
||||
footerLines: creditsLine(root["credits"]).map { [$0] } ?? []
|
||||
)
|
||||
}
|
||||
|
|
@ -164,6 +180,27 @@ enum CommandCodeSubscriptionService {
|
|||
)
|
||||
}
|
||||
|
||||
/// Monthly credits used of the plan price; purchased and free credits are not part of the plan.
|
||||
private static func monthlyWindow(_ credits: Any?, _ subscription: Subscription) -> QuotaSummary.Window? {
|
||||
guard subscription.status == "active",
|
||||
let planID = subscription.planID, let plan = planMonthlyUSD[planID],
|
||||
let left = number((credits as? [String: Any])?["monthlyCredits"]) else { return nil }
|
||||
let pool = max(plan, left)
|
||||
return QuotaSummary.Window(
|
||||
label: "Monthly",
|
||||
percent: min(1, max(0, (pool - left) / pool)),
|
||||
resetsAt: subscription.currentPeriodEnd.flatMap(parseDate)
|
||||
)
|
||||
}
|
||||
|
||||
private static func parseDate(_ raw: String) -> Date? {
|
||||
let iso = ISO8601DateFormatter()
|
||||
iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
if let date = iso.date(from: raw) { return date }
|
||||
iso.formatOptions = [.withInternetDateTime]
|
||||
return iso.date(from: raw)
|
||||
}
|
||||
|
||||
private static func creditsLine(_ raw: Any?) -> String? {
|
||||
guard let credits = raw as? [String: Any] else { return nil }
|
||||
let parts = [("monthlyCredits", "monthly"), ("purchasedCredits", "purchased"), ("freeCredits", "free")]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct CommandCodeQuotaTests {
|
|||
"fiveHour":{"used":0.75,"cap":3,"exceeded":false,"resetAt":0},
|
||||
"weekly":{"used":6.0293354601,"cap":6,"exceeded":true,"resetAt":1791653049551}}}
|
||||
"""
|
||||
nonisolated private static let subscriptionBody = #"{"success":true,"data":{"status":"active","planId":"individual-go-v1"}}"#
|
||||
nonisolated private static let subscriptionBody = #"{"success":true,"data":{"status":"active","planId":"individual-go-v1","currentPeriodEnd":"2026-11-01T10:38:29.000Z"}}"#
|
||||
|
||||
nonisolated private static func response(_ request: URLRequest, _ status: Int) -> HTTPURLResponse {
|
||||
HTTPURLResponse(url: request.url!, statusCode: status, httpVersion: nil, headerFields: nil)!
|
||||
|
|
@ -43,10 +43,13 @@ struct CommandCodeQuotaTests {
|
|||
let body = request.url == CommandCodeSubscriptionService.creditsURL ? Self.creditsBody : Self.subscriptionBody
|
||||
return (Data(body.utf8), Self.response(request, 200))
|
||||
})
|
||||
#expect(summary.details.map(\.label) == ["5-hour", "Weekly"])
|
||||
#expect(summary.details.map(\.percent) == [0.25, 1])
|
||||
#expect(summary.details.map(\.label) == ["5-hour", "Weekly", "Monthly"])
|
||||
#expect(summary.details[0].percent == 0.25)
|
||||
#expect(summary.details[1].percent == 1)
|
||||
#expect(abs(summary.details[2].percent - 0.6029) < 0.0001)
|
||||
#expect(summary.details[0].resetsAt == nil)
|
||||
#expect(summary.details[1].resetsAt == Date(timeIntervalSince1970: 1_791_653_049.551))
|
||||
#expect(summary.details[2].resetsAt == Date(timeIntervalSince1970: 1_793_529_509))
|
||||
#expect(summary.primary?.label == "Weekly")
|
||||
#expect(summary.planLabel == "Go")
|
||||
#expect(summary.footerLines == ["Credits left: $3.97 monthly"])
|
||||
|
|
@ -57,6 +60,23 @@ struct CommandCodeQuotaTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test("the monthly window needs an active subscription on a known plan")
|
||||
func monthlyNeedsKnownActivePlan() throws {
|
||||
let body = Data(Self.creditsBody.utf8)
|
||||
let known = CommandCodeSubscriptionService.Subscription(
|
||||
planID: "individual-go-v1", status: "active", currentPeriodEnd: "2026-11-01T10:38:29Z")
|
||||
let go = try CommandCodeSubscriptionService.decode(body, subscription: known)
|
||||
#expect(go.details.last?.label == "Monthly")
|
||||
#expect(go.details.last?.resetsAt == Date(timeIntervalSince1970: 1_793_529_509))
|
||||
#expect(go.primary?.label == "Weekly")
|
||||
var unknown = known
|
||||
unknown.planID = "individual-mystery"
|
||||
#expect(try CommandCodeSubscriptionService.decode(body, subscription: unknown).details.map(\.label) == ["5-hour", "Weekly"])
|
||||
var inactive = known
|
||||
inactive.status = "canceled"
|
||||
#expect(try CommandCodeSubscriptionService.decode(body, subscription: inactive).details.map(\.label) == ["5-hour", "Weekly"])
|
||||
}
|
||||
|
||||
@Test("a failed subscription call only drops the plan label")
|
||||
func subscriptionFailureKeepsWindows() async throws {
|
||||
let summary = try await CommandCodeSubscriptionService.refresh(deps: Self.deps { request in
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
// the window's cap; resetAt is epoch ms, 0 while no window is open.
|
||||
// credits.{monthlyCredits,purchasedCredits,freeCredits}: USD remaining.
|
||||
// - GET https://api.commandcode.ai/alpha/billing/subscriptions
|
||||
// data.planId for the plan label; optional, a failure only drops the label.
|
||||
// data.{planId,status,currentPeriodEnd} for the plan label and the monthly
|
||||
// window; optional, a failure only drops both.
|
||||
//
|
||||
// Credential: the CLI's own ~/.commandcode/auth.json `apiKey`, read-only.
|
||||
// The API refuses requests without a User-Agent, so one is always sent.
|
||||
|
|
@ -20,6 +21,11 @@ const EXPIRED_FOOTER = ['Command Code session expired. Sign in with the Command
|
|||
const RATE_LIMITED_FOOTER = ['Command Code rate-limited the quota request.']
|
||||
const UNAVAILABLE_FOOTER = ['Command Code is temporarily unavailable.']
|
||||
const PARSE_FOOTER = ['Command Code quota response was malformed.']
|
||||
// Command Code's API gives no monthly cap, so the plan price comes from this table.
|
||||
const PLAN_MONTHLY_USD: Record<string, number> = {
|
||||
'individual-go': 10, 'individual-go-v1': 10, 'individual-goat': 70, 'individual-pro': 30, 'individual-pro-v1': 80,
|
||||
'individual-provider': 15, 'individual-max': 150, 'individual-ultra': 300, 'teams-pro': 40,
|
||||
}
|
||||
|
||||
export type CommandCodeDeps = {
|
||||
fetch: typeof fetch
|
||||
|
|
@ -73,8 +79,25 @@ function creditsNote(raw: unknown): string | null {
|
|||
return `Credits left: ${shown.map(part => `$${part.value.toFixed(2)} ${part.name}`).join(', ')}`
|
||||
}
|
||||
|
||||
/** `null` when neither window is readable. */
|
||||
export function decodeCommandCodeCredits(body: unknown, planId?: unknown): QuotaProvider | null {
|
||||
export type CommandCodeSubscription = { planId?: unknown; status?: unknown; currentPeriodEnd?: unknown }
|
||||
|
||||
/** Monthly credits used of the plan price; purchased and free credits are not part of the plan. */
|
||||
function monthlyWindow(credits: unknown, subscription: CommandCodeSubscription): QuotaWindow | null {
|
||||
if (subscription.status !== 'active' || typeof subscription.planId !== 'string') return null
|
||||
const plan = Object.hasOwn(PLAN_MONTHLY_USD, subscription.planId) ? PLAN_MONTHLY_USD[subscription.planId]! : null
|
||||
const left = credits && typeof credits === 'object' ? num((credits as Record<string, unknown>)['monthlyCredits']) : null
|
||||
if (plan === null || left === null) return null
|
||||
const pool = Math.max(plan, left)
|
||||
const end = typeof subscription.currentPeriodEnd === 'string' ? Date.parse(subscription.currentPeriodEnd) : NaN
|
||||
return {
|
||||
label: 'Monthly',
|
||||
percent: fraction((pool - left) / pool * 100)!,
|
||||
resetsAt: Number.isFinite(end) ? new Date(end).toISOString() : null,
|
||||
}
|
||||
}
|
||||
|
||||
/** `null` when neither the 5-hour nor the weekly window is readable. */
|
||||
export function decodeCommandCodeCredits(body: unknown, subscription: CommandCodeSubscription = {}): QuotaProvider | null {
|
||||
if (!body || typeof body !== 'object') return null
|
||||
const root = body as Record<string, unknown>
|
||||
const limits = root['windowLimits'] && typeof root['windowLimits'] === 'object'
|
||||
|
|
@ -82,14 +105,15 @@ export function decodeCommandCodeCredits(body: unknown, planId?: unknown): Quota
|
|||
: {}
|
||||
const fiveHour = windowOf('5-hour', limits['fiveHour'])
|
||||
const weekly = windowOf('Weekly', limits['weekly'])
|
||||
const details = [fiveHour, weekly].filter((row): row is QuotaWindow => row !== null)
|
||||
if (details.length === 0) return null
|
||||
if (!fiveHour && !weekly) return null
|
||||
const details = [fiveHour, weekly, monthlyWindow(root['credits'], subscription)]
|
||||
.filter((row): row is QuotaWindow => row !== null)
|
||||
const note = creditsNote(root['credits'])
|
||||
return {
|
||||
provider: 'commandcode', connection: 'connected',
|
||||
primary: weekly ?? fiveHour!,
|
||||
details,
|
||||
planLabel: commandCodePlanLabel(planId),
|
||||
planLabel: commandCodePlanLabel(subscription.planId),
|
||||
footerLines: note ? [note] : [],
|
||||
...(note ? { notes: [note] } : {}),
|
||||
}
|
||||
|
|
@ -128,10 +152,10 @@ export async function fetchCommandCodeQuota(options: Partial<CommandCodeDeps> &
|
|||
if (credits.status >= 500) return { quota: empty('transientFailure', UNAVAILABLE_FOOTER) }
|
||||
if (!credits.ok) return { quota: empty('transientFailure', PARSE_FOOTER) }
|
||||
// Never log either body - they carry account data.
|
||||
const planId = subscription?.ok
|
||||
? await subscription.json().then(body => (body as { data?: { planId?: unknown } })?.data?.planId, () => undefined)
|
||||
const plan = subscription?.ok
|
||||
? await subscription.json().then(body => (body as { data?: CommandCodeSubscription })?.data, () => undefined)
|
||||
: undefined
|
||||
const quota = decodeCommandCodeCredits(await credits.json(), planId)
|
||||
const quota = decodeCommandCodeCredits(await credits.json(), plan && typeof plan === 'object' ? plan : {})
|
||||
return { quota: quota ?? empty('transientFailure', PARSE_FOOTER) }
|
||||
} catch (error) {
|
||||
console.warn(`Command Code quota unavailable: ${sanitizeError(error)}`)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function jsonResponse(body: unknown, status = 200): Response {
|
|||
|
||||
describe('Command Code quota decoding', () => {
|
||||
it('maps the 5-hour and weekly windows as used of cap, clamped', () => {
|
||||
const quota = decodeCommandCodeCredits(creditsBody, 'individual-go-v1')
|
||||
const quota = decodeCommandCodeCredits(creditsBody, { planId: 'individual-go-v1' })
|
||||
expect(quota?.connection).toBe('connected')
|
||||
expect(quota?.details).toEqual([
|
||||
{ label: '5-hour', percent: 0.25, resetsAt: null },
|
||||
|
|
@ -36,6 +36,33 @@ describe('Command Code quota decoding', () => {
|
|||
expect(quota?.notes).toEqual(['Credits left: $3.97 monthly'])
|
||||
})
|
||||
|
||||
it('adds the monthly window from the plan price for an active known plan', () => {
|
||||
const body = { ...creditsBody, windowLimits: { ...creditsBody.windowLimits, fiveHour: { used: 0, cap: 3, exceeded: false, resetAt: 0 } } }
|
||||
const quota = decodeCommandCodeCredits(body, subscriptionBody.data)
|
||||
expect(quota?.details.map(row => [row.label, row.resetsAt])).toEqual([
|
||||
['5-hour', null],
|
||||
['Weekly', '2026-10-10T17:24:09.551Z'],
|
||||
['Monthly', new Date('2026-11-01T10:38:29.000Z').toISOString()],
|
||||
])
|
||||
expect(quota?.details[0]!.percent).toBe(0)
|
||||
expect(quota?.details[1]!.percent).toBe(1)
|
||||
expect(quota?.details[2]!.percent).toBeCloseTo(0.6029, 4)
|
||||
expect(quota?.primary?.label).toBe('Weekly')
|
||||
})
|
||||
|
||||
it('leaves out the monthly window for an unknown plan or an inactive subscription', () => {
|
||||
const labels = (subscription: object) => decodeCommandCodeCredits(creditsBody, subscription)?.details.map(row => row.label)
|
||||
expect(labels({ ...subscriptionBody.data, planId: 'individual-mystery' })).toEqual(['5-hour', 'Weekly'])
|
||||
expect(labels({ ...subscriptionBody.data, planId: 'toString' })).toEqual(['5-hour', 'Weekly'])
|
||||
expect(labels({ ...subscriptionBody.data, status: 'canceled' })).toEqual(['5-hour', 'Weekly'])
|
||||
expect(labels({})).toEqual(['5-hour', 'Weekly'])
|
||||
})
|
||||
|
||||
it('uses the larger of plan price and credits left as the pool', () => {
|
||||
const body = { ...creditsBody, credits: { monthlyCredits: 12 } }
|
||||
expect(decodeCommandCodeCredits(body, subscriptionBody.data)?.details[2]).toMatchObject({ label: 'Monthly', percent: 0 })
|
||||
})
|
||||
|
||||
it('skips an unreadable window and rejects a body with none', () => {
|
||||
const quota = decodeCommandCodeCredits({ windowLimits: { fiveHour: { used: 1, cap: 0 }, weekly: { used: 3, cap: 6, resetAt: 0 } } })
|
||||
expect(quota?.details).toEqual([{ label: 'Weekly', percent: 0.5, resetsAt: null }])
|
||||
|
|
@ -70,6 +97,7 @@ describe('Command Code quota fetch', () => {
|
|||
})
|
||||
expect(result.quota.connection).toBe('connected')
|
||||
expect(result.quota.planLabel).toBe('Go')
|
||||
expect(result.quota.details.map(row => row.label)).toEqual(['5-hour', 'Weekly', 'Monthly'])
|
||||
expect(seen.map(row => row.url).sort()).toEqual([
|
||||
'https://api.commandcode.ai/alpha/billing/credits',
|
||||
'https://api.commandcode.ai/alpha/billing/subscriptions',
|
||||
|
|
@ -87,6 +115,7 @@ describe('Command Code quota fetch', () => {
|
|||
})
|
||||
expect(result.quota.connection).toBe('connected')
|
||||
expect(result.quota.planLabel).toBeNull()
|
||||
expect(result.quota.details.map(row => row.label)).toEqual(['5-hour', 'Weekly'])
|
||||
})
|
||||
|
||||
it('maps HTTP failures', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue