Skip to content

feat: add headless usage command with agent skill#35

Merged
ElbertePlinio merged 3 commits into
mainfrom
feat/usage-cli
Jul 10, 2026
Merged

feat: add headless usage command with agent skill#35
ElbertePlinio merged 3 commits into
mainfrom
feat/usage-cli

Conversation

@ElbertePlinio

Copy link
Copy Markdown
Member

Adds pickgauge usage --json (and a human pickgauge usage table): a headless command that prints a stable, versioned JSON snapshot of every enabled service's quota headroom and exits — no tray, no window. Agents read this before dispatching a wave.

Closes #26 (PR 4 of 4 — #28 grok.cli, #27 ollama.local, #34 web full-gauges all merged).

What changed

  • Args intercepted in lib.rs::run() before the Tauri builder; the tray path moved to run_tray(). Unknown args → help + exit 2.
  • Headless flow: read-only config load → UsageEngine::new_headlessrefresh_all() (live cli/local/daemon readings) → overlay persisted web-tier snapshots → the same merge the tray uses.
  • New snapshot_store.rs: the tray app persists sanitized per-provider snapshots to <app-data>/snapshots.json (atomic tmp+rename) after each refresh, so headless runs see cookie-gauge data (grok.web/ollama.web) fetched while the tray runs. Corrupt/missing/version-mismatched files are ignored.
  • Stable schema documented in docs/usage-json.md (version 1, golden-fixture tested).
  • Agent skill at skills/pickgauge-usage/SKILL.md (canonical; home-dir mirrors are a post-merge maintainer step).
  • README: "The gauge your agents read" section + privacy note for the new on-disk snapshots file (numbers/labels only — no tokens, cookies, or identity).

Tested

  • cargo test --locked --all-targets: 273 passed, 1 ignored.
  • bun run check clean; bun run test: 66 + 21 sidecar.
  • Live end-to-end on a real machine: cargo run -- usage --json | jq exits headlessly with real Codex/Claude gauges (parsed, staleSeconds 3); isolated default-config smoke shows all four services incl. Grok/Ollama plan-only rows. Human table verified.

Not tested yet

  • Headless overlay with a real populated snapshots.json from a long-running tray session (unit-covered; happens naturally after this release runs).

