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). 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); }); });