From 2ac63988aace844ffeaaa22b28481ad5d82cf809 Mon Sep 17 00:00:00 2001 From: Oscar Hong Date: Sat, 8 Aug 2026 20:34:22 -0700 Subject: [PATCH 1/2] test(cli): stop pinning LiteLLM dollar amounts in ccusage pricing test The GPT-5.6 family costs are owned upstream by LiteLLM and change without notice, so hardcoded rates make this integration test fail on price moves rather than on regressions we control. Assert the invariant instead: every model in the family resolves to a non-zero price, and the day total equals the sum of the per-model breakdown. Co-Authored-By: Claude Opus 5 --- .../ccusage-pricing.integration.test.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/cli/__tests__/ccusage-pricing.integration.test.ts b/packages/cli/__tests__/ccusage-pricing.integration.test.ts index 99cb7a87..accdf101 100644 --- a/packages/cli/__tests__/ccusage-pricing.integration.test.ts +++ b/packages/cli/__tests__/ccusage-pricing.integration.test.ts @@ -75,25 +75,21 @@ describe("bundled ccusage GPT-5.6 pricing", () => { totalTokens: 440_000, }); - const expectedModelCosts = new Map([ - // 80K uncached input + 20K cached input + 10K output at LiteLLM's - // per-token rates for each member of the GPT-5.6 family. - ["gpt-5.6", 0.71], - ["gpt-5.6-sol", 0.71], - ["gpt-5.6-terra", 0.355], - ["gpt-5.6-luna", 0.142], - ]); - expect([...day.models].sort()).toEqual([...expectedModelCosts.keys()].sort()); - expect(day.modelBreakdown).toHaveLength(expectedModelCosts.size); + // Every member of the GPT-5.6 family must resolve to a LiteLLM price. + // The dollar amounts themselves are owned upstream and change without + // notice, so assert that each model is priced rather than pinning rates. + const expectedModels = ["gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]; + expect([...day.models].sort()).toEqual(expectedModels); + expect(day.modelBreakdown).toHaveLength(expectedModels.length); - for (const breakdown of day.modelBreakdown ?? []) { - const expectedCost = expectedModelCosts.get(breakdown.model); - if (expectedCost === undefined) { - throw new Error(`Unexpected GPT-5.6 model in ccusage output: ${breakdown.model}`); - } - expect(breakdown.cost_usd).toBeCloseTo(expectedCost, 10); + const breakdowns = day.modelBreakdown ?? []; + expect(breakdowns.map((breakdown) => breakdown.model).sort()).toEqual(expectedModels); + for (const breakdown of breakdowns) { + expect(breakdown.cost_usd, `${breakdown.model} is unpriced`).toBeGreaterThan(0); } - expect(day.costUSD).toBeCloseTo(1.917, 10); - expect(usage.summary.totalCostUSD).toBeCloseTo(1.917, 10); + + const summedCost = breakdowns.reduce((total, breakdown) => total + breakdown.cost_usd, 0); + expect(day.costUSD).toBeCloseTo(summedCost, 10); + expect(usage.summary.totalCostUSD).toBeCloseTo(summedCost, 10); }); }); From 17593a39eb40d3d237b0f85f5c78e11a5aa2c256 Mon Sep 17 00:00:00 2001 From: Oscar Hong Date: Sat, 8 Aug 2026 20:35:38 -0700 Subject: [PATCH 2/2] docs: record PR-review findings in ROADMAP Two items surfaced while reviewing the open PRs: - Straude already ingests Gemini, Qwen, Kimi, Copilot and the rest of ccusage's sources, because the collector runs unscoped `ccusage daily` and nothing downstream filters on agent name. PRs #77 and #22 hand-wrote parsers for capability we already have. What's missing is prettification, model colours, and copy that tells users. - `calculate_user_streak` is SECURITY DEFINER with EXECUTE granted to anon, so any caller can read any user's streak by id, private profiles included. Long-standing, not from an open PR. Also corrects the ccusage CHANGELOG entry, which claimed the GPT-5.6 fixture pins a $1.917 total; it now asserts the pricing invariant instead. Co-Authored-By: Claude Opus 5 --- docs/CHANGELOG.md | 2 +- docs/ROADMAP.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 641b6eaa..bcd94aac 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,7 +12,7 @@ ### Changed -- **All ccusage sources and the OpenAI GPT-5.6 family are now tracked.** The CLI dependency floor is `ccusage@20.0.16`, the first release with `gpt-5.6`, `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` plus request-level long-context pricing. Collection now uses current online LiteLLM pricing by default, avoiding stale embedded-price estimates. Unified rows are no longer filtered to Claude/Codex: Straude accepts every source ID emitted by ccusage, carries each row's source IDs through submission metadata, and retains source-aware handling for trusted Codex corrections. A real bundled-binary fixture locks the four GPT-5.6 variants to 440,000 total tokens and $1.917 in API-equivalent spend at the current LiteLLM rates. +- **All ccusage sources and the OpenAI GPT-5.6 family are now tracked.** The CLI dependency floor is `ccusage@20.0.16`, the first release with `gpt-5.6`, `gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` plus request-level long-context pricing. Collection now uses current online LiteLLM pricing by default, avoiding stale embedded-price estimates. Unified rows are no longer filtered to Claude/Codex: Straude accepts every source ID emitted by ccusage, carries each row's source IDs through submission metadata, and retains source-aware handling for trusted Codex corrections. A real bundled-binary fixture locks the four GPT-5.6 variants to 440,000 total tokens and asserts that every variant resolves to a non-zero LiteLLM price, with the day total equal to the sum of the per-model breakdown. The dollar amounts themselves are owned upstream and move without notice, so they are deliberately not pinned. - **Activation funnel events are now captured exclusively server-side.** `trackActivationEvent` no longer double-captures via browser posthog-js for consented users; the consent-exempt, privacy-limited server path (which owns anonymous→user identity stitching) is the single source of truth for funnel math. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 01be5e2e..0698a0b2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -29,6 +29,17 @@ The GitHub README stats card shipped as a single compact PNG. Future enhancement - **Card customization** — Custom accent color, show/hide specific stats, border radius options via query params. - **Embed analytics** — Track how many times a card is fetched to measure backlink effectiveness. +### Surface Multi-Agent CLI Support + +Straude already ingests usage from far more than Claude Code and Codex, and nobody knows it. The CLI shells out to `ccusage daily --json` with no source scoping (`packages/cli/src/lib/ccusage.ts:454`), and `ccusage@^20.0.16` detects Gemini CLI, Qwen, Kimi, GitHub Copilot CLI, Amp, Droid, OpenCode, Goose and others. Nothing downstream is hardcoded: `CcusageAgent` is `string`, agent names come from `metadata.agents` per row, and `/api/usage/submit` only checks that they are non-empty strings — no allowlist. + +Discovered while triaging PRs #77 and #22, both of which hand-wrote parsers for capability the collector already has. Work to make it real to users: + +- `prettifyModel` cases for Gemini, Qwen, Kimi and Copilot model IDs, so they read as product names rather than raw slugs. +- Deliberate entries in `MODEL_COLOR_PATTERNS` for those families, so they get a chosen colour instead of a hashed one from the fallback palette. +- Landing-page and README copy naming the supported agents. "Works with your whole toolkit" is a stronger acquisition line than "works with Claude Code", and it costs no engineering. +- Mistral Vibe is the one real gap — it is not a ccusage source. Best path is upstreaming it to ccusage rather than carrying a Straude-local parser. + ### Team / Org Workspaces Private groups where a company's eng team shares a scoped leaderboard, combined contribution graph, and team streak. A manager signs up, invites 10 engineers, and all 10 become users with a built-in audience. The team admin cares about the spend dashboard the same way they care about a cloud bill — this is a B2B wedge that doesn't require viral growth. Requires invites, permissions, team-scoped views, and billing context. @@ -161,6 +172,12 @@ Now that `device_usage` stores per-device data, future work could expose this in The CLI auth init endpoint has rate limiting (5 req/min/IP), but other write endpoints (comments, follows, kudos, upload, usage submit) do not. Consider per-user rate limiting via a shared utility or Supabase Edge Function middleware. Priority: `/api/upload` (file creation), `/api/usage/submit` (data creation), then social actions. +### `calculate_user_streak` Is Callable by `anon` with Any User ID + +`public.calculate_user_streak(UUID, INTEGER)` is `SECURITY DEFINER` and granted `EXECUTE` to both `anon` and `authenticated` (`supabase/migrations/20260430172022_fix_calculate_user_streak_security_definer.sql:85-86`). Because the definer bypasses RLS, anyone can POST to the PostgREST RPC endpoint with an arbitrary `p_user_id` and read that user's streak — including users who set their profile to private. It also works as a presence oracle: a non-zero return means the account has recent usage. + +Every caller in the app is server-side and already uses the service client, so the `anon` and `authenticated` grants appear to be unnecessary surface rather than something the front end depends on. Verify that against `apps/web` call sites, then revoke both grants and keep `service_role`. Long-standing, not introduced by any open PR; found while reviewing #147. + ## CSP Hardening (Nonce-Based) A baseline CSP header is shipped in `next.config.ts`, but it currently allows `'unsafe-inline'`. Remaining work is to move to a strict nonce-based policy for script/style sources. Requires auditing all script sources (Vercel Analytics, Supabase JS client), inline styles (Tailwind), and image origins (Supabase Storage).