Skip to content

fix(cli): unblock activation — ccusage install, EPIPE, silent reauth, backfill filter#114

Merged
ohong merged 8 commits into
mainfrom
oh-cli-activation-fixes
May 5, 2026
Merged

fix(cli): unblock activation — ccusage install, EPIPE, silent reauth, backfill filter#114
ohong merged 8 commits into
mainfrom
oh-cli-activation-fixes

Conversation

@ohong

@ohong ohong commented May 4, 2026

Copy link
Copy Markdown
Owner

Reopens #113 — that PR auto-closed when I renamed its head branch from oscar/cli-activation-fixes to oh-cli-activation-fixes. Same commits, same diff.

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.

Error class Volume (last 7d) Fix
ccusage is not installed or not on PATH 1,627 events / 14 users Detect missing binary, prompt user (TTY only), run bun add -g / npm install -g. Non-TTY contexts keep the original throw.
write EPIPE 48 events / 48 users (every active user once) Top-level process.stdout.on('error', …) / stderr handlers exit cleanly on EPIPE.
Session expired or invalid 33 events / 9 users (a) Server returns refreshed JWT in X-Straude-Refreshed-Token when token >7 days old; CLI persists silently. (b) On 401, CLI re-runs loginCommand and retries the request once (TTY-gated).
Date X is outside the 30-day backfill window 13 events Mirror the server's window check client-side; drop out-of-window rows with a single warning instead of failing the whole submit.

Plus new activation-tracking events cli_first_run and cli_authenticated (paired with existing usage_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.md in the diff. Six commits, one per milestone.

Test plan

  • bun run typecheck (monorepo) — green
  • bun 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 monorepo bun run test flaked 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 imports MessagesInbox/FollowButton/ActivityCard only).
  • bun run build (monorepo) — green
  • Manual: node packages/cli/dist/index.js --help | head -1 exits 0 (was crashing with EPIPE on main).
  • Manual: deleting ~/.straude/ and running CLI creates exactly one .first-run marker; subsequent runs do not rewrite it.
  • Smoke-test against a local web instance: full straude login → straude push → simulate 401 → confirm auto-relogin (recommended pre-merge — would need a local Supabase + dev server).