Risks

  • snapshots.json is a new persisted file; write failures are deliberately non-fatal (let _ =) so the tray never breaks over it.

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d24ea0b40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/lib.rs
Comment on lines +1098 to +1099
if let Ok(snapshots) = engine.raw_snapshots() {
let _ = snapshot_store::save_in(&app_data_dir, &snapshots, &display_state.updated_at);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear the persisted snapshot cache too

Because snapshot persistence now happens only from after_snapshots_updated, the existing Settings → Clear cache path (clear_cached_snapshots) clears the in-memory engine and emits the update but never rewrites/removes snapshots.json. After a user clears cached usage, pickgauge usage --json can still load the old file and overlay the supposedly-cleared browser reading, so the clear action is incomplete for the new headless export.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e9387f9: the clear_cached_snapshots command now calls snapshot_store::clear_in (lib.rs:947), which removes snapshots.json non-fatally; covered by store-level tests.

Comment on lines +77 to +79
if args[0].as_os_str() != OsStr::new("usage") {
return Err(());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass non-usage argv through to the tray

With this branch, any first argument other than usage makes try_run_from_env() return an exit code, and run() exits before Tauri starts. In GUI launch contexts that add their own argv (for example launcher/autostart or platform-supplied flags), PickGauge now prints the usage-command help and terminates instead of opening the tray; only the usage subcommand should be consumed here, while unrelated argv should fall through to the normal app startup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e9387f9: parse_args returns Ok(None) for any first argument other than usage, so launcher/autostart argv falls through to the tray; help+exit 2 only applies to unknown flags after the usage subcommand (tests updated).

Comment thread src-tauri/src/lib.rs
Comment on lines +1098 to +1099
if let Ok(snapshots) = engine.raw_snapshots() {
let _ = snapshot_store::save_in(&app_data_dir, &snapshots, &display_state.updated_at);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve parsed web snapshots across placeholder refreshes

This persists whatever raw provider snapshots are currently in the engine, including the fail-closed web placeholders produced by refresh_all_and_publish during startup/settings saves (those paths call engine.refresh_all() rather than the headless-web refresh loop). For users with web readings enabled, restarting the tray can therefore overwrite a previously parsed snapshots.json with login_required placeholders before any browser refresh succeeds, so pickgauge usage --json loses the last saved browser gauge it was added to reuse.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e9387f9: snapshot_store::save_to_path now merges with the existing file — a stored snapshot with status == "parsed" is never replaced by a non-parsed one, so startup/settings-save placeholder passes can't erase a previously parsed browser gauge (tests cover parsed-survives-placeholder, parsed-replaces-parsed, and non-parsed-persists-when-nothing-parsed).

Comment thread src-tauri/src/usage_cli.rs Outdated
engine.refresh_all()?;

let app_data_dir = snapshot_store::app_data_dir()?;
let persisted_snapshots = snapshot_store::load_in(&app_data_dir)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Treat the snapshot cache as optional on read errors

The persisted snapshot file is only an overlay cache, but this ? makes the whole headless command exit 1 if snapshots.json exists and cannot be read (for example bad permissions or a stale directory at that path), even though freshly refreshed CLI/local/daemon rows are already available. A cache read failure should degrade to an empty overlay so pickgauge usage --json still returns provider statuses successfully.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d24ea0b40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src-tauri/src/lib.rs
Comment on lines +2240 to +2241
if let Some(exit_code) = usage_cli::try_run_from_env() {
std::process::exit(exit_code);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve console output for Windows usage command

In Windows release builds this new CLI path still runs inside the GUI-subsystem executable because src-tauri/src/main.rs has #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]; when pickgauge usage --json is launched from cmd/PowerShell, stdio is not attached like a console app, so agents cannot reliably read the documented JSON output. This needs a console-capable entry point, or equivalent console attachment/subsystem handling before routing to usage_cli.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/lib.rs
Comment on lines +1098 to +1099
if let Ok(snapshots) = engine.raw_snapshots() {
let _ = snapshot_store::save_in(&app_data_dir, &snapshots, &display_state.updated_at);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear persisted snapshots when clearing the cache

This new on-disk cache is written only from after_snapshots_updated, but the existing clear_cached_snapshots command emits the cleared display state without calling this helper, so using Settings → clear cached usage snapshots clears the in-memory/UI cache while leaving the old snapshots.json on disk. A later pickgauge usage --json can then overlay and report the stale web gauge the user just cleared unless the clear path also deletes or rewrites the persisted snapshot file.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e9387f9: the clear_cached_snapshots command now calls snapshot_store::clear_in (lib.rs:947), which removes snapshots.json non-fatally; covered by store-level tests.

Comment thread src-tauri/src/usage_cli.rs Outdated
engine.refresh_all()?;

let app_data_dir = snapshot_store::app_data_dir()?;
let persisted_snapshots = snapshot_store::load_in(&app_data_dir)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore unreadable optional snapshot caches

Because the optional persisted-cache load is part of the fallible ? chain, an unreadable snapshots.json (for example, a root-owned file left by a sudo run or damaged permissions) aborts pickgauge usage --json after the live refreshes instead of returning the available CLI/local rows. Cache reads should degrade to an empty overlay, like missing/corrupt/version-mismatched files, so an auxiliary web snapshot cannot make the headless command unusable.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/lib.rs
Comment on lines +1098 to +1099
if let Ok(snapshots) = engine.raw_snapshots() {
let _ = snapshot_store::save_in(&app_data_dir, &snapshots, &display_state.updated_at);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve parsed web gauges when saving placeholders

When the tray starts or settings are saved, refresh_all_and_publish runs engine.refresh_all() without the browser sidecar, so enabled web providers are represented by fail-closed login_required placeholders; this write then replaces snapshots.json with those raw snapshots. In that scenario a parsed browser gauge saved from the previous tray session is erased before the next real web refresh, and pickgauge usage --json loses the web headroom it was supposed to reuse, so the persisted store should preserve the last parsed web row or skip placeholder/error web rows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e9387f9: snapshot_store::save_to_path now merges with the existing file — a stored snapshot with status == "parsed" is never replaced by a non-parsed one, so startup/settings-save placeholder passes can't erase a previously parsed browser gauge (tests cover parsed-survives-placeholder, parsed-replaces-parsed, and non-parsed-persists-when-nothing-parsed).

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6607baecbf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +121 to +123
let write_result = write_temp_file(temp_path, bytes).and_then(|_| {
fs::rename(temp_path, path)
.map_err(|error| format!("Could not replace usage snapshots: {error}"))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace existing snapshot files on Windows

On Windows, std::fs::rename does not replace an existing destination, so after the first snapshots.json write every later tray refresh hits this error path and the caller silently drops the cache update. In that environment pickgauge usage --json will keep reading the first/stale browser snapshot instead of the latest tray reading; use a replace-capable write path for an existing snapshot file.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Dismissing on the Rust std contract: std::fs::rename on Windows calls MoveFileExW with MOVEFILE_REPLACE_EXISTING — replacing an existing destination file is documented, supported behavior (docs: "replacing the original file if to already exists"). The residual Windows failure mode is a concurrently-open destination without FILE_SHARE_DELETE, which is transient here: the write is deliberately non-fatal and the next refresh (≤45s) rewrites.

Comment on lines +82 to +85
use windows_sys::Win32::System::Console::{AttachConsole, ATTACH_PARENT_PROCESS};

unsafe {
let _ = AttachConsole(ATTACH_PARENT_PROCESS);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ship a console entry point for Windows usage

For Windows release builds this binary is still built with windows_subsystem = "windows" in src-tauri/src/main.rs, and attaching to the parent console here does not make cmd.exe/PowerShell treat the process as a synchronous console command. When users or agents run pickgauge usage --json in a pipeline on Windows, the shell can return before the JSON is produced and output/exit status are not reliably consumable; this needs a console-subsystem helper/entry point for the CLI path rather than only AttachConsole.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Partially valid, deferred to #37: agents — the consumers this feature targets — invoke via subprocess with piped stdout, and pipe handles are inherited by GUI-subsystem processes, so capture + wait + exit codes work today (that's the documented pattern in docs/usage-json.md). AttachConsole covers most interactive cases. The remaining gap is interactive-console ergonomics (prompt returning early), which needs a console-subsystem helper — tracked in #37 rather than grown inside this PR.

@ElbertePlinio ElbertePlinio merged commit db911af into main Jul 10, 2026
1 check passed
@ElbertePlinio ElbertePlinio deleted the feat/usage-cli branch July 10, 2026 01:19
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.

Agent-readable usage for all pools (Grok + Ollama Cloud) + headless usage export & skill

1 participant