diff --git a/package-lock.json b/package-lock.json index 444e384..167f7ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codebase-cli", - "version": "2.0.0-pre.77", + "version": "2.0.0-pre.78", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codebase-cli", - "version": "2.0.0-pre.77", + "version": "2.0.0-pre.78", "license": "MIT", "dependencies": { "@earendil-works/pi-agent-core": "0.74.0", diff --git a/package.json b/package.json index 65e1d11..cbbdca0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codebase-cli", - "version": "2.0.0-pre.77", + "version": "2.0.0-pre.78", "description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.", "keywords": [ "ai", diff --git a/src/commands/builtins/usage.test.ts b/src/commands/builtins/usage.test.ts index 844c0b9..c1452c4 100644 --- a/src/commands/builtins/usage.test.ts +++ b/src/commands/builtins/usage.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { fetchUsageReport, formatUsageBalance } from "./usage.js"; +import { fetchUsageReport, formatTurnQuotas, formatUsageBalance } from "./usage.js"; let homeDir: string | undefined; @@ -79,6 +79,20 @@ describe("formatUsageBalance", () => { }); }); +describe("formatTurnQuotas", () => { + it("distinguishes turn quotas from metered credits", () => { + expect(formatTurnQuotas({ anyBuildsRemaining: 5, cheapBuildsRemaining: 3 })).toEqual([ + "Any-model turn quota remaining: 5", + "Low-cost model turn quota remaining: 3", + "Metered runs require both an eligible turn and credits; builds that fail final QA are not charged.", + ]); + }); + + it("omits quota guidance when the endpoint returns no turn pools", () => { + expect(formatTurnQuotas({ creditsRemaining: 12 })).toEqual([]); + }); +}); + describe("fetchUsageReport", () => { it("prints the login hint when no Codebase session exists", async () => { isolateHome(); diff --git a/src/commands/builtins/usage.ts b/src/commands/builtins/usage.ts index 5d1c076..485ecda 100644 --- a/src/commands/builtins/usage.ts +++ b/src/commands/builtins/usage.ts @@ -24,7 +24,7 @@ interface Balance { */ export const usage: Command = { name: "usage", - description: "Show metered credits and included Codebase turn allowances.", + description: "Show metered credits and Codebase turn quotas.", handler: async (_args, ctx) => { ctx.emit(await fetchUsageReport()); return { handled: true }; @@ -67,18 +67,27 @@ export async function fetchUsageReport(): Promise { } else { lines.push("Monthly allowance was not returned yet; showing remaining credits only."); } - if (typeof b.anyBuildsRemaining === "number" && b.anyBuildsRemaining >= 0) { - lines.push(`Included web-build turns remaining: ${b.anyBuildsRemaining.toLocaleString()}`); - } - if (typeof b.cheapBuildsRemaining === "number" && b.cheapBuildsRemaining >= 0) { - lines.push(`Included fast coding turns remaining: ${b.cheapBuildsRemaining.toLocaleString()}`); - } + lines.push(...formatTurnQuotas(b)); return lines.join("\n"); } catch (err) { return `Couldn't fetch usage: ${(err as Error).message}`; } } +export function formatTurnQuotas(b: Balance): string[] { + const lines: string[] = []; + if (typeof b.anyBuildsRemaining === "number" && b.anyBuildsRemaining >= 0) { + lines.push(`Any-model turn quota remaining: ${b.anyBuildsRemaining.toLocaleString()}`); + } + if (typeof b.cheapBuildsRemaining === "number" && b.cheapBuildsRemaining >= 0) { + lines.push(`Low-cost model turn quota remaining: ${b.cheapBuildsRemaining.toLocaleString()}`); + } + if (lines.length > 0) { + lines.push("Metered runs require both an eligible turn and credits; builds that fail final QA are not charged."); + } + return lines; +} + export function formatUsageBalance(b: Balance): { creditLine: string; days: number | null;