@hashimkarim/usagestat-linux-x64-gnu 2.0.0-alpha.1 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/native-manifest.json +1088 -134
- package/package.json +160 -1
- package/plugins/UPSTREAM-LICENSES.md +113 -0
- package/plugins/_provider-icons/LICENSE +21 -0
- package/plugins/_provider-icons/NOTICE +12 -0
- package/plugins/_provider-icons/licenses/CodexBar-MIT.txt +21 -0
- package/plugins/_provider-icons/licenses/LobeHub-MIT.txt +21 -0
- package/plugins/_provider-icons/licenses/UsageStat-Bar-MIT.txt +21 -0
- package/plugins/_provider-icons/manifest.json +1219 -0
- package/plugins/_provider-icons/provenance.json +269 -0
- package/plugins/_provider-icons/source.json +4 -0
- package/plugins/abacus-ai/icon-color.svg +18 -0
- package/plugins/abacus-ai/icon.svg +0 -0
- package/plugins/abacus-ai/plugin.js +0 -0
- package/plugins/abacus-ai/plugin.json +0 -0
- package/plugins/aiand/icon.svg +6 -0
- package/plugins/aiand/plugin.js +108 -0
- package/plugins/aiand/plugin.json +22 -0
- package/plugins/aixy/LICENSE +21 -0
- package/plugins/aixy/plugin.js +261 -0
- package/plugins/aixy/plugin.json +24 -0
- package/plugins/alibaba/icon-color.svg +0 -0
- package/plugins/alibaba/icon.svg +0 -0
- package/plugins/alibaba/plugin.js +0 -0
- package/plugins/alibaba/plugin.json +0 -0
- package/plugins/alibaba-token-plan/icon-color.svg +0 -0
- package/plugins/alibaba-token-plan/icon.svg +0 -0
- package/plugins/alibaba-token-plan/plugin.js +0 -0
- package/plugins/alibaba-token-plan/plugin.json +0 -0
- package/plugins/amp/icon-color.svg +0 -0
- package/plugins/amp/icon-conflict.svg +0 -0
- package/plugins/amp/icon.svg +0 -0
- package/plugins/amp/plugin.js +0 -0
- package/plugins/amp/plugin.json +0 -0
- package/plugins/antigravity/icon-color.svg +0 -0
- package/plugins/antigravity/icon-conflict.svg +0 -0
- package/plugins/antigravity/icon.svg +0 -0
- package/plugins/antigravity/plugin.js +136 -46
- package/plugins/antigravity/plugin.json +0 -0
- package/plugins/antigravity-cli/icon-color.svg +1 -0
- package/plugins/antigravity-cli/icon.svg +1 -3
- package/plugins/antigravity-cli/plugin.js +11 -15
- package/plugins/antigravity-cli/plugin.json +0 -0
- package/plugins/antigravity-ide/icon-color.svg +1 -0
- package/plugins/antigravity-ide/icon.svg +1 -3
- package/plugins/antigravity-ide/plugin.js +0 -0
- package/plugins/antigravity-ide/plugin.json +0 -0
- package/plugins/atlascloud/LICENSE +21 -0
- package/plugins/atlascloud/icon-color.svg +1 -0
- package/plugins/atlascloud/icon.svg +1 -0
- package/plugins/atlascloud/plugin.js +49 -0
- package/plugins/atlascloud/plugin.json +23 -0
- package/plugins/augment/icon-color.svg +14 -0
- package/plugins/augment/icon.svg +0 -0
- package/plugins/augment/plugin.js +0 -0
- package/plugins/augment/plugin.json +0 -0
- package/plugins/aws-bedrock/icon-color.svg +0 -0
- package/plugins/aws-bedrock/icon.svg +0 -0
- package/plugins/aws-bedrock/plugin.js +0 -0
- package/plugins/aws-bedrock/plugin.json +0 -0
- package/plugins/azure-openai/icon-color.svg +0 -0
- package/plugins/azure-openai/icon.svg +0 -0
- package/plugins/azure-openai/plugin.js +0 -0
- package/plugins/azure-openai/plugin.json +0 -0
- package/plugins/bifrost/LICENSE +21 -0
- package/plugins/bifrost/plugin.js +361 -0
- package/plugins/bifrost/plugin.json +23 -0
- package/plugins/chutes/icon.svg +0 -0
- package/plugins/chutes/plugin.js +0 -0
- package/plugins/chutes/plugin.json +0 -0
- package/plugins/claude/icon-color.svg +0 -0
- package/plugins/claude/icon.svg +0 -0
- package/plugins/claude/plugin.js +358 -161
- package/plugins/claude/plugin.json +1 -1
- package/plugins/clawrouter/icon.svg +7 -0
- package/plugins/clawrouter/plugin.js +45 -0
- package/plugins/clawrouter/plugin.json +22 -0
- package/plugins/clinepass/icon.svg +4 -0
- package/plugins/clinepass/plugin.js +87 -0
- package/plugins/clinepass/plugin.json +23 -0
- package/plugins/codebuddy/plugin.js +331 -0
- package/plugins/codebuddy/plugin.json +30 -0
- package/plugins/codebuff/icon-color.svg +92 -0
- package/plugins/codebuff/icon.svg +0 -0
- package/plugins/codebuff/plugin.js +0 -0
- package/plugins/codebuff/plugin.json +0 -0
- package/plugins/coderabbit/plugin.js +25 -0
- package/plugins/coderabbit/plugin.json +12 -0
- package/plugins/codex/icon-color.svg +0 -0
- package/plugins/codex/icon.svg +0 -0
- package/plugins/codex/plugin.js +8 -2
- package/plugins/codex/plugin.json +0 -0
- package/plugins/command-code/icon-color.svg +3 -0
- package/plugins/command-code/icon.svg +0 -0
- package/plugins/command-code/plugin.js +14 -3
- package/plugins/command-code/plugin.json +0 -0
- package/plugins/copilot/icon-color.svg +0 -0
- package/plugins/copilot/icon.svg +1 -1
- package/plugins/copilot/plugin.js +61 -9
- package/plugins/copilot/plugin.json +4 -1
- package/plugins/crof/icon-color.svg +3 -0
- package/plugins/crof/icon.svg +0 -0
- package/plugins/crof/plugin.js +0 -0
- package/plugins/crof/plugin.json +0 -0
- package/plugins/crofai/icon-color.svg +3 -0
- package/plugins/crofai/icon.svg +2 -15
- package/plugins/crofai/plugin.js +0 -0
- package/plugins/crofai/plugin.json +0 -0
- package/plugins/cursor/icon-color.svg +1 -0
- package/plugins/cursor/icon-conflict.svg +0 -0
- package/plugins/cursor/icon.svg +0 -0
- package/plugins/cursor/plugin.js +46 -9
- package/plugins/cursor/plugin.json +3 -2
- package/plugins/cursor-nightly/icon.png +0 -0
- package/plugins/cursor-nightly/plugin.js +45 -8
- package/plugins/cursor-nightly/plugin.json +3 -2
- package/plugins/deepgram/icon.svg +0 -0
- package/plugins/deepgram/plugin.js +0 -0
- package/plugins/deepgram/plugin.json +0 -0
- package/plugins/deepinfra/icon-color.svg +1 -0
- package/plugins/deepinfra/icon.svg +1 -0
- package/plugins/deepinfra/plugin.js +97 -0
- package/plugins/deepinfra/plugin.json +28 -0
- package/plugins/deepseek/icon-color.svg +0 -0
- package/plugins/deepseek/icon.svg +0 -0
- package/plugins/deepseek/plugin.js +150 -59
- package/plugins/deepseek/plugin.json +3 -1
- package/plugins/devin/icon.svg +0 -0
- package/plugins/devin/plugin.js +114 -0
- package/plugins/devin/plugin.json +2 -1
- package/plugins/devpass/LICENSE +21 -0
- package/plugins/devpass/plugin.js +115 -0
- package/plugins/devpass/plugin.json +23 -0
- package/plugins/doubao/icon-color.svg +0 -0
- package/plugins/doubao/icon.svg +0 -0
- package/plugins/doubao/plugin.js +0 -0
- package/plugins/doubao/plugin.json +0 -0
- package/plugins/droid/icon.svg +0 -0
- package/plugins/droid/plugin.json +2 -1
- package/plugins/elevenlabs/icon-color.svg +4 -0
- package/plugins/elevenlabs/icon.svg +0 -0
- package/plugins/elevenlabs/plugin.js +17 -2
- package/plugins/elevenlabs/plugin.json +0 -0
- package/plugins/factory/icon-color.svg +1 -0
- package/plugins/factory/icon-conflict.svg +0 -0
- package/plugins/factory/icon.svg +0 -0
- package/plugins/factory/plugin.js +38 -14
- package/plugins/factory/plugin.json +2 -1
- package/plugins/fireworks-ai/icon-color.svg +1 -0
- package/plugins/fireworks-ai/icon.svg +1 -6
- package/plugins/fireworks-ai/plugin.js +0 -0
- package/plugins/fireworks-ai/plugin.json +0 -0
- package/plugins/gemini/icon-color.svg +0 -0
- package/plugins/gemini/icon.svg +0 -0
- package/plugins/gemini/plugin.js +0 -0
- package/plugins/gemini/plugin.json +0 -0
- package/plugins/gemini-apps/LICENSE +22 -0
- package/plugins/gemini-apps/icon-color.svg +1 -0
- package/plugins/gemini-apps/icon.svg +1 -0
- package/plugins/gemini-apps/plugin.js +470 -0
- package/plugins/gemini-apps/plugin.json +19 -0
- package/plugins/gitkraken/LICENSE +21 -0
- package/plugins/gitkraken/plugin.js +153 -0
- package/plugins/gitkraken/plugin.json +24 -0
- package/plugins/grok/icon-color.svg +1 -0
- package/plugins/grok/icon.svg +0 -0
- package/plugins/grok/plugin.js +30 -5
- package/plugins/grok/plugin.json +3 -1
- package/plugins/groqcloud/icon-color.svg +1 -0
- package/plugins/groqcloud/icon.svg +0 -0
- package/plugins/groqcloud/plugin.js +0 -0
- package/plugins/groqcloud/plugin.json +0 -0
- package/plugins/helmcode/LICENSE +21 -0
- package/plugins/helmcode/plugin.js +188 -0
- package/plugins/helmcode/plugin.json +24 -0
- package/plugins/huggingface/LICENSE +21 -0
- package/plugins/huggingface/icon-color.svg +1 -0
- package/plugins/huggingface/icon.svg +1 -0
- package/plugins/huggingface/plugin.js +293 -0
- package/plugins/huggingface/plugin.json +25 -0
- package/plugins/hyper/LICENSE +21 -0
- package/plugins/hyper/plugin.js +131 -0
- package/plugins/hyper/plugin.json +26 -0
- package/plugins/ibmbob/icon.svg +23 -0
- package/plugins/ibmbob/plugin.js +61 -0
- package/plugins/ibmbob/plugin.json +22 -0
- package/plugins/jetbrains-ai-assistant/icon-color.svg +0 -0
- package/plugins/jetbrains-ai-assistant/icon-conflict.svg +0 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +0 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +0 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +0 -0
- package/plugins/kilo/icon-color.svg +1 -0
- package/plugins/kilo/icon.svg +0 -0
- package/plugins/kilo/plugin.js +0 -0
- package/plugins/kilo/plugin.json +0 -0
- package/plugins/kimi/icon-color.svg +0 -0
- package/plugins/kimi/icon-conflict.svg +0 -0
- package/plugins/kimi/icon.svg +0 -0
- package/plugins/kimi/plugin.js +122 -141
- package/plugins/kimi/plugin.json +5 -2
- package/plugins/kimi-k2/icon-color.svg +0 -0
- package/plugins/kimi-k2/icon.svg +0 -0
- package/plugins/kimi-k2/plugin.js +0 -0
- package/plugins/kimi-k2/plugin.json +0 -0
- package/plugins/kiro/icon-color.svg +0 -0
- package/plugins/kiro/icon-conflict.svg +0 -0
- package/plugins/kiro/icon.svg +0 -0
- package/plugins/kiro/plugin.js +49 -10
- package/plugins/kiro/plugin.json +3 -1
- package/plugins/litellm/icon.svg +0 -0
- package/plugins/litellm/plugin.js +54 -3
- package/plugins/litellm/plugin.json +0 -0
- package/plugins/llm-proxy/icon.svg +0 -0
- package/plugins/llm-proxy/plugin.js +0 -0
- package/plugins/llm-proxy/plugin.json +0 -0
- package/plugins/llmman/LICENSE +21 -0
- package/plugins/llmman/plugin.js +133 -0
- package/plugins/llmman/plugin.json +23 -0
- package/plugins/longcat/icon-color.svg +1 -0
- package/plugins/longcat/icon.svg +1 -0
- package/plugins/longcat/plugin.js +182 -0
- package/plugins/longcat/plugin.json +23 -0
- package/plugins/manus/icon-color.svg +6 -0
- package/plugins/manus/icon.svg +0 -0
- package/plugins/manus/plugin.js +0 -0
- package/plugins/manus/plugin.json +0 -0
- package/plugins/mimo/icon-color.svg +4 -0
- package/plugins/mimo/icon.svg +4 -1
- package/plugins/mimo/plugin.js +0 -0
- package/plugins/mimo/plugin.json +0 -0
- package/plugins/minimax/icon-color.svg +0 -0
- package/plugins/minimax/icon-conflict.svg +0 -0
- package/plugins/minimax/icon.svg +0 -0
- package/plugins/minimax/plugin.js +25 -31
- package/plugins/minimax/plugin.json +0 -0
- package/plugins/mistral/icon-color.svg +0 -0
- package/plugins/mistral/icon.svg +0 -0
- package/plugins/mistral/plugin.js +110 -5
- package/plugins/mistral/plugin.json +0 -0
- package/plugins/moonshot/icon-color.svg +1 -0
- package/plugins/moonshot/icon.svg +0 -0
- package/plugins/moonshot/plugin.js +8 -6
- package/plugins/moonshot/plugin.json +0 -0
- package/plugins/muse/LICENSE +21 -0
- package/plugins/muse/plugin.js +319 -0
- package/plugins/muse/plugin.json +26 -0
- package/plugins/nanogpt/icon-color.svg +0 -0
- package/plugins/nanogpt/icon.svg +0 -0
- package/plugins/nanogpt/plugin.js +0 -0
- package/plugins/nanogpt/plugin.json +0 -0
- package/plugins/neuralwatt/icon.svg +0 -0
- package/plugins/neuralwatt/plugin.js +0 -0
- package/plugins/neuralwatt/plugin.json +0 -0
- package/plugins/notion/icon.svg +1 -0
- package/plugins/notion/plugin.js +282 -0
- package/plugins/notion/plugin.json +29 -0
- package/plugins/nous/LICENSE +21 -0
- package/plugins/nous/icon-color.svg +1 -0
- package/plugins/nous/icon.svg +1 -0
- package/plugins/nous/plugin.js +124 -0
- package/plugins/nous/plugin.json +24 -0
- package/plugins/ollama/icon-color.svg +1 -0
- package/plugins/ollama/icon.svg +0 -0
- package/plugins/ollama/plugin.js +257 -56
- package/plugins/ollama/plugin.json +6 -3
- package/plugins/openai-api/icon-color.svg +1 -0
- package/plugins/openai-api/icon.svg +0 -0
- package/plugins/openai-api/plugin.js +0 -0
- package/plugins/openai-api/plugin.json +0 -0
- package/plugins/opencode/icon-color.svg +1 -0
- package/plugins/opencode/icon.svg +0 -0
- package/plugins/opencode/plugin.js +0 -0
- package/plugins/opencode/plugin.json +0 -0
- package/plugins/opencode-go/icon-color.svg +1 -0
- package/plugins/opencode-go/icon-conflict.svg +0 -0
- package/plugins/opencode-go/icon.svg +0 -0
- package/plugins/opencode-go/plugin.js +103 -17
- package/plugins/opencode-go/plugin.json +5 -2
- package/plugins/openrouter/icon-color.svg +1 -0
- package/plugins/openrouter/icon.svg +0 -0
- package/plugins/openrouter/plugin.js +371 -114
- package/plugins/openrouter/plugin.json +6 -0
- package/plugins/perplexity/icon-color.svg +0 -0
- package/plugins/perplexity/icon-conflict.svg +0 -0
- package/plugins/perplexity/icon.svg +0 -0
- package/plugins/perplexity/plugin.js +0 -0
- package/plugins/perplexity/plugin.json +0 -0
- package/plugins/pi/plugin.js +113 -0
- package/plugins/pi/plugin.json +11 -0
- package/plugins/poe/icon.svg +0 -0
- package/plugins/poe/plugin.js +24 -18
- package/plugins/poe/plugin.json +2 -0
- package/plugins/qoder/icon.svg +3 -0
- package/plugins/qoder/plugin.js +227 -0
- package/plugins/qoder/plugin.json +23 -0
- package/plugins/qwencloud/icon-color.svg +1 -0
- package/plugins/qwencloud/icon.svg +1 -0
- package/plugins/qwencloud/plugin.js +677 -0
- package/plugins/qwencloud/plugin.json +29 -0
- package/plugins/raycast/LICENSE +21 -0
- package/plugins/raycast/plugin.js +174 -0
- package/plugins/raycast/plugin.json +24 -0
- package/plugins/replicate/LICENSE +21 -0
- package/plugins/replicate/icon-color.svg +1 -0
- package/plugins/replicate/icon.svg +1 -0
- package/plugins/replicate/plugin.js +165 -0
- package/plugins/replicate/plugin.json +24 -0
- package/plugins/sakana/icon.svg +5 -0
- package/plugins/sakana/plugin.js +156 -0
- package/plugins/sakana/plugin.json +23 -0
- package/plugins/stepfun/icon-color.svg +0 -0
- package/plugins/stepfun/icon.svg +0 -0
- package/plugins/stepfun/plugin.js +0 -0
- package/plugins/stepfun/plugin.json +0 -0
- package/plugins/sub2api/icon.svg +4 -0
- package/plugins/sub2api/plugin.js +191 -0
- package/plugins/sub2api/plugin.json +16 -0
- package/plugins/synthetic/icon-color.svg +14 -0
- package/plugins/synthetic/icon-conflict.svg +0 -0
- package/plugins/synthetic/icon.svg +0 -0
- package/plugins/synthetic/plugin.js +0 -0
- package/plugins/synthetic/plugin.json +0 -0
- package/plugins/t3chat/icon-color.svg +0 -0
- package/plugins/t3chat/icon.svg +0 -0
- package/plugins/t3chat/plugin.js +0 -0
- package/plugins/t3chat/plugin.json +0 -0
- package/plugins/typesafe/LICENSE +21 -0
- package/plugins/typesafe/plugin.js +225 -0
- package/plugins/typesafe/plugin.json +24 -0
- package/plugins/v0/LICENSE +21 -0
- package/plugins/v0/icon-color.svg +1 -0
- package/plugins/v0/icon.svg +1 -0
- package/plugins/v0/plugin.js +214 -0
- package/plugins/v0/plugin.json +24 -0
- package/plugins/venice/icon-color.svg +0 -0
- package/plugins/venice/icon.svg +0 -0
- package/plugins/venice/plugin.js +50 -1
- package/plugins/venice/plugin.json +2 -1
- package/plugins/vercel/LICENSE +21 -0
- package/plugins/vercel/icon-color.svg +1 -0
- package/plugins/vercel/icon.svg +1 -0
- package/plugins/vercel/plugin.js +55 -0
- package/plugins/vercel/plugin.json +23 -0
- package/plugins/vertex-ai/icon-color.svg +0 -0
- package/plugins/vertex-ai/icon.svg +0 -0
- package/plugins/vertex-ai/plugin.js +0 -0
- package/plugins/vertex-ai/plugin.json +0 -0
- package/plugins/warp/icon-color.svg +0 -0
- package/plugins/warp/icon.svg +0 -0
- package/plugins/warp/plugin.js +0 -0
- package/plugins/warp/plugin.json +0 -0
- package/plugins/wayfinder/icon.svg +5 -0
- package/plugins/wayfinder/plugin.js +47 -0
- package/plugins/wayfinder/plugin.json +16 -0
- package/plugins/windsurf/icon-color.svg +1 -0
- package/plugins/windsurf/icon-conflict.svg +0 -0
- package/plugins/windsurf/icon.svg +0 -0
- package/plugins/windsurf/plugin.js +0 -0
- package/plugins/windsurf/plugin.json +0 -0
- package/plugins/xai/icon-color.svg +1 -0
- package/plugins/xai/icon.svg +1 -0
- package/plugins/xai/plugin.js +162 -0
- package/plugins/xai/plugin.json +28 -0
- package/plugins/xkiro/LICENSE +21 -0
- package/plugins/xkiro/plugin.js +99 -0
- package/plugins/xkiro/plugin.json +23 -0
- package/plugins/zai/icon-color.svg +1 -0
- package/plugins/zai/icon-conflict.svg +0 -0
- package/plugins/zai/icon.svg +0 -0
- package/plugins/zai/plugin.js +111 -161
- package/plugins/zai/plugin.json +4 -1
- package/plugins/zed/icon.svg +0 -0
- package/plugins/zed/plugin.js +0 -0
- package/plugins/zed/plugin.json +0 -0
- package/plugins/zenmux/icon-color.svg +1 -0
- package/plugins/zenmux/icon.svg +1 -0
- package/plugins/zenmux/plugin.js +102 -0
- package/plugins/zenmux/plugin.json +23 -0
- package/plugins/zoommate/icon.svg +1 -0
- package/plugins/zoommate/plugin.js +244 -0
- package/plugins/zoommate/plugin.json +29 -0
- package/usagestat +0 -0
- package/usagestatd +0 -0
package/plugins/claude/plugin.js
CHANGED
|
@@ -20,13 +20,24 @@
|
|
|
20
20
|
const MIN_USAGE_FETCH_INTERVAL_MS = 5 * 60 * 1000 // never poll more than once per 5 min
|
|
21
21
|
const DEFAULT_RATE_LIMIT_BACKOFF_MS = 5 * 60 * 1000 // fallback when no Retry-After header
|
|
22
22
|
const LIVE_USAGE_CACHE_FILE = "live-usage-cache.json"
|
|
23
|
-
const LIVE_USAGE_CACHE_VERSION =
|
|
23
|
+
const LIVE_USAGE_CACHE_VERSION = 2
|
|
24
24
|
const LIVE_USAGE_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
25
25
|
let rateLimitedUntilMs = 0 // epoch ms; 0 = not rate-limited
|
|
26
26
|
let lastUsageFetchMs = 0 // epoch ms of the most-recent OAuth API attempt
|
|
27
27
|
let cachedUsageData = null // last successful OAuth API response body (parsed JSON)
|
|
28
28
|
let lastWebUsageFetchMs = 0 // epoch ms of the most-recent Web API attempt
|
|
29
29
|
let cachedWebUsageData = null // last successful Web API response body (parsed JSON)
|
|
30
|
+
let activeCacheIdentity = null
|
|
31
|
+
|
|
32
|
+
function bindUsageCache(ctx, creds, sessionKey) {
|
|
33
|
+
const identity = ctx.host.crypto.sha256(JSON.stringify([
|
|
34
|
+
creds && creds.oauth && creds.oauth.accessToken || "", sessionKey || "",
|
|
35
|
+
ctx.provider && ctx.provider.workspaceId || "",
|
|
36
|
+
]))
|
|
37
|
+
if (activeCacheIdentity !== identity) _resetState()
|
|
38
|
+
activeCacheIdentity = identity
|
|
39
|
+
ctx.claudeCacheIdentity = identity
|
|
40
|
+
}
|
|
30
41
|
|
|
31
42
|
function utf8DecodeBytes(bytes) {
|
|
32
43
|
// Prefer native TextDecoder when available (QuickJS may not expose it).
|
|
@@ -194,7 +205,7 @@
|
|
|
194
205
|
if (!path || !ctx.host.fs.exists(path)) return null
|
|
195
206
|
try {
|
|
196
207
|
const parsed = ctx.util.tryParseJson(ctx.host.fs.readText(path))
|
|
197
|
-
if (!parsed || typeof parsed !== "object") return null
|
|
208
|
+
if (!parsed || typeof parsed !== "object" || parsed.accountHash !== ctx.claudeCacheIdentity) return null
|
|
198
209
|
return parsed
|
|
199
210
|
} catch {
|
|
200
211
|
return null
|
|
@@ -210,6 +221,7 @@
|
|
|
210
221
|
cache[key] = updates[key]
|
|
211
222
|
}
|
|
212
223
|
cache.version = LIVE_USAGE_CACHE_VERSION
|
|
224
|
+
cache.accountHash = ctx.claudeCacheIdentity
|
|
213
225
|
cache.updatedAtMs = Date.now()
|
|
214
226
|
ctx.host.fs.writeText(path, JSON.stringify(cache, null, 2))
|
|
215
227
|
} catch (e) {
|
|
@@ -399,6 +411,8 @@
|
|
|
399
411
|
|
|
400
412
|
const envAccessToken = readEnvText(ctx, "CLAUDE_CODE_OAUTH_TOKEN")
|
|
401
413
|
const stored = loadStoredCredentials(ctx, !!envAccessToken)
|
|
414
|
+
// setup-token credentials cannot read quotas; prefer the signed-in profile.
|
|
415
|
+
if (stored && stored.oauth && stored.oauth.accessToken && hasProfileScope(stored)) return stored
|
|
402
416
|
if (!envAccessToken) {
|
|
403
417
|
return stored
|
|
404
418
|
}
|
|
@@ -569,6 +583,7 @@
|
|
|
569
583
|
// Persist updated credentials back to the same source we read from.
|
|
570
584
|
fullData.claudeAiOauth = oauth
|
|
571
585
|
saveCredentials(ctx, source, creds.serviceName, fullData)
|
|
586
|
+
bindUsageCache(ctx, creds, getSessionKey(ctx))
|
|
572
587
|
|
|
573
588
|
ctx.host.log.info("refresh succeeded, new token expires in " + (body.expires_in || "unknown") + "s")
|
|
574
589
|
return newAccessToken
|
|
@@ -581,15 +596,21 @@
|
|
|
581
596
|
|
|
582
597
|
function fetchUsage(ctx, accessToken) {
|
|
583
598
|
const oauthConfig = getOauthConfig(ctx)
|
|
599
|
+
let version = "2.1.0"
|
|
600
|
+
try {
|
|
601
|
+
const result = ctx.host.command.run({program: "claude", args: ["--version"], timeoutMs: 3000})
|
|
602
|
+
const match = result.status === 0 && /^\s*(\d+\.\d+\.\d+)(?:\s|$)/.exec(result.stdout || "")
|
|
603
|
+
if (match) version = match[1]
|
|
604
|
+
} catch (_) { /* The OAuth source also works without a local CLI. */ }
|
|
584
605
|
return ctx.util.request({
|
|
585
606
|
method: "GET",
|
|
586
|
-
url: oauthConfig.usageUrl,
|
|
607
|
+
url: oauthConfig.usageUrl + "?cedar_ember=1&skip_spend=1",
|
|
587
608
|
headers: {
|
|
588
609
|
Authorization: "Bearer " + accessToken.trim(),
|
|
589
610
|
Accept: "application/json",
|
|
590
611
|
"Content-Type": "application/json",
|
|
591
612
|
"anthropic-beta": "oauth-2025-04-20",
|
|
592
|
-
"User-Agent": "claude-code/
|
|
613
|
+
"User-Agent": "claude-code/" + version,
|
|
593
614
|
},
|
|
594
615
|
timeoutMs: 10000,
|
|
595
616
|
})
|
|
@@ -603,8 +624,8 @@
|
|
|
603
624
|
if (!str) return null
|
|
604
625
|
// Retry-After can be a delay-seconds or HTTP-date (RFC 7231).
|
|
605
626
|
// 0 means "retry immediately" — return 0 as a valid value.
|
|
606
|
-
const seconds =
|
|
607
|
-
if (Number.
|
|
627
|
+
const seconds = /^\d+$/.test(str) ? Number(str) : NaN
|
|
628
|
+
if (Number.isSafeInteger(seconds)) return seconds
|
|
608
629
|
const dateMs = Date.parse(str)
|
|
609
630
|
if (Number.isFinite(dateMs)) {
|
|
610
631
|
const delay = Math.ceil((dateMs - Date.now()) / 1000)
|
|
@@ -613,6 +634,29 @@
|
|
|
613
634
|
return null
|
|
614
635
|
}
|
|
615
636
|
|
|
637
|
+
function livePlanLabel(ctx, creds) {
|
|
638
|
+
const token = creds.oauth.accessToken
|
|
639
|
+
const tokenHash = ctx.host.crypto.sha256(token)
|
|
640
|
+
const cached = readLiveUsageCache(ctx)
|
|
641
|
+
if (cached && cached.profileTokenHash === tokenHash) return cached.livePlan || null
|
|
642
|
+
let plan = null
|
|
643
|
+
try {
|
|
644
|
+
const result = ctx.util.requestJson({method: "GET", url: getOauthConfig(ctx).baseApiUrl + "/api/oauth/profile",
|
|
645
|
+
headers: {Authorization: "Bearer " + token, Accept: "application/json", "anthropic-beta": "oauth-2025-04-20"}, timeoutMs: 3000})
|
|
646
|
+
const org = result.resp.status === 200 && result.json && result.json.organization
|
|
647
|
+
if (org && typeof org === "object") {
|
|
648
|
+
const type = cleanText(org.organization_type || org.organizationType)
|
|
649
|
+
const tier = cleanText(org.rate_limit_tier || org.rateLimitTier) || creds.oauth.rateLimitTier
|
|
650
|
+
if (type) plan = oauthPlanLabel(ctx, {oauth: {subscriptionType: type.replace(/^claude_/, ""), rateLimitTier: tier}})
|
|
651
|
+
}
|
|
652
|
+
} catch (_) {
|
|
653
|
+
ctx.host.log.warn("Claude profile unavailable; retaining credential plan.")
|
|
654
|
+
}
|
|
655
|
+
// Include failed lookups, so a profile outage cannot add a request on every poll.
|
|
656
|
+
writeLiveUsageCache(ctx, {profileTokenHash: tokenHash, livePlan: plan})
|
|
657
|
+
return plan
|
|
658
|
+
}
|
|
659
|
+
|
|
616
660
|
function fmtRateLimitMinutes(seconds) {
|
|
617
661
|
if (seconds <= 0) return "now"
|
|
618
662
|
const mins = Math.ceil(seconds / 60)
|
|
@@ -1136,9 +1180,9 @@
|
|
|
1136
1180
|
|
|
1137
1181
|
function getSessionKey(ctx) {
|
|
1138
1182
|
const candidates = [
|
|
1183
|
+
providerCookieHeader(ctx),
|
|
1139
1184
|
readEnvText(ctx, "CLAUDE_AI_SESSION_KEY"),
|
|
1140
1185
|
readEnvText(ctx, "CLAUDE_WEB_SESSION_KEY"),
|
|
1141
|
-
providerCookieHeader(ctx),
|
|
1142
1186
|
]
|
|
1143
1187
|
for (let i = 0; i < candidates.length; i++) {
|
|
1144
1188
|
const key = extractSessionKey(candidates[i])
|
|
@@ -1147,9 +1191,12 @@
|
|
|
1147
1191
|
return null
|
|
1148
1192
|
}
|
|
1149
1193
|
|
|
1150
|
-
function buildWebHeaders(sessionKey) {
|
|
1194
|
+
function buildWebHeaders(ctx, sessionKey) {
|
|
1195
|
+
const configured = providerCookieHeader(ctx)
|
|
1196
|
+
const cookie = configured && extractSessionKey(configured) === sessionKey && configured.includes("=")
|
|
1197
|
+
? configured : "sessionKey=" + sessionKey
|
|
1151
1198
|
return {
|
|
1152
|
-
Cookie:
|
|
1199
|
+
Cookie: cookie,
|
|
1153
1200
|
Accept: "application/json",
|
|
1154
1201
|
Origin: "https://claude.ai",
|
|
1155
1202
|
Referer: "https://claude.ai/settings/usage",
|
|
@@ -1158,8 +1205,22 @@
|
|
|
1158
1205
|
}
|
|
1159
1206
|
}
|
|
1160
1207
|
|
|
1208
|
+
function checkWebResponse(resp) {
|
|
1209
|
+
const headers = resp.headers || {}
|
|
1210
|
+
const challenge = String(headers["cf-mitigated"] || headers["Cf-Mitigated"] || "").toLowerCase() === "challenge"
|
|
1211
|
+
|| /<title>\s*just a moment|\/cdn-cgi\/challenge-platform\//i.test(String(resp.bodyText || "").slice(0,65536))
|
|
1212
|
+
if (resp.status === 403 && challenge) {
|
|
1213
|
+
throw "Claude web usage is blocked by a Cloudflare browser challenge. Your Claude login has not been rejected."
|
|
1214
|
+
}
|
|
1215
|
+
if (resp.status === 429) {
|
|
1216
|
+
const error = new Error("Claude web usage updates are temporarily rate-limited, not your message allowance.")
|
|
1217
|
+
error.retryAfterSeconds = parseRetryAfterSeconds(headers)
|
|
1218
|
+
throw error
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1161
1222
|
function fetchWebUsage(ctx, sessionKey) {
|
|
1162
|
-
const headers = buildWebHeaders(sessionKey)
|
|
1223
|
+
const headers = buildWebHeaders(ctx, sessionKey)
|
|
1163
1224
|
|
|
1164
1225
|
let orgResp
|
|
1165
1226
|
try {
|
|
@@ -1174,6 +1235,7 @@
|
|
|
1174
1235
|
throw "Web usage request failed. Check your connection."
|
|
1175
1236
|
}
|
|
1176
1237
|
|
|
1238
|
+
checkWebResponse(orgResp)
|
|
1177
1239
|
if (orgResp.status === 401 || orgResp.status === 403) {
|
|
1178
1240
|
throw "Web session invalid or expired. Set CLAUDE_AI_SESSION_KEY to your claude.ai sessionKey cookie."
|
|
1179
1241
|
}
|
|
@@ -1206,6 +1268,7 @@
|
|
|
1206
1268
|
}
|
|
1207
1269
|
|
|
1208
1270
|
lastUsageStatus = usageResp.status
|
|
1271
|
+
checkWebResponse(usageResp)
|
|
1209
1272
|
if (usageResp.status === 401 || usageResp.status === 403) {
|
|
1210
1273
|
ctx.host.log.info("web mode: usage unavailable for org " + candidateOrgId + " (HTTP " + usageResp.status + ")")
|
|
1211
1274
|
continue
|
|
@@ -1252,29 +1315,46 @@
|
|
|
1252
1315
|
|
|
1253
1316
|
function loadWebUsageData(ctx, sessionKey, nowMs) {
|
|
1254
1317
|
const liveCache = readLiveUsageCache(ctx)
|
|
1318
|
+
if (liveCache && Number(liveCache.webRetryAtMs) > nowMs) {
|
|
1319
|
+
throw liveCache.webError || "Claude web usage updates are temporarily unavailable."
|
|
1320
|
+
}
|
|
1255
1321
|
const cachedLastWebUsageFetchMs = Number(liveCache && liveCache.lastWebUsageFetchMs) || 0
|
|
1256
1322
|
const effectiveLastWebUsageFetchMs = Math.max(lastWebUsageFetchMs, cachedLastWebUsageFetchMs)
|
|
1257
|
-
|
|
1323
|
+
const cached = cachedWebUsageData || (liveCache && liveCache.usageSource === "web" && cachedLiveUsageData(liveCache, nowMs))
|
|
1324
|
+
if (cached && nowMs - effectiveLastWebUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
|
|
1258
1325
|
lastWebUsageFetchMs = effectiveLastWebUsageFetchMs
|
|
1259
1326
|
ctx.host.log.info(
|
|
1260
1327
|
"web usage fetch skipped: last fetch was " +
|
|
1261
1328
|
Math.round((nowMs - effectiveLastWebUsageFetchMs) / 1000) + "s ago"
|
|
1262
1329
|
)
|
|
1263
|
-
return { data:
|
|
1330
|
+
return { data: cached, plan: liveCache && liveCache.webPlan || null }
|
|
1264
1331
|
}
|
|
1265
1332
|
|
|
1266
1333
|
lastWebUsageFetchMs = nowMs
|
|
1267
|
-
|
|
1334
|
+
let result
|
|
1335
|
+
try {
|
|
1336
|
+
result = fetchWebUsage(ctx, sessionKey)
|
|
1337
|
+
const validLines = []
|
|
1338
|
+
addUsageWindowLines(ctx, result.usageData, validLines)
|
|
1339
|
+
if (!validLines.length) throw "Web usage response invalid. Try again later."
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
const delay = error && Number.isFinite(error.retryAfterSeconds) ? error.retryAfterSeconds * 1000 : MIN_USAGE_FETCH_INTERVAL_MS
|
|
1342
|
+
writeLiveUsageCache(ctx, {lastWebUsageFetchMs: nowMs, webRetryAtMs: nowMs + Math.max(MIN_USAGE_FETCH_INTERVAL_MS, delay),
|
|
1343
|
+
webError: error && error.message || String(error)})
|
|
1344
|
+
throw error
|
|
1345
|
+
}
|
|
1268
1346
|
cachedWebUsageData = result.usageData
|
|
1347
|
+
let plan = null
|
|
1348
|
+
if (result.accountInfo && result.accountInfo.rate_limit_tier) {
|
|
1349
|
+
plan = webTierToPlanLabel(result.accountInfo.rate_limit_tier)
|
|
1350
|
+
}
|
|
1269
1351
|
writeLiveUsageCache(ctx, {
|
|
1270
1352
|
usageData: result.usageData,
|
|
1353
|
+
usageSource: "web",
|
|
1271
1354
|
usageFetchedAtMs: nowMs,
|
|
1272
1355
|
lastWebUsageFetchMs: nowMs,
|
|
1356
|
+
webRetryAtMs: 0, webError: null, webPlan: plan,
|
|
1273
1357
|
})
|
|
1274
|
-
let plan = null
|
|
1275
|
-
if (result.accountInfo && result.accountInfo.rate_limit_tier) {
|
|
1276
|
-
plan = webTierToPlanLabel(result.accountInfo.rate_limit_tier)
|
|
1277
|
-
}
|
|
1278
1358
|
return { data: result.usageData, plan: plan }
|
|
1279
1359
|
}
|
|
1280
1360
|
|
|
@@ -1282,7 +1362,9 @@
|
|
|
1282
1362
|
const t = (tier || "").toLowerCase()
|
|
1283
1363
|
if (t === "free") return "Claude Free"
|
|
1284
1364
|
if (t === "pro" || t === "claude_pro") return "Claude Pro"
|
|
1285
|
-
if (t === "
|
|
1365
|
+
if (t === "claude_max_5" || t === "claude_max_5x") return "Claude Max 5x"
|
|
1366
|
+
if (t === "claude_max_20" || t === "claude_max_20x") return "Claude Max 20x"
|
|
1367
|
+
if (t === "max") return "Claude Max"
|
|
1286
1368
|
if (t === "team") return "Claude Team"
|
|
1287
1369
|
if (t === "enterprise") return "Claude Enterprise"
|
|
1288
1370
|
return "Claude (" + tier + ")"
|
|
@@ -1299,6 +1381,8 @@
|
|
|
1299
1381
|
}
|
|
1300
1382
|
|
|
1301
1383
|
function moneyMajorUnits(value) {
|
|
1384
|
+
if (typeof value !== "number" && typeof value !== "string") return null
|
|
1385
|
+
if (typeof value === "string" && !value.trim()) return null
|
|
1302
1386
|
const n = Number(value)
|
|
1303
1387
|
if (!Number.isFinite(n)) return null
|
|
1304
1388
|
// Claude usage APIs return Extra usage amounts in minor units (cents).
|
|
@@ -1333,9 +1417,18 @@
|
|
|
1333
1417
|
return months[d.getUTCMonth()] + " " + String(d.getUTCDate())
|
|
1334
1418
|
}
|
|
1335
1419
|
|
|
1420
|
+
function currentUsageWindow(data, kind, fallback) {
|
|
1421
|
+
const entries = Array.isArray(data.limits) ? data.limits.filter((entry) =>
|
|
1422
|
+
entry && entry.kind === kind && Number.isFinite(entry.percent) &&
|
|
1423
|
+
!(entry.scope && (entry.scope.model || entry.scope.surface))) : []
|
|
1424
|
+
const entry = entries.find((item) => item.is_active === true) || entries[0]
|
|
1425
|
+
return entry ? { utilization: entry.percent, resets_at: entry.resets_at } : fallback
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1336
1428
|
function addUsageWindowLines(ctx, data, lines) {
|
|
1337
|
-
const
|
|
1338
|
-
|
|
1429
|
+
const settings = ctx.provider && ctx.provider.settings || {}
|
|
1430
|
+
const fiveHour = currentUsageWindow(data, "session", data.five_hour)
|
|
1431
|
+
if (fiveHour && Number.isFinite(fiveHour.utilization)) {
|
|
1339
1432
|
lines.push(ctx.line.progress({
|
|
1340
1433
|
label: "Session",
|
|
1341
1434
|
used: fiveHour.utilization,
|
|
@@ -1346,8 +1439,8 @@
|
|
|
1346
1439
|
}))
|
|
1347
1440
|
}
|
|
1348
1441
|
|
|
1349
|
-
const sevenDay = data.seven_day
|
|
1350
|
-
if (sevenDay &&
|
|
1442
|
+
const sevenDay = currentUsageWindow(data, "weekly_all", data.seven_day)
|
|
1443
|
+
if (sevenDay && Number.isFinite(sevenDay.utilization)) {
|
|
1351
1444
|
lines.push(ctx.line.progress({
|
|
1352
1445
|
label: "Weekly",
|
|
1353
1446
|
used: sevenDay.utilization,
|
|
@@ -1359,7 +1452,7 @@
|
|
|
1359
1452
|
}
|
|
1360
1453
|
|
|
1361
1454
|
const sevenDaySonnet = data.seven_day_sonnet
|
|
1362
|
-
if (sevenDaySonnet &&
|
|
1455
|
+
if (settings.showModelQuotas === true && sevenDaySonnet && Number.isFinite(sevenDaySonnet.utilization)) {
|
|
1363
1456
|
lines.push(ctx.line.progress({
|
|
1364
1457
|
label: "Sonnet",
|
|
1365
1458
|
used: sevenDaySonnet.utilization,
|
|
@@ -1371,7 +1464,7 @@
|
|
|
1371
1464
|
}
|
|
1372
1465
|
|
|
1373
1466
|
const sevenDayOpus = data.seven_day_opus
|
|
1374
|
-
if (sevenDayOpus &&
|
|
1467
|
+
if (settings.showModelQuotas === true && sevenDayOpus && Number.isFinite(sevenDayOpus.utilization)) {
|
|
1375
1468
|
lines.push(ctx.line.progress({
|
|
1376
1469
|
label: "Opus",
|
|
1377
1470
|
used: sevenDayOpus.utilization,
|
|
@@ -1382,13 +1475,23 @@
|
|
|
1382
1475
|
}))
|
|
1383
1476
|
}
|
|
1384
1477
|
|
|
1478
|
+
if (settings.showModelQuotas === true && Array.isArray(data.limits)) {
|
|
1479
|
+
for (const entry of data.limits) {
|
|
1480
|
+
if (!entry || entry.kind !== "weekly_scoped" || !Number.isFinite(entry.percent)) continue
|
|
1481
|
+
const model = entry.scope && entry.scope.model && entry.scope.model.display_name
|
|
1482
|
+
if (typeof model !== "string" || !model.trim() || lines.some((line) => line.label === model.trim())) continue
|
|
1483
|
+
lines.push(ctx.line.progress({ label: model.trim(), used: entry.percent, limit: 100,
|
|
1484
|
+
format: { kind: "percent" }, resetsAt: ctx.util.toIso(entry.resets_at), periodDurationMs: 7 * 24 * 60 * 60 * 1000 }))
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1385
1488
|
// Routines: check canonical name first, then legacy aliases
|
|
1386
1489
|
const routinesWindow = pickUsageWindow(data, [
|
|
1387
1490
|
"seven_day_routines",
|
|
1388
1491
|
"seven_day_cowork",
|
|
1389
1492
|
"seven_day_claude_routines",
|
|
1390
1493
|
])
|
|
1391
|
-
if (routinesWindow &&
|
|
1494
|
+
if (settings.hideDailyRoutines !== true && routinesWindow && Number.isFinite(routinesWindow.utilization)) {
|
|
1392
1495
|
lines.push(ctx.line.progress({
|
|
1393
1496
|
label: "Routines",
|
|
1394
1497
|
used: routinesWindow.utilization,
|
|
@@ -1399,6 +1502,24 @@
|
|
|
1399
1502
|
}))
|
|
1400
1503
|
}
|
|
1401
1504
|
|
|
1505
|
+
const grants = data.cedar_ember
|
|
1506
|
+
if (grants && typeof grants === "object" && !Array.isArray(grants)) {
|
|
1507
|
+
let count = 0
|
|
1508
|
+
const expiries = []
|
|
1509
|
+
if (grants.eligible === true && Array.isArray(grants.grants)) {
|
|
1510
|
+
for (const grant of grants.grants) {
|
|
1511
|
+
if (!grant || !Number.isSafeInteger(grant.resets_left) || grant.resets_left <= 0) continue
|
|
1512
|
+
const end = ctx.util.toIso(grant.ends_at)
|
|
1513
|
+
if (end && Date.parse(end) <= Date.parse(ctx.nowIso)) continue
|
|
1514
|
+
if (!Number.isSafeInteger(count + grant.resets_left)) continue
|
|
1515
|
+
count += grant.resets_left
|
|
1516
|
+
if (end) expiries.push(end + " (" + grant.resets_left + ")")
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
lines.push(ctx.line.text({label: "Rate Limit Resets", value: count + " available",
|
|
1520
|
+
subtitle: expiries.length ? "Expires " + expiries.sort().join(", ") : undefined}))
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1402
1523
|
if (data.extra_usage && data.extra_usage.is_enabled) {
|
|
1403
1524
|
const used = moneyMajorUnits(data.extra_usage.used_credits)
|
|
1404
1525
|
const limit = moneyMajorUnits(data.extra_usage.monthly_limit)
|
|
@@ -1448,6 +1569,29 @@
|
|
|
1448
1569
|
|
|
1449
1570
|
// ── probe() ────────────────────────────────────────────────────────────────
|
|
1450
1571
|
|
|
1572
|
+
function readNativeQuota(ctx, creds, probe) {
|
|
1573
|
+
// Never borrow the machine's ambient CLI account for a named account or an
|
|
1574
|
+
// inference-only token. The native host also verifies the profile token.
|
|
1575
|
+
if (!creds || creds.source !== "file" || !hasProfileScope(creds)) return null
|
|
1576
|
+
const host = ctx.host.claude
|
|
1577
|
+
const read = host && host[probe ? "probeQuota" : "readQuota"]
|
|
1578
|
+
if (typeof read !== "function") return null
|
|
1579
|
+
try {
|
|
1580
|
+
const raw = read(ctx.host.crypto.sha256(creds.oauth.accessToken), ctx.provider && ctx.provider.workspaceId || null)
|
|
1581
|
+
const result = typeof raw === "string" ? ctx.util.tryParseJson(raw) : raw
|
|
1582
|
+
if (!result || !result.data || !Number.isFinite(result.fetchedAtMs)) return null
|
|
1583
|
+
const age = Date.now() - result.fetchedAtMs
|
|
1584
|
+
if (age < 0 || age > MIN_USAGE_FETCH_INTERVAL_MS) return null
|
|
1585
|
+
if (result.source !== "cli" && result.source !== "cli-cache") return null
|
|
1586
|
+
const valid = []
|
|
1587
|
+
addUsageWindowLines(ctx, result.data, valid)
|
|
1588
|
+
return valid.length ? result : null
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
ctx.host.log.info("Claude CLI quota unavailable: " + String(e))
|
|
1591
|
+
return null
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1451
1595
|
function probe(ctx) {
|
|
1452
1596
|
const creds = loadCredentials(ctx)
|
|
1453
1597
|
const sessionKey = getSessionKey(ctx)
|
|
@@ -1457,6 +1601,7 @@
|
|
|
1457
1601
|
const wantsAuto = sourceMode === "auto"
|
|
1458
1602
|
const wantsWeb = sourceMode === "web"
|
|
1459
1603
|
const wantsOAuth = sourceMode === "oauth"
|
|
1604
|
+
const wantsCli = sourceMode === "cli"
|
|
1460
1605
|
const wantsLocal = sourceMode === "local"
|
|
1461
1606
|
const wantsApi = sourceMode === "api"
|
|
1462
1607
|
|
|
@@ -1472,15 +1617,14 @@
|
|
|
1472
1617
|
ctx.host.log.error("api mode requested but no Claude Admin API key found")
|
|
1473
1618
|
throw "Claude API usage needs an Anthropic Admin API key. Set ANTHROPIC_ADMIN_KEY."
|
|
1474
1619
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
throw "Not logged in. Run `claude` to authenticate, set CLAUDE_AI_SESSION_KEY, or set ANTHROPIC_ADMIN_KEY."
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1620
|
+
const noLiveCredentials = !hasOAuth && !sessionKey && !adminApiKey
|
|
1621
|
+
bindUsageCache(ctx, creds, sessionKey)
|
|
1480
1622
|
const nowMs = Date.now()
|
|
1481
1623
|
const liveCache = readLiveUsageCache(ctx)
|
|
1482
1624
|
const homePath = getClaudeHomeOverride(ctx)
|
|
1483
1625
|
let data = null
|
|
1626
|
+
let quotaSource = null
|
|
1627
|
+
let quotaFetchedAtMs = null
|
|
1484
1628
|
let lines = []
|
|
1485
1629
|
let plan = null
|
|
1486
1630
|
let rateLimited = false
|
|
@@ -1488,145 +1632,194 @@
|
|
|
1488
1632
|
|
|
1489
1633
|
if (hasOAuth) {
|
|
1490
1634
|
plan = oauthPlanLabel(ctx, creds)
|
|
1635
|
+
if (liveCache && liveCache.profileTokenHash === ctx.host.crypto.sha256(creds.oauth.accessToken) && liveCache.livePlan) plan = liveCache.livePlan
|
|
1491
1636
|
}
|
|
1492
1637
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1638
|
+
let nativeQuota = null
|
|
1639
|
+
if (wantsAuto || wantsCli) {
|
|
1640
|
+
nativeQuota = readNativeQuota(ctx, creds, false)
|
|
1641
|
+
const cooldownUntil = Math.max(rateLimitedUntilMs, Number(liveCache && liveCache.rateLimitedUntilMs) || 0)
|
|
1642
|
+
const apiFetchedAt = Number(liveCache && liveCache.usageFetchedAtMs) || 0
|
|
1643
|
+
const freshApi = cachedLiveUsageData(liveCache, nowMs) && apiFetchedAt <= nowMs && nowMs - apiFetchedAt < MIN_USAGE_FETCH_INTERVAL_MS
|
|
1644
|
+
if (!nativeQuota && wantsCli && nowMs >= cooldownUntil) {
|
|
1645
|
+
nativeQuota = readNativeQuota(ctx, creds, true)
|
|
1646
|
+
}
|
|
1647
|
+
// Prefer the newer complete observation; passive reads never clear an
|
|
1648
|
+
// HTTP Retry-After deadline or rewrite the REST cache's fetch timestamp.
|
|
1649
|
+
if (nativeQuota && !wantsCli && freshApi && apiFetchedAt > nativeQuota.fetchedAtMs) nativeQuota = null
|
|
1650
|
+
}
|
|
1506
1651
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1652
|
+
try {
|
|
1653
|
+
if (wantsLocal) {
|
|
1654
|
+
ctx.host.log.info("local mode requested; skipping live usage fetch")
|
|
1655
|
+
} else if (wantsApi) {
|
|
1656
|
+
ctx.host.log.info("api mode requested; skipping live quota fetch")
|
|
1657
|
+
} else if (nativeQuota) {
|
|
1658
|
+
data = nativeQuota.data
|
|
1659
|
+
quotaSource = nativeQuota.source
|
|
1660
|
+
quotaFetchedAtMs = nativeQuota.fetchedAtMs
|
|
1661
|
+
} else if (wantsCli) {
|
|
1662
|
+
const cooldownUntil = Math.max(rateLimitedUntilMs, Number(liveCache && liveCache.rateLimitedUntilMs) || 0)
|
|
1663
|
+
if (nowMs < cooldownUntil) {
|
|
1510
1664
|
rateLimited = true
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
data = cachedData
|
|
1514
|
-
ctx.host.log.info("usage fetch skipped: rate-limited for " + retryAfterSeconds + "s more")
|
|
1665
|
+
retryAfterSeconds = Math.ceil((cooldownUntil - nowMs) / 1000)
|
|
1666
|
+
data = cachedLiveUsageData(liveCache, nowMs)
|
|
1515
1667
|
} else {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1668
|
+
throw "Claude CLI returned no fresh quota data. Usage checks may be temporarily unavailable."
|
|
1669
|
+
}
|
|
1670
|
+
} else if (hasOAuth && !wantsWeb) {
|
|
1671
|
+
quotaSource = "oauth"
|
|
1672
|
+
// ── OAuth mode ───────────────────────────────────────────────────────
|
|
1673
|
+
let accessToken = creds.oauth.accessToken
|
|
1674
|
+
const canFetchLiveUsage = hasProfileScope(creds)
|
|
1675
|
+
|
|
1676
|
+
if (canFetchLiveUsage) {
|
|
1677
|
+
const cachedData = cachedUsageData || cachedLiveUsageData(liveCache, nowMs)
|
|
1678
|
+
const cachedRateLimitedUntilMs = Number(liveCache && liveCache.rateLimitedUntilMs) || 0
|
|
1679
|
+
const effectiveRateLimitedUntilMs = Math.max(rateLimitedUntilMs, cachedRateLimitedUntilMs)
|
|
1680
|
+
|
|
1681
|
+
if (nowMs < effectiveRateLimitedUntilMs) {
|
|
1682
|
+
// Still within a rate-limit window from a previous probe call — skip the
|
|
1683
|
+
// API request entirely and surface the remaining wait time to the user.
|
|
1684
|
+
rateLimited = true
|
|
1685
|
+
rateLimitedUntilMs = effectiveRateLimitedUntilMs
|
|
1686
|
+
retryAfterSeconds = Math.ceil((effectiveRateLimitedUntilMs - nowMs) / 1000)
|
|
1527
1687
|
data = cachedData
|
|
1528
|
-
ctx.host.log.info(
|
|
1529
|
-
"usage fetch skipped: last fetch was " +
|
|
1530
|
-
Math.round((nowMs - effectiveLastUsageFetchMs) / 1000) + "s ago (min interval " +
|
|
1531
|
-
MIN_USAGE_FETCH_INTERVAL_MS / 1000 + "s)"
|
|
1532
|
-
)
|
|
1688
|
+
ctx.host.log.info("usage fetch skipped: rate-limited for " + retryAfterSeconds + "s more")
|
|
1533
1689
|
} else {
|
|
1534
|
-
//
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
lastUsageFetchMs = nowMs
|
|
1546
|
-
let resp
|
|
1547
|
-
let didRefresh = false
|
|
1548
|
-
try {
|
|
1549
|
-
resp = ctx.util.retryOnceOnAuth({
|
|
1550
|
-
request: (token) => {
|
|
1551
|
-
try {
|
|
1552
|
-
return fetchUsage(ctx, token || accessToken)
|
|
1553
|
-
} catch (e) {
|
|
1554
|
-
ctx.host.log.error("usage request exception: " + String(e))
|
|
1555
|
-
if (didRefresh) {
|
|
1556
|
-
throw "Usage request failed after refresh. Try again."
|
|
1557
|
-
}
|
|
1558
|
-
throw "Usage request failed. Check your connection."
|
|
1559
|
-
}
|
|
1560
|
-
},
|
|
1561
|
-
refresh: () => {
|
|
1562
|
-
ctx.host.log.info("usage returned 401, attempting refresh")
|
|
1563
|
-
didRefresh = true
|
|
1564
|
-
return refreshToken(ctx, creds)
|
|
1565
|
-
},
|
|
1566
|
-
})
|
|
1567
|
-
} catch (e) {
|
|
1568
|
-
if (typeof e === "string") throw e
|
|
1569
|
-
ctx.host.log.error("usage request failed: " + String(e))
|
|
1570
|
-
throw "Usage request failed. Check your connection."
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
if (ctx.util.isAuthStatus(resp.status)) {
|
|
1574
|
-
ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
|
|
1575
|
-
throw "Token expired. Run `claude` to log in again."
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
if (resp.status === 429) {
|
|
1579
|
-
rateLimited = true
|
|
1580
|
-
retryAfterSeconds = parseRetryAfterSeconds(resp.headers)
|
|
1581
|
-
const backoffMs = retryAfterSeconds !== null
|
|
1582
|
-
? retryAfterSeconds * 1000
|
|
1583
|
-
: DEFAULT_RATE_LIMIT_BACKOFF_MS
|
|
1584
|
-
rateLimitedUntilMs = nowMs + backoffMs
|
|
1690
|
+
// Rate-limit window has expired (or was never set). Check whether we were
|
|
1691
|
+
// previously rate-limited so we can bypass the min-interval guard: a short
|
|
1692
|
+
// Retry-After (< 5 min) must not be swallowed by the normal poll throttle.
|
|
1693
|
+
const wasRateLimited = effectiveRateLimitedUntilMs > 0
|
|
1694
|
+
rateLimitedUntilMs = 0
|
|
1695
|
+
|
|
1696
|
+
const cachedLastUsageFetchMs = Number(liveCache && liveCache.lastUsageFetchMs) || 0
|
|
1697
|
+
const effectiveLastUsageFetchMs = Math.max(lastUsageFetchMs, cachedLastUsageFetchMs)
|
|
1698
|
+
if (cachedData && !wasRateLimited && nowMs - effectiveLastUsageFetchMs < MIN_USAGE_FETCH_INTERVAL_MS) {
|
|
1699
|
+
// Polled too recently in normal operation — reuse last cached response.
|
|
1700
|
+
lastUsageFetchMs = effectiveLastUsageFetchMs
|
|
1585
1701
|
data = cachedData
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
usageFetchedAtMs: Number(liveCache && liveCache.usageFetchedAtMs) || null,
|
|
1591
|
-
lastRateLimitedAtMs: nowMs,
|
|
1592
|
-
})
|
|
1593
|
-
ctx.host.log.warn(
|
|
1594
|
-
"usage rate limited (429), backing off for " +
|
|
1595
|
-
Math.round(backoffMs / 1000) + "s"
|
|
1702
|
+
ctx.host.log.info(
|
|
1703
|
+
"usage fetch skipped: last fetch was " +
|
|
1704
|
+
Math.round((nowMs - effectiveLastUsageFetchMs) / 1000) + "s ago (min interval " +
|
|
1705
|
+
MIN_USAGE_FETCH_INTERVAL_MS / 1000 + "s)"
|
|
1596
1706
|
)
|
|
1597
|
-
} else if (resp.status < 200 || resp.status >= 300) {
|
|
1598
|
-
ctx.host.log.error("usage returned error: status=" + resp.status)
|
|
1599
|
-
throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
|
|
1600
1707
|
} else {
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1708
|
+
// Proactively refresh if token is expired or about to expire
|
|
1709
|
+
if (needsRefresh(ctx, creds.oauth, nowMs)) {
|
|
1710
|
+
ctx.host.log.info("token needs refresh (expired or expiring soon)")
|
|
1711
|
+
const refreshed = refreshToken(ctx, creds)
|
|
1712
|
+
if (refreshed) {
|
|
1713
|
+
accessToken = refreshed
|
|
1714
|
+
} else {
|
|
1715
|
+
ctx.host.log.warn("proactive refresh failed, trying with existing token")
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
lastUsageFetchMs = nowMs
|
|
1720
|
+
let resp
|
|
1721
|
+
let didRefresh = false
|
|
1722
|
+
try {
|
|
1723
|
+
resp = ctx.util.retryOnceOnAuth({
|
|
1724
|
+
request: (token) => {
|
|
1725
|
+
try {
|
|
1726
|
+
return fetchUsage(ctx, token || accessToken)
|
|
1727
|
+
} catch (e) {
|
|
1728
|
+
ctx.host.log.error("usage request exception: " + String(e))
|
|
1729
|
+
if (didRefresh) {
|
|
1730
|
+
throw "Usage request failed after refresh. Try again."
|
|
1731
|
+
}
|
|
1732
|
+
throw "Usage request failed. Check your connection."
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
refresh: () => {
|
|
1736
|
+
ctx.host.log.info("usage returned 401, attempting refresh")
|
|
1737
|
+
didRefresh = true
|
|
1738
|
+
return refreshToken(ctx, creds)
|
|
1739
|
+
},
|
|
1740
|
+
})
|
|
1741
|
+
} catch (e) {
|
|
1742
|
+
if (typeof e === "string") throw e
|
|
1743
|
+
ctx.host.log.error("usage request failed: " + String(e))
|
|
1744
|
+
throw "Usage request failed. Check your connection."
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
if (ctx.util.isAuthStatus(resp.status)) {
|
|
1748
|
+
ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
|
|
1749
|
+
throw "Token expired. Run `claude` to log in again."
|
|
1605
1750
|
}
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1751
|
+
|
|
1752
|
+
const responseData = ctx.util.tryParseJson(resp.bodyText)
|
|
1753
|
+
const inBandRateLimit = resp.status >= 200 && resp.status < 300
|
|
1754
|
+
&& responseData && responseData.error && responseData.error.type === "rate_limit_error"
|
|
1755
|
+
if (resp.status === 429 || inBandRateLimit) {
|
|
1756
|
+
rateLimited = true
|
|
1757
|
+
retryAfterSeconds = parseRetryAfterSeconds(resp.headers)
|
|
1758
|
+
const backoffMs = retryAfterSeconds !== null
|
|
1759
|
+
? retryAfterSeconds * 1000
|
|
1760
|
+
: DEFAULT_RATE_LIMIT_BACKOFF_MS
|
|
1761
|
+
rateLimitedUntilMs = nowMs + backoffMs
|
|
1762
|
+
data = cachedData
|
|
1763
|
+
writeLiveUsageCache(ctx, {
|
|
1764
|
+
rateLimitedUntilMs: rateLimitedUntilMs,
|
|
1765
|
+
lastUsageFetchMs: nowMs,
|
|
1766
|
+
usageData: data,
|
|
1767
|
+
usageFetchedAtMs: Number(liveCache && liveCache.usageFetchedAtMs) || null,
|
|
1768
|
+
lastRateLimitedAtMs: nowMs,
|
|
1769
|
+
})
|
|
1770
|
+
ctx.host.log.warn(
|
|
1771
|
+
"usage rate limited (429), backing off for " +
|
|
1772
|
+
Math.round(backoffMs / 1000) + "s"
|
|
1773
|
+
)
|
|
1774
|
+
} else if (resp.status < 200 || resp.status >= 300) {
|
|
1775
|
+
ctx.host.log.error("usage returned error: status=" + resp.status)
|
|
1776
|
+
throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
|
|
1777
|
+
} else {
|
|
1778
|
+
ctx.host.log.info("usage fetch succeeded")
|
|
1779
|
+
data = responseData
|
|
1780
|
+
const validLines = []
|
|
1781
|
+
if (data && typeof data === "object") addUsageWindowLines(ctx, data, validLines)
|
|
1782
|
+
if (!validLines.length) {
|
|
1783
|
+
throw "Usage response invalid. Try again later."
|
|
1784
|
+
}
|
|
1785
|
+
cachedUsageData = data
|
|
1786
|
+
rateLimitedUntilMs = 0
|
|
1787
|
+
writeLiveUsageCache(ctx, {
|
|
1788
|
+
rateLimitedUntilMs: 0,
|
|
1789
|
+
lastUsageFetchMs: nowMs,
|
|
1790
|
+
usageData: data,
|
|
1791
|
+
usageSource: "oauth",
|
|
1792
|
+
usageFetchedAtMs: nowMs,
|
|
1793
|
+
lastRateLimitedAtMs: null,
|
|
1794
|
+
})
|
|
1795
|
+
plan = livePlanLabel(ctx, creds) || plan
|
|
1796
|
+
}
|
|
1797
|
+
} // end fetch else-branch
|
|
1798
|
+
}
|
|
1799
|
+
} else {
|
|
1800
|
+
ctx.host.log.info("skipping live usage fetch for inference-only token")
|
|
1801
|
+
}
|
|
1802
|
+
} else if (sessionKey) {
|
|
1803
|
+
// ── Web mode ─────────────────────────────────────────────────────────
|
|
1804
|
+
const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
|
|
1805
|
+
data = webResult.data
|
|
1806
|
+
quotaSource = "web"
|
|
1807
|
+
if (webResult.plan) {
|
|
1808
|
+
plan = webResult.plan
|
|
1617
1809
|
}
|
|
1618
1810
|
} else {
|
|
1619
|
-
ctx.host.log.info("
|
|
1620
|
-
}
|
|
1621
|
-
} else if (sessionKey) {
|
|
1622
|
-
// ── Web mode ─────────────────────────────────────────────────────────
|
|
1623
|
-
const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
|
|
1624
|
-
data = webResult.data
|
|
1625
|
-
if (webResult.plan) {
|
|
1626
|
-
plan = webResult.plan
|
|
1811
|
+
ctx.host.log.info("auto mode using available Claude usage history")
|
|
1627
1812
|
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1813
|
+
} catch (primaryError) {
|
|
1814
|
+
const cooldownUntil = Math.max(rateLimitedUntilMs, Number(liveCache && liveCache.rateLimitedUntilMs) || 0)
|
|
1815
|
+
const fallback = wantsAuto && nowMs >= cooldownUntil &&
|
|
1816
|
+
(readNativeQuota(ctx, creds, false) || readNativeQuota(ctx, creds, true))
|
|
1817
|
+
if (!fallback) throw primaryError
|
|
1818
|
+
data = fallback.data
|
|
1819
|
+
quotaSource = fallback.source
|
|
1820
|
+
quotaFetchedAtMs = fallback.fetchedAtMs
|
|
1821
|
+
rateLimited = false
|
|
1822
|
+
retryAfterSeconds = null
|
|
1630
1823
|
}
|
|
1631
1824
|
|
|
1632
1825
|
if (rateLimited && wantsAuto && sessionKey) {
|
|
@@ -1635,6 +1828,7 @@
|
|
|
1635
1828
|
const webResult = loadWebUsageData(ctx, sessionKey, nowMs)
|
|
1636
1829
|
if (webResult.data) {
|
|
1637
1830
|
data = webResult.data
|
|
1831
|
+
quotaSource = "web"
|
|
1638
1832
|
rateLimited = false
|
|
1639
1833
|
retryAfterSeconds = null
|
|
1640
1834
|
if (webResult.plan) {
|
|
@@ -1695,18 +1889,21 @@
|
|
|
1695
1889
|
? fmtRateLimitMinutes(retryAfterSeconds)
|
|
1696
1890
|
: null
|
|
1697
1891
|
const waitText = retryText
|
|
1698
|
-
? "
|
|
1699
|
-
: "
|
|
1892
|
+
? "Usage updates paused; retry in ~" + retryText
|
|
1893
|
+
: "Usage updates temporarily unavailable"
|
|
1700
1894
|
lines.unshift(ctx.line.badge({ label: "Status", text: waitText, color: "#f59e0b" }))
|
|
1701
|
-
const noteText =
|
|
1702
|
-
? "Live usage rate limited — retry in ~" + retryText
|
|
1703
|
-
: "Live usage rate limited — data may be stale"
|
|
1895
|
+
const noteText = "Anthropic limited usage checks, not your message allowance." + (data ? " Showing the last successful reading." : "")
|
|
1704
1896
|
lines.push(ctx.line.text({ label: "Note", value: noteText }))
|
|
1705
1897
|
} else if (lines.length === 0) {
|
|
1898
|
+
if (noLiveCredentials && wantsAuto) throw "Not logged in and no local Claude usage found. Run `claude` to authenticate."
|
|
1706
1899
|
lines.push(ctx.line.badge({ label: "Status", text: "No usage data", color: "#a3a3a3" }))
|
|
1707
1900
|
}
|
|
1708
1901
|
|
|
1709
|
-
|
|
1902
|
+
const finalCache = readLiveUsageCache(ctx)
|
|
1903
|
+
const fetchedAt = data && ctx.util.toIso(quotaFetchedAtMs || finalCache && finalCache.usageFetchedAtMs)
|
|
1904
|
+
return { plan: plan, lines: lines, fetchedAt: fetchedAt || ctx.nowIso,
|
|
1905
|
+
state: rateLimited ? "failed" : lines.length ? "ready" : "no-data",
|
|
1906
|
+
source: data ? (rateLimited ? "cached" : quotaSource) : wantsApi ? "api" : "local" }
|
|
1710
1907
|
}
|
|
1711
1908
|
|
|
1712
1909
|
// _resetState is a testing hook — resets module-scope rate-limit state between tests.
|