diff --git a/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md b/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md index bce0c2f..8311f95 100644 --- a/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md +++ b/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md @@ -4,7 +4,7 @@ **Goal:** Build and verify the pure Command Code payload parser that Phase 2 will connect to the live provider. -**Architecture:** Add a provider-local pure parser with no network or cache responsibilities. Direct unit tests define rolling-window, monthly-credit, timestamp, balance, and plan-name behavior while the existing live adapter remains unchanged. +**Architecture:** Add one provider-local pure parser with no network or cache responsibilities. It emits only reliable 5-hour and weekly percentages/reset timing, preserves existing balances and plan names, and deliberately does not synthesize a monthly window from unrelated API values. The existing live adapter remains unchanged in this phase. **Tech Stack:** TypeScript 6, Node.js `>=24.15.0`, native Fetch API, Vitest 4, pnpm 11, Biome. @@ -19,8 +19,10 @@ - Change only `pi-usage`; `/Users/lanh/Developer/pi-packages/pi` and `/Users/lanh/Developer/pi-packages/codexbar` are read-only references. - Keep Node.js support at `>=24.15.0`. - Add no dependencies, public exports, shared usage types, environment variables, local estimates, or static plan-price catalog. -- Preserve the existing `COMMAND_CODE_COOKIE_HEADER` configuration, provider cache/backoff behavior, request/token balances, and partial-success behavior. -- Purchased credits remain a balance and never increase the monthly included-credit limit. +- Preserve the existing monthly and purchased USD balances, request/token balances, and plan-name behavior. +- Do not synthesize a monthly window from `summary.totalCost` and `credits.monthlyCredits`. +- Rolling windows expose percentage and reset timing only; do not expose raw `used`, `limit`, or `unit` fields. +- Do not modify the live provider or registry in Phase 1. --- @@ -43,17 +45,12 @@ export interface CommandCodePayloads { subscription?: Record; } -export type ParsedCommandCodeUsage = Pick< - ProviderUsageSnapshot, - "windows" | "balances" | "planName" ->; - export function parseCommandCodeUsage( payloads: CommandCodePayloads, -): ParsedCommandCodeUsage; +): Pick; ``` -- [ ] **Step 1: Add failing tests for root-level rolling windows and monthly usage** +- [ ] **Step 1: Add the failing root-payload parser test** Add this import to `tests/provider-command-code.test.ts`: @@ -61,13 +58,13 @@ Add this import to `tests/provider-command-code.test.ts`: import { parseCommandCodeUsage } from "../src/providers/command-code/usage-parser.ts"; ``` -Add a parser-focused describe block before the existing provider describe block: +Add this block before the existing provider tests: ```ts describe("Command Code usage parser", () => { - it("parses rolling windows at the response root before monthly usage", () => { + it("parses root rolling windows and preserves balances without a monthly window", () => { const parsed = parseCommandCodeUsage({ - summary: { totalCost: 4 }, + summary: { totalCost: 4, totalCount: 42, totalTokens: 1_234 }, credits: { credits: { monthlyCredits: 6, purchasedCredits: 5 }, windowLimits: { @@ -75,44 +72,31 @@ describe("Command Code usage parser", () => { weekly: { cap: 15, used: 1.5, resetAt: 1_780_100_000_000 }, }, }, - subscription: { - data: { - planId: "individual-go", - currentPeriodEnd: "2026-06-01T00:00:00Z", - }, - }, + subscription: { data: { planId: "individual-go" } }, }); - expect(parsed.windows.map((window) => [window.key, window.label])).toEqual([ - ["fiveHour", "5h"], - ["weekly", "Weekly"], - ["monthly", "Monthly"], + expect(parsed.windows).toEqual([ + { + key: "fiveHour", + label: "5h", + usedPercent: 25, + resetAt: 1_780_000_000_000, + windowDurationMins: 300, + }, + { + key: "weekly", + label: "Weekly", + usedPercent: 10, + resetAt: 1_780_100_000_000, + windowDurationMins: 10_080, + }, + ]); + expect(parsed.balances).toEqual([ + { label: "Monthly remaining", remaining: 6, unit: "USD" }, + { label: "Purchased remaining", remaining: 5, unit: "USD" }, + { label: "Requests", remaining: 42, unit: "count" }, + { label: "Tokens", remaining: 1_234, unit: "tok" }, ]); - expect(parsed.windows[0]).toMatchObject({ - used: 0.75, - limit: 3, - usedPercent: 25, - unit: "USD", - resetAt: 1_780_000_000_000, - windowDurationMins: 300, - }); - expect(parsed.windows[1]).toMatchObject({ - used: 1.5, - limit: 15, - usedPercent: 10, - windowDurationMins: 10_080, - }); - expect(parsed.windows[2]).toMatchObject({ - used: 4, - limit: 10, - usedPercent: 40, - resetAt: Date.parse("2026-06-01T00:00:00Z"), - }); - expect(parsed.balances).toContainEqual({ - label: "Purchased remaining", - remaining: 5, - unit: "USD", - }); expect(parsed.planName).toBe("Go"); }); }); @@ -128,9 +112,9 @@ pnpm exec vitest run tests/provider-command-code.test.ts Expected: FAIL because `src/providers/command-code/usage-parser.ts` does not exist. -- [ ] **Step 3: Implement the parser contract and three-window mapping** +- [ ] **Step 3: Implement the parser** -Create `src/providers/command-code/usage-parser.ts` with these imports and helpers: +Create `src/providers/command-code/usage-parser.ts` with this complete implementation: ```ts import type { @@ -145,10 +129,14 @@ export interface CommandCodePayloads { subscription?: Record; } -export type ParsedCommandCodeUsage = Pick< - ProviderUsageSnapshot, - "windows" | "balances" | "planName" ->; +const PLAN_NAMES: Record = { + "individual-go": "Go", + "individual-goat": "GOAT", + "individual-pro": "Pro", + "individual-pro-v1": "Pro", + "individual-max": "Max", + "individual-ultra": "Ultra", +}; function asRecord(value: unknown): Record | undefined { return value && typeof value === "object" && !Array.isArray(value) @@ -156,23 +144,18 @@ function asRecord(value: unknown): Record | undefined { : undefined; } +function strictFinite(value: unknown): number | undefined { + return typeof value === "string" && !value.trim() ? undefined : toFinite(value); +} + function parseTimestamp(value: unknown): number | undefined { - const epoch = parseEpochMs(value); - if (epoch != null) return epoch; + const numeric = strictFinite(value); + if (numeric != null) return numeric > 0 ? parseEpochMs(numeric) : undefined; if (typeof value !== "string") return undefined; const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : undefined; } -const PLAN_NAMES: Record = { - "individual-go": "Go", - "individual-goat": "GOAT", - "individual-pro": "Pro", - "individual-pro-v1": "Pro", - "individual-max": "Max", - "individual-ultra": "Ultra", -}; - function parseRateWindow( value: unknown, key: string, @@ -180,28 +163,21 @@ function parseRateWindow( windowDurationMins: number, ): LiveUsageWindow | undefined { const record = asRecord(value); - const limit = toFinite(record?.cap); + const limit = strictFinite(record?.cap); if (limit == null || limit <= 0) return undefined; - const used = toFinite(record?.used) ?? 0; + const used = strictFinite(record?.used) ?? 0; return { key, label, - used, - limit, - unit: "USD", usedPercent: clampPercent((used / limit) * 100), resetAt: parseTimestamp(record?.resetAt), windowDurationMins, }; } -``` - -Implement `parseCommandCodeUsage()` with the exact data rules below: -```ts export function parseCommandCodeUsage( payloads: CommandCodePayloads, -): ParsedCommandCodeUsage { +): Pick { const credits = asRecord(payloads.credits?.credits); const windowLimits = asRecord(payloads.credits?.windowLimits) ?? asRecord(credits?.windowLimits); @@ -223,42 +199,9 @@ export function parseCommandCodeUsage( if (fiveHour) windows.push(fiveHour); if (weekly) windows.push(weekly); - const totalCost = toFinite(payloads.summary?.totalCost); - const monthlyCredits = toFinite(credits?.monthlyCredits); - const purchasedCredits = toFinite(credits?.purchasedCredits) ?? 0; - const monthlyResetAt = parseTimestamp(subscription?.currentPeriodEnd); - - if (totalCost != null && monthlyCredits != null) { - const limit = totalCost + monthlyCredits; - windows.push({ - key: "monthly", - label: "Monthly", - used: totalCost, - limit, - unit: "USD", - usedPercent: limit > 0 ? clampPercent((totalCost / limit) * 100) : 0, - resetAt: monthlyResetAt, - }); - } else if (totalCost != null) { - windows.push({ - key: "monthly", - label: "Monthly", - used: totalCost, - unit: "USD", - usedPercent: 0, - unavailableReason: "Remaining balance unavailable", - }); - } else if (monthlyCredits != null) { - windows.push({ - key: "monthly", - label: "Monthly", - unit: "USD", - usedPercent: 0, - unavailableReason: "Consumed cost unavailable", - }); - } - const balances: ProviderUsageSnapshot["balances"] = []; + const monthlyCredits = strictFinite(credits?.monthlyCredits); + const purchasedCredits = strictFinite(credits?.purchasedCredits) ?? 0; if (monthlyCredits != null) { balances.push({ label: "Monthly remaining", @@ -274,10 +217,10 @@ export function parseCommandCodeUsage( }); } - const totalCount = toFinite(payloads.summary?.totalCount); - const totalTokens = toFinite(payloads.summary?.totalTokens); - const totalTokensIn = toFinite(payloads.summary?.totalTokensIn); - const totalTokensOut = toFinite(payloads.summary?.totalTokensOut); + const totalCount = strictFinite(payloads.summary?.totalCount); + const totalTokens = strictFinite(payloads.summary?.totalTokens); + const totalTokensIn = strictFinite(payloads.summary?.totalTokensIn); + const totalTokensOut = strictFinite(payloads.summary?.totalTokensOut); if (totalCount != null) { balances.push({ label: "Requests", remaining: totalCount, unit: "count" }); } @@ -310,7 +253,7 @@ export function parseCommandCodeUsage( } ``` -- [ ] **Step 4: Run the focused parser test and confirm it passes** +- [ ] **Step 4: Run the focused test and confirm it passes** Run: @@ -320,7 +263,7 @@ pnpm exec vitest run tests/provider-command-code.test.ts Expected: PASS, including the new parser case and all existing provider cases. -- [ ] **Step 5: Add coercion, invalid-cap, and plan-name regression tests** +- [ ] **Step 5: Add nested payload, coercion, cap, token, and plan regressions** Add these tests inside `describe("Command Code usage parser", ...)`: @@ -340,70 +283,149 @@ it("parses nested string windows and supported reset formats", () => { }, }, }, - subscription: { data: { planId: "individual-goat" } }, }); - expect( - parsed.windows.slice(0, 2).map((window) => window.usedPercent), - ).toEqual([25, 20]); + expect(parsed.windows.map((window) => window.usedPercent)).toEqual([25, 20]); expect(parsed.windows[0].resetAt).toBe(1_780_200_000_000); expect(parsed.windows[1].resetAt).toBe(Date.parse("2026-06-01T00:00:00Z")); - expect(parsed.planName).toBe("GOAT"); + expect(parsed.balances).toContainEqual({ + label: "Monthly remaining", + remaining: 7.25, + unit: "USD", + }); }); +it.each(["0", 0, "-1", -1])( + "rejects non-positive numeric reset sentinel %j", + (resetAt) => { + const parsed = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 1, resetAt } }, + }, + }); + + expect(parsed.windows[0].resetAt).toBeUndefined(); + }, +); + it("omits invalid caps, defaults missing usage, and clamps overuse", () => { const parsed = parseCommandCodeUsage({ credits: { - credits: {}, windowLimits: { fiveHour: { cap: 3 }, weekly: { cap: 0, used: 2 }, }, }, }); - expect(parsed.windows).toHaveLength(1); - expect(parsed.windows[0]).toMatchObject({ - key: "fiveHour", - used: 0, - usedPercent: 0, - }); + expect(parsed.windows).toEqual([ + { + key: "fiveHour", + label: "5h", + usedPercent: 0, + resetAt: undefined, + windowDurationMins: 300, + }, + ]); const overused = parseCommandCodeUsage({ credits: { - credits: {}, windowLimits: { fiveHour: { cap: 3, used: 4 } }, }, }); expect(overused.windows[0].usedPercent).toBe(100); + + const fractional = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 3, used: 1 } }, + }, + }); + expect(fractional.windows[0].usedPercent).toBeCloseTo(100 / 3); + + const negative = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 3, used: -1 } }, + }, + }); + expect(negative.windows[0].usedPercent).toBe(0); }); -it("maps Pro v1 and preserves unknown plan IDs", () => { +it("uses combined tokens before separate input and output totals", () => { expect( parseCommandCodeUsage({ - subscription: { data: { planId: "individual-pro-v1" } }, - }).planName, - ).toBe("Pro"); + summary: { totalTokens: 30, totalTokensIn: 10, totalTokensOut: 20 }, + }).balances, + ).toEqual([{ label: "Tokens", remaining: 30, unit: "tok" }]); expect( parseCommandCodeUsage({ - subscription: { data: { planId: "team-future" } }, - }).planName, - ).toBe("team-future"); + summary: { totalTokensIn: 10, totalTokensOut: 20 }, + }).balances, + ).toEqual([ + { label: "Tokens in", remaining: 10, unit: "tok" }, + { label: "Tokens out", remaining: 20, unit: "tok" }, + ]); +}); + +it("ignores blank numeric fields and retains separate token totals", () => { + const parsed = parseCommandCodeUsage({ + summary: { + totalCount: " ", + totalTokens: "", + totalTokensIn: 10, + totalTokensOut: 20, + }, + credits: { + credits: { monthlyCredits: "", purchasedCredits: " " }, + }, + }); + + expect(parsed.balances).toEqual([ + { label: "Tokens in", remaining: 10, unit: "tok" }, + { label: "Tokens out", remaining: 20, unit: "tok" }, + ]); +}); + +it.each([ + ["individual-go", "Go"], + ["individual-goat", "GOAT"], + ["individual-pro", "Pro"], + ["individual-pro-v1", "Pro"], + ["individual-max", "Max"], + ["individual-ultra", "Ultra"], + ["team-future", "team-future"], +])("maps plan %s to %s", (planId, expected) => { + expect( + parseCommandCodeUsage({ subscription: { data: { planId } } }).planName, + ).toBe(expected); }); ``` -- [ ] **Step 6: Run the focused tests again** +- [ ] **Step 6: Run focused tests, type checking, and the full project check** Run: ```bash pnpm exec vitest run tests/provider-command-code.test.ts +pnpm typecheck +pnpm check +``` + +Expected: all commands exit 0 on Node.js `>=24.15.0`. The full check runs Biome lint, TypeScript compilation, and every Vitest suite. + +- [ ] **Step 7: Review the Phase 1 diff** + +Run: + +```bash +git diff --check +git status --short +git diff -- src/providers/command-code/usage-parser.ts tests/provider-command-code.test.ts ``` -Expected: PASS with all parser and existing integration cases green. +Expected: `git diff --check` exits 0; Phase 1 changes only the parser and Command Code test file, in addition to the already-updated planning documents. -- [ ] **Step 7: Commit the pure parser** +- [ ] **Step 8: Commit the pure parser** ```bash -git add src/providers/command-code/usage-parser.ts tests/provider-command-code.test.ts +git add src/providers/command-code/usage-parser.ts tests/provider-command-code.test.ts docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md git commit -m "feat(command-code): parse rolling usage windows" ``` diff --git a/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md b/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md index 34b1217..0decad5 100644 --- a/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md +++ b/docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Connect the Phase 1 parser to a split Command Code API client and provider adapter, delivering live 5-hour, weekly, and monthly usage windows. +**Goal:** Connect the Phase 1 parser to a split Command Code API client and provider adapter, delivering live 5-hour and weekly windows with existing balance data. **Architecture:** Move cookie and HTTP concerns into a provider-local API client, then replace the monolithic provider with a thin directory entrypoint that composes the client, parser, and existing live cache runtime. Preserve partial endpoint success and the current rate-limit, credential, and cache behavior. @@ -12,7 +12,7 @@ **Parent Plan:** `docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md` -**Prerequisite:** Complete `docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md`; `parseCommandCodeUsage()` and its exported payload/result types must exist with Phase 1 tests passing. +**Prerequisite:** Complete `docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md`; `parseCommandCodeUsage()` and its exported payload type must exist with Phase 1 tests passing. **Atomic Result:** The provider registry uses the split Command Code adapter, live snapshots expose all available quota windows, partial failures retain usable data, and the complete project check passes. @@ -22,7 +22,8 @@ - Keep Node.js support at `>=24.15.0`. - Add no dependencies, public exports, shared usage types, environment variables, local estimates, or static plan-price catalog. - Preserve the existing `COMMAND_CODE_COOKIE_HEADER` configuration, provider cache/backoff behavior, request/token balances, and partial-success behavior. -- Purchased credits remain a balance and never increase the monthly included-credit limit. +- Preserve monthly and purchased credits as balances; do not synthesize a monthly usage window. +- Rolling windows expose percentages and reset timing, not currency-valued ratios. --- @@ -63,7 +64,7 @@ export function createCommandCodeProvider( ): UsageProviderAdapter; ``` -- [ ] **Step 1: Update the integration test to require all three windows** +- [ ] **Step 1: Update the integration test to require both rolling windows and balances** In the existing `uses cookie auth and parses aggregate usage` test, return rolling limits with the credits payload and make purchased credits nonzero: @@ -88,9 +89,12 @@ Replace the old current-cycle assertions with: expect(snapshot.windows.map((window) => [window.key, window.label])).toEqual([ ["fiveHour", "5h"], ["weekly", "Weekly"], - ["monthly", "Monthly"], ]); -expect(snapshot.windows[2].limit).toBeCloseTo(10); +expect(snapshot.balances).toContainEqual({ + label: "Monthly remaining", + remaining: 5.7112, + unit: "USD", +}); expect(snapshot.balances).toContainEqual({ label: "Purchased remaining", remaining: 5, @@ -108,7 +112,7 @@ Run: pnpm exec vitest run tests/provider-command-code.test.ts ``` -Expected: FAIL because the old adapter emits only `current-cycle` and includes purchased credits in that limit. +Expected: FAIL because the old adapter emits only `current-cycle` and does not emit the rolling windows. - [ ] **Step 3: Move cookie and HTTP behavior into the API client** @@ -330,7 +334,7 @@ Run: pnpm exec vitest run tests/provider-command-code.test.ts ``` -Expected: PASS. The integration snapshot contains `fiveHour`, `weekly`, and `monthly`; its monthly limit remains approximately 10 despite five purchased credits. +Expected: PASS. The integration snapshot contains only `fiveHour` and `weekly` windows, plus separate monthly and purchased balances. - [ ] **Step 6: Add a partial-failure integration regression** @@ -366,11 +370,12 @@ it("keeps rolling limits when summary and subscription fail", async () => { expect(snapshot.windows.map((window) => window.key)).toEqual([ "fiveHour", "weekly", - "monthly", ]); - expect(snapshot.windows[2].unavailableReason).toBe( - "Consumed cost unavailable", - ); + expect(snapshot.balances).toContainEqual({ + label: "Monthly remaining", + remaining: 6, + unit: "USD", + }); expect(snapshot.diagnostics).toEqual( expect.arrayContaining([ "Summary endpoint unavailable.", @@ -408,6 +413,6 @@ Expected: `git diff --check` exits 0; only the planned provider, registry, test, - [ ] **Step 9: Commit the provider split and integration** ```bash -git add src/providers/command-code.ts src/providers/command-code src/providers/index.ts tests/provider-command-code.test.ts docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md +git add src/providers/command-code.ts src/providers/command-code src/providers/index.ts tests/provider-command-code.test.ts git commit -m "refactor(command-code): expose usage limit windows" ``` diff --git a/docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md b/docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md index f18afd8..31660db 100644 --- a/docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md +++ b/docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md @@ -2,9 +2,9 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Refactor the Command Code provider into three focused modules and publish its 5-hour, weekly, and monthly limits through the existing usage dashboard. +**Goal:** Refactor the Command Code provider into three focused modules and publish its reliable 5-hour and weekly limits with existing balance data. -**Architecture:** A pure parser converts Command Code JSON payloads into the existing window/balance model. A provider-specific API client owns authentication and endpoint behavior, while a thin directory entrypoint connects both pieces to the existing live cache runtime. +**Architecture:** A pure parser converts Command Code JSON payloads into rolling windows, balances, and a plan name. A provider-local API client owns authentication and endpoint behavior, while a thin directory entrypoint connects both pieces to the existing live cache runtime. The phase plans below are authoritative and intentionally avoid duplicating their implementation steps here. **Tech Stack:** TypeScript 6, Node.js `>=24.15.0`, native Fetch API, Vitest 4, pnpm 11, Biome. @@ -15,779 +15,32 @@ - Change only `pi-usage`; `/Users/lanh/Developer/pi-packages/pi` and `/Users/lanh/Developer/pi-packages/codexbar` are read-only references. - Keep Node.js support at `>=24.15.0`. - Add no dependencies, public exports, shared usage types, environment variables, local estimates, or static plan-price catalog. -- Preserve the existing `COMMAND_CODE_COOKIE_HEADER` configuration, provider cache/backoff behavior, request/token balances, and partial-success behavior. -- Purchased credits remain a balance and never increase the monthly included-credit limit. +- Preserve the existing `COMMAND_CODE_COOKIE_HEADER` configuration, provider cache/backoff behavior, monthly and purchased balances, request/token balances, and partial-success behavior. +- Do not synthesize a monthly usage window from request-history cost and remaining credits. +- Rolling windows expose percentages and reset timing, not currency-valued ratios. --- -### Task 1: Build the Pure Command Code Usage Parser +### Phase 1: Build the Pure Usage Parser -**Files:** +**Plan:** `docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-1-usage-parser.md` -- Create: `src/providers/command-code/usage-parser.ts` -- Modify: `tests/provider-command-code.test.ts` +**Atomic Result:** A tested `parseCommandCodeUsage()` implementation exists. It emits reliable 5-hour and weekly windows, preserves balances and plan names, and leaves the live adapter unchanged. -**Interfaces:** +- [ ] Execute every Phase 1 checkbox in order. +- [ ] Confirm `pnpm exec vitest run tests/provider-command-code.test.ts` passes. +- [ ] Confirm the Phase 1 commit exists before starting Phase 2. -- Consumes: `LiveUsageWindow`, `ProviderUsageSnapshot`, `clampPercent()`, `parseEpochMs()`, and `toFinite()` from the existing shared/provider runtime. -- Produces: +### Phase 2: Split and Integrate the Provider -```ts -export interface CommandCodePayloads { - summary?: Record; - credits?: Record; - subscription?: Record; -} +**Plan:** `docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md` -export type ParsedCommandCodeUsage = Pick< - ProviderUsageSnapshot, - "windows" | "balances" | "planName" ->; +**Prerequisite:** Phase 1 is complete and its focused tests pass. -export function parseCommandCodeUsage( - payloads: CommandCodePayloads, -): ParsedCommandCodeUsage; -``` +**Atomic Result:** The registry uses the split provider, snapshots expose available rolling windows and balances, partial failures retain usable data, and the complete project check passes. -- [ ] **Step 1: Add failing tests for root-level rolling windows and monthly usage** - -Add this import to `tests/provider-command-code.test.ts`: - -```ts -import { parseCommandCodeUsage } from "../src/providers/command-code/usage-parser.ts"; -``` - -Add a parser-focused describe block before the existing provider describe block: - -```ts -describe("Command Code usage parser", () => { - it("parses rolling windows at the response root before monthly usage", () => { - const parsed = parseCommandCodeUsage({ - summary: { totalCost: 4 }, - credits: { - credits: { monthlyCredits: 6, purchasedCredits: 5 }, - windowLimits: { - fiveHour: { cap: 3, used: 0.75, resetAt: 1_780_000_000_000 }, - weekly: { cap: 15, used: 1.5, resetAt: 1_780_100_000_000 }, - }, - }, - subscription: { - data: { - planId: "individual-go", - currentPeriodEnd: "2026-06-01T00:00:00Z", - }, - }, - }); - - expect(parsed.windows.map((window) => [window.key, window.label])).toEqual([ - ["fiveHour", "5h"], - ["weekly", "Weekly"], - ["monthly", "Monthly"], - ]); - expect(parsed.windows[0]).toMatchObject({ - used: 0.75, - limit: 3, - usedPercent: 25, - unit: "USD", - resetAt: 1_780_000_000_000, - windowDurationMins: 300, - }); - expect(parsed.windows[1]).toMatchObject({ - used: 1.5, - limit: 15, - usedPercent: 10, - windowDurationMins: 10_080, - }); - expect(parsed.windows[2]).toMatchObject({ - used: 4, - limit: 10, - usedPercent: 40, - resetAt: Date.parse("2026-06-01T00:00:00Z"), - }); - expect(parsed.balances).toContainEqual({ - label: "Purchased remaining", - remaining: 5, - unit: "USD", - }); - expect(parsed.planName).toBe("Go"); - }); -}); -``` - -- [ ] **Step 2: Run the focused test and confirm the parser is missing** - -Run: - -```bash -pnpm exec vitest run tests/provider-command-code.test.ts -``` - -Expected: FAIL because `src/providers/command-code/usage-parser.ts` does not exist. - -- [ ] **Step 3: Implement the parser contract and three-window mapping** - -Create `src/providers/command-code/usage-parser.ts` with these imports and helpers: - -```ts -import type { - LiveUsageWindow, - ProviderUsageSnapshot, -} from "../../shared/types.ts"; -import { clampPercent, parseEpochMs, toFinite } from "../runtime.ts"; - -export interface CommandCodePayloads { - summary?: Record; - credits?: Record; - subscription?: Record; -} - -export type ParsedCommandCodeUsage = Pick< - ProviderUsageSnapshot, - "windows" | "balances" | "planName" ->; - -function asRecord(value: unknown): Record | undefined { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : undefined; -} - -function parseTimestamp(value: unknown): number | undefined { - const epoch = parseEpochMs(value); - if (epoch != null) return epoch; - if (typeof value !== "string") return undefined; - const parsed = Date.parse(value); - return Number.isFinite(parsed) ? parsed : undefined; -} - -const PLAN_NAMES: Record = { - "individual-go": "Go", - "individual-goat": "GOAT", - "individual-pro": "Pro", - "individual-pro-v1": "Pro", - "individual-max": "Max", - "individual-ultra": "Ultra", -}; - -function parseRateWindow( - value: unknown, - key: string, - label: string, - windowDurationMins: number, -): LiveUsageWindow | undefined { - const record = asRecord(value); - const limit = toFinite(record?.cap); - if (limit == null || limit <= 0) return undefined; - const used = toFinite(record?.used) ?? 0; - return { - key, - label, - used, - limit, - unit: "USD", - usedPercent: clampPercent((used / limit) * 100), - resetAt: parseTimestamp(record?.resetAt), - windowDurationMins, - }; -} -``` - -Implement `parseCommandCodeUsage()` with the exact data rules below: - -```ts -export function parseCommandCodeUsage( - payloads: CommandCodePayloads, -): ParsedCommandCodeUsage { - const credits = asRecord(payloads.credits?.credits); - const windowLimits = - asRecord(payloads.credits?.windowLimits) ?? asRecord(credits?.windowLimits); - const subscription = asRecord(payloads.subscription?.data); - - const windows: LiveUsageWindow[] = []; - const fiveHour = parseRateWindow( - windowLimits?.fiveHour, - "fiveHour", - "5h", - 5 * 60, - ); - const weekly = parseRateWindow( - windowLimits?.weekly, - "weekly", - "Weekly", - 7 * 24 * 60, - ); - if (fiveHour) windows.push(fiveHour); - if (weekly) windows.push(weekly); - - const totalCost = toFinite(payloads.summary?.totalCost); - const monthlyCredits = toFinite(credits?.monthlyCredits); - const purchasedCredits = toFinite(credits?.purchasedCredits) ?? 0; - const monthlyResetAt = parseTimestamp(subscription?.currentPeriodEnd); - - if (totalCost != null && monthlyCredits != null) { - const limit = totalCost + monthlyCredits; - windows.push({ - key: "monthly", - label: "Monthly", - used: totalCost, - limit, - unit: "USD", - usedPercent: limit > 0 ? clampPercent((totalCost / limit) * 100) : 0, - resetAt: monthlyResetAt, - }); - } else if (totalCost != null) { - windows.push({ - key: "monthly", - label: "Monthly", - used: totalCost, - unit: "USD", - usedPercent: 0, - unavailableReason: "Remaining balance unavailable", - }); - } else if (monthlyCredits != null) { - windows.push({ - key: "monthly", - label: "Monthly", - unit: "USD", - usedPercent: 0, - unavailableReason: "Consumed cost unavailable", - }); - } - - const balances: ProviderUsageSnapshot["balances"] = []; - if (monthlyCredits != null) { - balances.push({ - label: "Monthly remaining", - remaining: monthlyCredits, - unit: "USD", - }); - } - if (purchasedCredits > 0) { - balances.push({ - label: "Purchased remaining", - remaining: purchasedCredits, - unit: "USD", - }); - } - - const totalCount = toFinite(payloads.summary?.totalCount); - const totalTokens = toFinite(payloads.summary?.totalTokens); - const totalTokensIn = toFinite(payloads.summary?.totalTokensIn); - const totalTokensOut = toFinite(payloads.summary?.totalTokensOut); - if (totalCount != null) { - balances.push({ label: "Requests", remaining: totalCount, unit: "count" }); - } - if (totalTokens != null) { - balances.push({ label: "Tokens", remaining: totalTokens, unit: "tok" }); - } else { - if (totalTokensIn != null) { - balances.push({ - label: "Tokens in", - remaining: totalTokensIn, - unit: "tok", - }); - } - if (totalTokensOut != null) { - balances.push({ - label: "Tokens out", - remaining: totalTokensOut, - unit: "tok", - }); - } - } - - const planId = - typeof subscription?.planId === "string" ? subscription.planId : undefined; - return { - windows, - balances, - planName: planId ? (PLAN_NAMES[planId.toLowerCase()] ?? planId) : undefined, - }; -} -``` - -- [ ] **Step 4: Run the focused parser test and confirm it passes** - -Run: - -```bash -pnpm exec vitest run tests/provider-command-code.test.ts -``` - -Expected: PASS, including the new parser case and all existing provider cases. - -- [ ] **Step 5: Add coercion, invalid-cap, and plan-name regression tests** - -Add these tests inside `describe("Command Code usage parser", ...)`: - -```ts -it("parses nested string windows and supported reset formats", () => { - const parsed = parseCommandCodeUsage({ - credits: { - credits: { - monthlyCredits: "7.25", - windowLimits: { - fiveHour: { cap: "4", used: "1", resetAt: "1780200000" }, - weekly: { - cap: "20", - used: "4", - resetAt: "2026-06-01T00:00:00Z", - }, - }, - }, - }, - subscription: { data: { planId: "individual-goat" } }, - }); - - expect( - parsed.windows.slice(0, 2).map((window) => window.usedPercent), - ).toEqual([25, 20]); - expect(parsed.windows[0].resetAt).toBe(1_780_200_000_000); - expect(parsed.windows[1].resetAt).toBe(Date.parse("2026-06-01T00:00:00Z")); - expect(parsed.planName).toBe("GOAT"); -}); - -it("omits invalid caps, defaults missing usage, and clamps overuse", () => { - const parsed = parseCommandCodeUsage({ - credits: { - credits: {}, - windowLimits: { - fiveHour: { cap: 3 }, - weekly: { cap: 0, used: 2 }, - }, - }, - }); - expect(parsed.windows).toHaveLength(1); - expect(parsed.windows[0]).toMatchObject({ - key: "fiveHour", - used: 0, - usedPercent: 0, - }); - - const overused = parseCommandCodeUsage({ - credits: { - credits: {}, - windowLimits: { fiveHour: { cap: 3, used: 4 } }, - }, - }); - expect(overused.windows[0].usedPercent).toBe(100); -}); - -it("maps Pro v1 and preserves unknown plan IDs", () => { - expect( - parseCommandCodeUsage({ - subscription: { data: { planId: "individual-pro-v1" } }, - }).planName, - ).toBe("Pro"); - expect( - parseCommandCodeUsage({ - subscription: { data: { planId: "team-future" } }, - }).planName, - ).toBe("team-future"); -}); -``` - -- [ ] **Step 6: Run the focused tests again** - -Run: - -```bash -pnpm exec vitest run tests/provider-command-code.test.ts -``` - -Expected: PASS with all parser and existing integration cases green. - -- [ ] **Step 7: Commit the pure parser** - -```bash -git add src/providers/command-code/usage-parser.ts tests/provider-command-code.test.ts -git commit -m "feat(command-code): parse rolling usage windows" -``` - ---- - -### Task 2: Split the API Client and Wire the Provider Adapter - -**Files:** - -- Create: `src/providers/command-code/api-client.ts` -- Create: `src/providers/command-code/index.ts` -- Delete: `src/providers/command-code.ts` -- Modify: `src/providers/index.ts` -- Modify: `tests/provider-command-code.test.ts` - -**Interfaces:** - -- Consumes: `CommandCodePayloads` and `parseCommandCodeUsage()` from Task 1; existing `UsageDeps`, `fetchWithTimeout()`, `readJsonObject()`, `retryAfterMs()`, and `fetchWithLiveRuntime()`. -- Produces: - -```ts -export interface CommandCodeApiResult { - payloads: CommandCodePayloads; - diagnostics: string[]; - primaryResponses: Response[]; -} - -export function normalizeCommandCodeCookie( - raw: string | undefined, -): string | undefined; - -export function fetchCommandCodeApi( - deps: UsageDeps, - cookie: string, - signal?: AbortSignal, -): Promise; - -export function createCommandCodeProvider( - deps: UsageDeps, -): UsageProviderAdapter; -``` - -- [ ] **Step 1: Update the integration test to require all three windows** - -In the existing `uses cookie auth and parses aggregate usage` test, return rolling limits with the credits payload and make purchased credits nonzero: - -```ts -if (url.toString().includes("/billing/credits")) { - return new Response( - JSON.stringify({ - credits: { monthlyCredits: 5.7112, purchasedCredits: 5 }, - windowLimits: { - fiveHour: { cap: 3, used: 0.75, resetAt: 1_780_000_000_000 }, - weekly: { cap: 15, used: 1.5, resetAt: 1_780_100_000_000 }, - }, - }), - { status: 200 }, - ); -} -``` - -Replace the old current-cycle assertions with: - -```ts -expect(snapshot.windows.map((window) => [window.key, window.label])).toEqual([ - ["fiveHour", "5h"], - ["weekly", "Weekly"], - ["monthly", "Monthly"], -]); -expect(snapshot.windows[2].limit).toBeCloseTo(10); -expect(snapshot.balances).toContainEqual({ - label: "Purchased remaining", - remaining: 5, - unit: "USD", -}); -expect(snapshot.planName).toBe("Go"); -expect(fetchImpl).toHaveBeenCalledTimes(3); -``` - -- [ ] **Step 2: Run the focused test and verify the old adapter fails the new expectations** - -Run: - -```bash -pnpm exec vitest run tests/provider-command-code.test.ts -``` - -Expected: FAIL because the old adapter emits only `current-cycle` and includes purchased credits in that limit. - -- [ ] **Step 3: Move cookie and HTTP behavior into the API client** - -Create `src/providers/command-code/api-client.ts`. Move the existing cookie-name list and normalization behavior unchanged, exporting it as `normalizeCommandCodeCookie()`. Keep the existing headers and URLs, then implement the public result contract as follows: - -```ts -import type { UsageDeps } from "../../shared/deps.ts"; -import { fetchWithTimeout, readJsonObject } from "../runtime.ts"; -import type { CommandCodePayloads } from "./usage-parser.ts"; - -export interface CommandCodeApiResult { - payloads: CommandCodePayloads; - diagnostics: string[]; - primaryResponses: Response[]; -} - -export function normalizeCommandCodeCookie( - raw: string | undefined, -): string | undefined { - const input = raw?.trim(); - if (!input) return undefined; - const bare = input.replace(/^cookie\s*:\s*/i, "").trim(); - const cookieNames = [ - "__Secure-commandcode_prod_.session_token", - "__Host-better-auth.session_token", - "__Secure-better-auth.session_token", - "better-auth.session_token", - ]; - const parts = bare - .split(";") - .map((part) => part.trim()) - .filter(Boolean); - for (const name of cookieNames) { - const found = parts.find((part) => part.startsWith(`${name}=`)); - if (found?.slice(name.length + 1).trim()) return found; - } - if ( - parts.length === 1 && - !parts[0].includes("=") && - !/[\s,]/.test(parts[0]) - ) { - return `__Secure-commandcode_prod_.session_token=${parts[0]}`; - } - return undefined; -} - -export async function fetchCommandCodeApi( - deps: UsageDeps, - cookie: string, - signal?: AbortSignal, -): Promise { - const headers = { - Cookie: cookie, - Accept: "application/json, text/plain, */*", - "Accept-Language": "en-US,en;q=0.9", - "User-Agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/137 Safari/537.36", - Origin: "https://commandcode.ai", - Referer: "https://commandcode.ai/", - }; - const diagnostics: string[] = []; - const request = async (url: string, label: string) => { - try { - return await fetchWithTimeout(deps, url, { headers, signal }); - } catch { - diagnostics.push(`${label} endpoint unavailable.`); - return undefined; - } - }; - const [summaryResponse, creditsResponse, subscriptionResponse] = - await Promise.all([ - request("https://api.commandcode.ai/internal/usage/summary", "Summary"), - request("https://api.commandcode.ai/internal/billing/credits", "Credits"), - request( - "https://api.commandcode.ai/internal/billing/subscriptions", - "Subscription", - ), - ]); - - const readJson = async (response: Response | undefined, label: string) => { - if (!response) return undefined; - if (response.status === 401 || response.status === 403) { - diagnostics.push(`${label} rejected the Command Code session.`); - return undefined; - } - if (response.status === 429) { - diagnostics.push(`${label} endpoint rate limited.`); - return undefined; - } - if (!response.ok) { - diagnostics.push(`${label} endpoint unavailable.`); - return undefined; - } - const json = await readJsonObject(response); - if (!json) diagnostics.push(`${label} response shape unsupported.`); - return json; - }; - - const [summary, credits, subscription] = await Promise.all([ - readJson(summaryResponse, "Summary"), - readJson(creditsResponse, "Credits"), - readJson(subscriptionResponse, "Subscription"), - ]); - return { - payloads: { summary, credits, subscription }, - diagnostics, - primaryResponses: [summaryResponse, creditsResponse].filter( - (response): response is Response => Boolean(response), - ), - }; -} -``` - -- [ ] **Step 4: Replace the old provider file with the thin directory entrypoint** - -Create `src/providers/command-code/index.ts` using the client and parser contracts: - -```ts -import { PROVIDER_LABELS, PROVIDER_TTLS_MS } from "../../shared/constants.ts"; -import type { UsageDeps } from "../../shared/deps.ts"; -import type { UsageProviderAdapter } from "../../shared/types.ts"; -import { fetchWithLiveRuntime, retryAfterMs } from "../runtime.ts"; -import { - fetchCommandCodeApi, - normalizeCommandCodeCookie, -} from "./api-client.ts"; -import { parseCommandCodeUsage } from "./usage-parser.ts"; - -export function createCommandCodeProvider( - deps: UsageDeps, -): UsageProviderAdapter { - return { - id: "command-code", - label: PROVIDER_LABELS["command-code"], - strategy: "api", - fetch: (input) => - fetchWithLiveRuntime( - deps, - { - id: "command-code", - fetchLive: async ({ now, signal }) => { - const configuredCookie = deps.env.COMMAND_CODE_COOKIE_HEADER; - const cookie = normalizeCommandCodeCookie(configuredCookie); - if (!cookie) { - return { - kind: "credentials" as const, - message: configuredCookie?.trim() - ? "Malformed COMMAND_CODE_COOKIE_HEADER." - : "Missing COMMAND_CODE_COOKIE_HEADER.", - }; - } - - const api = await fetchCommandCodeApi(deps, cookie, signal); - const parsed = parseCommandCodeUsage(api.payloads); - if (parsed.windows.length === 0 && parsed.balances.length === 0) { - const rateLimited = api.primaryResponses.find( - (response) => response.status === 429, - ); - if (rateLimited) { - return { - kind: "rate-limited" as const, - message: "Rate limited.", - nextRetryAt: now + retryAfterMs(rateLimited.headers, now), - }; - } - if ( - api.primaryResponses.some( - (response) => - response.status === 401 || response.status === 403, - ) - ) { - return { - kind: "credentials" as const, - message: - "Command Code session expired. Update COMMAND_CODE_COOKIE_HEADER.", - }; - } - return { - kind: "error" as const, - message: api.diagnostics[0] ?? "Live source unavailable.", - }; - } - - return { - kind: "ok" as const, - snapshot: { - providerId: "command-code", - providerLabel: PROVIDER_LABELS["command-code"], - available: true, - diagnostic: "", - fetchedAt: now, - expiresAt: now + PROVIDER_TTLS_MS["command-code"], - status: "live", - sourceLabel: "Command Code web usage API", - sourceKind: "live", - diagnostics: api.diagnostics, - ...parsed, - }, - }; - }, - }, - input, - ), - }; -} -``` - -Delete `src/providers/command-code.ts`. In `src/providers/index.ts`, change only the import path: - -```ts -import { createCommandCodeProvider } from "./command-code/index.ts"; -``` - -- [ ] **Step 5: Run the focused provider tests** - -Run: - -```bash -pnpm exec vitest run tests/provider-command-code.test.ts -``` - -Expected: PASS. The integration snapshot contains `fiveHour`, `weekly`, and `monthly`; its monthly limit remains approximately 10 despite five purchased credits. - -- [ ] **Step 6: Add a partial-failure integration regression** - -Add this provider test to `tests/provider-command-code.test.ts`: - -```ts -it("keeps rolling limits when summary and subscription fail", async () => { - const root = mkTmp(); - const fetchImpl = vi.fn(async (url) => { - if (url.toString().includes("/billing/credits")) { - return new Response( - JSON.stringify({ - credits: { monthlyCredits: 6 }, - windowLimits: { - fiveHour: { cap: 3, used: 1, resetAt: 1_780_000_000_000 }, - weekly: { cap: 15, used: 2, resetAt: 1_780_100_000_000 }, - }, - }), - { status: 200 }, - ); - } - throw new Error("endpoint unavailable"); - }); - - const snapshot = ( - await commandCodeProvider( - createLiveDeps(root, () => 1_000, fetchImpl, { - COMMAND_CODE_COOKIE_HEADER: "abc", - }), - ).fetch() - ).snapshot; - expect(snapshot.status).toBe("live"); - expect(snapshot.windows.map((window) => window.key)).toEqual([ - "fiveHour", - "weekly", - "monthly", - ]); - expect(snapshot.windows[2].unavailableReason).toBe( - "Consumed cost unavailable", - ); - expect(snapshot.diagnostics).toEqual( - expect.arrayContaining([ - "Summary endpoint unavailable.", - "Subscription endpoint unavailable.", - ]), - ); - rmSync(root, { recursive: true, force: true }); -}); -``` - -- [ ] **Step 7: Run focused tests, type checking, and the full project check** - -Run: - -```bash -pnpm exec vitest run tests/provider-command-code.test.ts -pnpm typecheck -pnpm check -``` - -Expected: all commands exit 0. The full check runs Biome lint, TypeScript compilation, and every Vitest suite. - -- [ ] **Step 8: Review the final diff for scope and whitespace errors** - -Run: - -```bash -git diff --check -git status --short -git diff -- src/providers/command-code src/providers/index.ts tests/provider-command-code.test.ts -``` - -Expected: `git diff --check` exits 0; only the planned provider, registry, test, spec, and plan files are changed or added. - -- [ ] **Step 9: Commit the provider split and integration** - -```bash -git add src/providers/command-code.ts src/providers/command-code src/providers/index.ts tests/provider-command-code.test.ts docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md docs/superpowers/plans/2026-08-30-command-code-usage-refactor.md -git commit -m "refactor(command-code): expose usage limit windows" -``` +- [ ] Execute every Phase 2 checkbox in order. +- [ ] Confirm `pnpm exec vitest run tests/provider-command-code.test.ts` passes. +- [ ] Confirm `pnpm typecheck` passes. +- [ ] Confirm `pnpm check` passes on Node.js `>=24.15.0`. +- [ ] Confirm `git diff --check` exits 0 and the final diff stays within the planned provider, registry, tests, spec, and plan files. diff --git a/docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md b/docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md index 14cc03d..7e4b9d1 100644 --- a/docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md +++ b/docs/superpowers/specs/2026-08-30-command-code-usage-refactor-design.md @@ -2,13 +2,15 @@ ## Goal -Refactor the Command Code provider into focused modules and expose its current 5-hour, weekly, and monthly usage windows through the existing dashboard model. +Refactor the Command Code provider into focused modules and expose its reliable 5-hour and weekly usage windows alongside the existing credit and activity balances. ## Context - Command Code documents two rolling limits—5 hours and 7 days—on top of the included monthly credit pool. Purchased credits bypass those rolling limits. - CodexBar's captured production payloads show `windowLimits.fiveHour` and `windowLimits.weekly`, each containing `cap`, `used`, and `resetAt`. `windowLimits` may appear at the response root or inside `credits`. -- `pi-usage` already has the required `LiveUsageWindow` model, cache runtime, and dashboard rendering. Pi itself has no usage-limit integration to change. +- The rolling values are credit-value quotas, not a universally safe USD ratio. CodexBar therefore retains their percentage and reset timing without presenting a currency ratio. +- `summary.totalCost` is request-history cost, while `credits.monthlyCredits` is the remaining included grant. Purchased usage can increase the former without reducing the latter, so the two values cannot reliably reconstruct a monthly allowance. +- `pi-usage` already has the required `LiveUsageWindow` model, cache runtime, balance model, and dashboard rendering. Pi itself has no usage-limit integration to change. Reference: @@ -18,23 +20,22 @@ Replace the single `src/providers/command-code.ts` file with three modules: - `src/providers/command-code/index.ts` owns provider orchestration, cache-runtime integration, and final error classification. - `src/providers/command-code/api-client.ts` owns cookie normalization, request headers, concurrent endpoint fetching, response decoding, and endpoint diagnostics. -- `src/providers/command-code/usage-parser.ts` is a pure payload parser that returns windows, balances, and the display plan name. +- `src/providers/command-code/usage-parser.ts` is a pure payload parser that returns rolling windows, balances, and the display plan name. The provider registry imports the new directory entrypoint. No public exports, shared types, configuration keys, dependencies, or TUI code change. ## Usage Semantics -Emit windows in this order: +Emit reliable rolling windows in this order: 1. `fiveHour`, labeled `5h`, with a 300-minute duration. 2. `weekly`, labeled `Weekly`, with a 10,080-minute duration. -3. `monthly`, labeled `Monthly`, resetting at the subscription period end. -For rolling windows, require a positive `cap`, default missing `used` to zero, retain USD `used` and `limit`, and clamp the calculated percentage to 0–100 without discarding fractional precision. Accept reset timestamps expressed as epoch seconds, epoch milliseconds, numeric strings, or ISO strings. +For each rolling window, require a positive `cap`, default missing `used` to zero, and clamp the calculated percentage to 0–100 without discarding fractional precision. Accept reset timestamps expressed as epoch seconds, epoch milliseconds, numeric strings, or ISO strings. Expose only the window key, label, percentage, reset timestamp, and duration; do not label raw rolling values as USD or render a ratio. -Calculate the monthly window from existing API data: `used = summary.totalCost` and `limit = totalCost + credits.monthlyCredits`. Purchased credits remain a separate balance and never increase this monthly limit. If only one side of the calculation is available, retain the existing unavailable-reason window. +Do not synthesize a monthly usage window from `summary.totalCost` and `credits.monthlyCredits`. Preserve `monthlyCredits` and positive `purchasedCredits` as separate USD balances. Preserve existing request/token balances and their precedence: use `totalTokens` when available, otherwise retain the separate input/output totals. -Preserve existing request/token balances. Recognize `individual-go`, `individual-goat`, `individual-pro`, `individual-pro-v1`, `individual-max`, and `individual-ultra` display names; use an unknown plan ID verbatim. +Recognize `individual-go`, `individual-goat`, `individual-pro`, `individual-pro-v1`, `individual-max`, and `individual-ultra` display names; use an unknown plan ID verbatim. ## Failure Behavior @@ -44,6 +45,6 @@ Publish a live snapshot whenever parsing yields at least one window or balance, ## Verification -Extend the existing Command Code tests with pure parser coverage for both payload locations, value coercion, reset formats, invalid caps, percentage clamping, window ordering, monthly purchased-credit exclusion, and plan names. Preserve the cookie, subscription-enrichment, and partial-failure integration coverage. +Extend the existing Command Code tests with pure parser coverage for both payload locations, value coercion, reset formats, invalid caps, percentage clamping, window ordering, the absence of a synthesized monthly window, balance preservation, token precedence, and plan names. Preserve the cookie, subscription-enrichment, and partial-failure integration coverage. -Run the focused provider test and then the complete project check. Node.js remains `>=24.15.0`. +Run the focused provider test and then the complete project check on Node.js `>=24.15.0`. diff --git a/src/providers/command-code/usage-parser.ts b/src/providers/command-code/usage-parser.ts new file mode 100644 index 0000000..fb24e21 --- /dev/null +++ b/src/providers/command-code/usage-parser.ts @@ -0,0 +1,119 @@ +import type { LiveUsageWindow, ProviderUsageSnapshot } from "../../shared/types.ts"; +import { clampPercent, parseEpochMs, toFinite } from "../runtime.ts"; + +export interface CommandCodePayloads { + summary?: Record; + credits?: Record; + subscription?: Record; +} + +const PLAN_NAMES: Record = { + "individual-go": "Go", + "individual-goat": "GOAT", + "individual-pro": "Pro", + "individual-pro-v1": "Pro", + "individual-max": "Max", + "individual-ultra": "Ultra", +}; + +function asRecord(value: unknown): Record | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function strictFinite(value: unknown): number | undefined { + return typeof value === "string" && !value.trim() ? undefined : toFinite(value); +} + +function parseTimestamp(value: unknown): number | undefined { + const numeric = strictFinite(value); + if (numeric != null) return numeric > 0 ? parseEpochMs(numeric) : undefined; + if (typeof value !== "string") return undefined; + const parsed = Date.parse(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function parseRateWindow( + value: unknown, + key: string, + label: string, + windowDurationMins: number, +): LiveUsageWindow | undefined { + const record = asRecord(value); + const limit = strictFinite(record?.cap); + if (limit == null || limit <= 0) return undefined; + const used = strictFinite(record?.used) ?? 0; + return { + key, + label, + usedPercent: clampPercent((used / limit) * 100), + resetAt: parseTimestamp(record?.resetAt), + windowDurationMins, + }; +} + +export function parseCommandCodeUsage( + payloads: CommandCodePayloads, +): Pick { + const credits = asRecord(payloads.credits?.credits); + const windowLimits = asRecord(payloads.credits?.windowLimits) ?? asRecord(credits?.windowLimits); + const subscription = asRecord(payloads.subscription?.data); + + const windows: LiveUsageWindow[] = []; + const fiveHour = parseRateWindow(windowLimits?.fiveHour, "fiveHour", "5h", 5 * 60); + const weekly = parseRateWindow(windowLimits?.weekly, "weekly", "Weekly", 7 * 24 * 60); + if (fiveHour) windows.push(fiveHour); + if (weekly) windows.push(weekly); + + const balances: ProviderUsageSnapshot["balances"] = []; + const monthlyCredits = strictFinite(credits?.monthlyCredits); + const purchasedCredits = strictFinite(credits?.purchasedCredits) ?? 0; + if (monthlyCredits != null) { + balances.push({ + label: "Monthly remaining", + remaining: monthlyCredits, + unit: "USD", + }); + } + if (purchasedCredits > 0) { + balances.push({ + label: "Purchased remaining", + remaining: purchasedCredits, + unit: "USD", + }); + } + + const totalCount = strictFinite(payloads.summary?.totalCount); + const totalTokens = strictFinite(payloads.summary?.totalTokens); + const totalTokensIn = strictFinite(payloads.summary?.totalTokensIn); + const totalTokensOut = strictFinite(payloads.summary?.totalTokensOut); + if (totalCount != null) { + balances.push({ label: "Requests", remaining: totalCount, unit: "count" }); + } + if (totalTokens != null) { + balances.push({ label: "Tokens", remaining: totalTokens, unit: "tok" }); + } else { + if (totalTokensIn != null) { + balances.push({ + label: "Tokens in", + remaining: totalTokensIn, + unit: "tok", + }); + } + if (totalTokensOut != null) { + balances.push({ + label: "Tokens out", + remaining: totalTokensOut, + unit: "tok", + }); + } + } + + const planId = typeof subscription?.planId === "string" ? subscription.planId : undefined; + return { + windows, + balances, + planName: planId ? (PLAN_NAMES[planId.toLowerCase()] ?? planId) : undefined, + }; +} diff --git a/tests/provider-command-code.test.ts b/tests/provider-command-code.test.ts index bb9e3e8..364ce58 100644 --- a/tests/provider-command-code.test.ts +++ b/tests/provider-command-code.test.ts @@ -2,8 +2,9 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; -import { createDefaultDeps, type UsageDeps } from "../src/shared/deps.ts"; +import { parseCommandCodeUsage } from "../src/providers/command-code/usage-parser.ts"; import { createProviderRegistry } from "../src/providers/index.ts"; +import { createDefaultDeps, type UsageDeps } from "../src/shared/deps.ts"; function mkTmp(): string { return mkdtempSync(join(tmpdir(), "pi-usage-live-")); @@ -26,21 +27,182 @@ function createLiveDeps( } function commandCodeProvider(deps: UsageDeps) { - const provider = createProviderRegistry(deps).find( - (item) => item.id === "command-code", - ); + const provider = createProviderRegistry(deps).find((item) => item.id === "command-code"); if (!provider) throw new Error("missing command-code provider"); return provider; } +describe("Command Code usage parser", () => { + it("parses root rolling windows and preserves balances without a monthly window", () => { + const parsed = parseCommandCodeUsage({ + summary: { totalCost: 4, totalCount: 42, totalTokens: 1_234 }, + credits: { + credits: { monthlyCredits: 6, purchasedCredits: 5 }, + windowLimits: { + fiveHour: { cap: 3, used: 0.75, resetAt: 1_780_000_000_000 }, + weekly: { cap: 15, used: 1.5, resetAt: 1_780_100_000_000 }, + }, + }, + subscription: { data: { planId: "individual-go" } }, + }); + + expect(parsed.windows).toEqual([ + { + key: "fiveHour", + label: "5h", + usedPercent: 25, + resetAt: 1_780_000_000_000, + windowDurationMins: 300, + }, + { + key: "weekly", + label: "Weekly", + usedPercent: 10, + resetAt: 1_780_100_000_000, + windowDurationMins: 10_080, + }, + ]); + expect(parsed.balances).toEqual([ + { label: "Monthly remaining", remaining: 6, unit: "USD" }, + { label: "Purchased remaining", remaining: 5, unit: "USD" }, + { label: "Requests", remaining: 42, unit: "count" }, + { label: "Tokens", remaining: 1_234, unit: "tok" }, + ]); + expect(parsed.planName).toBe("Go"); + }); + + it("parses nested string windows and supported reset formats", () => { + const parsed = parseCommandCodeUsage({ + credits: { + credits: { + monthlyCredits: "7.25", + windowLimits: { + fiveHour: { cap: "4", used: "1", resetAt: "1780200000" }, + weekly: { + cap: "20", + used: "4", + resetAt: "2026-06-01T00:00:00Z", + }, + }, + }, + }, + }); + + expect(parsed.windows.map((window) => window.usedPercent)).toEqual([25, 20]); + expect(parsed.windows[0].resetAt).toBe(1_780_200_000_000); + expect(parsed.windows[1].resetAt).toBe(Date.parse("2026-06-01T00:00:00Z")); + expect(parsed.balances).toContainEqual({ + label: "Monthly remaining", + remaining: 7.25, + unit: "USD", + }); + }); + + it.each(["0", 0, "-1", -1])("rejects non-positive numeric reset sentinel %j", (resetAt) => { + const parsed = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 1, resetAt } }, + }, + }); + + expect(parsed.windows[0].resetAt).toBeUndefined(); + }); + + it("omits invalid caps, defaults missing usage, and clamps overuse", () => { + const parsed = parseCommandCodeUsage({ + credits: { + windowLimits: { + fiveHour: { cap: 3 }, + weekly: { cap: 0, used: 2 }, + }, + }, + }); + expect(parsed.windows).toEqual([ + { + key: "fiveHour", + label: "5h", + usedPercent: 0, + resetAt: undefined, + windowDurationMins: 300, + }, + ]); + + const overused = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 3, used: 4 } }, + }, + }); + expect(overused.windows[0].usedPercent).toBe(100); + + const fractional = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 3, used: 1 } }, + }, + }); + expect(fractional.windows[0].usedPercent).toBeCloseTo(100 / 3); + + const negative = parseCommandCodeUsage({ + credits: { + windowLimits: { fiveHour: { cap: 3, used: -1 } }, + }, + }); + expect(negative.windows[0].usedPercent).toBe(0); + }); + + it("uses combined tokens before separate input and output totals", () => { + expect( + parseCommandCodeUsage({ + summary: { totalTokens: 30, totalTokensIn: 10, totalTokensOut: 20 }, + }).balances, + ).toEqual([{ label: "Tokens", remaining: 30, unit: "tok" }]); + expect( + parseCommandCodeUsage({ + summary: { totalTokensIn: 10, totalTokensOut: 20 }, + }).balances, + ).toEqual([ + { label: "Tokens in", remaining: 10, unit: "tok" }, + { label: "Tokens out", remaining: 20, unit: "tok" }, + ]); + }); + + it("ignores blank numeric fields and retains separate token totals", () => { + const parsed = parseCommandCodeUsage({ + summary: { + totalCount: " ", + totalTokens: "", + totalTokensIn: 10, + totalTokensOut: 20, + }, + credits: { + credits: { monthlyCredits: "", purchasedCredits: " " }, + }, + }); + + expect(parsed.balances).toEqual([ + { label: "Tokens in", remaining: 10, unit: "tok" }, + { label: "Tokens out", remaining: 20, unit: "tok" }, + ]); + }); + + it.each([ + ["individual-go", "Go"], + ["individual-goat", "GOAT"], + ["individual-pro", "Pro"], + ["individual-pro-v1", "Pro"], + ["individual-max", "Max"], + ["individual-ultra", "Ultra"], + ["team-future", "team-future"], + ])("maps plan %s to %s", (planId, expected) => { + expect(parseCommandCodeUsage({ subscription: { data: { planId } } }).planName).toBe(expected); + }); +}); + describe("Command Code provider", () => { it("uses cookie auth and parses aggregate usage", async () => { const root = mkTmp(); const fetchImpl = vi.fn(async (url, init) => { const headers = new Headers(init?.headers); - expect(headers.get("cookie")).toBe( - "__Secure-commandcode_prod_.session_token=abc", - ); + expect(headers.get("cookie")).toBe("__Secure-commandcode_prod_.session_token=abc"); if (url.toString().includes("/usage/summary")) { return new Response( JSON.stringify({ @@ -89,15 +251,10 @@ describe("Command Code provider", () => { it("returns credential diagnostic when cookie is missing", async () => { const root = mkTmp(); - const snapshot = ( - await commandCodeProvider( - createLiveDeps(root, () => 1_000, vi.fn()), - ).fetch() - ).snapshot; + const snapshot = (await commandCodeProvider(createLiveDeps(root, () => 1_000, vi.fn())).fetch()) + .snapshot; expect(snapshot.available).toBe(false); - expect(snapshot.diagnostics.join(" ")).toContain( - "Missing COMMAND_CODE_COOKIE_HEADER", - ); + expect(snapshot.diagnostics.join(" ")).toContain("Missing COMMAND_CODE_COOKIE_HEADER"); rmSync(root, { recursive: true, force: true }); }); @@ -110,10 +267,7 @@ describe("Command Code provider", () => { }); } if (url.toString().includes("/billing/credits")) { - return new Response( - JSON.stringify({ credits: { monthlyCredits: 6 } }), - { status: 200 }, - ); + return new Response(JSON.stringify({ credits: { monthlyCredits: 6 } }), { status: 200 }); } throw new Error("subscription endpoint unavailable"); }); @@ -128,9 +282,7 @@ describe("Command Code provider", () => { expect(snapshot.status).toBe("live"); expect(snapshot.windows[0].limit).toBe(10); expect(snapshot.planName).toBeUndefined(); - expect(snapshot.diagnostics).toContain( - "Subscription endpoint unavailable.", - ); + expect(snapshot.diagnostics).toContain("Subscription endpoint unavailable."); rmSync(root, { recursive: true, force: true }); }); @@ -148,9 +300,7 @@ describe("Command Code provider", () => { }), ).fetch() ).snapshot; - expect(snapshot.diagnostics.join(" ")).toContain( - "Malformed COMMAND_CODE_COOKIE_HEADER", - ); + expect(snapshot.diagnostics.join(" ")).toContain("Malformed COMMAND_CODE_COOKIE_HEADER"); } rmSync(root, { recursive: true, force: true }); });