feat(usage): Claude/Codex usage monitoring via ccusage sidecar#193
feat(usage): Claude/Codex usage monitoring via ccusage sidecar#193Fldicoahkiin wants to merge 18 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds usage monitoring endpoints to the API by invoking a bundled ChangesUsage Monitoring Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/api/src/routes/usage.rs`:
- Around line 43-55: The error messages in the ccusage spawn/path and stderr
branches leak absolute filesystem paths and stderr into API responses (see usage
of bin.to_string_lossy() and String::from_utf8_lossy(&output.stderr) inside
ApiError::internal), which later surface via
fetch_claude_usage/fetch_codex_usage into UsageReportDto.warnings; instead, log
full details (including bin path, error e, output.status and stderr) to the
server logs using your logger/tracing, and return a generic, path-free ApiError
message (e.g. "failed to spawn ccusage" or "ccusage exited with an error") from
the map_err and non-success branches; update the code around the
ApiError::internal calls in spawn/map_err and the output.status failure branch
to stop including bin.to_string_lossy() and raw stderr in the returned error
while retaining them in server-side logs.
- Around line 39-48: The ccusage child can be orphaned when tokio::time::timeout
drops the output() future; update the Command pipeline that builds the child in
the usage handler by calling .kill_on_drop(true) on the tokio::process::Command
(the same expression starting with
tokio::process::Command::new(bin).args(&args)) before invoking .output(), so
that timed-out children are killed; keep the existing timeout/error handling
using CCUSAGE_TIMEOUT and the ApiError mappings.
- Around line 88-101: The deserialization fails when ccusage emits null costs;
change the numeric cost fields to Option<f64> with #[serde(default)] so nulls
deserialize to None instead of causing an error: update CcClaudeDay.total_cost,
CcClaudeModel.cost, CcClaudeTotals.total_cost and the Codex costUSD mapping (the
field that maps to cost_usd) to Option<f64> with default, and adjust any
downstream mapping logic that currently expects f64 to pass through Option<f64>
into the existing output cost_usd: Option<f64> (i.e., map None -> None, Some(v)
-> Some(v)).
In `@crates/desktop/src-tauri/src/commands/server.rs`:
- Around line 35-38: The Windows sidecar path currently joins "ccusage" without
the executable suffix; update the map closure (the chain starting at
tauri::process::current_binary(&app.env()) -> exe.parent().map(|dir|
dir.join("ccusage"))) to append std::env::consts::EXE_SUFFIX to the filename
when joining (e.g., build the filename as "ccusage" + EXE_SUFFIX) so the
resolved path points to the staged .exe on Windows.
In `@scripts/fetch-ccusage.mjs`:
- Around line 102-113: The download code for pkg/tarballUrl creates tarballBytes
without verifying npm's dist.integrity; fetch the package metadata from the npm
registry for `${pkg}` and version `${CCUSAGE_VERSION}`, read its
`dist.integrity` (expecting the "sha512-..." value), compute a sha512 hash of
the downloaded tarballBytes and base64-encode it, then compare to the integrity
suffix and throw/log an error if they differ before any extraction or use;
integrate this verification in the same scope that defines pkg, tarballUrl, and
tarballBytes so the check runs immediately after downloading and before further
processing.
🪄 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: acd55984-a9d7-476c-b7e4-164aa93d20fd
⛔ Files ignored due to path filters (7)
Cargo.lockis excluded by!**/*.lockcrates/desktop/src/generated/dto/AgentUsageDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageDayDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageModelDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageReportDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageTotalsDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/index.tsis excluded by!**/generated/**
📒 Files selected for processing (12)
.gitignorecrates/api/Cargo.tomlcrates/api/src/bin/export-dto.rscrates/api/src/dto/mod.rscrates/api/src/dto/usage.rscrates/api/src/lib.rscrates/api/src/routes/mod.rscrates/api/src/routes/usage.rscrates/api/src/state.rscrates/desktop/src-tauri/src/commands/server.rscrates/desktop/src-tauri/tauri.conf.jsonscripts/fetch-ccusage.mjs
| let run = tokio::process::Command::new(bin).args(&args).output(); | ||
| let output = tokio::time::timeout(CCUSAGE_TIMEOUT, run) | ||
| .await | ||
| .map_err(|_| ApiError::internal("ccusage timed out after 30s"))? | ||
| .map_err(|e| { | ||
| ApiError::internal(format!( | ||
| "failed to spawn ccusage ({}): {e}", | ||
| bin.to_string_lossy() | ||
| )) | ||
| })?; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does tokio::process::Command kill the child on drop by default, or is kill_on_drop(true) required?
💡 Result:
By default, tokio::process::Command does not kill the child process when the Child handle is dropped [1][2]. It behaves similarly to the standard library's std::process::Command, where the spawned process will continue to execute even after the Child handle has been dropped [1][2]. To ensure that the child process is killed when the Child handle is dropped, you must explicitly call kill_on_drop(true) on your Command instance before spawning the process [1][2]. In summary: - Default behavior: Child process is NOT killed on drop (it continues running) [1][2]. - To kill on drop: You must explicitly set kill_on_drop(true) [1][2].
Citations:
- 1: https://docs.rs/tokio/latest/%20tokio/process/struct.Child.html
- 2: https://tikv.github.io/doc/tokio/process/struct.Command.html
Set kill_on_drop(true) to prevent orphaned ccusage processes on timeout
tokio::time::timeout drops the output() future on expiry, and tokio::process::Command does not kill the child by default when the child handle is dropped. Add kill_on_drop(true) so a timed-out ccusage subprocess is reclaimed.
♻️ Proposed change
- let run = tokio::process::Command::new(bin).args(&args).output();
+ let run = tokio::process::Command::new(bin)
+ .args(&args)
+ .kill_on_drop(true)
+ .output();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let run = tokio::process::Command::new(bin).args(&args).output(); | |
| let output = tokio::time::timeout(CCUSAGE_TIMEOUT, run) | |
| .await | |
| .map_err(|_| ApiError::internal("ccusage timed out after 30s"))? | |
| .map_err(|e| { | |
| ApiError::internal(format!( | |
| "failed to spawn ccusage ({}): {e}", | |
| bin.to_string_lossy() | |
| )) | |
| })?; | |
| let run = tokio::process::Command::new(bin) | |
| .args(&args) | |
| .kill_on_drop(true) | |
| .output(); | |
| let output = tokio::time::timeout(CCUSAGE_TIMEOUT, run) | |
| .await | |
| .map_err(|_| ApiError::internal("ccusage timed out after 30s"))? | |
| .map_err(|e| { | |
| ApiError::internal(format!( | |
| "failed to spawn ccusage ({}): {e}", | |
| bin.to_string_lossy() | |
| )) | |
| })?; |
🤖 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 `@crates/api/src/routes/usage.rs` around lines 39 - 48, The ccusage child can
be orphaned when tokio::time::timeout drops the output() future; update the
Command pipeline that builds the child in the usage handler by calling
.kill_on_drop(true) on the tokio::process::Command (the same expression starting
with tokio::process::Command::new(bin).args(&args)) before invoking .output(),
so that timed-out children are killed; keep the existing timeout/error handling
using CCUSAGE_TIMEOUT and the ApiError mappings.
| .map_err(|e| { | ||
| ApiError::internal(format!( | ||
| "failed to spawn ccusage ({}): {e}", | ||
| bin.to_string_lossy() | ||
| )) | ||
| })?; | ||
| if !output.status.success() { | ||
| return Err(ApiError::internal(format!( | ||
| "ccusage {:?} exited with {}: {}", | ||
| args, | ||
| output.status, | ||
| String::from_utf8_lossy(&output.stderr) | ||
| ))); |
There was a problem hiding this comment.
Spawn/exit error text leaks the sidecar's filesystem path (and stderr) into the API response.
bin.to_string_lossy() here is the absolute path to the bundled ccusage binary (e.g. /Users/<user>/.../ccusage). This ApiError string is unwrapped via e.body.error in fetch_claude_usage/fetch_codex_usage (Lines 266, 276) and pushed into warnings (Lines 327, 331), which is serialized in the UsageReportDto response. The same applies to the raw stderr on Line 54, which can contain home-directory JSONL paths. This exposes raw filesystem paths (and the OS username) to API clients.
Keep the detailed path/stderr in server-side logs only, and return a generic, path-free message in the error surfaced to callers.
🔒 Sketch: log details, return generic message
- .map_err(|e| {
- ApiError::internal(format!(
- "failed to spawn ccusage ({}): {e}",
- bin.to_string_lossy()
- ))
- })?;
+ .map_err(|e| {
+ log::warn!(
+ "failed to spawn ccusage ({}): {e}",
+ bin.to_string_lossy()
+ );
+ ApiError::internal("failed to spawn ccusage")
+ })?;
if !output.status.success() {
- return Err(ApiError::internal(format!(
- "ccusage {:?} exited with {}: {}",
- args,
- output.status,
- String::from_utf8_lossy(&output.stderr)
- )));
+ log::warn!(
+ "ccusage {:?} exited with {}: {}",
+ args,
+ output.status,
+ String::from_utf8_lossy(&output.stderr)
+ );
+ return Err(ApiError::internal("ccusage exited unsuccessfully"));
}As per coding guidelines: "NEVER expose raw filesystem paths in API responses".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .map_err(|e| { | |
| ApiError::internal(format!( | |
| "failed to spawn ccusage ({}): {e}", | |
| bin.to_string_lossy() | |
| )) | |
| })?; | |
| if !output.status.success() { | |
| return Err(ApiError::internal(format!( | |
| "ccusage {:?} exited with {}: {}", | |
| args, | |
| output.status, | |
| String::from_utf8_lossy(&output.stderr) | |
| ))); | |
| .map_err(|e| { | |
| log::warn!( | |
| "failed to spawn ccusage ({}): {e}", | |
| bin.to_string_lossy() | |
| ); | |
| ApiError::internal("failed to spawn ccusage") | |
| })?; | |
| if !output.status.success() { | |
| log::warn!( | |
| "ccusage {:?} exited with {}: {}", | |
| args, | |
| output.status, | |
| String::from_utf8_lossy(&output.stderr) | |
| ); | |
| return Err(ApiError::internal("ccusage exited unsuccessfully")); | |
| } |
🤖 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 `@crates/api/src/routes/usage.rs` around lines 43 - 55, The error messages in
the ccusage spawn/path and stderr branches leak absolute filesystem paths and
stderr into API responses (see usage of bin.to_string_lossy() and
String::from_utf8_lossy(&output.stderr) inside ApiError::internal), which later
surface via fetch_claude_usage/fetch_codex_usage into UsageReportDto.warnings;
instead, log full details (including bin path, error e, output.status and
stderr) to the server logs using your logger/tracing, and return a generic,
path-free ApiError message (e.g. "failed to spawn ccusage" or "ccusage exited
with an error") from the map_err and non-success branches; update the code
around the ApiError::internal calls in spawn/map_err and the output.status
failure branch to stop including bin.to_string_lossy() and raw stderr in the
returned error while retaining them in server-side logs.
| total_cost: f64, | ||
| #[serde(default)] | ||
| model_breakdowns: Vec<CcClaudeModel>, | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| #[serde(rename_all = "camelCase")] | ||
| struct CcClaudeModel { | ||
| model_name: String, | ||
| input_tokens: u64, | ||
| output_tokens: u64, | ||
| cache_creation_tokens: u64, | ||
| cache_read_tokens: u64, | ||
| cost: f64, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
ccusage daily --json output: is the per-model and daily cost field null for unpriced models?
💡 Result:
In the output of ccusage daily --json, the cost fields—specifically costUSD within the data entries and totalCost within the summary—are typically calculated based on token usage and known pricing models [1][2][3]. There is no specific behavior documented indicating that these fields become null for unpriced models; rather, ccusage relies on its internal pricing data to estimate these costs [4]. If a model is not recognized or lacks associated pricing data in the tool's configuration, the cost estimation logic may fail or produce a zero/default value depending on the calculation mode (auto, calculate, or display) [4]. Technical discussions and documentation suggest that when data is missing or a tool encounters an error, fields like totals may be returned as null [5], but for standard operation, the cost is a numerical estimate derived from tokens [1][3]. If you are encountering null values for cost fields, it is likely because the specific model version used does not exist in the tool's current pricing database, causing it to return an undefined or null cost calculation for those entries.
Citations:
- 1: https://github.com/ryoppippi/ccusage/blob/0adbb4f7/docs/guide/json-output.md
- 2: https://ccusage.com/guide/json-output
- 3: https://github.com/ryoppippi/ccusage/blob/main/docs/guide/json-output.md
- 4: https://github.com/ryoppippi/ccusage/blob/main/docs/guide/daily-reports.md
- 5: Inconsistent totals handling when no data exists across ccusage apps ccusage/ccusage#832
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the targeted Rust file around the cited lines
FILE="crates/api/src/routes/usage.rs"
echo "== $FILE =="
wc -l "$FILE"
# Show around the provided line range
sed -n '60,190p' "$FILE" | nl -ba | sed -n '1,140p'
# Search for the relevant cost fields/types in this file
rg -n "total_cost|costUSD|cost\b|model_breakdowns|CcClaudeModel" "$FILE"
# Show the full struct definitions if they are near those hits (bounded)
for pat in "struct .*CcClaudeModel" "costUSD" "totalCost" "total_cost"; do
echo "---- Searching for: $pat"
doneRepository: AkaraChen/aghub
Length of output: 193
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="crates/api/src/routes/usage.rs"
echo "== $FILE =="
wc -l "$FILE"
echo
echo "== Lines 70-130 =="
sed -n '70,130p' "$FILE" | cat -n
echo
echo "== Lines 130-220 =="
sed -n '130,220p' "$FILE" | cat -n
echo
echo "== Grep cost-related fields =="
rg -n "total_cost|totalCost|costUSD|cache_creation_tokens|cache_read_tokens|model_breakdowns|CcClaudeModel|cost:" "$FILE"
echo
echo "== Show derive/serde attributes near CcClaudeModel =="
rg -n "struct CcClaudeModel|derive\(Deserialize\)|rename_all|costUSD|totalCost" "$FILE" -nRepository: AkaraChen/aghub
Length of output: 7038
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="crates/api/src/routes/usage.rs"
echo "== Search for serde_json parsing + error handling =="
rg -n "serde_json::from_(slice|str)|from_slice|from_str" "$FILE"
echo
echo "== Search for warning/logging/error mapping =="
rg -n "warning|warn!|tracing::warn|error!|tracing::error|StatusCode|bad request|400|500" "$FILE"Repository: AkaraChen/aghub
Length of output: 566
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="crates/api/src/routes/usage.rs"
echo "== Lines 240-360 =="
sed -n '240,360p' "$FILE" | cat -nRepository: AkaraChen/aghub
Length of output: 3596
🌐 Web query:
ccusage daily --json costUSD null unpriced models
💡 Result:
The command ccusage daily --json allows you to export your daily token usage and cost data in a structured JSON format [1][2][3]. Regarding the specific parameters you mentioned: - costUSD: In the JSON output, this field represents the estimated cost in US dollars for the reported period [2][3]. It may appear as null if the cost cannot be calculated—for instance, if neither pre-calculated cost data is available in the logs nor sufficient model information exists to perform a calculation [4]. - unpriced models: When ccusage processes your data, it uses model pricing information (typically from LiteLLM) to estimate costs [4]. If a model used in your logs is not recognized or lacks a defined price in the pricing database, the tool may not be able to assign a specific dollar value to that activity [4]. In such cases, if you are using the default auto mode, the cost for that specific activity may be treated as $0.00, or the cost field may reflect a null or omitted value depending on the specific aggregation logic [4]. To manage these scenarios, you can use the --mode flag to control how costs are calculated [4][5]: - --mode auto (default): Uses pre-calculated costUSD values if available; otherwise, it calculates costs from tokens using known model pricing [4]. If no model information is available, it defaults to $0.00 [4]. - --mode calculate: Ignores any existing costUSD values and forces a re-calculation from token counts using current pricing [4]. - --mode display: Uses only the pre-calculated costUSD values found in your local files and will show $0.00 for any entries that lack them [4]. If you are seeing null or unexpected costs for certain models, ensuring your ccusage version is up to date (npx ccusage@latest) is recommended, as it fetches the most recent model pricing definitions [1][6].
Citations:
- 1: https://github.com/ryoppippi/ccusage/blob/dc83c23f/apps/ccusage/README.md
- 2: https://github.com/ryoppippi/ccusage/blob/0adbb4f7/docs/guide/daily-reports.md
- 3: https://github.com/ryoppippi/ccusage/blob/0adbb4f7/docs/guide/json-output.md
- 4: https://www.mintlify.com/ryoppippi/ccusage/advanced/cost-modes
- 5: https://github.com/ryoppippi/ccusage/blob/main/docs/guide/daily-reports.md
- 6: https://github.com/ryoppippi/ccusage/tree/cd88cb6e5d5801292609faf5e5e80d802e2789da
🌐 Web query:
site:github.com ryoppippi/ccusage costUSD null
💡 Result:
The term costUSD refers to a specific field found in the local JSONL usage logs generated by Claude Code [1][2]. The CLI tool ryoppippi/ccusage uses this field to track and report estimated costs for AI agent interactions [3][2]. Issues involving costUSD and null values in the context of ccusage typically arise from two main scenarios: 1. Missing Fields in Newer Versions: Newer versions of Claude Code and other coding agents sometimes omit the pre-calculated costUSD field in their logs [1][4]. To handle this, ccusage introduced flexible cost modes (e.g., --mode calculate), which allow the tool to compute costs from raw token counts (input, output, and cache tokens) using model pricing data instead of relying on the pre-calculated field [4][2]. 2. Inconsistent API/JSON Outputs: Users have reported inconsistencies where some components or sub-tools (such as ccusage-codex) historically used costUSD while others standardized on totalCost [5]. Additionally, when no usage data is present, some commands may return a null value for the totals object in JSON output, which has been identified as an inconsistency that the maintainers have addressed in various updates [6]. If you are encountering a null value for costUSD or an unexpected cost report, you can generally resolve or troubleshoot it by using the --mode calculate flag or verifying your version of the tool, as frequent updates are released to align these fields across the ccusage ecosystem [4][7].
Citations:
- 1: Claude Code 1.0.9 no longer reports the costUSD field ccusage/ccusage#4
- 2: https://github.com/ryoppippi/ccusage/blob/main/docs/guide/cost-modes.md
- 3: https://github.com/ryoppippi/ccusage/blob/main/docs/guide/index.md
- 4: https://github.com/ryoppippi/ccusage/releases/tag/v0.4.0
- 5: ccusage-codex uses costUSD instead of totalCost in totals dict (JSON output) ccusage/ccusage#831
- 6: Inconsistent totals handling when no data exists across ccusage apps ccusage/ccusage#832
- 7: https://github.com/ryoppippi/ccusage/
🌐 Web query:
ryoppippi ccusage issue 832 null totals cost
💡 Result:
Issue #832 in the ryoppippi/ccusage repository, titled "Inconsistent totals handling when no data exists across ccusage apps," identifies an issue where the JSON output of the ccusage CLI tools is inconsistent when no usage data is available [1]. Specifically, when generating reports such as daily, monthly, session, or weekly using the --json flag, different apps return varying structures for the totals field [1]. While some tools return a totals object with zeroed values, others may return a totals: null value or omit the totals field entirely [1]. This inconsistency complicates the development of scripts that rely on a predictable output schema across the ccusage ecosystem [1]. The issue report explicitly notes that: - The ccusage reference app is inconsistent internally: monthly and weekly commands include a totals field with zeros, while daily and session may omit it [1]. - Other related apps (like ccusage-codex, ccusage-opencode, ccusage-amp, and ccusage-pi) have been observed returning totals: null or lacking the expected zero-valued totals object [1]. The suggested resolution is to standardize the JSON output across all ccusage apps so that, even when no data exists, they consistently return a totals object populated with zeros (e.g., totals: {inputTokens: 0, outputTokens: 0, totalCost: 0, ...}) rather than null or missing fields [1]. This would improve reliability for automated usage analysis tools and scripts [1].
Citations:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="crates/api/src/routes/usage.rs"
echo "== Search DTO definitions in usage.rs =="
rg -n "struct Usage(DayDto|ModelDto|TotalsDto)|cost_usd|costUSD" "$FILE"
echo
echo "== If found, show the surrounding DTO structs =="
# Show a bounded window around the first occurrence of UsageDayDto/UsageTotalsDto/UsageModelDto
for line in $(rg -n "struct Usage(DayDto|ModelDto|TotalsDto)" "$FILE" | cut -d: -f1 | head -n 5); do
start=$((line-10))
end=$((line+60))
echo "--- window ${start}-${end} (around line $line) ---"
sed -n "${start},${end}p" "$FILE" | cat -n
doneRepository: AkaraChen/aghub
Length of output: 563
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="crates/api/src/routes/usage.rs"
rg -n "struct Usage(DayDto|TotalsDto|ModelDto)|UsageDayDto|UsageTotalsDto|UsageModelDto|cost_usd:" "$FILE"Repository: AkaraChen/aghub
Length of output: 598
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n --hidden "struct UsageDayDto|struct UsageTotalsDto|struct UsageModelDto|struct UsageReportDto" .Repository: AkaraChen/aghub
Length of output: 299
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="crates/api/src/dto/usage.rs"
echo "== $FILE =="
wc -l "$FILE"
echo
sed -n '1,120p' "$FILE" | cat -nRepository: AkaraChen/aghub
Length of output: 2645
Make ccusage cost fields tolerant of null to avoid dropping whole agent usage
In crates/api/src/routes/usage.rs, the ccusage parsing structs type cost as non-optional f64 (CcClaudeDay.total_cost, CcClaudeModel.cost, CcClaudeTotals.total_cost, and Codex costUSD mapped into cost_usd). If ccusage emits null (possible when cost can’t be calculated / pricing is missing), serde_json::from_slice will fail and that agent’s entire usage will be omitted with a warnings entry. Consider parsing these as Option<f64> (e.g., Option<f64> + #[serde(default)]) and mapping None to the existing output cost_usd: Option<f64>.
🤖 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 `@crates/api/src/routes/usage.rs` around lines 88 - 101, The deserialization
fails when ccusage emits null costs; change the numeric cost fields to
Option<f64> with #[serde(default)] so nulls deserialize to None instead of
causing an error: update CcClaudeDay.total_cost, CcClaudeModel.cost,
CcClaudeTotals.total_cost and the Codex costUSD mapping (the field that maps to
cost_usd) to Option<f64> with default, and adjust any downstream mapping logic
that currently expects f64 to pass through Option<f64> into the existing output
cost_usd: Option<f64> (i.e., map None -> None, Some(v) -> Some(v)).
| tauri::process::current_binary(&app.env()) | ||
| .ok() | ||
| .and_then(|exe| exe.parent().map(|dir| dir.join("ccusage"))) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Tauri externalBin sidecar bundled binary name on Windows: does it keep the .exe extension after stripping the target triple?
💡 Result:
Yes, the.exe extension must be kept after the target triple on Windows [1][2][3]. When bundling external binaries (sidecars) for Tauri, the binary must follow a specific naming convention: name-target-triple.extension [1][2]. On Windows, this means your bundled file should include both the target triple and the.exe extension (e.g., my-sidecar-x86_64-pc-windows-msvc.exe) [1][4][2]. Official Tauri documentation and examples consistently demonstrate that the extension is preserved as part of this naming pattern [1][2][3]. When preparing your sidecar binaries, you must ensure the extension is present for Windows targets [1][2]. Failure to include the.exe extension will prevent Tauri from correctly identifying and executing the sidecar on Windows [1][2].
Citations:
- 1: https://tauri.ubitools.com/develop/sidecar/
- 2: https://v2.tauri.app/develop/sidecar/
- 3: https://tauri.app/v1/guides/building/sidecar
- 4: https://tauri.ubitools.com/learn/sidecar-nodejs/
Fix Windows sidecar path to include the .exe suffix
On Windows, Tauri’s sidecar external binary keeps the .exe extension, so resolving dir.join("ccusage") (no extension) won’t find the staged binary. Use std::env::consts::EXE_SUFFIX.
🛠 Proposed change
tauri::process::current_binary(&app.env())
.ok()
- .and_then(|exe| exe.parent().map(|dir| dir.join("ccusage")))
+ .and_then(|exe| {
+ exe.parent().map(|dir| {
+ dir.join(format!("ccusage{}", std::env::consts::EXE_SUFFIX))
+ })
+ })🤖 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 `@crates/desktop/src-tauri/src/commands/server.rs` around lines 35 - 38, The
Windows sidecar path currently joins "ccusage" without the executable suffix;
update the map closure (the chain starting at
tauri::process::current_binary(&app.env()) -> exe.parent().map(|dir|
dir.join("ccusage"))) to append std::env::consts::EXE_SUFFIX to the filename
when joining (e.g., build the filename as "ccusage" + EXE_SUFFIX) so the
resolved path points to the staged .exe on Windows.
| const pkg = `@ccusage/ccusage-${platform}`; | ||
| const tarballUrl = `https://registry.npmjs.org/${pkg}/-/ccusage-${platform}-${CCUSAGE_VERSION}.tgz`; | ||
| console.log(`[fetch-ccusage] downloading ${pkg}@${CCUSAGE_VERSION}`); | ||
|
|
||
| const res = await fetch(tarballUrl); | ||
| if (!res.ok) { | ||
| throw new Error( | ||
| `download failed: ${res.status} ${res.statusText} (${tarballUrl})`, | ||
| ); | ||
| } | ||
| const tarballBytes = Buffer.from(await res.arrayBuffer()); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify current script does not validate npm dist.integrity and inspect metadata availability.
set -euo pipefail
echo "== Script check =="
rg -n 'dist\\.integrity|createHash|sha512-' scripts/fetch-ccusage.mjs || true
echo
echo "== npm metadata check (linux-x64 example) =="
curl -fsSL 'https://registry.npmjs.org/@ccusage/ccusage-linux-x64/20.0.6' \
| jq -r '.dist | {tarball, integrity}'Repository: AkaraChen/aghub
Length of output: 342
Add npm tarball integrity (SRI) verification before using/extracting the downloaded bytes
scripts/fetch-ccusage.mjs downloads the .tgz and converts it to tarballBytes without validating npm dist.integrity (no dist.integrity / createHash / sha512- hashing logic is present). npm metadata provides dist.integrity (e.g. sha512-*) for the package/version, so the script should fetch that and compare it against the downloaded tarball’s computed sha512 before extracting native binaries.
Suggested hardening patch
+import { createHash } from "node:crypto";
...
const pkg = `@ccusage/ccusage-${platform}`;
const tarballUrl = `https://registry.npmjs.org/${pkg}/-/ccusage-${platform}-${CCUSAGE_VERSION}.tgz`;
console.log(`[fetch-ccusage] downloading ${pkg}@${CCUSAGE_VERSION}`);
+
+ const metaUrl = `https://registry.npmjs.org/${pkg}/${CCUSAGE_VERSION}`;
+ const metaRes = await fetch(metaUrl);
+ if (!metaRes.ok) {
+ throw new Error(`metadata fetch failed: ${metaRes.status} ${metaRes.statusText}`);
+ }
+ const meta = await metaRes.json();
+ const expectedIntegrity = meta?.dist?.integrity; // e.g. "sha512-<base64>"
+ if (typeof expectedIntegrity !== "string" || !expectedIntegrity.startsWith("sha512-")) {
+ throw new Error(`missing/invalid dist.integrity for ${pkg}@${CCUSAGE_VERSION}`);
+ }
const res = await fetch(tarballUrl);
...
const tarballBytes = Buffer.from(await res.arrayBuffer());
+ const actualIntegrity =
+ "sha512-" + createHash("sha512").update(tarballBytes).digest("base64");
+ if (actualIntegrity !== expectedIntegrity) {
+ throw new Error(`integrity mismatch for ${pkg}@${CCUSAGE_VERSION}`);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const pkg = `@ccusage/ccusage-${platform}`; | |
| const tarballUrl = `https://registry.npmjs.org/${pkg}/-/ccusage-${platform}-${CCUSAGE_VERSION}.tgz`; | |
| console.log(`[fetch-ccusage] downloading ${pkg}@${CCUSAGE_VERSION}`); | |
| const res = await fetch(tarballUrl); | |
| if (!res.ok) { | |
| throw new Error( | |
| `download failed: ${res.status} ${res.statusText} (${tarballUrl})`, | |
| ); | |
| } | |
| const tarballBytes = Buffer.from(await res.arrayBuffer()); | |
| const pkg = `@ccusage/ccusage-${platform}`; | |
| const tarballUrl = `https://registry.npmjs.org/${pkg}/-/ccusage-${platform}-${CCUSAGE_VERSION}.tgz`; | |
| console.log(`[fetch-ccusage] downloading ${pkg}@${CCUSAGE_VERSION}`); | |
| const metaUrl = `https://registry.npmjs.org/${pkg}/${CCUSAGE_VERSION}`; | |
| const metaRes = await fetch(metaUrl); | |
| if (!metaRes.ok) { | |
| throw new Error(`metadata fetch failed: ${metaRes.status} ${metaRes.statusText}`); | |
| } | |
| const meta = await metaRes.json(); | |
| const expectedIntegrity = meta?.dist?.integrity; // e.g. "sha512-<base64>" | |
| if (typeof expectedIntegrity !== "string" || !expectedIntegrity.startsWith("sha512-")) { | |
| throw new Error(`missing/invalid dist.integrity for ${pkg}@${CCUSAGE_VERSION}`); | |
| } | |
| const res = await fetch(tarballUrl); | |
| if (!res.ok) { | |
| throw new Error( | |
| `download failed: ${res.status} ${res.statusText} (${tarballUrl})`, | |
| ); | |
| } | |
| const tarballBytes = Buffer.from(await res.arrayBuffer()); | |
| const actualIntegrity = | |
| "sha512-" + createHash("sha512").update(tarballBytes).digest("base64"); | |
| if (actualIntegrity !== expectedIntegrity) { | |
| throw new Error(`integrity mismatch for ${pkg}@${CCUSAGE_VERSION}`); | |
| } |
🤖 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 `@scripts/fetch-ccusage.mjs` around lines 102 - 113, The download code for
pkg/tarballUrl creates tarballBytes without verifying npm's dist.integrity;
fetch the package metadata from the npm registry for `${pkg}` and version
`${CCUSAGE_VERSION}`, read its `dist.integrity` (expecting the "sha512-..."
value), compute a sha512 hash of the downloaded tarballBytes and base64-encode
it, then compare to the integrity suffix and throw/log an error if they differ
before any extraction or use; integrate this verification in the same scope that
defines pkg, tarballUrl, and tarballBytes so the check runs immediately after
downloading and before further processing.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@justfile`:
- Line 18: The `test` recipe references a missing `_fetch-ccusage` dependency
causing `just test` to fail; either add a `_fetch-ccusage` recipe that performs
the intended setup (e.g., fetch or generate CC usage data) and ensure it is
defined before or in the same justfile, or remove `_fetch-ccusage` from the
`test` recipe's dependency list if it's no longer required; update the `test`
recipe (and add a short comment) to reflect the chosen approach so `just` can
resolve all recipes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
|
@Fldicoahkiin 可以在这个新 crate 的 build.rs 里加上 fetch binary 的环节,就不用手动做了 |
|
@AkaraChen 我一开始也是准备放build.rs的但是感觉编译期跑外部下载有点诡异,, |
- aghub-api: GET /api/v1/usage/summary spawns the bundled ccusage binary and normalizes claude + codex --json into a unified DTO (token counts + USD cost); the two calls run in parallel and degrade per-agent on failure - export the unified DTOs through ts-rs (UsageReportDto, AgentUsageDto, UsageDayDto, UsageModelDto, UsageTotalsDto) - bundle ccusage as a Tauri sidecar: scripts/fetch-ccusage.mjs fetches the per-target-triple prebuilt binary at build time, wired via externalBin and beforeBuildCommand; the binaries dir stays out of git - resolve the sidecar path via current_binary and inject it through ApiOptions/UsageState, falling back to AGHUB_CCUSAGE_BIN then PATH
- tauri-build validates the externalBin path at compile time, but the ccusage binary is gitignored and only fetched by tauri's beforeBuildCommand, which plain cargo build/clippy never trigger - CI's `just lint`/`test`/`build` therefore failed compiling the desktop crate on a clean checkout (resource path binaries/ccusage-... missing) - add a `_fetch-ccusage` just task; make dev/build/test/lint depend on it so any just-driven cargo compile fetches the per-triple binary first - CI already runs through just, so the workflow needs no change
The previous commit added `_fetch-ccusage` as a dependency of the test task but never defined the recipe, so `just` aborted with "unknown dependency". Define the recipe and make both lint and test depend on it (they compile the desktop crate, which needs the gitignored sidecar binary present for tauri-build's externalBin check).
- port scripts/fetch-ccusage.mjs into src-tauri/build.rs via ureq + flate2 + tar - drop the node fetch from beforeBuildCommand and the _fetch-ccusage justfile recipe - workspace builds now stage the sidecar before tauri-build validates externalBin
- GET /api/v1/usage/limits queries each vendor's private OAuth usage endpoint for how much of the current rate-limit window is left, complementing usage/summary (which only reports consumed tokens). - Reuses the OAuth token from each agent's local credential store: Claude from the macOS login keychain (service "Claude Code-credentials") or ~/.claude/.credentials.json; Codex from ~/.codex/auth.json. - Unified DTOs (UsageLimitsReportDto / AgentLimitsDto / LimitWindowDto) exported via ts-rs. Claude windows: 5h, weekly, weekly_opus, weekly_sonnet; Codex primary/secondary parsed defensively. - Degrades per-agent: a not-logged-in or failing agent becomes a warnings entry instead of failing the whole request.
110a7f6 to
eb081ec
Compare
|
更新了描述并且支持了claude/codex 订阅额度查询(详见How to test) |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
crates/api/src/routes/usage.rs (2)
46-58:⚠️ Potential issue | 🟠 MajorStop returning the sidecar path and raw stderr to clients.
fetch_claude_usage()andfetch_codex_usage()forwarde.body.errorintoUsageReportDto.warnings, so theseApiError::internal(...)strings exposebin.to_string_lossy()and rawstderrin the HTTP response. Keep the detailed path/stderr in server logs only and return a generic message here instead.As per coding guidelines, "NEVER expose raw filesystem paths in API responses".
🤖 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 `@crates/api/src/routes/usage.rs` around lines 46 - 58, The ApiError::internal messages in fetch_claude_usage()/fetch_codex_usage() currently include sensitive data (bin.to_string_lossy() path and raw output.stderr); instead, log the detailed error (including bin, args, output.stderr) to the server log and return a generic error message in the ApiError::internal returned to the client (e.g., "failed to invoke usage reporter" or "usage reporter failed"); update the error branches around spawn (the closure using bin) and the !output.status branch to call the logger with the full details and construct ApiError::internal with a non-sensitive, generic string while keeping the original detailed text out of the HTTP response.
84-115:⚠️ Potential issue | 🟠 MajorMake ccusage cost fields nullable at the deserialization boundary.
The PR context explicitly calls out
nullcost for unpriced models, but these ccusage structs still deserialize cost fields as plainf64. Onenullmakesserde_json::from_slicefail and drops the entire agent intowarningsinstead of returning partial usage. Parse those fields asOption<f64>with#[serde(default)], then pass them through to the existing DTOcost_usd: Option<f64>fields.In Rust serde, can a JSON field with value `null` deserialize into `f64`, or does it need `Option<f64>` and/or `#[serde(default)]` to accept `null`? Please cite the serde documentation.Also applies to: 128-160, 173-215, 220-260
🤖 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 `@crates/api/src/routes/usage.rs` around lines 84 - 115, The cost fields in the ccusage deserialization structs (e.g., CcClaudeModel.cost, CcClaudeDay.total_cost, CcClaudeTotals.total_cost and any other cost f64s in the indicated ranges) must accept JSON nulls: change their types from f64 to Option<f64> and add #[serde(default)] to each such field so serde will accept missing or null values; then propagate those Option<f64> values into the existing DTO field cost_usd: Option<f64> when mapping. Locate and update CcClaudeDay, CcClaudeModel, CcClaudeTotals (and the analogous structs in the other ranges) accordingly.
🧹 Nitpick comments (1)
crates/desktop/src-tauri/build.rs (1)
47-49: 💤 Low valueWrap the npm tarball URL to stay under the Rust line-width limit.
As per coding guidelines,
**/*.rs: Maintain maximum line width of 80 characters in Rust code.🤖 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 `@crates/desktop/src-tauri/build.rs` around lines 47 - 49, The URL string assigned to `url` exceeds the Rust 80-char line-width; update the `format!` call in `build.rs` (the `url` variable) to wrap the literal so lines stay <=80 chars—either split the string into adjacent quoted pieces or move interpolated parts into `format!` arguments (e.g. use "https://registry.npmjs.org/" "{}", "/-/ccusage-" "{}" "-" "{}.tgz" with `pkg`, `platform`, `CCUSAGE_VERSION`) while keeping the same `format!`/variable names.
🤖 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 `@crates/api/src/routes/usage.rs`:
- Around line 387-390: The map_err closures that build errors from reading
credential files (the block that constructs path =
home_dir()?.join(".claude/.credentials.json") and the similar block for
".codex") currently embed path.display() into the returned error string and
those errors get serialized into usage_limits() warnings; change them to return
a generic, path-free error message (e.g., "failed to read credentials" or
"failed to read codex credentials") while logging the full path and underlying
error to server logs instead. Locate the read_to_string(...).map_err(...)
closures and replace the map_err payload with a non-sensitive message, and add a
server-side log call (using the existing logger) that includes path.display()
and the original error for diagnostics; do the same for the other block around
lines 405-410 that reads the ".codex" file.
In `@crates/desktop/src-tauri/build.rs`:
- Around line 46-57: The download step that fetches the ccusage tarball
(variables pkg, url, resp, tarball and constant CCUSAGE_VERSION) lacks integrity
verification; before reading and extracting resp.into_reader() or bundling
tarball, compute and verify a cryptographic digest or signature (e.g., SHA256 of
tarball or a detached signature) against a trusted value supplied via
environment/config (or fetched securely from a signed metadata endpoint), and
fail the build with a clear error if verification fails; ensure this
verification is performed immediately after reading the response into tarball
and before any further processing, and surface detailed errors on mismatch or
missing expected digest/signature.
- Around line 39-42: The build script currently returns early if dest.exists(),
but dest is version-independent so old ccusage binaries can be reused across
CCUSAGE_VERSION bumps; update the logic in build.rs to invalidate and redownload
when CCUSAGE_VERSION changes by either (A) including CCUSAGE_VERSION in the dest
filename/path (so dest depends on CCUSAGE_VERSION) or (B) add a small version
marker (e.g. write/read a .ccusage_version file in the binaries directory) and
if the marker != CCUSAGE_VERSION delete the existing binary and proceed to
download; adjust the early-return that checks dest.exists() (and the variable
named dest) to perform the version check before returning.
---
Duplicate comments:
In `@crates/api/src/routes/usage.rs`:
- Around line 46-58: The ApiError::internal messages in
fetch_claude_usage()/fetch_codex_usage() currently include sensitive data
(bin.to_string_lossy() path and raw output.stderr); instead, log the detailed
error (including bin, args, output.stderr) to the server log and return a
generic error message in the ApiError::internal returned to the client (e.g.,
"failed to invoke usage reporter" or "usage reporter failed"); update the error
branches around spawn (the closure using bin) and the !output.status branch to
call the logger with the full details and construct ApiError::internal with a
non-sensitive, generic string while keeping the original detailed text out of
the HTTP response.
- Around line 84-115: The cost fields in the ccusage deserialization structs
(e.g., CcClaudeModel.cost, CcClaudeDay.total_cost, CcClaudeTotals.total_cost and
any other cost f64s in the indicated ranges) must accept JSON nulls: change
their types from f64 to Option<f64> and add #[serde(default)] to each such field
so serde will accept missing or null values; then propagate those Option<f64>
values into the existing DTO field cost_usd: Option<f64> when mapping. Locate
and update CcClaudeDay, CcClaudeModel, CcClaudeTotals (and the analogous structs
in the other ranges) accordingly.
---
Nitpick comments:
In `@crates/desktop/src-tauri/build.rs`:
- Around line 47-49: The URL string assigned to `url` exceeds the Rust 80-char
line-width; update the `format!` call in `build.rs` (the `url` variable) to wrap
the literal so lines stay <=80 chars—either split the string into adjacent
quoted pieces or move interpolated parts into `format!` arguments (e.g. use
"https://registry.npmjs.org/" "{}", "/-/ccusage-" "{}" "-" "{}.tgz" with `pkg`,
`platform`, `CCUSAGE_VERSION`) while keeping the same `format!`/variable names.
🪄 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: f3674d99-6548-442a-a0f2-954d820c49b7
⛔ Files ignored due to path filters (10)
Cargo.lockis excluded by!**/*.lockcrates/desktop/src/generated/dto/AgentLimitsDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/AgentUsageDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/LimitWindowDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageDayDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageLimitsReportDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageModelDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageReportDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/UsageTotalsDto.tsis excluded by!**/generated/**crates/desktop/src/generated/dto/index.tsis excluded by!**/generated/**
📒 Files selected for processing (13)
.gitignorecrates/api/Cargo.tomlcrates/api/src/bin/export-dto.rscrates/api/src/dto/mod.rscrates/api/src/dto/usage.rscrates/api/src/lib.rscrates/api/src/routes/mod.rscrates/api/src/routes/usage.rscrates/api/src/state.rscrates/desktop/src-tauri/Cargo.tomlcrates/desktop/src-tauri/build.rscrates/desktop/src-tauri/src/commands/server.rscrates/desktop/src-tauri/tauri.conf.json
✅ Files skipped from review due to trivial changes (4)
- crates/desktop/src-tauri/Cargo.toml
- crates/api/src/routes/mod.rs
- .gitignore
- crates/api/src/dto/mod.rs
🚧 Files skipped from review as they are similar to previous changes (4)
- crates/api/Cargo.toml
- crates/api/src/state.rs
- crates/desktop/src-tauri/src/commands/server.rs
- crates/api/src/lib.rs
| let path = home_dir()?.join(".claude/.credentials.json"); | ||
| let json = std::fs::read_to_string(&path) | ||
| .map_err(|e| format!("read {}: {e}", path.display()))?; | ||
| parse(&json) |
There was a problem hiding this comment.
Credential read failures still leak absolute home paths in /usage/limits.
Both helpers format path.display() into their error strings, and usage_limits() serializes those strings into warnings on Lines 581 and 585. That exposes user-specific ~/.claude / ~/.codex filesystem paths to API callers. Return a path-free message here and keep the full path only in server-side logs.
As per coding guidelines, "NEVER expose raw filesystem paths in API responses".
Also applies to: 405-410
🤖 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 `@crates/api/src/routes/usage.rs` around lines 387 - 390, The map_err closures
that build errors from reading credential files (the block that constructs path
= home_dir()?.join(".claude/.credentials.json") and the similar block for
".codex") currently embed path.display() into the returned error string and
those errors get serialized into usage_limits() warnings; change them to return
a generic, path-free error message (e.g., "failed to read credentials" or
"failed to read codex credentials") while logging the full path and underlying
error to server logs instead. Locate the read_to_string(...).map_err(...)
closures and replace the map_err payload with a non-sensitive message, and add a
server-side log call (using the existing logger) that includes path.display()
and the original error for diagnostics; do the same for the other block around
lines 405-410 that reads the ".codex" file.
| // Filename is version-independent, so bumping CCUSAGE_VERSION needs binaries/ cleared. | ||
| if dest.exists() { | ||
| return; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Invalidate the cached sidecar when CCUSAGE_VERSION changes.
dest is version-independent, so once binaries/ccusage-<triple> exists this build script stops downloading forever. A later version bump will still package the old executable until someone manually deletes binaries/, which makes upgrades easy to ship incorrectly.
Proposed fix
+ let version_marker =
+ binaries_dir.join(format!("ccusage-{triple}.version"));
+
// Filename is version-independent, so bumping CCUSAGE_VERSION needs binaries/ cleared.
- if dest.exists() {
+ if dest.exists()
+ && fs::read_to_string(&version_marker).ok().as_deref()
+ == Some(CCUSAGE_VERSION)
+ {
return;
}
@@
fs::rename(&tmp, &dest).expect("rename sidecar into place");
+ fs::write(&version_marker, CCUSAGE_VERSION)
+ .expect("write sidecar version marker");
staged = true;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Filename is version-independent, so bumping CCUSAGE_VERSION needs binaries/ cleared. | |
| if dest.exists() { | |
| return; | |
| } | |
| let version_marker = | |
| binaries_dir.join(format!("ccusage-{triple}.version")); | |
| // Filename is version-independent, so bumping CCUSAGE_VERSION needs binaries/ cleared. | |
| if dest.exists() | |
| && fs::read_to_string(&version_marker).ok().as_deref() | |
| == Some(CCUSAGE_VERSION) | |
| { | |
| return; | |
| } |
🤖 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 `@crates/desktop/src-tauri/build.rs` around lines 39 - 42, The build script
currently returns early if dest.exists(), but dest is version-independent so old
ccusage binaries can be reused across CCUSAGE_VERSION bumps; update the logic in
build.rs to invalidate and redownload when CCUSAGE_VERSION changes by either (A)
including CCUSAGE_VERSION in the dest filename/path (so dest depends on
CCUSAGE_VERSION) or (B) add a small version marker (e.g. write/read a
.ccusage_version file in the binaries directory) and if the marker !=
CCUSAGE_VERSION delete the existing binary and proceed to download; adjust the
early-return that checks dest.exists() (and the variable named dest) to perform
the version check before returning.
| let pkg = format!("@ccusage/ccusage-{platform}"); | ||
| let url = format!( | ||
| "https://registry.npmjs.org/{pkg}/-/ccusage-{platform}-{CCUSAGE_VERSION}.tgz" | ||
| ); | ||
|
|
||
| let resp = ureq::get(&url) | ||
| .call() | ||
| .unwrap_or_else(|e| panic!("download {url} failed: {e}")); | ||
| let mut tarball = Vec::new(); | ||
| resp.into_reader() | ||
| .read_to_end(&mut tarball) | ||
| .expect("read ccusage tarball"); |
There was a problem hiding this comment.
Verify the tarball before extracting and bundling it.
This build step downloads a prebuilt executable and ships it inside the desktop app, but it never checks an expected digest or signature. If the registry artifact is tampered with, the compromised binary is what gets bundled.
🤖 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 `@crates/desktop/src-tauri/build.rs` around lines 46 - 57, The download step
that fetches the ccusage tarball (variables pkg, url, resp, tarball and constant
CCUSAGE_VERSION) lacks integrity verification; before reading and extracting
resp.into_reader() or bundling tarball, compute and verify a cryptographic
digest or signature (e.g., SHA256 of tarball or a detached signature) against a
trusted value supplied via environment/config (or fetched securely from a signed
metadata endpoint), and fail the build with a clear error if verification fails;
ensure this verification is performed immediately after reading the response
into tarball and before any further processing, and surface detailed errors on
mismatch or missing expected digest/signature.
Move the ccusage shell-out, normalization, and vendor limit-endpoint logic out of crates/api into a standalone aghub-usage crate so it can be unit-tested without Rocket or the api binary. The api routes become thin handlers over aghub_usage::summary / aghub_usage::limits, and the usage DTOs now live in the new crate (re-exported for ts-rs generation). - run_ccusage returns String instead of ApiError (no api coupling) - resolve_ccusage_bin centralizes sidecar/env/PATH lookup for callers - add unit tests for codex_window parsing and claude/codex normalization
Expose the aghub-usage crate through aghub-cli: aghub-cli usage summary [--since --until --timezone] aghub-cli usage limits Both print the same JSON the desktop app consumes. Usage is agent-agnostic, so it is dispatched before agent/scope resolution and config loading. The CLI does not bundle the ccusage sidecar, so summary resolves it from AGHUB_CCUSAGE_BIN or PATH.
|
@codex 帮我 resolve 掉 outdate 的 comment,顺便解决一下跟主分支的冲突 |
|
Summary
Main-branch conflict note
Testing
References consulted
|
|
Summary
Testing
References consulted
|
ccusage emits a null cost when it cannot price a model (e.g. a freshly released model with no pricing data yet). The deserialization structs typed cost as f64, so a single null failed the whole parse and dropped that agent's entire report. - make the five ccusage cost fields Option<f64> with serde(default) - drop the Some() wrappers in claude_to_agent / codex_to_agent - add a null-cost deserialization regression test The DTO already exposed cost_usd as Option<f64>, so no TS regeneration.
- run_usage_report and run_limits_report use tokio::join!, which is gated behind tokio's "macros" feature - workspace builds passed via feature unification (another member enabled it), but cargo check -p aghub-usage failed standalone with "could not find join in tokio"
…usage - drop the binary path and credential/auth file paths from error strings; they leak local layout into API responses without aiding diagnosis - set kill_on_drop(true) so a ccusage child is reaped when the 30s timeout fires and the output future is dropped, instead of lingering
- Tauri strips the -<triple> part of the sidecar name but keeps the executable extension, so on Windows the bundled file is ccusage.exe - joining a bare "ccusage" missed it; use std::env::consts::EXE_SUFFIX so the lookup works on every platform the sidecar is fetched for
- Tauri resolves the sidecar by its fixed ccusage-<triple> name, so the filename can't carry a version; the old early-return kept a stale binary after a CCUSAGE_VERSION bump - record the staged version in a sibling .ccusage-<triple>.version stamp and re-fetch whenever it no longer matches - fix the .gitignore comment that still referenced the removed scripts/fetch-ccusage.mjs
- read the version's dist.tarball URL and dist.integrity hash from the package metadata (abbreviated install-v1 format) instead of constructing the URL and trusting whatever bytes come back - compute the tarball's SHA-512 and compare it to the registry's sha512-<base64> integrity string; a mismatch fails the build - the integrity ships with each version's metadata, so bumping CCUSAGE_VERSION needs no hardcoded hash to maintain - add sha2, base64, serde_json build-dependencies
# Conflicts: # Cargo.lock
# Conflicts: # crates/api/Cargo.toml
# Conflicts: # crates/api/src/lib.rs
# Conflicts: # crates/api/src/lib.rs # crates/desktop/src-tauri/src/commands/server.rs
# Conflicts: # crates/api/Cargo.toml
Closes #197
What
Adds usage monitoring for Claude Code and Codex, exposed both over HTTP (consumed later by the desktop home page, #141) and as
aghub-cli usagesubcommands. Two complementary reports:ccusageas a build-time-fetched sidecar rather than reimplementing JSONL parsing/dedup/pricing.The reporting + normalization logic lives in a new
aghub-usagecrate, shared by both the API server and the CLI.How
aghub-usagecrateccusage claude daily --json --offlineandcodex daily --json --offlinein parallel (tokio::join!) and normalizes the two different per-agent JSON shapes into one DTO; also queries the two limits endpoints.warningsinstead of failing the whole report.ts-rs:UsageReportDto / AgentUsageDto / UsageDayDto / UsageModelDto / UsageTotalsDtoandUsageLimitsReportDto / AgentLimitsDto / LimitWindowDto. Token counts areu64→ TSnumber;cost_usdis nullable (unpriced third-party models report tokens withnullcost rather than a fabricated number).ccusageis spawned withkill_on_drop(true)behind a 30s timeout so a hung child is reaped on timeout; error strings carry no local filesystem paths.GET /api/v1/usage/summary?since&until&timezone(consumed)ccusagebinary, runs the twodaily --jsoncalls in parallel, and returns the unified usage DTO.GET /api/v1/usage/limits(remaining quota)GET api.anthropic.com/api/oauth/usage(Bearer +anthropic-beta: oauth-2025-04-20). Windows:5h,weekly, and optionalweekly_opus/weekly_sonnet.GET chatgpt.com/backend-api/wham/usage(Bearer +ChatGPT-Account-Id).primary→5h,secondary→weekly, parsed defensively (the shape is undocumented and field names vary by version).Claude Code-credentials), falling back to~/.claude/.credentials.json.~/.codex/auth.json.utilization_pct(0–100; Codex'spercent_leftis inverted so all windows share one meaning) and an optional ISOresets_at. Same per-agent degradation assummary.aghub-cli usage(new)The same two reports without the desktop app:
Agent-agnostic (no
-aneeded; dispatched before agent resolution). Prints the same JSON the API returns. The CLI does not bundle the sidecar —summaryresolvesccusagefromAGHUB_CCUSAGE_BINthenPATH.ccusage as a Tauri sidecar
crates/desktop/src-tauri/build.rsdownloads the prebuilt per-target-triple binary at build time (pure-Rust build-deps:ureq+flate2+tar; no npm/JS), stages it intobinaries/ccusage-<triple>and wires it viaexternalBin. All six supported triples are mapped (darwin / linux / windows × arm64 / x64). Binaries stay out of git; fails hard offline. (Replaces the earlierscripts/fetch-ccusage.mjs.)CCUSAGE_VERSIONverifies against that version's own published hash. A tampered or truncated download panics the build instead of shipping a bad binary.ccusage-<triple>name, so the filename can't carry a version; the staged version is tracked in a sibling.ccusage-<triple>.versionstamp file and re-downloaded wheneverCCUSAGE_VERSIONchanges.ccusagenext to the executable (keeping the platform.exesuffix on Windows); dev falls back toAGHUB_CCUSAGE_BINthenPATH.Verification
cargo clippy --workspace -- -D warnings✅cargo test -p aghub-usage✅ (15 tests: Claude/Codex normalization, limit-window parsing, DTO bindings export)just fmt(cargo fmt + prettier) ✅usage/summary: live request against real local data matches theccusageCLI (Claude/Codex totals).nullcost for unpriced third-party models (e.g.mimo-*); tokens still counted.How to test
CLI (quickest)
API server
Run the server standalone (listens on
127.0.0.1:8000):usage/limits(remaining quota)Needs the agent logged in on the same machine (it reads the local OAuth token):
Claude Code-credentials, or~/.claude/.credentials.json).~/.codex/auth.jsonmust containtokens.access_token+tokens.account_id. API-key-only Codex setups have no subscription quota and report awarningsentry.curl -s http://127.0.0.1:8000/api/v1/usage/limits | jqExpected shape (verified live against the Claude endpoint):
{ "agents": [ { "agent": "claude", "windows": [ { "kind": "5h", "utilization_pct": 4.0, "resets_at": "2026-06-02T22:20:00+00:00" }, { "kind": "weekly", "utilization_pct": 2.0, "resets_at": "2026-06-09T12:00:00+00:00" }, { "kind": "weekly_sonnet", "utilization_pct": 0.0, "resets_at": "2026-06-09T12:00:00+00:00" } ]} ], "generated_at": "2026-06-02T18:40:29+00:00", "warnings": ["codex limits unavailable: ..."] }A not-logged-in / failing agent does not 500 the request — it appears in
warningsand the other agent still returns.weekly_opus/weekly_sonnetare optional and omitted when the endpoint doesn't report them.usage/summary(consumed tokens + cost)Needs the
ccusagebinary. In a packaged desktop build it's the bundled sidecar; for the standalone server point at one explicitly:Without it, both agents degrade to a
warningsentry (failed to spawn ccusage ...).Regenerating TS bindings
DTO changes must be mirrored to the frontend:
Out of scope (follow-ups)
usage/limitswas developed from researched endpoint shapes; a live round-trip against the real endpoints still needs confirming on a logged-in machine (Codex's response shape in particular is undocumented and handled defensively).tauri buildpackaging size check on a real bundle (sidecar adds ~2.6–3.8 MB per platform).Summary by CodeRabbit
New Features