fix(cli): unblock activation — ccusage install, EPIPE, silent reauth, backfill filter#114
Conversation
Stops the `write EPIPE` exception that fires once for every active CLI user (48/48 in the last 7 days), and adds `cli_first_run` / `cli_authenticated` events so the install→push activation funnel is measurable. - Top-level EPIPE handlers on stdout/stderr exit cleanly when piped to a closing consumer (`straude --help | head`). - New `~/.straude/.first-run` marker fires `cli_first_run` exactly once per machine, before the --help/--version short-circuits so install attempts count too. - `cli_authenticated` fires when a stored config loads for a real command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "ccusage is not installed or not on PATH" error fired 1,627 times across 14 users in the last week — the single biggest activation blocker. Replace the hard throw with an interactive install prompt that handles the common case (interactive npx straude) while preserving the original throw for non-TTY contexts (auto-push, CI). - New `lib/prompt.ts` with isInteractive() + promptYesNo() helpers. - `ensureCcusageInstalled()` in ccusage.ts: prompts the user, runs `bun add -g ccusage` if bun is on PATH else `npm install -g ccusage`, with stdio inherited so install progress is visible. - Telemetry: ccusage_install_attempted / _succeeded / _failed / _declined / _skipped so we can measure the conversion of this prompt. - pushCommand calls ensureCcusageInstalled() before any ccusage spawn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the server's backfill check in pushCommand so a single edge-case row from ccusage doesn't reject the whole submit with HTTP 400. Drop out-of-window entries with a heads-up log line instead. 13 events from this error class hit users in the last week, all caused by ccusage returning entries on the boundary of the 30-day window. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eliminates "Session expired or invalid" failures (33 events / 9 users in the last week) by making auth recovery automatic for interactive users and pre-emptively rotating tokens that are nearing expiry. Server (apps/web/lib/api/cli-auth.ts): - New verifyCliTokenWithRefresh() returns userId + maybe a freshly minted JWT when the verified token is older than 7 days (TOKEN_REFRESH_AFTER_DAYS). - The dashboard and usage/submit routes attach that token to the response via X-Straude-Refreshed-Token. Purely additive — older clients ignore it. Client (packages/cli/src/lib/api.ts): - On every 2xx, read X-Straude-Refreshed-Token, persist via saveConfig, and mutate the in-memory config so subsequent calls in the same flow use the new token. - On 401, run a registered AuthRefreshStrategy (loginCommand, wired in index.ts), reload config, and retry once. Gated on isInteractive() so auto-push and CI runs still surface the original error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CHANGELOG and DECISIONS entries for the four error-class fixes plus the new activation-tracking events. Bumps `straude` to 0.1.24 — the version the activation funnel insight (DV22QC1d) is filtered to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR implements CLI activation and resilience features: first-run detection and PostHog events, interactive ccusage detection/installation, client-side backfill filtering, server/client JWT refresh with header-based rotation and a 401 retry strategy, EPIPE handling, tests, docs, mission artifacts, and a CLI version bump. ChangesCLI Activation, Token Refresh & ccusage Install (single cohesive DAG)
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI App
participant API as Server API
participant AuthStrat as Auth Refresh<br/>Strategy
participant Login as Login Flow<br/>(Browser)
participant Config as Local Config<br/>(Disk)
rect rgba(200, 150, 255, 0.5)
note over CLI,API: 401-triggered silent re-auth + retry
CLI->>API: Request with old token
API-->>CLI: 401 Session Expired
alt Interactive & Strategy Registered
CLI->>AuthStrat: invoke strategy(apiUrl)
AuthStrat->>Login: loginCommand → open browser
Login-->>AuthStrat: user authenticates
AuthStrat->>Config: loadConfig() → new token
AuthStrat-->>CLI: return refreshed config
CLI->>API: Retry request with refreshed token
API-->>CLI: 200 OK + optional X-Straude-Refreshed-Token
else Non-Interactive or No Strategy
CLI-->>CLI: surface original 401 error
end
end
sequenceDiagram
participant CLI as CLI App
participant FS as Filesystem
participant PATH as PATH Probe
participant Prompt as TTY Prompt
participant PM as Package Manager
participant PostHog as PostHog
rect rgba(150, 200, 150, 0.5)
note over CLI,PostHog: Interactive ccusage install on first-time push
CLI->>FS: check FIRST_RUN_MARKER
FS-->>CLI: marker missing
CLI->>PostHog: emit cli_first_run
CLI->>PATH: is ccusage on PATH?
PATH-->>CLI: not found
alt TTY Interactive
CLI->>Prompt: promptYesNo("Install ccusage?")
Prompt-->>CLI: user agrees
CLI->>PostHog: emit install_attempted
alt Bun on PATH
CLI->>PM: bun add -g ccusage
else
CLI->>PM: npm install -g ccusage
end
PM-->>CLI: install result
CLI->>PATH: re-check ccusage on PATH
alt Found
CLI->>CLI: continue push
CLI->>PostHog: emit install_succeeded
else Not found
CLI-->>CLI: throw "may need to open a new shell"
CLI->>PostHog: emit install_failed
end
else Non-TTY
CLI->>PostHog: emit install_skipped
CLI-->>CLI: throw "not installed, manual install required"
end
end
sequenceDiagram
participant CLI as CLI App
participant API as Server API
participant File as Config<br/>(Disk)
rect rgba(255, 150, 150, 0.5)
note over CLI,API: Header-based token rotation on successful response
CLI->>API: Request with aged token (>7 days)
API-->>CLI: 200 OK + X-Straude-Refreshed-Token
CLI->>CLI: read header, update in-memory token
CLI->>File: saveConfig() attempt (swallow read-only errs)
File-->>CLI: persisted or ignored
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.mission/progress.md:
- Line 5: Update the recorded PR reference in .mission/progress.md by replacing
the URL and PR number that currently point to pull/113 with the correct pull/114
(i.e., change "PR https://github.com/ohong/straude/pull/113" to "PR
https://github.com/ohong/straude/pull/114") so the status line in the
"**Status:** Complete." entry accurately reflects this review.
In `@packages/cli/src/commands/push.ts`:
- Around line 266-270: The call to ensureCcusageInstalled(config) runs too early
and blocks Codex-only pushes; move or guard that call so it only runs when
Claude/ccusage data is actually required. Specifically, defer or wrap
ensureCcusageInstalled(config) behind the same condition that detects Claude
data (the block that currently treats missing Claude data as non‑fatal around
the later lines handling Claude sync) so Codex-only paths skip it; e.g., check
for presence/need of Claude data before calling ensureCcusageInstalled or move
the call below the Claude-data existence check.
In `@packages/cli/src/index.ts`:
- Around line 29-41: The silenceEpipe handler currently calls process.exit(0) on
EPIPE, which masks any existing failure status; update the EPIPE branch in
silenceEpipe(stream) to exit with the current process exit code instead of 0
(e.g., use process.exit(process.exitCode ?? 0) or set process.exitCode
appropriately before exiting) and do not throw the error; also ensure any
failure paths that previously relied on throwing set process.exitCode = 1 so the
handler preserves the intended non-zero exit status.
In `@packages/cli/src/lib/api.ts`:
- Around line 71-76: The current try/catch around saveConfig(config) swallows
all errors; change it to only ignore expected read-only-home-directory write
failures and surface other write errors. In the catch block for saveConfig (in
packages/cli/src/lib/api.ts) inspect the thrown error's code/property and if it
matches read-only filesystem indicators (e.g. error.code === 'EROFS' or
permission errors like 'EACCES'/'EPERM' that you decide represent read-only home
scenarios) then handle silently or log a debug message; otherwise rethrow or log
an error so disk-full/other unexpected failures are not silently ignored.
In `@packages/cli/src/lib/ccusage.ts`:
- Around line 73-80: installCcusage currently calls execFileSync(cmd, args, {
stdio: "inherit", timeout: ... }) which fails on Windows because execFileSync
won't resolve .cmd shims; update the options passed to execFileSync in
installCcusage to include shell: process.platform === "win32" (matching the
pattern used in execCcusage and execCcusageAsync) so npm/bun are resolved on
Windows while keeping stdio and timeout intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 07d9a6ce-e420-47a2-99ec-2c7b02ae658b
📒 Files selected for processing (23)
.gitignore.mission/plan.md.mission/progress.mdapps/web/__tests__/api/usage-submit.test.tsapps/web/__tests__/flows/cli-push-flow.test.tsapps/web/__tests__/flows/web-import-flow.test.tsapps/web/__tests__/unit/cli-auth.test.tsapps/web/app/api/cli/dashboard/route.tsapps/web/app/api/usage/submit/route.tsapps/web/lib/api/cli-auth.tsdocs/CHANGELOG.mddocs/DECISIONS.mdpackages/cli/__tests__/api.test.tspackages/cli/__tests__/ccusage-install.test.tspackages/cli/__tests__/commands/push.test.tspackages/cli/__tests__/first-run.test.tspackages/cli/package.jsonpackages/cli/src/commands/push.tspackages/cli/src/index.tspackages/cli/src/lib/api.tspackages/cli/src/lib/ccusage.tspackages/cli/src/lib/first-run.tspackages/cli/src/lib/prompt.ts
- index.ts: preserve process.exitCode on EPIPE so a failing run piped to `head` no longer reports success. - api.ts: only swallow read-only-fs errors when persisting refreshed tokens; surface other write failures. - ccusage.ts: pass shell:true on Windows when invoking npm/bun for installCcusage so .cmd shims resolve. - push.ts: don't block Codex-only users when ccusage is missing — catch ensureCcusageInstalled failures and treat the resulting "not installed" error like the existing missing-Claude-data path. - .mission/progress.md: point status line at PR #114. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/cli/src/lib/ccusage.ts (1)
133-145: 💤 Low valuePreserve the original error cause when rethrowing install failure.
The re-thrown
Erroron line 141 already includes(err as Error).messagein the message string, but the originalcausechain is lost. Using{ cause: err }enables proper stack trace propagation for debugging.♻️ Proposed fix
- throw new Error( - `Failed to install ccusage automatically: ${(err as Error).message}\n` + - "Install it manually with `npm install -g ccusage` and run straude again.", - ); + throw new Error( + `Failed to install ccusage automatically: ${(err as Error).message}\n` + + "Install it manually with `npm install -g ccusage` and run straude again.", + { cause: err }, + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/src/lib/ccusage.ts` around lines 133 - 145, The catch block around installCcusage() currently rethrows a new Error that includes the original error message but discards the original error object; update the rethrow in the catch for installCcusage() to attach the original error as the cause (pass the caught err as the cause on the new Error) so stack traces and error chaining are preserved while leaving the existing posthog.capture call and message intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/cli/src/lib/ccusage.ts`:
- Around line 100-165: Replace calls to the private method posthog._shutdown()
with the public posthog.shutdown() API where they are used (e.g., the
finally/exit cleanup in index.ts and the shutdown call in push.ts that currently
reference posthog._shutdown()); update both call sites to invoke
posthog.shutdown() and keep the surrounding await/try-finally behavior to ensure
the client flushes before exit. Ensure you do not change the PostHog client
instantiation or config—only replace the method name to use the documented
shutdown() method.
---
Nitpick comments:
In `@packages/cli/src/lib/ccusage.ts`:
- Around line 133-145: The catch block around installCcusage() currently
rethrows a new Error that includes the original error message but discards the
original error object; update the rethrow in the catch for installCcusage() to
attach the original error as the cause (pass the caught err as the cause on the
new Error) so stack traces and error chaining are preserved while leaving the
existing posthog.capture call and message intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b549cf5d-479a-4268-8970-1dfd36b939c0
📒 Files selected for processing (5)
.mission/progress.mdpackages/cli/src/commands/push.tspackages/cli/src/index.tspackages/cli/src/lib/api.tspackages/cli/src/lib/ccusage.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/cli/src/commands/push.ts
| if (!isInteractive()) { | ||
| posthog.capture({ | ||
| distinctId, | ||
| event: "ccusage_install_skipped", | ||
| properties: { reason: "non_interactive" }, | ||
| }); | ||
| throw new Error( | ||
| "ccusage is not installed or not on PATH. Install it globally and retry (e.g. `npm install -g ccusage`).", | ||
| ); | ||
| } | ||
|
|
||
| console.log("\nStraude needs `ccusage` to read your Claude Code usage."); | ||
| const accepted = await promptYesNo( | ||
| "Install ccusage globally now? [Y/n] ", | ||
| true, | ||
| ); | ||
|
|
||
| if (!accepted) { | ||
| posthog.capture({ | ||
| distinctId, | ||
| event: "ccusage_install_declined", | ||
| }); | ||
| throw new Error( | ||
| "ccusage is required. Install it manually with `npm install -g ccusage` and run straude again.", | ||
| ); | ||
| } | ||
|
|
||
| posthog.capture({ | ||
| distinctId, | ||
| event: "ccusage_install_attempted", | ||
| properties: { manager: isOnPath("bun") ? "bun" : "npm" }, | ||
| }); | ||
|
|
||
| try { | ||
| installCcusage(); | ||
| } catch (err) { | ||
| posthog.capture({ | ||
| distinctId, | ||
| event: "ccusage_install_failed", | ||
| properties: { error: (err as Error).message?.slice(0, 200) ?? "unknown" }, | ||
| }); | ||
| throw new Error( | ||
| `Failed to install ccusage automatically: ${(err as Error).message}\n` + | ||
| "Install it manually with `npm install -g ccusage` and run straude again.", | ||
| ); | ||
| } | ||
|
|
||
| // Reset resolver cache so the freshly installed binary is picked up. | ||
| _resolved = undefined; | ||
|
|
||
| if (!isCcusageInstalled()) { | ||
| posthog.capture({ | ||
| distinctId, | ||
| event: "ccusage_install_failed", | ||
| properties: { error: "not_on_path_after_install" }, | ||
| }); | ||
| throw new Error( | ||
| "ccusage installed but not found on PATH. You may need to open a new shell, then re-run straude.", | ||
| ); | ||
| } | ||
|
|
||
| posthog.capture({ | ||
| distinctId, | ||
| event: "ccusage_install_succeeded", | ||
| }); | ||
| console.log("ccusage installed successfully.\n"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for posthog shutdown/flush calls across the CLI package
rg -n --type=ts "posthog\.(shutdown|flush)" packages/cli/
# Also check the main entry point and error-handling wrappers
rg -n --type=ts "process\.(exit|on\s*\()" packages/cli/src/index.tsRepository: ohong/straude
Length of output: 263
🏁 Script executed:
#!/bin/bash
# Get context around line 248 in index.ts
cat -n packages/cli/src/index.ts | sed -n '230,260p'
# Also check the overall structure of the main function/execution
cat -n packages/cli/src/index.ts | head -60Repository: ohong/straude
Length of output: 3309
🏁 Script executed:
#!/bin/bash
# Check the posthog module/implementation
cat -n packages/cli/src/lib/posthog.ts
# Also check package.json to see which version of posthog is being used
rg -A 5 '"posthog"' packages/cli/package.jsonRepository: ohong/straude
Length of output: 3330
🌐 Web query:
posthog-node shutdown method API
💡 Result:
The PostHog Node.js SDK (posthog-node) provides a shutdown() method on the client instance to gracefully shut down the client, flush all pending events, stop pollers, and ensure everything is sent before process exit. It returns a Promise and should be awaited [1][2][3][4]. Usage example from official docs: const client = new PostHog('<ph_project_api_key>', { host: '<ph_client_api_host>' }); // ... capture events ... await client.shutdown(); // Stops pending pollers and flushes remaining events [1][12]. For process exit handlers: process.on('SIGINT', async () => { await posthog.shutdown(); process.exit(0); }) [4][5]. Key recommendations: - In serverless (e.g., AWS Lambda), set flushAt: 1, flushInterval: 0, and call shutdown() at the end [1][2][4]. - Use flush() instead for per-request cleanup if continuing to use the client after [3][5]. - Optional shutdownTimeoutMs parameter for max wait time [3]. - Known issues: May timeout in dev/CI or with invalid events (e.g., missing distinctId object), logging warnings [6][7]. Official API reference confirms: "Shuts down the PostHog instance and ensures all events are sent. Call once before process exits" [3].
Citations:
- 1: https://posthog.com/docs/integrate/server/node
- 2: https://posthog.com/docs/libraries/node
- 3: https://posthog.com/docs/references/posthog-node
- 4: https://preview.posthog.com/docs/libraries/node
- 5: fix: add shutdown method for node.js in the getting started docs PostHog/posthog#17100
- 6: posthog-node shows "[PostHog] Timed out while shutting down PostHog" messages in dev PostHog/posthog-js#2854
- 7:
await client.shutdown()causing errors PostHog/posthog#21110
🏁 Script executed:
#!/bin/bash
# Check if _shutdown is actually used elsewhere or if there are test failures
rg -n "_shutdown" packages/cli/
# Also check if there's a type definition that shows what methods are available
rg -n "shutdown|flush" packages/cli/src/lib/posthog.tsRepository: ohong/straude
Length of output: 448
Use the documented shutdown() method instead of _shutdown() — the private method is not part of the PostHog-node public API.
The finally block at line 248 of index.ts does call posthog._shutdown() before exiting, which was the intended safeguard. However, _shutdown() is a private method (underscore prefix) and not documented in the official PostHog-node API; the correct public method is shutdown() without the underscore.
Additionally, push.ts line 461 also calls _shutdown() directly.
That said, the immediate flush configuration (flushAt: 1, flushInterval: 0 in posthog.ts lines 78–79) ensures events are flushed to PostHog immediately after each capture() call, so the practical risk of event loss is low. Regardless, use the documented public API for correctness and future compatibility:
Change posthog._shutdown() to posthog.shutdown() in both locations.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/cli/src/lib/ccusage.ts` around lines 100 - 165, Replace calls to the
private method posthog._shutdown() with the public posthog.shutdown() API where
they are used (e.g., the finally/exit cleanup in index.ts and the shutdown call
in push.ts that currently reference posthog._shutdown()); update both call sites
to invoke posthog.shutdown() and keep the surrounding await/try-finally behavior
to ensure the client flushes before exit. Ensure you do not change the PostHog
client instantiation or config—only replace the method name to use the
documented shutdown() method.
Update apiRequest test to match the new contract: only EACCES/EPERM/EROFS errors are swallowed during token-rotation persistence; unexpected errors (e.g. ENOSPC) propagate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
PostHog analysis of the last 7 days showed 53% of CLI users (55 of 103) install Straude and never push once — almost all blocked by four fixable errors. This PR removes them and adds the events to measure the lift.
ccusage is not installed or not on PATHbun add -g/npm install -g. Non-TTY contexts keep the original throw.write EPIPEprocess.stdout.on('error', …)/stderrhandlers exit cleanly on EPIPE.Session expired or invalidX-Straude-Refreshed-Tokenwhen token >7 days old; CLI persists silently. (b) On 401, CLI re-runsloginCommandand retries the request once (TTY-gated).Date X is outside the 30-day backfill windowPlus new activation-tracking events
cli_first_runandcli_authenticated(paired with existingusage_pushed) for a clean install→push funnel.Activation funnel insight: DV22QC1d — baseline 47% on
cli_version≤ 0.1.23. Target ≥75% on 0.1.24+.Notebook: QQ7eCe7G (mission context, since PostHog Subscriptions are paid-tier only).
Mission plan:
.mission/plan.mdin the diff. Six commits, one per milestone.Test plan
bun run typecheck(monorepo) — greenbun run --cwd packages/cli test— 240 tests, all green (was 220 on main; +20 new)bun run --cwd apps/web test— 578 tests passing in isolation. The full monorepobun run testflaked on__tests__/performance/authenticated-100ms.test.tsx > messages optimistic send(1018ms vs 1000ms budget) when the test runner was under heavy concurrent load; ran in isolation 3/3 times green. Unrelated to anything in this PR (the test importsMessagesInbox/FollowButton/ActivityCardonly).bun run build(monorepo) — greennode packages/cli/dist/index.js --help | head -1exits 0 (was crashing with EPIPE on main).~/.straude/and running CLI creates exactly one.first-runmarker; subsequent runs do not rewrite it.straude login → straude push → simulate 401 → confirm auto-relogin(recommended pre-merge — would need a local Supabase + dev server).Notes for reviewers
packages/cli/src/lib/ccusage.tsfor the interactive path only. Reasoning, alternatives considered, and the safety argument are documented indocs/DECISIONS.md.apps/web/__tests__/unit/cli-auth.test.ts.first-run.test.ts,ccusage-install.test.ts). A follow-up PR will replace them with real-fs / pure-helper unit tests — see [issue link TBD].authenticated-100ms.test.tsx) is environmental, not introduced here. Worth a follow-up to relax the budget or skip under load.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Documentation
Tests