From 46cd23c087b3d1fa9168ea16cfca10677caea01a Mon Sep 17 00:00:00 2001 From: Jacky Lam Date: Mon, 10 Aug 2026 12:49:05 +0800 Subject: [PATCH 1/3] feat: add Command Code subscription provider --- README.md | 11 +- docs/providers/commandcode.md | 30 +++ .../verify-provider-registry-contract.js | 3 +- src-tauri/src/lib.rs | 9 +- src-tauri/src/menu_bar.rs | 4 + src-tauri/src/providers/commandcode/auth.rs | 51 +++++ src-tauri/src/providers/commandcode/client.rs | 86 +++++++++ src-tauri/src/providers/commandcode/mapper.rs | 179 ++++++++++++++++++ src-tauri/src/providers/commandcode/mod.rs | 139 ++++++++++++++ src-tauri/src/providers/mod.rs | 1 + src/assets/provider-icons/commandcode.svg | 1 + src/lib/providerIconPaths.ts | 2 + 12 files changed, 507 insertions(+), 9 deletions(-) create mode 100644 docs/providers/commandcode.md create mode 100644 src-tauri/src/providers/commandcode/auth.rs create mode 100644 src-tauri/src/providers/commandcode/client.rs create mode 100644 src-tauri/src/providers/commandcode/mapper.rs create mode 100644 src-tauri/src/providers/commandcode/mod.rs create mode 100644 src/assets/provider-icons/commandcode.svg diff --git a/README.md b/README.md index 63d4e11..1f7716d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ OpenQuota checks for updates automatically. Installable updates are cryptographi - **[Claude Code](docs/providers/claude.md)** — multiple accounts, session and weekly limits, model-specific usage, token history, and estimated spend +- **[Command Code](docs/providers/commandcode.md)** — session and weekly credit limits, monthly + subscription credits, and extra-credit balance - **[Codex](docs/providers/codex.md)** — session and weekly limits, credits, token history, model breakdown, and estimated spend - **[Cursor](docs/providers/cursor.md)** — total, Auto and API usage, credits, token history, and @@ -61,10 +63,11 @@ OpenQuota checks for updates automatically. Installable updates are cryptographi - **[Kimi](docs/providers/kimi.md)** — Kimi Code session and weekly quotas (API key) - **[MiniMax](docs/providers/minimax.md)** — Token Plan session and weekly quotas (API key) -Most providers use credentials already available on your computer. OpenRouter, Z.ai, Kimi, and -MiniMax require API keys, which you can add in Customize; OpenQuota stores them securely in your -operating system's credential store. Codex subscription limits require a ChatGPT login and are not -available in API-key-only sessions. +Most providers use credentials already available on your computer. Command Code reads the local +session created by `command-code login`. OpenRouter, Z.ai, Kimi, and MiniMax require API keys, +which you can add in Customize; OpenQuota stores them securely in your operating system's credential +store. Codex subscription limits require a ChatGPT login and are not available in API-key-only +sessions. ## Features diff --git a/docs/providers/commandcode.md b/docs/providers/commandcode.md new file mode 100644 index 0000000..57a328e --- /dev/null +++ b/docs/providers/commandcode.md @@ -0,0 +1,30 @@ +# Command Code + +OpenQuota reads the local session created by the Command Code CLI and tracks the subscription +windows reported by Command Code. + +## What it tracks + +| Metric | Meaning | +| --------------- | ---------------------------------------------------------- | +| Session | Credit usage in the rolling 5-hour subscription window | +| Weekly | Credit usage in the rolling 7-day subscription window | +| Monthly Credits | Remaining credits from the current subscription allocation | +| Extra Credits | Remaining purchased or top-up credits, when available | + +## Setup + +Install the Command Code CLI and sign in: + +```sh +command-code login +``` + +OpenQuota reads `~/.commandcode/auth.json` locally. The session key remains on your device and is +used only to request Command Code usage data. + +## Troubleshooting + +- **Not logged in** — run `command-code login`, then refresh OpenQuota. +- **Login expired** — sign in again with `command-code login`. +- **Usage unavailable** — check the connection and refresh again. diff --git a/scripts/verify/verify-provider-registry-contract.js b/scripts/verify/verify-provider-registry-contract.js index 9c47d51..98d6ab5 100644 --- a/scripts/verify/verify-provider-registry-contract.js +++ b/scripts/verify/verify-provider-registry-contract.js @@ -22,7 +22,7 @@ const frontendConsumers = [ 'src/lib/shareCard.ts', ]; const providerLiteral = - /["'](?:claude|codex|cursor|antigravity|copilot|devin|grok|opencode|openrouter|zai|kimi|minimax)["']/; + /["'](?:claude|commandcode|codex|cursor|antigravity|copilot|devin|grok|opencode|openrouter|zai|kimi|minimax)["']/; for (const file of rustConsumers) { const source = fs.readFileSync(new URL(file, root), 'utf8').split('#[cfg(test)]')[0]; @@ -82,6 +82,7 @@ const runtimeOrder = [ const expectedRuntimeOrder = [ 'ClaudeProvider', 'CodexProvider', + 'CommandCodeProvider', 'CursorProvider', 'AntigravityProvider', 'CopilotProvider', diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f822a48..b712d3d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -48,10 +48,10 @@ use crate::{ pricing::PricingStore, providers::{ antigravity::AntigravityProvider, claude, codex::reset_claim::CodexResetClaimService, - codex::CodexProvider, copilot::CopilotProvider, cursor::CursorProvider, - detect_local_credentials, devin::DevinProvider, grok::GrokProvider, kimi::KimiProvider, - minimax::MiniMaxProvider, opencode::OpenCodeProvider, openrouter::OpenRouterProvider, - zai::ZaiProvider, ProviderRegistry, UsageProvider, + codex::CodexProvider, commandcode::CommandCodeProvider, copilot::CopilotProvider, + cursor::CursorProvider, detect_local_credentials, devin::DevinProvider, grok::GrokProvider, + kimi::KimiProvider, minimax::MiniMaxProvider, opencode::OpenCodeProvider, + openrouter::OpenRouterProvider, zai::ZaiProvider, ProviderRegistry, UsageProvider, }, storage::Storage, window::{ @@ -230,6 +230,7 @@ pub fn run() { providers.extend(vec![ Arc::new(CodexProvider::new(storage.clone(), pricing.clone())?) as Arc, + Arc::new(CommandCodeProvider::new()?) as Arc, Arc::new(CursorProvider::new(pricing.clone())?) as Arc, Arc::new(AntigravityProvider::new( app_data_dir.join("antigravity").join("auth.json"), diff --git a/src-tauri/src/menu_bar.rs b/src-tauri/src/menu_bar.rs index c17ed8b..0d268e7 100644 --- a/src-tauri/src/menu_bar.rs +++ b/src-tauri/src/menu_bar.rs @@ -23,6 +23,7 @@ const STACKED_BASELINES: [f32; 2] = [15.0, 32.0]; const FONT_SOURCE: &[u8] = include_bytes!("../assets/fonts/Poppins-SemiBold.ttf"); const CLAUDE_ICON: &str = include_str!("../../src/assets/provider-icons/claude.svg"); +const COMMANDCODE_ICON: &str = include_str!("../../src/assets/provider-icons/commandcode.svg"); const CODEX_ICON: &str = include_str!("../../src/assets/provider-icons/codex.svg"); const COPILOT_ICON: &str = include_str!("../../src/assets/provider-icons/copilot.svg"); const CURSOR_ICON: &str = include_str!("../../src/assets/provider-icons/cursor.svg"); @@ -270,6 +271,7 @@ fn provider_path(provider_id: &str) -> Option<&'static Path> { } static CLAUDE: OnceLock = OnceLock::new(); + static COMMANDCODE: OnceLock = OnceLock::new(); static CODEX: OnceLock = OnceLock::new(); static COPILOT: OnceLock = OnceLock::new(); static CURSOR: OnceLock = OnceLock::new(); @@ -283,6 +285,7 @@ fn provider_path(provider_id: &str) -> Option<&'static Path> { static MINIMAX: OnceLock = OnceLock::new(); match crate::providers::provider_family(provider_id) { "claude" => Some(parsed(CLAUDE_ICON, &CLAUDE)), + "commandcode" => Some(parsed(COMMANDCODE_ICON, &COMMANDCODE)), "codex" => Some(parsed(CODEX_ICON, &CODEX)), "copilot" => Some(parsed(COPILOT_ICON, &COPILOT)), "cursor" => Some(parsed(CURSOR_ICON, &CURSOR)), @@ -588,6 +591,7 @@ mod tests { fn bundled_provider_marks_and_font_render_into_a_retina_text_strip() { for provider in [ "claude", + "commandcode", "codex", "copilot", "cursor", diff --git a/src-tauri/src/providers/commandcode/auth.rs b/src-tauri/src/providers/commandcode/auth.rs new file mode 100644 index 0000000..dcaf480 --- /dev/null +++ b/src-tauri/src/providers/commandcode/auth.rs @@ -0,0 +1,51 @@ +use std::{env, fs, path::PathBuf}; + +use serde::Deserialize; + +use super::CommandCodeError; + +#[derive(Debug, Deserialize)] +struct AuthDocument { + #[serde(rename = "apiKey")] + api_key: Option, +} + +#[derive(Clone)] +pub struct CommandCodeAuthStore { + path: PathBuf, +} + +impl CommandCodeAuthStore { + pub fn new() -> Self { + Self { + path: home_directory().join(".commandcode").join("auth.json"), + } + } + + pub fn load(&self) -> Result { + let text = fs::read_to_string(&self.path).map_err(|_| CommandCodeError::NotLoggedIn)?; + let document: AuthDocument = + serde_json::from_str(&text).map_err(|_| CommandCodeError::InvalidAuth)?; + document + .api_key + .map(|key| key.trim().to_owned()) + .filter(|key| !key.is_empty()) + .ok_or(CommandCodeError::InvalidAuth) + } + + pub fn has_local_credentials(&self) -> bool { + self.load().is_ok() + } +} + +impl Default for CommandCodeAuthStore { + fn default() -> Self { + Self::new() + } +} + +fn home_directory() -> PathBuf { + env::var_os("HOME") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")) +} diff --git a/src-tauri/src/providers/commandcode/client.rs b/src-tauri/src/providers/commandcode/client.rs new file mode 100644 index 0000000..7ab7f4b --- /dev/null +++ b/src-tauri/src/providers/commandcode/client.rs @@ -0,0 +1,86 @@ +use std::time::Duration; + +use reqwest::{blocking::Client, StatusCode}; +use serde_json::Value; + +use super::CommandCodeError; + +const CREDITS_URL: &str = "https://api.commandcode.ai/alpha/billing/credits"; +const SUBSCRIPTION_URL: &str = "https://api.commandcode.ai/alpha/billing/subscriptions"; + +#[derive(Debug)] +pub struct EndpointResponse { + pub status: StatusCode, + pub body: Value, +} + +pub struct CommandCodeClient { + client: Client, + credits_url: String, + subscription_url: String, +} + +impl CommandCodeClient { + pub fn new() -> Result { + Self::with_endpoints(CREDITS_URL, SUBSCRIPTION_URL, Duration::from_secs(15)) + } + + fn with_endpoints( + credits_url: &str, + subscription_url: &str, + timeout: Duration, + ) -> Result { + Ok(Self { + client: Client::builder() + .connect_timeout(Duration::from_secs(8)) + .timeout(timeout) + .user_agent(concat!("OpenQuota/", env!("CARGO_PKG_VERSION"))) + .build() + .map_err(|_| CommandCodeError::ConnectionFailed)?, + credits_url: credits_url.into(), + subscription_url: subscription_url.into(), + }) + } + + pub fn fetch( + &self, + api_key: &str, + ) -> Result<(EndpointResponse, EndpointResponse), CommandCodeError> { + Ok(( + self.fetch_endpoint(&self.credits_url, api_key, "credits")?, + self.fetch_endpoint(&self.subscription_url, api_key, "subscriptions")?, + )) + } + + fn fetch_endpoint( + &self, + url: &str, + api_key: &str, + endpoint: &str, + ) -> Result { + let started = std::time::Instant::now(); + let response = self + .client + .get(url) + .bearer_auth(api_key) + .header("Accept", "application/json") + .send() + .map_err(|_| { + crate::app_warn!("http", "command-code {endpoint} request failed (transport)"); + CommandCodeError::ConnectionFailed + })?; + let status = response.status(); + crate::app_debug!( + "http", + "command-code {endpoint} HTTP {} ({}ms)", + status.as_u16(), + started.elapsed().as_millis() + ); + let body = response + .text() + .ok() + .and_then(|text| serde_json::from_str(&text).ok()) + .unwrap_or(Value::Null); + Ok(EndpointResponse { status, body }) + } +} diff --git a/src-tauri/src/providers/commandcode/mapper.rs b/src-tauri/src/providers/commandcode/mapper.rs new file mode 100644 index 0000000..3309c36 --- /dev/null +++ b/src-tauri/src/providers/commandcode/mapper.rs @@ -0,0 +1,179 @@ +use chrono::{DateTime, Utc}; +use serde_json::Value; + +use crate::models::{MetricValue, MetricValueKind, QuotaFormat, QuotaWindow, ValueMetric}; + +use super::{client::EndpointResponse, CommandCodeError}; + +#[derive(Debug, PartialEq)] +pub struct CommandCodeMappedUsage { + pub plan: Option, + pub quotas: Vec, + pub value_metrics: Vec, +} + +pub fn map_usage( + credits: &EndpointResponse, + subscription: &EndpointResponse, +) -> Result { + require_success(credits)?; + require_success(subscription)?; + let windows = credits + .body + .get("windowLimits") + .and_then(Value::as_object) + .ok_or(CommandCodeError::InvalidResponse)?; + let quotas = [ + quota(windows.get("fiveHour"), "session", "Session", 5 * 60 * 60), + quota(windows.get("weekly"), "weekly", "Weekly", 7 * 24 * 60 * 60), + ] + .into_iter() + .collect::, _>>()?; + let credits_body = credits + .body + .get("credits") + .and_then(Value::as_object) + .ok_or(CommandCodeError::InvalidResponse)?; + let mut values = Vec::new(); + if let Some(monthly) = number(credits_body.get("monthlyCredits")) { + values.push(dollars_metric("monthlyCredits", "Monthly Credits", monthly)); + } + if let Some(purchased) = + number(credits_body.get("purchasedCredits")).filter(|value| *value > 0.0) + { + values.push(dollars_metric("extraCredits", "Extra Credits", purchased)); + } + Ok(CommandCodeMappedUsage { + plan: subscription + .body + .get("data") + .and_then(|data| data.get("planId")) + .and_then(Value::as_str) + .map(display_plan), + quotas, + value_metrics: values, + }) +} + +fn require_success(response: &EndpointResponse) -> Result<(), CommandCodeError> { + if response.status.is_success() { + Ok(()) + } else if response.status.as_u16() == 401 || response.status.as_u16() == 403 { + Err(CommandCodeError::InvalidAuth) + } else { + Err(CommandCodeError::RequestFailed(response.status.as_u16())) + } +} + +fn quota( + value: Option<&Value>, + id: &str, + label: &str, + period_seconds: u64, +) -> Result { + let value = value + .and_then(Value::as_object) + .ok_or(CommandCodeError::InvalidResponse)?; + let cap = number(value.get("cap")) + .filter(|cap| *cap > 0.0) + .ok_or(CommandCodeError::InvalidResponse)?; + let used = number(value.get("used")) + .filter(|used| *used >= 0.0) + .ok_or(CommandCodeError::InvalidResponse)?; + Ok(QuotaWindow { + id: id.into(), + label: label.into(), + used_percent: (used / cap * 100.0).clamp(0.0, 100.0), + resets_at: value.get("resetAt").and_then(iso_time), + period_seconds, + format: QuotaFormat::Dollars, + used_value: Some(used.min(cap)), + limit_value: Some(cap), + unit: None, + estimated: false, + source_note: None, + }) +} + +fn dollars_metric(id: &str, label: &str, amount: f64) -> ValueMetric { + ValueMetric { + id: id.into(), + label: label.into(), + values: vec![MetricValue { + number: amount.max(0.0), + kind: MetricValueKind::Dollars, + label: Some("remaining".into()), + estimated: false, + }], + expiries_at: Vec::new(), + } +} + +fn display_plan(value: &str) -> String { + value + .rsplit('-') + .next() + .unwrap_or(value) + .replace('_', " ") + .split_whitespace() + .map(|word| { + let mut chars = word.chars(); + chars + .next() + .map(|first| first.to_uppercase().collect::() + chars.as_str()) + .unwrap_or_default() + }) + .collect::>() + .join(" ") +} + +fn number(value: Option<&Value>) -> Option { + value + .and_then(|value| { + value + .as_f64() + .or_else(|| value.as_str().and_then(|value| value.trim().parse().ok())) + }) + .filter(|value| value.is_finite()) +} + +fn iso_time(value: &Value) -> Option> { + value + .as_str() + .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) + .map(|value| value.with_timezone(&Utc)) +} + +#[cfg(test)] +mod tests { + use reqwest::StatusCode; + use serde_json::json; + + use super::{map_usage, EndpointResponse}; + + #[test] + fn maps_live_window_limits_and_credit_balances() { + let credits = EndpointResponse { + status: StatusCode::OK, + body: json!({ + "credits": {"monthlyCredits": 7.5, "purchasedCredits": 2.0}, + "windowLimits": { + "fiveHour": {"cap": 3, "used": 0.75, "resetAt": "2026-08-10T12:00:00Z"}, + "weekly": {"cap": 6, "used": 3, "resetAt": "2026-08-15T12:00:00Z"} + } + }), + }; + let subscription = EndpointResponse { + status: StatusCode::OK, + body: json!({"success": true, "data": {"planId": "individual-goat"}}), + }; + + let mapped = map_usage(&credits, &subscription).unwrap(); + assert_eq!(mapped.plan.as_deref(), Some("Goat")); + assert_eq!(mapped.quotas[0].used_percent, 25.0); + assert_eq!(mapped.quotas[1].used_percent, 50.0); + assert_eq!(mapped.value_metrics.len(), 2); + assert_eq!(mapped.value_metrics[0].id, "monthlyCredits"); + assert_eq!(mapped.value_metrics[0].values[0].number, 7.5); + } +} diff --git a/src-tauri/src/providers/commandcode/mod.rs b/src-tauri/src/providers/commandcode/mod.rs new file mode 100644 index 0000000..5006034 --- /dev/null +++ b/src-tauri/src/providers/commandcode/mod.rs @@ -0,0 +1,139 @@ +mod auth; +mod client; +mod mapper; + +use chrono::Utc; +use thiserror::Error; + +use crate::models::{ + MetricDefinition, MetricSection, ProviderDefinition, ProviderErrorKind, ProviderLink, + ProviderSnapshot, UsageHistory, +}; + +use self::{auth::CommandCodeAuthStore, client::CommandCodeClient, mapper::map_usage}; + +use super::{ProviderError, UsageProvider}; + +pub(crate) fn definition() -> ProviderDefinition { + ProviderDefinition { + id: "commandcode".into(), + display_name: "Command Code".into(), + short_name: "CC".into(), + fallback_enabled: false, + local_usage_source_note: None, + links: vec![ProviderLink::new("Usage", "https://commandcode.ai/usage")], + metrics: vec![ + MetricDefinition::quota( + "commandcode.session", + "Session", + "session", + false, + true, + MetricSection::AlwaysVisible, + true, + "S", + ), + MetricDefinition::quota( + "commandcode.weekly", + "Weekly", + "weekly", + false, + true, + MetricSection::AlwaysVisible, + true, + "W", + ), + MetricDefinition::value( + "commandcode.monthly", + "Monthly Credits", + "monthlyCredits", + true, + MetricSection::OnDemand, + false, + "M", + None, + ), + MetricDefinition::value( + "commandcode.extra", + "Extra Credits", + "extraCredits", + true, + MetricSection::OnDemand, + false, + "E", + None, + ), + ], + } +} + +#[derive(Debug, Error)] +pub(crate) enum CommandCodeError { + #[error("Command Code is not logged in. Run `command-code login`.")] + NotLoggedIn, + #[error("Command Code login data is invalid or expired. Run `command-code login` again.")] + InvalidAuth, + #[error("Could not reach Command Code. Check your internet connection.")] + ConnectionFailed, + #[error("Command Code returned an invalid usage response.")] + InvalidResponse, + #[error("Command Code usage request failed (HTTP {0}).")] + RequestFailed(u16), +} + +impl From for ProviderError { + fn from(error: CommandCodeError) -> Self { + let kind = match error { + CommandCodeError::NotLoggedIn | CommandCodeError::InvalidAuth => { + ProviderErrorKind::Authentication + } + CommandCodeError::ConnectionFailed => ProviderErrorKind::Network, + CommandCodeError::RequestFailed(429) => ProviderErrorKind::RateLimited, + CommandCodeError::RequestFailed(_) | CommandCodeError::InvalidResponse => { + ProviderErrorKind::InvalidResponse + } + }; + ProviderError::from_display(kind, error) + } +} + +pub struct CommandCodeProvider { + auth: CommandCodeAuthStore, + client: CommandCodeClient, +} + +impl CommandCodeProvider { + pub fn new() -> Result { + Ok(Self { + auth: CommandCodeAuthStore::new(), + client: CommandCodeClient::new().map_err(ProviderError::from)?, + }) + } +} + +impl UsageProvider for CommandCodeProvider { + fn definition(&self) -> ProviderDefinition { + definition() + } + + fn has_local_credentials(&self) -> bool { + self.auth.has_local_credentials() + } + + fn refresh(&self) -> Result { + let api_key = self.auth.load().map_err(ProviderError::from)?; + let (credits, subscription) = self.client.fetch(&api_key).map_err(ProviderError::from)?; + let mapped = map_usage(&credits, &subscription).map_err(ProviderError::from)?; + Ok(ProviderSnapshot { + provider_id: "commandcode".into(), + plan: mapped.plan, + quotas: mapped.quotas, + value_metrics: mapped.value_metrics, + status_metrics: Vec::new(), + notices: Vec::new(), + usage: UsageHistory::default(), + warnings: Vec::new(), + refreshed_at: Utc::now(), + }) + } +} diff --git a/src-tauri/src/providers/mod.rs b/src-tauri/src/providers/mod.rs index 09623f1..3eab979 100644 --- a/src-tauri/src/providers/mod.rs +++ b/src-tauri/src/providers/mod.rs @@ -2,6 +2,7 @@ pub mod antigravity; pub mod api_key; pub mod claude; pub mod codex; +pub mod commandcode; pub mod copilot; pub mod credential_store; pub mod cursor; diff --git a/src/assets/provider-icons/commandcode.svg b/src/assets/provider-icons/commandcode.svg new file mode 100644 index 0000000..f33d9d1 --- /dev/null +++ b/src/assets/provider-icons/commandcode.svg @@ -0,0 +1 @@ + diff --git a/src/lib/providerIconPaths.ts b/src/lib/providerIconPaths.ts index 21b13a0..48728d6 100644 --- a/src/lib/providerIconPaths.ts +++ b/src/lib/providerIconPaths.ts @@ -1,5 +1,6 @@ import antigravity from '../assets/provider-icons/antigravity.svg?raw'; import claude from '../assets/provider-icons/claude.svg?raw'; +import commandcode from '../assets/provider-icons/commandcode.svg?raw'; import codex from '../assets/provider-icons/codex.svg?raw'; import copilot from '../assets/provider-icons/copilot.svg?raw'; import cursor from '../assets/provider-icons/cursor.svg?raw'; @@ -14,6 +15,7 @@ import zai from '../assets/provider-icons/zai.svg?raw'; const visuals: Record = { antigravity: { source: antigravity, color: '#4285F4' }, claude: { source: claude, color: '#DE7356' }, + commandcode: { source: commandcode, color: '#6A5CFF' }, codex: { source: codex, color: null }, copilot: { source: copilot, color: null }, cursor: { source: cursor, color: null }, From 80717f08930397603b439badee83faeeb9a2cb2c Mon Sep 17 00:00:00 2001 From: Jacky Lam Date: Mon, 10 Aug 2026 17:41:54 +0800 Subject: [PATCH 2/3] fix: use official Command Code logomark --- src/assets/provider-icons/commandcode.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/provider-icons/commandcode.svg b/src/assets/provider-icons/commandcode.svg index f33d9d1..669b7d0 100644 --- a/src/assets/provider-icons/commandcode.svg +++ b/src/assets/provider-icons/commandcode.svg @@ -1 +1 @@ - + From ed64ad244065b4e56211321d2491f5d9a183cb2f Mon Sep 17 00:00:00 2001 From: Jacky Lam Date: Tue, 11 Aug 2026 08:20:24 +0800 Subject: [PATCH 3/3] fix: show Command Code quota resets --- src-tauri/src/providers/commandcode/mapper.rs | 90 ++++++++++++++++--- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/providers/commandcode/mapper.rs b/src-tauri/src/providers/commandcode/mapper.rs index 3309c36..3c00302 100644 --- a/src-tauri/src/providers/commandcode/mapper.rs +++ b/src-tauri/src/providers/commandcode/mapper.rs @@ -34,14 +34,28 @@ pub fn map_usage( .get("credits") .and_then(Value::as_object) .ok_or(CommandCodeError::InvalidResponse)?; + let monthly_reset = subscription + .body + .pointer("/data/currentPeriodEnd") + .and_then(timestamp); let mut values = Vec::new(); if let Some(monthly) = number(credits_body.get("monthlyCredits")) { - values.push(dollars_metric("monthlyCredits", "Monthly Credits", monthly)); + values.push(dollars_metric( + "monthlyCredits", + "Monthly Credits", + monthly, + monthly_reset.into_iter().collect(), + )); } if let Some(purchased) = number(credits_body.get("purchasedCredits")).filter(|value| *value > 0.0) { - values.push(dollars_metric("extraCredits", "Extra Credits", purchased)); + values.push(dollars_metric( + "extraCredits", + "Extra Credits", + purchased, + Vec::new(), + )); } Ok(CommandCodeMappedUsage { plan: subscription @@ -84,7 +98,7 @@ fn quota( id: id.into(), label: label.into(), used_percent: (used / cap * 100.0).clamp(0.0, 100.0), - resets_at: value.get("resetAt").and_then(iso_time), + resets_at: value.get("resetAt").and_then(timestamp), period_seconds, format: QuotaFormat::Dollars, used_value: Some(used.min(cap)), @@ -95,7 +109,12 @@ fn quota( }) } -fn dollars_metric(id: &str, label: &str, amount: f64) -> ValueMetric { +fn dollars_metric( + id: &str, + label: &str, + amount: f64, + expiries_at: Vec>, +) -> ValueMetric { ValueMetric { id: id.into(), label: label.into(), @@ -105,7 +124,7 @@ fn dollars_metric(id: &str, label: &str, amount: f64) -> ValueMetric { label: Some("remaining".into()), estimated: false, }], - expiries_at: Vec::new(), + expiries_at, } } @@ -137,15 +156,23 @@ fn number(value: Option<&Value>) -> Option { .filter(|value| value.is_finite()) } -fn iso_time(value: &Value) -> Option> { - value - .as_str() - .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) - .map(|value| value.with_timezone(&Utc)) +fn timestamp(value: &Value) -> Option> { + if let Some(value) = value.as_str() { + return DateTime::parse_from_rfc3339(value) + .ok() + .map(|value| value.with_timezone(&Utc)); + } + let value = value.as_i64()?; + if value.unsigned_abs() >= 100_000_000_000 { + DateTime::from_timestamp_millis(value) + } else { + DateTime::from_timestamp(value, 0) + } } #[cfg(test)] mod tests { + use chrono::{DateTime, Utc}; use reqwest::StatusCode; use serde_json::json; @@ -165,7 +192,13 @@ mod tests { }; let subscription = EndpointResponse { status: StatusCode::OK, - body: json!({"success": true, "data": {"planId": "individual-goat"}}), + body: json!({ + "success": true, + "data": { + "planId": "individual-goat", + "currentPeriodEnd": "2026-09-01T12:00:00Z" + } + }), }; let mapped = map_usage(&credits, &subscription).unwrap(); @@ -175,5 +208,40 @@ mod tests { assert_eq!(mapped.value_metrics.len(), 2); assert_eq!(mapped.value_metrics[0].id, "monthlyCredits"); assert_eq!(mapped.value_metrics[0].values[0].number, 7.5); + assert_eq!( + mapped.value_metrics[0].expiries_at, + vec![DateTime::parse_from_rfc3339("2026-09-01T12:00:00Z") + .unwrap() + .with_timezone(&Utc)] + ); + } + + #[test] + fn parses_epoch_seconds_and_milliseconds_for_window_resets() { + let credits = EndpointResponse { + status: StatusCode::OK, + body: json!({ + "credits": {"monthlyCredits": 7.5}, + "windowLimits": { + "fiveHour": {"cap": 3, "used": 0, "resetAt": 1_800_000_000}, + "weekly": {"cap": 6, "used": 0, "resetAt": 1_800_000_000_000i64} + } + }), + }; + let subscription = EndpointResponse { + status: StatusCode::OK, + body: json!({"data": {"planId": "individual-go"}}), + }; + + let mapped = map_usage(&credits, &subscription).unwrap(); + assert_eq!( + mapped.quotas[0].resets_at, + Some( + DateTime::parse_from_rfc3339("2027-01-15T08:00:00Z") + .unwrap() + .with_timezone(&Utc) + ) + ); + assert_eq!(mapped.quotas[1].resets_at, mapped.quotas[0].resets_at); } }