Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 17 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down
32 changes: 14 additions & 18 deletions packages/cli/__tests__/ccusage-pricing.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading