Skip to content

feat: add Grok zero-setup usage provider (grok.cli)#28

Merged
ElbertePlinio merged 4 commits into
mainfrom
feat/grok-cli-provider
Jul 9, 2026
Merged

feat: add Grok zero-setup usage provider (grok.cli)#28
ElbertePlinio merged 4 commits into
mainfrom
feat/grok-cli-provider

Conversation

@ElbertePlinio

Copy link
Copy Markdown
Member

Adds Grok as a fourth PickGauge service with the grok.cli zero-setup provider: reads the bearer the grok CLI already stored in ~/.grok/auth.json (read-only) and calls GET grok.com/rest/subscriptions once per refresh to show the active plan + billing-period end as a plan-only card. No usage percentages yet — Grok's weekly quota numbers are cookie-gated and land with the web provider PR.

Refs #26 (PR 1 of the 4-PR plan; PR 2 = Ollama zero-setup is #27, PR 3 web full-gauges and PR 4 headless export remain).

What changed

  • Service::Grok + UsageProviderId::GrokCli (grok.cli); collector in cli_provider.rs; registered when Grok + CLI readings are enabled. No web provider, browser profile, or 5-hour window for Grok anywhere (Grok is weekly-only, and this PR is plan-only).
  • Deliberate design decision: no token refresh. x.ai rotates refresh tokens on use, so refreshing from PickGauge would invalidate the grok CLI's stored refresh token and silently log the user out of their own CLI (verified live). PickGauge reads the bearer, never refreshes/stores/writes it; an expired bearer surfaces as an honest "Sign in with the Grok CLI" state.
  • Plan-only snapshot contract: null percentages, confidence: medium, details.plan (+ billingPeriodEnd); reset_at stays null (billing renewal is not a quota reset).
  • Config back-compat: existing config.json files without the grok key parse fine and default to enabled (#[serde(default)] + test).
  • Frontend: Grok service toggle, plan-only dashboard card, no web-action buttons for Grok; tray icon intentionally absent while Grok has no percentage.
  • README privacy section + Settings copy updated; sanitized live-captured fixture for parser tests.

Tested

  • cargo test --locked --all-targets: 232 passed.
  • bun run check clean; bun run test: 61 + 15 sidecar passed.
  • Live verification against real CLI credentials post fix: auth.json shape matches the extraction, GET /rest/subscriptions HTTP 200, parsed plan "Grok Pro".

Not tested yet

  • Long-session tray behavior with the bearer expiring mid-session (unit-covered as login_required mapping only).

Risks

@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: 18803ca630

ℹ️ 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".

const CLAUDE_USAGE_URL: &str = "https://api.anthropic.com/api/oauth/usage";
const CLAUDE_OAUTH_BETA: &str = "oauth-2025-04-20";

const GROK_SUBSCRIPTIONS_URL: &str = "https://grok.com/rest/subscriptions";

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 Point Grok CLI auth at the CLI billing surface

For users authenticated only with grok login, this endpoint does not match the documented CLI billing surface: the Grok usage integrations I checked describe the CLI path as cli-chat-proxy.grok.com/v1/billing?format=credits / grok agent stdio x.ai/billing, not the browser grok.com/rest/subscriptions route. Sending the CLI bearer here can turn a valid CLI login into 401/403 or parse failures, so PickGauge records login_required instead of showing the Grok plan; please target the CLI billing/settings endpoint or use a browser-cookie flow consistently. Source checked: https://github.com/robinebers/openusage/blob/main/docs/providers/grok.md

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 this one on direct evidence: we verified live on a machine with only a grok login CLI credential — twice, including immediately after a fresh device-code login — that GET grok.com/rest/subscriptions with the CLI bearer returns HTTP 200 with a parseable subscriptions payload (that live capture is the test fixture). The cited doc describes a different tool's chosen surface, not a constraint of this token. If this ever regresses in the wild it degrades to an honest login_required/parse_failed state, never wrong data.

Comment thread src-tauri/src/usage.rs
Service::Codex => 0,
Service::Claude => 1,
Service::Ollama => 2,
Service::Grok => 2,

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 Keep plan-only Grok out of gauge rotation

When Grok is enabled (now the default), a successful Grok snapshot still has remaining_percent: None, so including it in tray_states() makes the tray rotate to an empty/unknown gauge and tooltip every cycle even though there is no quota percentage to show. This reduces the usefulness of the primary tray gauge for Codex/Claude users; filter plan-only Grok snapshots from tray rotation or render a distinct plan state instead of an unknown remaining gauge.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/cli_provider.rs Outdated
Comment on lines +407 to +409
.get("tier")
.and_then(Value::as_str)
.is_some_and(|tier| tier.starts_with("SUBSCRIPTION_TIER_GROK_"))

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 Include X Premium tiers when detecting Grok entitlements

When the user only has an X Premium Plus entitlement, Grok Build is still a valid CLI login (xAI says Grok Build is available to SuperGrok and X Premium Plus), and the fixture already shows X tiers as SUBSCRIPTION_TIER_X_PREMIUM. This predicate rejects every active SUBSCRIPTION_TIER_X_* subscription, so those users get missing_data instead of their plan; accept X Premium/Premium+ tiers or derive this from the CLI settings/billing response. Source checked: https://x.ai/news/grok-build-cli

Useful? React with 👍 / 👎.

@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: ebd81ac715

ℹ️ 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/usage.rs
Comment on lines +310 to +314
|| snapshot
.details
.get("plan")
.and_then(serde_json::Value::as_str)
.is_none()

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 Keep Grok-only tray state from falling back to Codex

When a user disables Codex/Claude and leaves only Grok enabled, a successful Grok CLI snapshot has a plan but no remaining_percent, so this filter drops the only snapshot. tray_states() then falls through to the existing empty-state fallback and returns Codex with unknown remaining, causing the tray tooltip/icon rotation to show the wrong service even though Grok is enabled and parsed successfully.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/cli_provider.rs Outdated
Comment on lines +371 to +374
entries.values().find_map(|entry| {
entry
.get("key")
.and_then(Value::as_str)

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 Prefer the current Grok auth entry before legacy bearers

When auth.json contains both the current https://auth.x.ai::<client> entry and an older https://accounts.x.ai/sign-in entry, serde_json::Map is key-ordered here, so .values().find_map(...) visits the legacy entry first and can return an expired/less-capable bearer. In that state PickGauge reports login_required even though the Grok CLI has a valid current login; look up the known current scope key before falling back to legacy entries.

Useful? React with 👍 / 👎.

@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: 5255b6ad26

ℹ️ 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/Float.svelte
const ringColors: Record<Service, string> = {
codex: "var(--text)",
claude: "var(--ember)",
grok: "var(--ember)",

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 Hide plan-only Grok from the float gauge

When the Grok CLI provider succeeds it emits a plan-only snapshot (details.plan is set, but remainingPercent is null). Adding Grok to the float ring path means the existing {#each snapshots} below renders that snapshot as an extra /unknown mini-gauge whenever the floating button is enabled, even though the backend tray logic deliberately filters successful plan-only Grok snapshots. Filter those plan-only Grok snapshots out of the float display until Grok has an actual usage percentage.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/config.rs Outdated
Comment on lines +36 to +37
#[serde(default = "default_true")]
pub grok: bool,

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 Keep Grok opt-in for migrated configs

For existing users, their saved config.json has no enabledServices.grok; this default makes it load as enabled, and with the existing default/likely cliEnabled setting the next refresh registers grok.cli, reads ~/.grok/auth.json, and contacts grok.com even though the user never opted into this new service. Given the app's privacy boundary around not widening what it touches unexpectedly, migrated configs should default Grok off (or require an explicit toggle) while new installs can still choose the desired default separately.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/lib.rs
Comment on lines 1307 to 1311
let profile_path = match service {
Service::Codex => &paths.codex,
Service::Claude => &paths.claude,
Service::Grok => unreachable!("Grok has no managed browser profile"),
Service::Ollama => &paths.ollama,

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 Reject manual Grok web refreshes before browser setup

Now that Service can be Grok, a caller can invoke the public refresh_provider command with { service: "grok", source: "web" } (the TS API type allows it). That path sends every Web refresh through refresh_web_provider_headless, reaches this unreachable!, panics the blocking task, and skips the normal failed refresh cleanup/event flow; it should return the same explicit “web provider unavailable” error used for other Grok browser commands instead of panicking on user input.

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.

False positive on the code path: refresh_provider(grok, web)refresh_web_provider_headless passes the sidecar call as a closure into refresh_provider_source_with_snapshot, whose first guard is UsageProviderId::for_service_source(Grok, Web)None → returns "Provider source cannot be refreshed directly" (usage.rs:626) before the closure containing the unreachable! is ever invoked. The command returns a clean error, no panic. The TS type allowing the call is fine — it gets an explicit error, same as other unsupported combos (e.g. ollama+local before this PR series).

@ElbertePlinio

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 95bb949303

ℹ️ 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".

@ElbertePlinio ElbertePlinio merged commit cd855b5 into main Jul 9, 2026
1 check passed
@ElbertePlinio ElbertePlinio deleted the feat/grok-cli-provider branch July 9, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant