feat: add local Claude/Codex usage tracking (via ccusage)#193
feat: add local Claude/Codex usage tracking (via ccusage)#193robinebers merged 19 commits intomainfrom
Conversation
Scan Claude's JSONL session logs to show "Today" and "Last 30 days" cost and token counts in the Claude plugin detail view. - Add host.fs.glob API (Rust + JS wrapper) for recursive file discovery with path, size, and mtimeMs metadata - Implement incremental JSONL scanner with per-file caching, streaming dedup, and 60s rate limiting - Add model pricing table with tiered rates for Sonnet models - Support CLAUDE_CONFIG_DIR env var for custom config locations - Handle edge cases: oversized files clear stale cache, scan failures preserve old data, NaN/malformed entries are skipped
There was a problem hiding this comment.
Pull request overview
This pull request adds comprehensive local token usage tracking for the Claude plugin by scanning JSONL session logs and displaying aggregated "Today" and "Last 30 days" cost and token counts in the plugin detail view. The PR also introduces a new file system glob API for recursive file discovery.
Changes:
- Implements a new
host.fs.globAPI in Rust that supports recursive file pattern matching with glob syntax, returning file metadata (path, size, mtime) for cache invalidation - Adds comprehensive token usage tracking logic in the Claude plugin that scans local JSONL logs, deduplicates streaming chunks, aggregates tokens and costs per day/model, and caches results
- Documents the new glob API with examples and usage guidelines in the plugin API documentation
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src-tauri/src/plugin_engine/host_api.rs | Implements the Rust _globRaw function for file discovery using glob patterns with tilde expansion and metadata collection |
| src-tauri/src/plugin_engine/runtime.rs | Adds wrapper patch initialization for the new fs.glob API |
| src-tauri/Cargo.toml | Adds glob crate dependency for pattern matching |
| src-tauri/Cargo.lock | Updates lockfile with glob dependency |
| plugins/claude/plugin.js | Implements token usage scanning, caching, aggregation, and cost calculation with model-specific tiered pricing |
| plugins/claude/plugin.json | Adds "Today" and "Last 30 days" text line definitions for the UI |
| plugins/claude/plugin.test.js | Adds 14 comprehensive test cases covering scanning, deduplication, caching, error handling, and edge cases |
| plugins/test-helpers.js | Adds glob mock to test context |
| docs/plugins/api.md | Documents the new host.fs.glob API with signature, behavior, and usage examples |
| bun.lock | Updates lucide-react dependency to 0.564.0 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
1 issue found across 10 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src-tauri/src/plugin_engine/host_api.rs">
<violation number="1" location="src-tauri/src/plugin_engine/host_api.rs:7">
P2: Avoid expanding the plugin env allowlist beyond CODEX_HOME. Exposing additional env vars like CLAUDE_CONFIG_DIR increases the risk of leaking local configuration paths/secrets to plugins.
(Based on your team's feedback about exposing only CODEX_HOME to plugins when allowing environment variable access.) [FEEDBACK_USED]</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
Currently it works without any problem, with minimal dep. In the future we can migrate to https://ccusage.com/guide/library-usage, with consolidating other providers. Let's start with that way! |
|
Thanks Mert! I looked into this a bit deeper. This well, well done. Only problem is the initial start time which in my case (with not a lot of cache files) took 14.2s (!) on first launch before cache creation. This has the potential to confuse people, especially if they have a lot more cache or more data in general. I had It would also enable usage for Codex (and Amp, if we'd like to). What do you think? |
|
@robinebers @davidarny thank you 🙏
It didn't take me that long, but I agree with you, it should be much faster.
Also agree. We shouldn't maintain a list of models, and I'm really not a fan of changing the configuration every time a new model releases. Doing something which leverages ccusage now. I expect this feature to be completed today 🤞 |
Replace the hand-rolled JSONL scanner (~370 lines) with the ccusage library via a Bun subprocess bridge, gaining accurate LiteLLM pricing and support for newer models like claude-opus-4-6. The bridge uses an adapter-pattern dispatch so adding future providers (Codex, OpenCode) requires only a new adapter function. Rust auto-injects `_provider` from the plugin ID, keeping plugin JS unchanged. Also removes dead `host.fs.glob` code and the `glob` crate, which were only needed by the old scanner.
There was a problem hiding this comment.
1 issue found across 13 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src-tauri/src/plugin_engine/host_api.rs">
<violation number="1" location="src-tauri/src/plugin_engine/host_api.rs:1223">
P2: After timing out you kill the child process but never wait to reap it. This can leave zombie processes if ccusage repeatedly times out. Call `child.wait()` after `kill()` before returning.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Call child.wait() after child.kill() to prevent zombie processes when the ccusage bridge repeatedly times out.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@robinebers all done! and we can add codex/amp easily with existing structure, adhering ocp :) |
…ayKey function for consistent date formatting. Update test cases to utilize this new function for improved readability and maintainability.
|
cursor review |
@robinebers I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
5 issues found across 12 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="plugins/codex/plugin.test.js">
<violation number="1" location="plugins/codex/plugin.test.js:235">
P2: This test relies on the real system clock to build the ccusage date key, which can make it flaky across time zones or around midnight. Use a fixed system time (e.g., vi.useFakeTimers/vi.setSystemTime) so the plugin’s “Today” logic matches a deterministic date.</violation>
</file>
<file name="src-tauri/src/plugin_engine/host_api.rs">
<violation number="1" location="src-tauri/src/plugin_engine/host_api.rs:1487">
P1: Pipe deadlock: stdout and stderr are piped but never drained while waiting for the child to exit. If ccusage outputs more than the OS pipe buffer (~64 KB), the child blocks on a write, the parent is stuck in the `try_wait` poll loop, and the process times out instead of succeeding. Use `child.wait_with_output()` to read pipes and wait concurrently, removing the manual poll loop.</violation>
</file>
<file name="plugins/claude/plugin.js">
<violation number="1" location="plugins/claude/plugin.js:487">
P2: `ccusage` daily entries don’t include `totalTokens`, so this will always treat token counts as 0. Compute totals from `inputTokens`/`outputTokens`/cache token fields instead.</violation>
</file>
<file name="docs/plugins/api.md">
<violation number="1" location="docs/plugins/api.md:454">
P2: The ccusage API docs omit supported options (`provider` and `homePath`). This misleads plugin authors and hides the Codex/home override support implemented in the host API.</violation>
<violation number="2" location="docs/plugins/api.md:461">
P3: The description says ccusage queries “Claude Code” only, but the host API supports Codex too. Update the docs to describe it as provider-agnostic.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| } | ||
| } | ||
|
|
||
| const todayTokens = Number(todayEntry && todayEntry.totalTokens) || 0 |
There was a problem hiding this comment.
P2: ccusage daily entries don’t include totalTokens, so this will always treat token counts as 0. Compute totals from inputTokens/outputTokens/cache token fields instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At plugins/claude/plugin.js, line 487:
<comment>`ccusage` daily entries don’t include `totalTokens`, so this will always treat token counts as 0. Compute totals from `inputTokens`/`outputTokens`/cache token fields instead.</comment>
<file context>
@@ -397,6 +464,86 @@
+ }
+ }
+
+ const todayTokens = Number(todayEntry && todayEntry.totalTokens) || 0
+ const todayCost = usageCostUsd(todayEntry)
+ if (todayTokens > 0) {
</file context>
…rt multiple providers (Claude and Codex). Update query options for improved flexibility, and refine daily usage reporting structure. Modify plugin JSON files to include new metrics for "Yesterday" and "Last 30 Days". Update tests to validate new provider handling and ensure accurate token reporting.
|
@robinebers looked through this - I wonder what if ccusage is not installed? Seems we'll still show all these new stats (Today/Yesterday/Last 30 Days) and they'll remain always zeroed. When UPD: The same behavior is confirmed by the test |
Excellent point about the runner. Two things:
I do wonder how many people do not have at least one of the 4 runners installed. Nevertheless, I think hiding it if all runners failed is a good idea. Will add something along these lines. |
…e plugin Introduce a new function, dayKeyFromUsageDate, to parse various date formats for usage data. Update the plugin to utilize this function for improved date handling in usage reporting. Modify tests to reflect changes, ensuring accurate display of "Today" and "Yesterday" lines when no usage data is available, and validate locale-formatted date handling.
|
@robinebers I've misunderstood actually. Thought we'll try to run kind of globally installed ccusage. Now seeing npx (and alternatives) used for that seems my previous comments is not that valid anymore.
Yes, only considering we implicitly require runners to be installed - that's the valid case. Still good to be covered though 😄 |
…ctured status responses. Update API documentation to reflect new status envelopes for usage queries, including handling cases for no available runners and execution failures. Modify tests to validate new response structures and ensure accurate reporting of usage data.
… reusable function for displaying daily token usage. Update tests to ensure accurate representation of "Today" and "Yesterday" states, including handling cases with zero tokens.
|
Okay, I added a Rust to TypeScript result contract that now only renders the tokens when a runner is available and did not fail. A couple of things I'd love to improve in a future version:
|
|
Thanks for the changes @robinebers, don't have time to review there rn, planning to check later today (GMT +3), if you both @robinebers @davidarny think that the final form is OK it's OK to me also, sorry for the delay |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| - **Runtime runners**: Executes pinned `ccusage@18.0.5` (Claude) or `@ccusage/codex@18.0.5` (Codex) via fallback chain `bunx -> pnpm dlx -> yarn dlx -> npm exec -> npx` | ||
| - **Provider-aware**: Resolves provider from `opts.provider` or plugin id (`claude`/`codex`) | ||
| - **Offline only**: Reads local JSONL session files, no network requests |
There was a problem hiding this comment.
The docs state this API is “Offline only” with “no network requests”. However, the current implementation runs bunx/pnpm/yarn/npm/npx against a package spec, which can trigger network access to download the CLI the first time (or when cache is missing). Consider rewording to clarify that it doesn’t call provider APIs, but may require registry access to obtain the runner/package.
| - **Offline only**: Reads local JSONL session files, no network requests | |
| - **No provider API calls**: Usage is computed from local JSONL session files; the host does not call Claude/Codex (or other provider) APIs, but package runners may contact a package registry to download the `ccusage` CLI if it is not already available locally |
| const since = new Date() | ||
| since.setDate(since.getDate() - 31) | ||
| const y = since.getFullYear() | ||
| const m = since.getMonth() + 1 | ||
| const d = since.getDate() | ||
| const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d | ||
|
|
There was a problem hiding this comment.
since is computed as “today - 31 days”, but the UI label is “Last 30 Days” and the aggregation sums all returned daily entries. If ccusage treats --since as inclusive (typical), this can include 32 calendar days of data. Consider aligning the query/aggregation window to 30 days (e.g., use 29/30 days back, and/or cap client-side by day key).
| const since = new Date() | ||
| since.setDate(since.getDate() - 31) | ||
| const y = since.getFullYear() | ||
| const m = since.getMonth() + 1 | ||
| const d = since.getDate() | ||
| const sinceStr = "" + y + (m < 10 ? "0" : "") + m + (d < 10 ? "0" : "") + d | ||
| const queryOpts = { provider: "codex", since: sinceStr } |
There was a problem hiding this comment.
since is computed as “today - 31 days”, but the UI label is “Last 30 Days” and the aggregation sums all returned daily entries. If ccusage treats --since as inclusive (typical), this can include 32 calendar days of data. Consider aligning the query/aggregation window to 30 days (e.g., use 29/30 days back, and/or cap client-side by day key).
| const usageResult = queryTokenUsage(ctx) | ||
| if (usageResult.status === "ok") { | ||
| const usage = usageResult.data | ||
| const now = new Date() |
There was a problem hiding this comment.
queryTokenUsage is invoked on every probe, and the host implementation may spawn multiple external runner processes (plus runner detection) per call. Given probes can run on an interval, this can become a noticeable performance/latency hit. Consider adding a short TTL cache (the PR description mentions 60s) and/or only refreshing token usage when the detail view is open.
| const tokenUsageResult = queryTokenUsage(ctx) | ||
| if (tokenUsageResult.status === "ok") { | ||
| const tokenUsage = tokenUsageResult.data | ||
| const now = new Date() |
There was a problem hiding this comment.
queryTokenUsage is invoked on every probe, and the host implementation may spawn multiple external runner processes (plus runner detection) per call. Given probes can run on an interval, this can become a noticeable performance/latency hit. Consider adding a short TTL cache (the PR description mentions 60s) and/or only refreshing token usage when the detail view is open.
| var rawFn = __openusage_ctx.host.ccusage._queryRaw; | ||
| __openusage_ctx.host.ccusage.query = function(opts) { | ||
| var result = rawFn(JSON.stringify(opts || {})); | ||
| try { | ||
| var parsed = JSON.parse(result); | ||
| if (parsed && typeof parsed === "object" && typeof parsed.status === "string") { | ||
| return parsed; | ||
| } | ||
| } catch (e) {} | ||
| return { status: "runner_failed" }; |
There was a problem hiding this comment.
patch_ccusage_wrapper calls JSON.stringify(opts || {}) without a try/catch. If a plugin passes a non-serializable value (e.g., circular reference), this wrapper will throw and can abort the whole probe. Consider mirroring patch_ls_wrapper by catching stringify/parse failures and returning a stable fallback { status: "runner_failed" } (or no_runner) instead of throwing.
| let provider = resolve_ccusage_provider(&opts, &pid); | ||
| let runners = collect_ccusage_runners(); | ||
| if runners.is_empty() { | ||
| log::warn!( | ||
| "[plugin:{}] no package runner found for ccusage query", | ||
| pid | ||
| ); | ||
| return Ok(serde_json::json!({ "status": "no_runner" }).to_string()); |
There was a problem hiding this comment.
collect_ccusage_runners() is executed on every host.ccusage.query call, and resolve_ccusage_runner_binary spawns --version processes for each candidate path. Since probes can run on an interval, this adds unnecessary process churn even when the runner set never changes. Consider caching the resolved runner list (e.g., OnceLock + optional TTL) and only re-detecting on failure.
@copilot which model do you use for code reviews? |
|
great feedback thanks. will to iterate tomorrow! |
|
checked the current implementation, much much cleaner approach thanks @robinebers ! actually don’t see anything problematic from my side, but copilot blames some. agree with caching though, i don’t want to recalculate things over and over again if we have the computed result. we should improve this. ui doesn’t “bother” me though, but can be improved, yes. looks perfect to me for the first iteration of ccusage support! |
|
OK, making some minor changes and then merging this for now. let's roll it out and see :) |
…ex plugins. Change offline usage reporting to clarify that no provider API calls are made, and adjust date calculations to reflect a 31-day inclusive range. Enhance tests to validate new date handling and ensure accurate usage queries.
Description
Scan Claude's JSONL session logs to show "Today" and "Last 30 days" cost and token counts in the Claude plugin detail view.
This pull request introduces integration with the
ccusagelibrary to provide detailed, offline token usage and cost reporting for Claude Code. The integration spans backend (Rust, Tauri), plugin API, frontend plugin logic, and documentation, and includes comprehensive tests. The main focus is to enable plugins (starting with Claude) to query and display local token usage and cost metrics, with graceful fallback if dependencies are missing.Key changes:
ccusage Integration and Infrastructure
inject_ccusage) that exposes accusage.querymethod to plugins, spawning a Bun subprocess to run a bundled bridge script for local token usage queries. This includes robust resource path resolution, Bun binary detection, and result parsing with timeouts and logging. (src-tauri/src/plugin_engine/host_api.rsR1100-R1284, F73fb5fbL628R628)scripts/ccusage-bridge.mjsscript, which loads and aggregates daily usage data via theccusagelibrary, and outputs results in a plugin-friendly format.ccusageas a dependency inpackage.json.Plugin API and Documentation
host.ccusage.queryAPI indocs/plugins/api.md, detailing its usage, behavior, and result schema.Claude Plugin Usage Reporting
Testing and Helpers
ccusageAPI.References:
[1] F73fb5fbL628R628, [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
Related Issue
#19
https://x.com/RhysSullivan/status/2024183273577206090
😁
Type of Change
Testing
bun run buildand it succeededbun run testand all tests passbun tauri devScreenshots
Checklist
mainbranchSummary by cubic
Adds local, offline token and cost tracking via ccusage for Claude and Codex, showing Today, Yesterday, and Last 30 Days in plugin detail views (31-day inclusive). Replaces the bridge with a Rust host.ccusage.query that runs pinned ccusage CLIs across multiple runners and returns a status envelope.
New Features
Bug Fixes
Written for commit 03a220c. Summary will update on new commits.