From e5b9ea88153d59b869486ee0b647433e8f51235a Mon Sep 17 00:00:00 2001 From: Oscar Hong Date: Fri, 4 Sep 2026 17:03:34 -0700 Subject: [PATCH 1/2] Track every released ccusage agent Raise the bundled collector floor to 20.0.20, verify all released source IDs and native Gemini/Qwen/Grok accounting, and document source support. Use generic join/share copy instead of inferring an agent from its model. Co-authored-by: Codex --- README.md | 2 +- apps/web/__tests__/api/usage-submit.test.ts | 32 +--- apps/web/__tests__/unit/join-page.test.tsx | 161 ++++++++++++++++++ apps/web/app/(landing)/cli/page.tsx | 28 +-- .../join/[username]/opengraph-image.tsx | 22 +-- .../app/(landing)/join/[username]/page.tsx | 35 +--- .../components/landing/ProductExplanation.tsx | 5 +- apps/web/lib/agent-content.ts | 6 +- apps/web/public/llms.txt | 2 +- bun.lock | 16 +- docs/CHANGELOG.md | 2 + docs/CLI.md | 6 +- docs/DECISIONS.md | 10 ++ docs/ROADMAP.md | 13 +- packages/cli/README.md | 10 +- .../ccusage-pricing.integration.test.ts | 61 ++----- .../ccusage-sources.integration.test.ts | 65 +++++++ packages/cli/__tests__/ccusage.test.ts | 44 ++--- packages/cli/__tests__/commands/push.test.ts | 37 +++- .../__tests__/fixtures/ccusage-sources.json | 18 ++ .../fixtures/ccusage-sources/README.md | 13 ++ .../ccusage-sources/gemini/chats/session.json | 1 + .../sessions/synthetic/session/updates.jsonl | 1 + .../projects/synthetic/chats/session.jsonl | 1 + .../cli/__tests__/flows/cli-sync-flow.test.ts | 4 +- .../cli/__tests__/helpers/ccusage-binary.ts | 36 ++++ packages/cli/package.json | 4 +- packages/cli/src/lib/ccusage.ts | 4 +- 28 files changed, 451 insertions(+), 188 deletions(-) create mode 100644 apps/web/__tests__/unit/join-page.test.tsx create mode 100644 packages/cli/__tests__/ccusage-sources.integration.test.ts create mode 100644 packages/cli/__tests__/fixtures/ccusage-sources.json create mode 100644 packages/cli/__tests__/fixtures/ccusage-sources/README.md create mode 100644 packages/cli/__tests__/fixtures/ccusage-sources/gemini/chats/session.json create mode 100644 packages/cli/__tests__/fixtures/ccusage-sources/grok/sessions/synthetic/session/updates.jsonl create mode 100644 packages/cli/__tests__/fixtures/ccusage-sources/qwen/projects/synthetic/chats/session.jsonl create mode 100644 packages/cli/__tests__/helpers/ccusage-binary.ts diff --git a/README.md b/README.md index a2bc07d0..4f614694 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Sync your stats with a single command — no install needed: npx straude@latest ``` -The CLI reads your local [ccusage](https://github.com/ccusage/ccusage) data (cost, tokens, models, sessions), uploads it to Straude, and auto-creates a post on your feed. That includes every source ccusage detects, currently Claude Code, Codex, OpenCode, Amp, Droid, Codebuff, Hermes Agent, pi-agent, Goose, OpenClaw, Kilo, Kimi, Qwen, GitHub Copilot CLI, and Gemini CLI. First run opens a browser login; after that, just run `npx straude@latest` daily. It automatically pushes new stats since your last sync. +The CLI reads your local [ccusage](https://github.com/ccusage/ccusage) data (cost, tokens, models, sessions), uploads it to Straude, and auto-creates a post on your feed. That includes all 16 built-in sources in the bundled ccusage 20.0.20 release: Claude Code, Codex, OpenCode, Amp, Droid, Codebuff, Hermes Agent, pi-agent, Goose, OpenClaw, Kilo, Kimi, Qwen, GitHub Copilot CLI, Gemini CLI, and Grok Build CLI. First run opens a browser login; after that, just run `npx straude@latest` daily. It automatically pushes new stats since your last sync. Options: `--date YYYY-MM-DD` to push a specific date, `--days N` to backfill the last N days (max 7), `--dry-run` to collect usage without submitting it. Run `npx straude@latest status` to check your streak and rank. diff --git a/apps/web/__tests__/api/usage-submit.test.ts b/apps/web/__tests__/api/usage-submit.test.ts index 43144432..22658f85 100644 --- a/apps/web/__tests__/api/usage-submit.test.ts +++ b/apps/web/__tests__/api/usage-submit.test.ts @@ -28,23 +28,7 @@ import { verifyCliToken, verifyCliTokenWithRefresh } from "@/lib/api/cli-auth"; import { getServiceClient } from "@/lib/supabase/service"; import { resetRateLimiters } from "@/lib/rate-limit"; -const ALL_BUILT_IN_CCUSAGE_AGENTS = [ - "claude", - "codex", - "opencode", - "amp", - "droid", - "codebuff", - "hermes", - "pi", - "goose", - "openclaw", - "kilo", - "kimi", - "qwen", - "copilot", - "gemini", -]; +import ALL_BUILT_IN_CCUSAGE_AGENTS from "../../../../packages/cli/__tests__/fixtures/ccusage-sources.json"; function mockAllowedRpc() { return vi.fn((fn: string) => Promise.resolve( @@ -1601,7 +1585,7 @@ describe("POST /api/usage/submit", () => { }); }); - it("accepts every ccusage source and stores row-specific collector metadata", async () => { + it.each([...ALL_BUILT_IN_CCUSAGE_AGENTS, "custom-agent"])("accepts source %s and stores row-specific collector metadata", async (agent) => { (verifyCliToken as any).mockReturnValue("user-collector-meta"); const svc = mockServiceClient(); svc.single @@ -1612,7 +1596,7 @@ describe("POST /api/usage/submit", () => { const res = await POST( mockRequest({ entries: [makeEntry(todayStr(), { - agents: ["opencode"], + agents: [agent], models: ["gpt-5.6"], modelBreakdown: [{ model: "gpt-5.6", cost_usd: 0.05 }], })], @@ -1620,16 +1604,18 @@ describe("POST /api/usage/submit", () => { collector: { claude: "ccusage-claude-v20", codex: "ccusage-codex-v20", - ccusage_version: "20.0.16", - ccusage_agents: ALL_BUILT_IN_CCUSAGE_AGENTS, + ccusage_version: "20.0.20", + ccusage_agents: [...ALL_BUILT_IN_CCUSAGE_AGENTS, "custom-agent"], pricing_mode: "offline", }, }) ); const expectedMeta = { - ccusage_version: "20.0.16", - ccusage_agents: ["opencode"], + ...(agent === "claude" ? { claude: "ccusage-claude-v20" } : {}), + ...(agent === "codex" ? { codex: "ccusage-codex-v20" } : {}), + ccusage_version: "20.0.20", + ccusage_agents: [agent], pricing_mode: "offline", }; expect(res.status).toBe(200); diff --git a/apps/web/__tests__/unit/join-page.test.tsx b/apps/web/__tests__/unit/join-page.test.tsx new file mode 100644 index 00000000..68ec3096 --- /dev/null +++ b/apps/web/__tests__/unit/join-page.test.tsx @@ -0,0 +1,161 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ReactElement } from "react"; + +const mocks = vi.hoisted(() => ({ + getServiceClient: vi.fn(), + imageElement: null as unknown, +})); + +vi.mock("@/lib/supabase/service", () => ({ + getServiceClient: mocks.getServiceClient, +})); + +vi.mock("@/lib/og-fonts", () => ({ + loadFonts: vi.fn().mockResolvedValue([]), +})); + +vi.mock("@/lib/og-safe-image", () => ({ + loadSafeOgAvatar: vi.fn().mockResolvedValue(null), +})); + +vi.mock("next/og", () => ({ + ImageResponse: class MockImageResponse { + constructor(element: unknown) { + mocks.imageElement = element; + } + }, +})); + +vi.mock("@/components/landing/Navbar", () => ({ + Navbar: () =>