Notes for reviewers

  • The auto-install of ccusage reverses the prior security stance in packages/cli/src/lib/ccusage.ts for the interactive path only. Reasoning, alternatives considered, and the safety argument are documented in docs/DECISIONS.md.
  • The sliding-window token refresh is purely additive — older CLI versions ignore the new response header. Tested via the fake-clock unit tests in apps/web/__tests__/unit/cli-auth.test.ts.
  • Several new tests in this PR rely on heavy mocking (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].
  • Flaky perf test (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

    • CLI can refresh session tokens and may retry interactive requests; first-run tracking and an interactive ccusage install prompt added.
  • Bug Fixes

    • Graceful exit on closed stdout/stderr (EPIPE); client now filters out-of-window backfill dates before submission.
  • Chores

    • CLI version bumped to 0.1.24
  • Documentation

    • Added mission planning/progress, changelog and decisions entries.
  • Tests

    • Expanded test coverage around CLI auth, token refresh, ccusage install, first-run and push flows.

ohong and others added 6 commits May 4, 2026 02:38
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>
@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
straude Ready Ready Preview, Comment May 4, 2026 11:03pm

Request Review

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 00d2550b-dc77-48f3-a3dd-ed426c767783

📥 Commits

Reviewing files that changed from the base of the PR and between b2a978a and 3abfe85.

📒 Files selected for processing (1)
  • packages/cli/__tests__/api.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/tests/api.test.ts

📝 Walkthrough

Walkthrough

This 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.

Changes

CLI Activation, Token Refresh & ccusage Install (single cohesive DAG)

Layer / File(s) Summary
Data Shape & Constants
apps/web/lib/api/cli-auth.ts, packages/cli/src/lib/first-run.ts, packages/cli/src/lib/prompt.ts
Adds CliAuthResult and TOKEN_REFRESH_AFTER_DAYS = 7; introduces FIRST_RUN_MARKER, isFirstRun()/markFirstRun(), and prompt helpers (isInteractive, promptYesNo).
JWT Decode / Verify Helper
apps/web/lib/api/cli-auth.ts
Adds decodeAndVerify(authHeader) to parse/validate Bearer JWTs and expiry; refactors verifyCliToken to delegate to it.
Token Refresh Logic (Server)
apps/web/lib/api/cli-auth.ts
Adds verifyCliTokenWithRefresh returning `{ userId, username
Token Rotation & 401 Strategy (Client core)
packages/cli/src/lib/api.ts
Adds REFRESHED_TOKEN_HEADER, doRequest helper, SessionExpiredError; rotates token from response header and attempts saveConfig (swallowing read-only FS errors). Exposes setAuthRefreshStrategy and single-retry logic for interactive 401 flows.
CLI Entry-Point Wiring
packages/cli/src/index.ts
Registers auth refresh strategy (runs loginCommand + loadConfig() on 401), adds EPIPE handlers for stdout/stderr, and emits cli_first_run / cli_authenticated telemetry using first-run helpers.
ccusage Detection & Interactive Install
packages/cli/src/lib/ccusage.ts, packages/cli/src/lib/prompt.ts
Adds isCcusageInstalled() and ensureCcusageInstalled(config?) which records PostHog events, prompts on TTY, prefers bun then npm, runs installer with inherited stdio, re-verifies PATH, and surfaces clear errors for non-TTY or failures.
push Command: Pre-check & Filtering
packages/cli/src/commands/push.ts
Calls ensureCcusageInstalled() before collection (falls back to Codex-only when appropriate), adds exported isWithinBackfillWindow(dateStr) and client-side filtering/logging of out-of-window dates to avoid server 400s.
Server Route Integration for Refresh Header
apps/web/app/api/cli/dashboard/route.ts, apps/web/app/api/usage/submit/route.ts
Routes now call verifyCliTokenWithRefresh, include refreshedToken in AuthContext, and propagate X-Straude-Refreshed-Token header when present.
Tests
apps/web & packages/cli tests/*
Mocks and tests updated to include verifyCliTokenWithRefresh, token refresh and header persistence, silent 401 re-auth retry behavior, ccusage install flows (interactive/non-interactive, bun/npm, failure cases), first-run marker behavior, and client-side backfill filtering.
Docs, Mission Artifacts & Misc
.gitignore, .mission/plan.md, .mission/progress.md, docs/CHANGELOG.md, docs/DECISIONS.md, packages/cli/package.json
Adds mission plan/progress, documents the changes in CHANGELOG/DECISIONS, ignores .claude/scheduled_tasks.lock, and bumps CLI version to 0.1.24.

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
Loading
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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped to check the first-run light,
Tokens spun and then rotated right,
A prompt for ccusage, bun or npm bright,
401 retried gently in the night,
EPIPE hushed — the CLI hops light ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and clearly describes the four main technical changes: ccusage install, EPIPE handling, silent reauth, and backfill filtering—all core components of the activation fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch oh-cli-activation-fixes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 61c95df and 8171726.

📒 Files selected for processing (23)
  • .gitignore
  • .mission/plan.md
  • .mission/progress.md
  • apps/web/__tests__/api/usage-submit.test.ts
  • apps/web/__tests__/flows/cli-push-flow.test.ts
  • apps/web/__tests__/flows/web-import-flow.test.ts
  • apps/web/__tests__/unit/cli-auth.test.ts
  • apps/web/app/api/cli/dashboard/route.ts
  • apps/web/app/api/usage/submit/route.ts
  • apps/web/lib/api/cli-auth.ts
  • docs/CHANGELOG.md
  • docs/DECISIONS.md
  • packages/cli/__tests__/api.test.ts
  • packages/cli/__tests__/ccusage-install.test.ts
  • packages/cli/__tests__/commands/push.test.ts
  • packages/cli/__tests__/first-run.test.ts
  • packages/cli/package.json
  • packages/cli/src/commands/push.ts
  • packages/cli/src/index.ts
  • packages/cli/src/lib/api.ts
  • packages/cli/src/lib/ccusage.ts
  • packages/cli/src/lib/first-run.ts
  • packages/cli/src/lib/prompt.ts

Comment thread .mission/progress.md Outdated
Comment thread packages/cli/src/commands/push.ts Outdated
Comment thread packages/cli/src/index.ts
Comment thread packages/cli/src/lib/api.ts
Comment thread packages/cli/src/lib/ccusage.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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/cli/src/lib/ccusage.ts (1)

133-145: 💤 Low value

Preserve the original error cause when rethrowing install failure.

The re-thrown Error on line 141 already includes (err as Error).message in the message string, but the original cause chain 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8171726 and b2a978a.

📒 Files selected for processing (5)
  • .mission/progress.md
  • packages/cli/src/commands/push.ts
  • packages/cli/src/index.ts
  • packages/cli/src/lib/api.ts
  • packages/cli/src/lib/ccusage.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/cli/src/commands/push.ts

Comment on lines +100 to +165
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 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.ts

Repository: 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 -60

Repository: 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.json

Repository: 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:


🏁 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.ts

Repository: 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant