From 31e250346ac9f1375db2ce93926e5eed665ca3f3 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Sun, 30 Aug 2026 22:33:19 -0400 Subject: [PATCH 1/3] docs: simplify Command Code provider integration --- .../2026-08-30-command-code-usage-refactor-design.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 7e4b9d1..7245e3e 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 @@ -16,13 +16,12 @@ Reference: ## Architecture -Replace the single `src/providers/command-code.ts` file with three modules: +Keep the existing provider entrypoint and the Phase 1 parser as two focused 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.ts` owns cookie normalization, concurrent endpoint fetching, response diagnostics, cache-runtime integration, and final error classification. - `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. +Wire the parser into the existing provider instead of adding a single-consumer API-client module. The provider registry remains unchanged. Add the current `commandcode_prod_` host and non-secure cookie-name variants observed in CodexBar while preserving all existing accepted cookie forms. No public exports, shared types, configuration keys, dependencies, or TUI code change. ## Usage Semantics @@ -47,4 +46,4 @@ Publish a live snapshot whenever parsing yields at least one window or balance, 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 on Node.js `>=24.15.0`. +The provider integration must assert rolling windows, monthly and purchased balances, request and token balances, plan enrichment, supported cookie aliases, primary rate-limit handling, and expired-session handling. Run the focused provider test and then the complete project check on Node.js `>=24.15.0`; a pass on an unsupported Node.js version is not sufficient completion evidence. From 461c60df14254aee712ed67547037e61b1c74e5c Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Sun, 30 Aug 2026 22:37:54 -0400 Subject: [PATCH 2/3] docs: replan Command Code provider integration --- ...e-refactor-phase-2-provider-integration.md | 466 +++++++----------- .../2026-08-30-command-code-usage-refactor.md | 8 +- 2 files changed, 183 insertions(+), 291 deletions(-) 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 0decad5..926242c 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,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:** 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. +**Goal:** Wire the Phase 1 parser into the existing Command Code provider so live snapshots expose 5-hour and weekly windows with all existing balances. -**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. +**Architecture:** Keep `src/providers/command-code.ts` as the provider entrypoint and `src/providers/command-code/usage-parser.ts` as the pure parser. Replace the provider's duplicate payload interpretation with `parseCommandCodeUsage()`, extend its accepted cookie aliases, and preserve its current requests, diagnostics, cache runtime, and failure priority. **Tech Stack:** TypeScript 6, Node.js `>=24.15.0`, native Fetch API, Vitest 4, pnpm 11, Biome. @@ -12,61 +12,53 @@ **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 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. +**Prerequisite:** Phase 1 is complete: `parseCommandCodeUsage()` and `CommandCodePayloads` exist and `pnpm exec vitest run tests/provider-command-code.test.ts` passes. ## Global Constraints - 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. +- Add no files, dependencies, public exports, shared usage types, environment variables, local estimates, or static plan-price catalog. +- Preserve `COMMAND_CODE_COOKIE_HEADER`, all three current endpoint requests, provider cache/backoff behavior, request/token balances, and partial-success behavior. - 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. +- Keep the provider registry import unchanged. + +## Reference Findings + +- Command Code documents 5-hour and weekly rolling caps over included monthly credits; purchased credits bypass both limits: . +- Current CodexBar reads both rolling windows from `/internal/billing/credits`, accepts root and nested `windowLimits`, and supports `commandcode_prod_.session_token` plus `__Host-commandcode_prod_.session_token`. +- Pi has no Command Code usage-limit integration to reuse or modify. --- -### Task 2: Split the API Client and Wire the Provider Adapter +### Task 2: Wire the Parser into the Existing Provider **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: `src/providers/command-code.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[]; -} +- Consumes: `parseCommandCodeUsage(payloads: CommandCodePayloads): Pick` from Phase 1; existing `fetchWithTimeout()`, `readJsonObject()`, `retryAfterMs()`, and `fetchWithLiveRuntime()`. +- Produces: unchanged `createCommandCodeProvider(deps: UsageDeps): UsageProviderAdapter` behavior with rolling windows supplied by the parser. -export function normalizeCommandCodeCookie( - raw: string | undefined, -): string | undefined; +- [ ] **Step 1: Confirm the supported runtime and clean starting state** -export function fetchCommandCodeApi( - deps: UsageDeps, - cookie: string, - signal?: AbortSignal, -): Promise; +Run: -export function createCommandCodeProvider( - deps: UsageDeps, -): UsageProviderAdapter; +```bash +node --version +git status --short +pnpm exec vitest run tests/provider-command-code.test.ts ``` -- [ ] **Step 1: Update the integration test to require both rolling windows and balances** +Expected: Node.js is `v24.15.0` or newer, `git status --short` is empty, and all focused tests pass. If Node.js is older, switch to a supported runtime before using later test results as completion evidence. -In the existing `uses cookie auth and parses aggregate usage` test, return rolling limits with the credits payload and make purchased credits nonzero: +- [ ] **Step 2: Update provider integration coverage** + +In `tests/provider-command-code.test.ts`, rename `uses cookie auth and parses aggregate usage` to `uses cookie auth and exposes rolling usage with balances`. Change its credits response to: ```ts if (url.toString().includes("/billing/credits")) { @@ -83,262 +75,28 @@ if (url.toString().includes("/billing/credits")) { } ``` -Replace the old current-cycle assertions with: +Replace its current-cycle assertions with: ```ts +expect(snapshot.status).toBe("live"); expect(snapshot.windows.map((window) => [window.key, window.label])).toEqual([ ["fiveHour", "5h"], ["weekly", "Weekly"], ]); -expect(snapshot.balances).toContainEqual({ - label: "Monthly remaining", - remaining: 5.7112, - unit: "USD", -}); -expect(snapshot.balances).toContainEqual({ - label: "Purchased remaining", - remaining: 5, - unit: "USD", -}); +expect(snapshot.balances).toEqual( + expect.arrayContaining([ + { label: "Monthly remaining", remaining: 5.7112, unit: "USD" }, + { label: "Purchased remaining", remaining: 5, unit: "USD" }, + { label: "Requests", remaining: 42, unit: "count" }, + { label: "Tokens", remaining: 1_234, unit: "tok" }, + ]), +); expect(snapshot.planName).toBe("Go"); +expect(snapshot.sourceLabel).toContain("Command Code"); 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 does not emit the rolling windows. - -- [ ] **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 only `fiveHour` and `weekly` windows, plus separate monthly and purchased balances. - -- [ ] **Step 6: Add a partial-failure integration regression** - -Add this provider test to `tests/provider-command-code.test.ts`: +Replace `keeps aggregate usage when subscription enrichment fails` with this stronger partial-success regression: ```ts it("keeps rolling limits when summary and subscription fail", async () => { @@ -386,33 +144,167 @@ it("keeps rolling limits when summary and subscription fail", async () => { }); ``` -- [ ] **Step 7: Run focused tests, type checking, and the full project check** +Add cookie-alias coverage: + +```ts +it.each([ + "commandcode_prod_.session_token", + "__Host-commandcode_prod_.session_token", +])("accepts current Command Code cookie alias %s", async (cookieName) => { + const root = mkTmp(); + const fetchImpl = vi.fn(async (url, init) => { + expect(new Headers(init?.headers).get("cookie")).toBe( + `${cookieName}=token`, + ); + if (url.toString().includes("/billing/credits")) { + return new Response( + JSON.stringify({ credits: { monthlyCredits: 0 } }), + { status: 200 }, + ); + } + return new Response("{}", { status: 200 }); + }); + + const snapshot = ( + await commandCodeProvider( + createLiveDeps(root, () => 1_000, fetchImpl, { + COMMAND_CODE_COOKIE_HEADER: `${cookieName}=token`, + }), + ).fetch() + ).snapshot; + expect(snapshot.status).toBe("live"); + expect(fetchImpl).toHaveBeenCalledTimes(3); + rmSync(root, { recursive: true, force: true }); +}); +``` + +Add compact status-classification coverage: + +```ts +it.each([ + { status: 429, diagnostic: "Rate limited.", hasRetry: true }, + { status: 401, diagnostic: "session expired", hasRetry: false }, +])( + "classifies primary $status responses", + async ({ status, diagnostic, hasRetry }) => { + const root = mkTmp(); + const fetchImpl = vi.fn(async () => + new Response("{}", { status }), + ); + const outcome = await commandCodeProvider( + createLiveDeps(root, () => 1_000, fetchImpl, { + COMMAND_CODE_COOKIE_HEADER: "abc", + }), + ).fetch(); + + expect(outcome.snapshot.available).toBe(false); + expect(outcome.snapshot.diagnostics.join(" ")).toContain(diagnostic); + expect(Boolean(outcome.nextRetryAt)).toBe(hasRetry); + rmSync(root, { recursive: true, force: true }); + }, +); +``` + +- [ ] **Step 3: Run the focused test and verify the old provider fails the new expectations** Run: ```bash pnpm exec vitest run tests/provider-command-code.test.ts +``` + +Expected: FAIL because the existing provider still emits `current-cycle` and rejects the two newly covered cookie aliases. + +- [ ] **Step 4: Replace duplicate provider parsing with the Phase 1 parser** + +In `src/providers/command-code.ts`, reduce the type import to `UsageProviderAdapter`, remove `toFinite` from the runtime import, remove the local `asRecord()` helper, and add: + +```ts +import { parseCommandCodeUsage } from "./command-code/usage-parser.ts"; +``` + +Extend the existing `cookieNames` array without changing its current entries or bare-token default: + +```ts +const cookieNames = [ + "__Secure-commandcode_prod_.session_token", + "commandcode_prod_.session_token", + "__Host-commandcode_prod_.session_token", + "__Host-better-auth.session_token", + "__Secure-better-auth.session_token", + "better-auth.session_token", +]; +``` + +After decoding the three endpoint responses, replace the local cost, window, balance, and plan parsing block with: + +```ts +const summary = await readJson(summaryRes, "Summary"); +const credits = await readJson(creditsRes, "Credits"); +const subscription = await readJson(subsRes, "Subscription"); +const parsed = parseCommandCodeUsage({ summary, credits, subscription }); +``` + +Change the empty-result check to: + +```ts +if (parsed.windows.length === 0 && parsed.balances.length === 0) { +``` + +Keep the existing primary-response status classification unchanged. In the successful snapshot, remove the old `balances`, `windows`, and `planName` fields and spread the parser result after diagnostics: + +```ts +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, + ...parsed, +}, +``` + +- [ ] **Step 5: Run the focused provider suite** + +Run: + +```bash +pnpm exec vitest run tests/provider-command-code.test.ts +``` + +Expected: PASS. The provider emits only `fiveHour` and `weekly` windows, retains all balances, accepts all covered cookies, preserves partial success, and classifies primary 429/401 responses. + +- [ ] **Step 6: Run type checking and the complete project check** + +Run under Node.js `>=24.15.0`: + +```bash pnpm typecheck pnpm check ``` -Expected: all commands exit 0. The full check runs Biome lint, TypeScript compilation, and every Vitest suite. +Expected: both commands exit 0. `pnpm check` runs Biome lint, TypeScript compilation, and every Vitest suite without an unsupported-engine warning. -- [ ] **Step 8: Review the final diff for scope and whitespace errors** +- [ ] **Step 7: Review scope and commit the integration** Run: ```bash git diff --check git status --short -git diff -- src/providers/command-code src/providers/index.ts tests/provider-command-code.test.ts +git diff -- src/providers/command-code.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. +Expected: `git diff --check` exits 0 and implementation changes are limited to the provider and its test. -- [ ] **Step 9: Commit the provider split and integration** +Commit: ```bash -git add src/providers/command-code.ts src/providers/command-code src/providers/index.ts tests/provider-command-code.test.ts +git add src/providers/command-code.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 31660db..5d56834 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 reliable 5-hour and weekly limits with existing balance data. +**Goal:** Refactor the Command Code provider into two 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 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. +**Architecture:** A pure parser converts Command Code JSON payloads into rolling windows, balances, and a plan name. The existing provider entrypoint keeps authentication, endpoint, cache-runtime, and error-classification behavior and delegates payload interpretation to that parser. 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. @@ -31,13 +31,13 @@ - [ ] Confirm `pnpm exec vitest run tests/provider-command-code.test.ts` passes. - [ ] Confirm the Phase 1 commit exists before starting Phase 2. -### Phase 2: Split and Integrate the Provider +### Phase 2: Integrate the Parser with the Provider **Plan:** `docs/superpowers/plans/2026-08-30-command-code-usage-refactor-phase-2-provider-integration.md` **Prerequisite:** Phase 1 is complete and its focused tests pass. -**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. +**Atomic Result:** The existing provider uses the parser, snapshots expose available rolling windows and balances, partial failures retain usable data, and the complete project check passes. - [ ] Execute every Phase 2 checkbox in order. - [ ] Confirm `pnpm exec vitest run tests/provider-command-code.test.ts` passes. From fa6398f5a0df6cd308b823860f06f85d736727a3 Mon Sep 17 00:00:00 2001 From: Lanh Hoang Date: Sun, 30 Aug 2026 22:40:39 -0400 Subject: [PATCH 3/3] refactor(command-code): expose usage limit windows --- src/providers/command-code.ts | 129 +++------------------------- tests/provider-command-code.test.ts | 116 +++++++++++++++++++++---- 2 files changed, 110 insertions(+), 135 deletions(-) diff --git a/src/providers/command-code.ts b/src/providers/command-code.ts index fe83e01..7fd44ca 100644 --- a/src/providers/command-code.ts +++ b/src/providers/command-code.ts @@ -1,16 +1,12 @@ import { PROVIDER_LABELS, PROVIDER_TTLS_MS } from "../shared/constants.ts"; import type { UsageDeps } from "../shared/deps.ts"; -import type { - LiveUsageWindow, - ProviderUsageSnapshot, - UsageProviderAdapter, -} from "../shared/types.ts"; +import type { UsageProviderAdapter } from "../shared/types.ts"; +import { parseCommandCodeUsage } from "./command-code/usage-parser.ts"; import { fetchWithLiveRuntime, fetchWithTimeout, readJsonObject, retryAfterMs, - toFinite, } from "./runtime.ts"; function normalizeCookieHeader(raw: string | undefined): string | undefined { @@ -19,6 +15,8 @@ function normalizeCookieHeader(raw: string | undefined): string | undefined { const bare = input.replace(/^cookie\s*:\s*/i, "").trim(); const cookieNames = [ "__Secure-commandcode_prod_.session_token", + "commandcode_prod_.session_token", + "__Host-commandcode_prod_.session_token", "__Host-better-auth.session_token", "__Secure-better-auth.session_token", "better-auth.session_token", @@ -43,12 +41,6 @@ function normalizeCookieHeader(raw: string | undefined): string | undefined { return undefined; } -function asRecord(value: unknown): Record | undefined { - return value && typeof value === "object" - ? (value as Record) - : undefined; -} - export function createCommandCodeProvider( deps: UsageDeps, ): UsageProviderAdapter { @@ -136,110 +128,13 @@ export function createCommandCodeProvider( const creditsPayload = await readJson(creditsRes, "Credits"); const subsPayload = await readJson(subsRes, "Subscription"); - const totalCost = toFinite(summary?.totalCost); - const totalCount = toFinite(summary?.totalCount); - const totalTokens = toFinite(summary?.totalTokens); - const totalTokensIn = toFinite(summary?.totalTokensIn); - const totalTokensOut = toFinite(summary?.totalTokensOut); - - const credits = asRecord(creditsPayload?.credits); - const monthlyCredits = toFinite(credits?.monthlyCredits); - const purchasedCredits = toFinite(credits?.purchasedCredits) ?? 0; - - const subsData = asRecord(subsPayload?.data); - const planId = - typeof subsData?.planId === "string" - ? subsData.planId - : undefined; - const planName = - planId === "individual-go" - ? "Go" - : planId === "individual-pro" - ? "Pro" - : planId === "individual-max" - ? "Max" - : planId === "individual-ultra" - ? "Ultra" - : planId; - const resetAt = - typeof subsData?.currentPeriodEnd === "string" - ? Date.parse(subsData.currentPeriodEnd) - : undefined; - - const windows: LiveUsageWindow[] = []; - if (totalCost != null && monthlyCredits != null) { - const remaining = monthlyCredits + purchasedCredits; - const limit = totalCost + remaining; - windows.push({ - key: "current-cycle", - label: "Current cycle", - used: totalCost, - limit, - unit: "USD", - usedPercent: - limit > 0 ? Math.round((totalCost / limit) * 100) : 0, - resetAt: Number.isFinite(resetAt) ? resetAt : undefined, - }); - } else if (totalCost != null) { - windows.push({ - key: "current-cycle-used", - label: "Current cycle", - used: totalCost, - unit: "USD", - usedPercent: 0, - unavailableReason: "Remaining balance unavailable", - }); - } else if (monthlyCredits != null) { - windows.push({ - key: "current-cycle-remaining", - label: "Current cycle", - unit: "USD", - usedPercent: 0, - unavailableReason: "Consumed cost unavailable", - }); - } - - const balances = [] as 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", - }); - 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 || totalTokensOut != null) { - 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 parsed = parseCommandCodeUsage({ + summary, + credits: creditsPayload, + subscription: subsPayload, + }); - if (windows.length === 0 && balances.length === 0) { + if (parsed.windows.length === 0 && parsed.balances.length === 0) { const primaryResponses = [summaryRes, creditsRes].filter( (res): res is Response => Boolean(res), ); @@ -279,13 +174,11 @@ export function createCommandCodeProvider( diagnostic: "", fetchedAt: now, expiresAt: now + PROVIDER_TTLS_MS["command-code"], - balances, status: "live", sourceLabel: "Command Code web usage API", sourceKind: "live", - windows, diagnostics, - planName, + ...parsed, }, }; }, diff --git a/tests/provider-command-code.test.ts b/tests/provider-command-code.test.ts index 364ce58..a0a0c43 100644 --- a/tests/provider-command-code.test.ts +++ b/tests/provider-command-code.test.ts @@ -198,7 +198,7 @@ describe("Command Code usage parser", () => { }); describe("Command Code provider", () => { - it("uses cookie auth and parses aggregate usage", async () => { + it("uses cookie auth and exposes rolling usage with balances", async () => { const root = mkTmp(); const fetchImpl = vi.fn(async (url, init) => { const headers = new Headers(init?.headers); @@ -206,7 +206,6 @@ describe("Command Code provider", () => { if (url.toString().includes("/usage/summary")) { return new Response( JSON.stringify({ - totalCost: 4.2888, totalCount: 42, totalTokens: 1234, }), @@ -216,7 +215,11 @@ describe("Command Code provider", () => { if (url.toString().includes("/billing/credits")) { return new Response( JSON.stringify({ - credits: { monthlyCredits: 5.7112, purchasedCredits: 0 }, + 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 }, ); @@ -241,11 +244,21 @@ describe("Command Code provider", () => { ).fetch() ).snapshot; expect(snapshot.status).toBe("live"); + expect(snapshot.windows.map((window) => [window.key, window.label])).toEqual([ + ["fiveHour", "5h"], + ["weekly", "Weekly"], + ]); + expect(snapshot.balances).toEqual( + expect.arrayContaining([ + { label: "Monthly remaining", remaining: 5.7112, unit: "USD" }, + { label: "Purchased remaining", remaining: 5, unit: "USD" }, + { label: "Requests", remaining: 42, unit: "count" }, + { label: "Tokens", remaining: 1_234, unit: "tok" }, + ]), + ); expect(snapshot.planName).toBe("Go"); - expect(snapshot.windows[0].key).toBe("current-cycle"); - expect(snapshot.windows[0].label).toBe("Current cycle"); - expect(snapshot.windows[0].limit).toBeCloseTo(10); expect(snapshot.sourceLabel).toContain("Command Code"); + expect(fetchImpl).toHaveBeenCalledTimes(3); rmSync(root, { recursive: true, force: true }); }); @@ -258,18 +271,22 @@ describe("Command Code provider", () => { rmSync(root, { recursive: true, force: true }); }); - it("keeps aggregate usage when subscription enrichment fails", async () => { + it("keeps rolling limits when summary and subscription fail", async () => { const root = mkTmp(); const fetchImpl = vi.fn(async (url) => { - if (url.toString().includes("/usage/summary")) { - return new Response(JSON.stringify({ totalCost: 4, totalCount: 2 }), { - status: 200, - }); - } if (url.toString().includes("/billing/credits")) { - return new Response(JSON.stringify({ credits: { monthlyCredits: 6 } }), { status: 200 }); + 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("subscription endpoint unavailable"); + throw new Error("endpoint unavailable"); }); const snapshot = ( @@ -280,12 +297,77 @@ describe("Command Code provider", () => { ).fetch() ).snapshot; 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.windows.map((window) => window.key)).toEqual([ + "fiveHour", + "weekly", + ]); + expect(snapshot.balances).toContainEqual({ + label: "Monthly remaining", + remaining: 6, + unit: "USD", + }); + expect(snapshot.diagnostics).toEqual( + expect.arrayContaining([ + "Summary endpoint unavailable.", + "Subscription endpoint unavailable.", + ]), + ); + rmSync(root, { recursive: true, force: true }); + }); + + it.each([ + "commandcode_prod_.session_token", + "__Host-commandcode_prod_.session_token", + ])("accepts current Command Code cookie alias %s", async (cookieName) => { + const root = mkTmp(); + const fetchImpl = vi.fn(async (url, init) => { + expect(new Headers(init?.headers).get("cookie")).toBe( + `${cookieName}=token`, + ); + if (url.toString().includes("/billing/credits")) { + return new Response( + JSON.stringify({ credits: { monthlyCredits: 0 } }), + { status: 200 }, + ); + } + return new Response("{}", { status: 200 }); + }); + + const snapshot = ( + await commandCodeProvider( + createLiveDeps(root, () => 1_000, fetchImpl, { + COMMAND_CODE_COOKIE_HEADER: `${cookieName}=token`, + }), + ).fetch() + ).snapshot; + expect(snapshot.status).toBe("live"); + expect(fetchImpl).toHaveBeenCalledTimes(3); rmSync(root, { recursive: true, force: true }); }); + it.each([ + { status: 429, diagnostic: "Rate limited.", hasRetry: true }, + { status: 401, diagnostic: "session expired", hasRetry: false }, + ])( + "classifies primary $status responses", + async ({ status, diagnostic, hasRetry }) => { + const root = mkTmp(); + const fetchImpl = vi.fn(async () => + new Response("{}", { status }), + ); + const outcome = await commandCodeProvider( + createLiveDeps(root, () => 1_000, fetchImpl, { + COMMAND_CODE_COOKIE_HEADER: "abc", + }), + ).fetch(); + + expect(outcome.snapshot.available).toBe(false); + expect(outcome.snapshot.diagnostics.join(" ")).toContain(diagnostic); + expect(Boolean(outcome.nextRetryAt)).toBe(hasRetry); + rmSync(root, { recursive: true, force: true }); + }, + ); + it("distinguishes malformed cookie configuration", async () => { const root = mkTmp(); for (const configuredCookie of [