@phnx-labs/agents-cli 1.22.46 → 1.22.49
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/CHANGELOG.md +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +6 -11
- package/dist/lib/config-keys.js +18 -19
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +35 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
|
@@ -26,6 +26,7 @@ import { getCacheDir } from '../state.js';
|
|
|
26
26
|
import { mapBounded } from '../concurrency.js';
|
|
27
27
|
import { atomicWriteFileSync, ensureLockTarget, withFileLock } from '../fs-atomic.js';
|
|
28
28
|
import { withRefreshLease } from '../refresh-coordinator.js';
|
|
29
|
+
import { padToWidth } from '../session/width.js';
|
|
29
30
|
const execFileAsync = promisify(execFile);
|
|
30
31
|
const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
31
32
|
const CLAUDE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
@@ -118,6 +119,57 @@ export function usageUnreachableError(agent, cause) {
|
|
|
118
119
|
? `${agent} usage read failed: ${detail}`
|
|
119
120
|
: `${agent} usage read failed.`;
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Marker for a log-based (`network: false`) provider — Codex, Grok — that has
|
|
124
|
+
* simply never recorded a rate-limit event on this machine yet: no session
|
|
125
|
+
* log exists, or no session in it carries usage data. Distinct on purpose from
|
|
126
|
+
* `usageUnreachableError`: that one means the local log COULDN'T be read (a
|
|
127
|
+
* real failure worth surfacing distinctly); this one means there is nothing to
|
|
128
|
+
* read because the account has not run here, which is expected for a fresh
|
|
129
|
+
* install and should render as a benign state, not an error (RUSH-3040).
|
|
130
|
+
*/
|
|
131
|
+
export const USAGE_NO_RECENT_USAGE_MARKER = 'no usage recorded yet';
|
|
132
|
+
export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
|
|
133
|
+
/**
|
|
134
|
+
* Shared error-classification + 429 backoff for a networked usage fetch whose
|
|
135
|
+
* only signal is an HTTP status (or none at all, on a network failure) —
|
|
136
|
+
* Antigravity's :retrieveUserQuota and Muse's Meta Model API probe are both
|
|
137
|
+
* this shape. They were added to `USAGE_SOURCES` after the four original
|
|
138
|
+
* `usageXError` constructors and did not get their own scheme (usage.ts's
|
|
139
|
+
* error handling was written for "four networked providers"; RUSH-3040).
|
|
140
|
+
* Route every no-snapshot outcome for either through this one function so a
|
|
141
|
+
* future entry cannot be added second-class again — it owns noting the 429
|
|
142
|
+
* backoff, so callers must NOT also call {@link noteUsageRateLimited} for the
|
|
143
|
+
* same response.
|
|
144
|
+
*/
|
|
145
|
+
export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHeader, usageScope) {
|
|
146
|
+
if (status === 429) {
|
|
147
|
+
noteUsageRateLimited(agentId, retryAfterHeader ?? null, { account: usageScope });
|
|
148
|
+
return usageRejectedError(agent, 429);
|
|
149
|
+
}
|
|
150
|
+
if (status !== null)
|
|
151
|
+
return usageRejectedError(agent, status);
|
|
152
|
+
return usageUnreachableError(agent);
|
|
153
|
+
}
|
|
154
|
+
/** Classify a `UsageInfo.error` string into its {@link UsageErrorKind}, or null when there is no error. */
|
|
155
|
+
export function classifyUsageErrorKind(error) {
|
|
156
|
+
if (!error)
|
|
157
|
+
return null;
|
|
158
|
+
if (isUsageHeadlessScopeError(error))
|
|
159
|
+
return 'headless-scope';
|
|
160
|
+
if (error.startsWith('No readable '))
|
|
161
|
+
return 'no-credential';
|
|
162
|
+
if (error.includes('credential expired'))
|
|
163
|
+
return 'expired-credential';
|
|
164
|
+
if (error.includes('rate-limited this machine') || error.includes('is rate-limiting the usage endpoint')) {
|
|
165
|
+
return 'rate-limited';
|
|
166
|
+
}
|
|
167
|
+
if (error.includes('rejected the usage read'))
|
|
168
|
+
return 'rejected';
|
|
169
|
+
if (error.includes('usage read failed'))
|
|
170
|
+
return 'unreachable';
|
|
171
|
+
return 'rejected';
|
|
172
|
+
}
|
|
121
173
|
/**
|
|
122
174
|
* True when a Claude OAuth access token is within the refresh leeway of expiry
|
|
123
175
|
* (or already expired) — i.e. it "would need a refresh" before the next use.
|
|
@@ -165,6 +217,15 @@ const COMPACT_BAR_LEN = 5;
|
|
|
165
217
|
const USAGE_BAR_LEN = 10;
|
|
166
218
|
const FULL = '\u2588';
|
|
167
219
|
const EMPTY = '\u2591';
|
|
220
|
+
const PARTIAL_BLOCKS = ['', '\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589'];
|
|
221
|
+
/** Construct the benign no-local-log result without overloading `error`. */
|
|
222
|
+
export function usageNoRecentUsageInfo() {
|
|
223
|
+
return { snapshot: null, error: null, [USAGE_BENIGN_STATE]: 'no-recent-usage' };
|
|
224
|
+
}
|
|
225
|
+
/** Read a benign state for the human renderer; symbols are omitted by JSON serialization. */
|
|
226
|
+
export function getUsageBenignState(info) {
|
|
227
|
+
return info[USAGE_BENIGN_STATE] ?? null;
|
|
228
|
+
}
|
|
168
229
|
/** The single registry of agent usage sources and their transport. */
|
|
169
230
|
const USAGE_SOURCES = {
|
|
170
231
|
claude: { fetch: getClaudeUsageInfo, network: true },
|
|
@@ -333,6 +394,9 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
|
|
|
333
394
|
home: input.home,
|
|
334
395
|
cliVersion: input.cliVersion,
|
|
335
396
|
organizationId: input.info.organizationId,
|
|
397
|
+
// Scope this fetch's 429 backoff to the account being fetched, so one
|
|
398
|
+
// throttled account cannot park the whole provider (RUSH-3036).
|
|
399
|
+
usageScope: usageKey,
|
|
336
400
|
fileOnly,
|
|
337
401
|
});
|
|
338
402
|
if (usage.snapshot) {
|
|
@@ -391,6 +455,26 @@ export function pickCompactUsageWindows(windows, maxWindows) {
|
|
|
391
455
|
}
|
|
392
456
|
return chosen;
|
|
393
457
|
}
|
|
458
|
+
/** Human label for a classified usage error, for the no-bars branch of {@link formatUsageSummary}. */
|
|
459
|
+
function formatUsageErrorKindLabel(kind, detail) {
|
|
460
|
+
switch (kind) {
|
|
461
|
+
case 'no-credential':
|
|
462
|
+
return 'sign in / provision token';
|
|
463
|
+
case 'expired-credential':
|
|
464
|
+
return 're-auth for usage';
|
|
465
|
+
case 'rate-limited': {
|
|
466
|
+
const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
|
|
467
|
+
return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
|
|
468
|
+
}
|
|
469
|
+
case 'rejected':
|
|
470
|
+
case 'unreachable':
|
|
471
|
+
case 'headless-scope':
|
|
472
|
+
case null:
|
|
473
|
+
case undefined:
|
|
474
|
+
default:
|
|
475
|
+
return 'usage unavailable';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
394
478
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
395
479
|
export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
396
480
|
const parts = [];
|
|
@@ -416,11 +500,20 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
416
500
|
// multi-meter agent cannot force the whole table to wrap.
|
|
417
501
|
const selected = pickCompactUsageWindows(snapshot.windows, opts?.maxWindows);
|
|
418
502
|
const hidden = Math.max(0, snapshot.windows.filter((w) => w.key !== 'sonnet_week').length - selected.length);
|
|
419
|
-
const
|
|
503
|
+
const expected = opts?.expectedWindows;
|
|
504
|
+
const windowsToRender = expected
|
|
505
|
+
? expected.map(({ key, shortLabel }) => ({ window: selected.find((item) => item.key === key), shortLabel }))
|
|
506
|
+
: selected.map((window) => ({ window, shortLabel: window.shortLabel }));
|
|
507
|
+
const windowParts = windowsToRender.map(({ window, shortLabel }, index) => {
|
|
508
|
+
if (!window) {
|
|
509
|
+
const missing = chalk.red(`${shortLabel}: ${FULL.repeat(COMPACT_BAR_LEN)} unavailable`);
|
|
510
|
+
return index < windowsToRender.length - 1 ? padToWidth(missing, 20) : missing;
|
|
511
|
+
}
|
|
420
512
|
const bar = renderCompactUsageBar(window.usedPercent);
|
|
421
513
|
const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
|
|
422
514
|
const reset = window.resetsAt ? chalk.dim(` (${formatResetHint(window.resetsAt)})`) : '';
|
|
423
|
-
|
|
515
|
+
const rendered = `${chalk.gray(`${shortLabel}:`)} ${bar} ${pct}${reset}`;
|
|
516
|
+
return index < windowsToRender.length - 1 ? padToWidth(rendered, 20) : rendered;
|
|
424
517
|
});
|
|
425
518
|
if (hidden > 0) {
|
|
426
519
|
windowParts.push(chalk.dim(`+${hidden}`));
|
|
@@ -428,6 +521,9 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
428
521
|
if (windowParts.length > 0) {
|
|
429
522
|
parts.push(windowParts.join(' '));
|
|
430
523
|
}
|
|
524
|
+
else if (snapshot.refreshHint) {
|
|
525
|
+
parts.push(chalk.dim(snapshot.refreshHint));
|
|
526
|
+
}
|
|
431
527
|
// The bars came from the cache and the live read that should have confirmed
|
|
432
528
|
// them failed, so they are the last thing we saw — not the current state.
|
|
433
529
|
// Drawing them unmarked is what let a 26h-old "48% used" read as fact.
|
|
@@ -445,11 +541,16 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
445
541
|
// mistaken for a missing setup-token or seeding failure (RUSH-2392).
|
|
446
542
|
parts.push(chalk.dim(USAGE_HEADLESS_SCOPE_MARKER));
|
|
447
543
|
}
|
|
544
|
+
else if (opts?.benignState === 'no-recent-usage') {
|
|
545
|
+
parts.push(chalk.dim(opts.noRecentUsageLabel || USAGE_NO_RECENT_USAGE_MARKER));
|
|
546
|
+
}
|
|
448
547
|
else if (opts?.unavailable) {
|
|
449
548
|
// Signed-in account we could NOT fetch usage for (no live token in a reachable
|
|
450
549
|
// home / org mismatch / fetch error). Say so explicitly instead of drawing a
|
|
451
|
-
// blank gauge that reads like "0% used"
|
|
452
|
-
|
|
550
|
+
// blank gauge that reads like "0% used" — and name the SPECIFIC cause when
|
|
551
|
+
// the caller passed one, rather than the generic bucket that used to cover
|
|
552
|
+
// ~6 different failures (RUSH-3040).
|
|
553
|
+
parts.push(chalk.dim(formatUsageErrorKindLabel(opts.errorKind, opts.errorDetail)));
|
|
453
554
|
}
|
|
454
555
|
return parts.join(' ');
|
|
455
556
|
}
|
|
@@ -598,11 +699,17 @@ async function getCodexUsageInfo(options) {
|
|
|
598
699
|
return { snapshot: null, error: null };
|
|
599
700
|
}
|
|
600
701
|
const files = collectCodexSessionFiles(options?.home, sinceMs);
|
|
702
|
+
const now = new Date();
|
|
601
703
|
for (const filePath of files) {
|
|
602
704
|
const match = await readLatestCodexRateLimits(filePath);
|
|
603
705
|
if (!match)
|
|
604
706
|
continue;
|
|
605
|
-
|
|
707
|
+
// Same freshness filter Grok already applies (RUSH-3040): a window whose
|
|
708
|
+
// reset time or windowMinutes-derived expiry has passed is a STALE read,
|
|
709
|
+
// not a current one — rendering it as-is is how a codex bar kept showing
|
|
710
|
+
// "100% used" past its own reset. Try the next-older session file rather
|
|
711
|
+
// than surfacing a stale bar.
|
|
712
|
+
const windows = normalizeCodexWindows(match.rateLimits).filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
|
|
606
713
|
if (windows.length === 0)
|
|
607
714
|
continue;
|
|
608
715
|
return {
|
|
@@ -615,10 +722,14 @@ async function getCodexUsageInfo(options) {
|
|
|
615
722
|
error: null,
|
|
616
723
|
};
|
|
617
724
|
}
|
|
618
|
-
|
|
725
|
+
// No session ever recorded a rate-limit event on this machine (or none of
|
|
726
|
+
// the ones found were still fresh) — a benign "nothing to show yet", not a
|
|
727
|
+
// failure (RUSH-3040). Distinct from the outer catch below, which is a
|
|
728
|
+
// genuine read/parse failure.
|
|
729
|
+
return usageNoRecentUsageInfo();
|
|
619
730
|
}
|
|
620
|
-
catch {
|
|
621
|
-
return { snapshot: null, error:
|
|
731
|
+
catch (err) {
|
|
732
|
+
return { snapshot: null, error: usageUnreachableError('Codex', err) };
|
|
622
733
|
}
|
|
623
734
|
}
|
|
624
735
|
/**
|
|
@@ -671,7 +782,7 @@ async function getClaudeUsageInfo(options) {
|
|
|
671
782
|
}
|
|
672
783
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
673
784
|
// usage-backoff.ts). No request at all while the window is open.
|
|
674
|
-
const throttledUntil = usageRateLimitedUntil('claude');
|
|
785
|
+
const throttledUntil = usageRateLimitedUntil('claude', Date.now(), options?.usageScope);
|
|
675
786
|
if (throttledUntil) {
|
|
676
787
|
return { snapshot: null, error: usageThrottledError('Claude', throttledUntil) };
|
|
677
788
|
}
|
|
@@ -687,7 +798,7 @@ async function getClaudeUsageInfo(options) {
|
|
|
687
798
|
});
|
|
688
799
|
if (!response.ok) {
|
|
689
800
|
if (response.status === 429) {
|
|
690
|
-
noteUsageRateLimited('claude', response.headers.get('retry-after'));
|
|
801
|
+
noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: options?.usageScope });
|
|
691
802
|
}
|
|
692
803
|
// Setup-token is user:inference only; usage needs user:profile → 403
|
|
693
804
|
// with a scope-requirement body. Distinct from a real rejection so the
|
|
@@ -780,7 +891,7 @@ async function getKimiUsageInfo(options) {
|
|
|
780
891
|
}
|
|
781
892
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
782
893
|
// usage-backoff.ts). No request at all while the window is open.
|
|
783
|
-
const throttledUntil = usageRateLimitedUntil('kimi');
|
|
894
|
+
const throttledUntil = usageRateLimitedUntil('kimi', Date.now(), options?.usageScope);
|
|
784
895
|
if (throttledUntil) {
|
|
785
896
|
return { snapshot: null, error: usageThrottledError('Kimi', throttledUntil) };
|
|
786
897
|
}
|
|
@@ -796,7 +907,7 @@ async function getKimiUsageInfo(options) {
|
|
|
796
907
|
// way there are no bars to draw, and the status is what tells them apart.
|
|
797
908
|
if (!response.ok) {
|
|
798
909
|
if (response.status === 429) {
|
|
799
|
-
noteUsageRateLimited('kimi', response.headers.get('retry-after'));
|
|
910
|
+
noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: options?.usageScope });
|
|
800
911
|
}
|
|
801
912
|
return { snapshot: null, error: usageRejectedError('Kimi', response.status) };
|
|
802
913
|
}
|
|
@@ -914,7 +1025,7 @@ async function getDroidUsageInfo(options) {
|
|
|
914
1025
|
}
|
|
915
1026
|
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
916
1027
|
// usage-backoff.ts). No request at all while the window is open.
|
|
917
|
-
const throttledUntil = usageRateLimitedUntil('droid');
|
|
1028
|
+
const throttledUntil = usageRateLimitedUntil('droid', Date.now(), options?.usageScope);
|
|
918
1029
|
if (throttledUntil) {
|
|
919
1030
|
return { snapshot: null, error: usageThrottledError('Droid', throttledUntil) };
|
|
920
1031
|
}
|
|
@@ -929,7 +1040,7 @@ async function getDroidUsageInfo(options) {
|
|
|
929
1040
|
// 401 => revoked/expired token. No bars to draw, and the status says why.
|
|
930
1041
|
if (!response.ok) {
|
|
931
1042
|
if (response.status === 429) {
|
|
932
|
-
noteUsageRateLimited('droid', response.headers.get('retry-after'));
|
|
1043
|
+
noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: options?.usageScope });
|
|
933
1044
|
}
|
|
934
1045
|
return { snapshot: null, error: usageRejectedError('Droid', response.status) };
|
|
935
1046
|
}
|
|
@@ -956,7 +1067,7 @@ async function getDroidUsageInfo(options) {
|
|
|
956
1067
|
}
|
|
957
1068
|
}
|
|
958
1069
|
/** Probe Claude's OAuth token against the usage endpoint. Never refreshes — reports `expired` for a near-expiry token; see the comment below (RUSH-1822). */
|
|
959
|
-
export async function probeClaudeStatus(home, cliVersion) {
|
|
1070
|
+
export async function probeClaudeStatus(home, cliVersion, usageScope) {
|
|
960
1071
|
// accessTokenCache: the daemon warms this probe every ~3 min per account, so it
|
|
961
1072
|
// reads ONLY the file-based setup-token and never the interactive login —
|
|
962
1073
|
// transmitting that ACL-bound token to the usage API from a background loop is
|
|
@@ -981,7 +1092,7 @@ export async function probeClaudeStatus(home, cliVersion) {
|
|
|
981
1092
|
// window is open, report the throttle from the recorded state instead of
|
|
982
1093
|
// firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
|
|
983
1094
|
// probe is what created the loop it now respects.
|
|
984
|
-
if (usageRateLimitedUntil('claude'))
|
|
1095
|
+
if (usageRateLimitedUntil('claude', Date.now(), usageScope))
|
|
985
1096
|
return { status: 429, token: 'present' };
|
|
986
1097
|
try {
|
|
987
1098
|
const response = await fetch(CLAUDE_USAGE_URL, {
|
|
@@ -995,7 +1106,7 @@ export async function probeClaudeStatus(home, cliVersion) {
|
|
|
995
1106
|
signal: AbortSignal.timeout(8000),
|
|
996
1107
|
});
|
|
997
1108
|
if (response.status === 429) {
|
|
998
|
-
noteUsageRateLimited('claude', response.headers.get('retry-after'));
|
|
1109
|
+
noteUsageRateLimited('claude', response.headers.get('retry-after'), { account: usageScope });
|
|
999
1110
|
}
|
|
1000
1111
|
// Setup-token is user:inference only; usage needs user:profile → 403.
|
|
1001
1112
|
// That is NOT a revocation — the account still runs (RUSH-2392).
|
|
@@ -1023,7 +1134,7 @@ export async function probeClaudeStatus(home, cliVersion) {
|
|
|
1023
1134
|
}
|
|
1024
1135
|
}
|
|
1025
1136
|
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
1026
|
-
export async function probeKimiStatus(home) {
|
|
1137
|
+
export async function probeKimiStatus(home, usageScope) {
|
|
1027
1138
|
const credPath = resolveKimiCredentialPath(home);
|
|
1028
1139
|
if (!credPath)
|
|
1029
1140
|
return { status: null, token: 'missing' };
|
|
@@ -1047,7 +1158,7 @@ export async function probeKimiStatus(home) {
|
|
|
1047
1158
|
// is what created the loop it now respects. It sits AFTER the local
|
|
1048
1159
|
// missing/expired checks — as in probeClaudeStatus and probeDroidStatus — so
|
|
1049
1160
|
// a genuinely broken credential is never misreported as merely throttled.
|
|
1050
|
-
if (usageRateLimitedUntil('kimi'))
|
|
1161
|
+
if (usageRateLimitedUntil('kimi', Date.now(), usageScope))
|
|
1051
1162
|
return { status: 429, token: 'present' };
|
|
1052
1163
|
try {
|
|
1053
1164
|
const response = await fetch(KIMI_USAGES_URL, {
|
|
@@ -1056,7 +1167,7 @@ export async function probeKimiStatus(home) {
|
|
|
1056
1167
|
signal: AbortSignal.timeout(8000),
|
|
1057
1168
|
});
|
|
1058
1169
|
if (response.status === 429) {
|
|
1059
|
-
noteUsageRateLimited('kimi', response.headers.get('retry-after'));
|
|
1170
|
+
noteUsageRateLimited('kimi', response.headers.get('retry-after'), { account: usageScope });
|
|
1060
1171
|
}
|
|
1061
1172
|
return { status: response.status, token: 'present' };
|
|
1062
1173
|
}
|
|
@@ -1065,7 +1176,7 @@ export async function probeKimiStatus(home) {
|
|
|
1065
1176
|
}
|
|
1066
1177
|
}
|
|
1067
1178
|
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
1068
|
-
export async function probeDroidStatus(home) {
|
|
1179
|
+
export async function probeDroidStatus(home, usageScope) {
|
|
1069
1180
|
const cred = decryptDroidAuthPayload(home || os.homedir());
|
|
1070
1181
|
const accessToken = cred?.access_token;
|
|
1071
1182
|
if (typeof accessToken !== 'string' || !accessToken)
|
|
@@ -1077,7 +1188,7 @@ export async function probeDroidStatus(home) {
|
|
|
1077
1188
|
// window is open, report the throttle from the recorded state instead of
|
|
1078
1189
|
// firing again and re-arming it (usage-backoff.ts). This 3-min-cadence
|
|
1079
1190
|
// probe is what created the loop it now respects.
|
|
1080
|
-
if (usageRateLimitedUntil('droid'))
|
|
1191
|
+
if (usageRateLimitedUntil('droid', Date.now(), usageScope))
|
|
1081
1192
|
return { status: 429, token: 'present' };
|
|
1082
1193
|
try {
|
|
1083
1194
|
const response = await fetch(DROID_USAGE_URL, {
|
|
@@ -1086,7 +1197,7 @@ export async function probeDroidStatus(home) {
|
|
|
1086
1197
|
signal: AbortSignal.timeout(8000),
|
|
1087
1198
|
});
|
|
1088
1199
|
if (response.status === 429) {
|
|
1089
|
-
noteUsageRateLimited('droid', response.headers.get('retry-after'));
|
|
1200
|
+
noteUsageRateLimited('droid', response.headers.get('retry-after'), { account: usageScope });
|
|
1090
1201
|
}
|
|
1091
1202
|
return { status: response.status, token: 'present' };
|
|
1092
1203
|
}
|
|
@@ -1313,7 +1424,7 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1313
1424
|
* a file-based setup-token and, when none is provisioned, gets `null` — it never
|
|
1314
1425
|
* reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
|
|
1315
1426
|
* to Anthropic's API is what gets it revoked; see the branch body and
|
|
1316
|
-
* docs/
|
|
1427
|
+
* docs/secrets.md). It is OFF by default so full-credential
|
|
1317
1428
|
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
|
|
1318
1429
|
* read the interactive login. Rush Cloud dispatch does not call this helper
|
|
1319
1430
|
* at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
|
|
@@ -1346,7 +1457,7 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1346
1457
|
// interactive credential used programmatically, which Anthropic flags and
|
|
1347
1458
|
// revokes (the fleet-wide-logout class, RUSH-1822), and which violates the
|
|
1348
1459
|
// invariant that the interactive/rotating login is untouchable
|
|
1349
|
-
// (docs/
|
|
1460
|
+
// (docs/secrets.md). Report unprovisioned (-> probe
|
|
1350
1461
|
// token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
|
|
1351
1462
|
// setup-token via the mint-auth path to restore usage/probe for the account.
|
|
1352
1463
|
return null;
|
|
@@ -1507,6 +1618,32 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
|
|
|
1507
1618
|
/* best-effort cache write — lock busy or disk full */
|
|
1508
1619
|
}
|
|
1509
1620
|
}
|
|
1621
|
+
/** Atomically merge partial native Claude windows into the current fresh row. */
|
|
1622
|
+
export function mergeClaudeUsageCacheWindows(usageKey, snapshot, cachePath = getClaudeUsageCachePath()) {
|
|
1623
|
+
try {
|
|
1624
|
+
ensureLockTarget(cachePath, '{}');
|
|
1625
|
+
withFileLock(cachePath, () => {
|
|
1626
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1627
|
+
const prior = cache[usageKey];
|
|
1628
|
+
const priorSnapshot = prior
|
|
1629
|
+
? deserializeClaudeUsageSnapshot(prior, snapshot.capturedAt ?? new Date())
|
|
1630
|
+
: null;
|
|
1631
|
+
const windows = new Map(priorSnapshot?.windows.map((window) => [window.key, window]) ?? []);
|
|
1632
|
+
for (const window of snapshot.windows)
|
|
1633
|
+
windows.set(window.key, window);
|
|
1634
|
+
cache[usageKey] = serializeClaudeUsageSnapshot({
|
|
1635
|
+
...snapshot,
|
|
1636
|
+
windows: [...windows.values()],
|
|
1637
|
+
plan: snapshot.plan ?? priorSnapshot?.plan ?? null,
|
|
1638
|
+
unavailable: carryForwardUnavailable(prior?.unavailable, snapshot.unavailable),
|
|
1639
|
+
});
|
|
1640
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
catch {
|
|
1644
|
+
/* best-effort cache write — lock busy or disk full */
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1510
1647
|
/** Read the entire usage cache file from disk. */
|
|
1511
1648
|
function readClaudeUsageCacheFile(cachePath) {
|
|
1512
1649
|
if (!fs.existsSync(cachePath)) {
|
|
@@ -1535,6 +1672,7 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1535
1672
|
return {
|
|
1536
1673
|
capturedAt: snapshot.capturedAt?.toISOString() || null,
|
|
1537
1674
|
plan: snapshot.plan ?? null,
|
|
1675
|
+
refreshHint: snapshot.refreshHint ?? null,
|
|
1538
1676
|
unavailable: snapshot.unavailable
|
|
1539
1677
|
? {
|
|
1540
1678
|
reason: snapshot.unavailable.reason,
|
|
@@ -1561,6 +1699,10 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1561
1699
|
* candidate (RUSH-2858). Dropping mirrors the Grok collector, and an all-expired
|
|
1562
1700
|
* snapshot deserializes to null so `readClaudeUsageCache` deletes the entry and
|
|
1563
1701
|
* callers surface "usage unavailable" plus the recorded throttle reason.
|
|
1702
|
+
*
|
|
1703
|
+
* A row that carries a plan survives even with no fresh windows: the plan is a
|
|
1704
|
+
* truthful reading in its own right, and losing it is what made the cached view
|
|
1705
|
+
* contradict the refreshed one for meterless harnesses. See the guard below.
|
|
1564
1706
|
*/
|
|
1565
1707
|
function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
1566
1708
|
const capturedAt = parseDateValue(snapshot.capturedAt);
|
|
@@ -1575,7 +1717,17 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1575
1717
|
}))
|
|
1576
1718
|
.filter((window) => isCachedUsageWindowFresh(window, capturedAt, now));
|
|
1577
1719
|
const unavailable = deserializeUnavailable(snapshot.unavailable, now);
|
|
1578
|
-
|
|
1720
|
+
// A windowless row is not automatically worthless. Grok's collector reports
|
|
1721
|
+
// the subscription tier and no meters at all, so treating "no fresh windows"
|
|
1722
|
+
// as "nothing cached" deleted the only truthful thing we knew about the
|
|
1723
|
+
// account: `--refresh` wrote {plan: 'SuperGrok Heavy', windows: []}, the very
|
|
1724
|
+
// next plain `agents view` deserialized it to null, `readClaudeUsageCache`
|
|
1725
|
+
// pruned the row, and the row rendered "usage unavailable" one read after a
|
|
1726
|
+
// successful refresh. Keep a plan-bearing row — it renders as the plan alone,
|
|
1727
|
+
// and `deriveUsageStatusFromSnapshot` still returns null for zero windows, so
|
|
1728
|
+
// it can never read as a 0% bar or an "available" badge (the RUSH-2858
|
|
1729
|
+
// property that made expired windows drop in the first place).
|
|
1730
|
+
if (windows.length === 0 && !unavailable && !snapshot.plan && !snapshot.refreshHint) {
|
|
1579
1731
|
return null;
|
|
1580
1732
|
}
|
|
1581
1733
|
return {
|
|
@@ -1584,6 +1736,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1584
1736
|
capturedAt,
|
|
1585
1737
|
windows,
|
|
1586
1738
|
plan: snapshot.plan ?? null,
|
|
1739
|
+
refreshHint: snapshot.refreshHint ?? null,
|
|
1587
1740
|
unavailable,
|
|
1588
1741
|
};
|
|
1589
1742
|
}
|
|
@@ -1862,14 +2015,17 @@ function renderUsageBar(usedPercent) {
|
|
|
1862
2015
|
}
|
|
1863
2016
|
/** Render a compact usage bar for inline summaries. */
|
|
1864
2017
|
function renderCompactUsageBar(usedPercent) {
|
|
1865
|
-
return renderBar(usedPercent, COMPACT_BAR_LEN
|
|
1866
|
-
}
|
|
1867
|
-
/** Render a colored
|
|
1868
|
-
export function renderBar(usedPercent, length
|
|
1869
|
-
const
|
|
1870
|
-
const
|
|
2018
|
+
return renderBar(usedPercent, COMPACT_BAR_LEN);
|
|
2019
|
+
}
|
|
2020
|
+
/** Render a proportional colored progress bar at one-eighth-cell resolution. */
|
|
2021
|
+
export function renderBar(usedPercent, length) {
|
|
2022
|
+
const clamped = Math.max(0, Math.min(100, usedPercent));
|
|
2023
|
+
const eighths = Math.round((clamped / 100) * length * 8);
|
|
2024
|
+
const filled = Math.floor(eighths / 8);
|
|
2025
|
+
const partial = eighths % 8;
|
|
1871
2026
|
const color = getUsageColor(usedPercent);
|
|
1872
|
-
|
|
2027
|
+
const gauge = FULL.repeat(filled) + PARTIAL_BLOCKS[partial];
|
|
2028
|
+
return color(gauge) + chalk.dim(EMPTY.repeat(length - filled - (partial > 0 ? 1 : 0)));
|
|
1873
2029
|
}
|
|
1874
2030
|
/** Apply the appropriate color to a text string based on usage percentage. */
|
|
1875
2031
|
function colorUsage(text, usedPercent) {
|
|
@@ -1955,11 +2111,12 @@ async function getGrokUsageInfo(options) {
|
|
|
1955
2111
|
try {
|
|
1956
2112
|
const base = options?.home || os.homedir();
|
|
1957
2113
|
const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
|
|
2114
|
+
// No log yet: a benign "nothing recorded here", not a failure (RUSH-3040).
|
|
1958
2115
|
if (!fs.existsSync(logPath))
|
|
1959
|
-
return
|
|
2116
|
+
return usageNoRecentUsageInfo();
|
|
1960
2117
|
const match = await readLatestGrokBilling(logPath);
|
|
1961
2118
|
if (!match)
|
|
1962
|
-
return
|
|
2119
|
+
return usageNoRecentUsageInfo();
|
|
1963
2120
|
// Grok has no live usage API (`network: false`) — bars are last-seen from
|
|
1964
2121
|
// this machine's unified.jsonl only. Drop windows whose billing period has
|
|
1965
2122
|
// already ended so a stale 100% does not paint "rate-limited" after reset,
|
|
@@ -1968,6 +2125,10 @@ async function getGrokUsageInfo(options) {
|
|
|
1968
2125
|
// (see readLatestGrokBilling).
|
|
1969
2126
|
const now = new Date();
|
|
1970
2127
|
const windows = match.windows.filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
|
|
2128
|
+
const version = options?.cliVersion;
|
|
2129
|
+
const refreshHint = windows.length === 0
|
|
2130
|
+
? `run grok${version ? `@${version}` : ''} once to refresh usage`
|
|
2131
|
+
: null;
|
|
1971
2132
|
return {
|
|
1972
2133
|
snapshot: {
|
|
1973
2134
|
source: 'last_seen',
|
|
@@ -1975,12 +2136,13 @@ async function getGrokUsageInfo(options) {
|
|
|
1975
2136
|
capturedAt: match.capturedAt,
|
|
1976
2137
|
windows,
|
|
1977
2138
|
plan: match.subscriptionTier,
|
|
2139
|
+
refreshHint,
|
|
1978
2140
|
},
|
|
1979
2141
|
error: null,
|
|
1980
2142
|
};
|
|
1981
2143
|
}
|
|
1982
|
-
catch {
|
|
1983
|
-
return { snapshot: null, error:
|
|
2144
|
+
catch (err) {
|
|
2145
|
+
return { snapshot: null, error: usageUnreachableError('Grok', err) };
|
|
1984
2146
|
}
|
|
1985
2147
|
}
|
|
1986
2148
|
/**
|
|
@@ -1994,16 +2156,40 @@ async function getGrokUsageInfo(options) {
|
|
|
1994
2156
|
async function getMuseUsageInfo(options) {
|
|
1995
2157
|
try {
|
|
1996
2158
|
const base = options?.home || os.homedir();
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2159
|
+
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
2160
|
+
// usage-backoff.ts). The local log fallback still works while the live
|
|
2161
|
+
// probe is throttled — only report the throttle when there is truly
|
|
2162
|
+
// nothing else to show.
|
|
2163
|
+
const throttledUntil = usageRateLimitedUntil('muse', Date.now(), options?.usageScope);
|
|
2164
|
+
if (throttledUntil) {
|
|
2165
|
+
const local = await readMuseLocalSessionUsage(base);
|
|
2166
|
+
if (local)
|
|
2167
|
+
return { snapshot: local, error: null };
|
|
2168
|
+
return { snapshot: null, error: usageThrottledError('Muse', throttledUntil) };
|
|
2169
|
+
}
|
|
2170
|
+
const probe = await probeMuseRateLimits(base);
|
|
2171
|
+
if (probe.snapshot)
|
|
2172
|
+
return { snapshot: probe.snapshot, error: null };
|
|
2000
2173
|
const local = await readMuseLocalSessionUsage(base);
|
|
2001
|
-
if (
|
|
2002
|
-
return { snapshot:
|
|
2003
|
-
|
|
2174
|
+
if (local)
|
|
2175
|
+
return { snapshot: local, error: null };
|
|
2176
|
+
// No live snapshot and no local log — every source came up empty. Only
|
|
2177
|
+
// now does the probe's own outcome become the reported error, mirroring
|
|
2178
|
+
// Cursor's "surface the last resort's failure" pattern above.
|
|
2179
|
+
if (!probe.hasKey)
|
|
2180
|
+
return { snapshot: null, error: usageNoCredentialError('Muse') };
|
|
2181
|
+
if (probe.noHeaders)
|
|
2182
|
+
return usageNoRecentUsageInfo();
|
|
2183
|
+
return {
|
|
2184
|
+
snapshot: null,
|
|
2185
|
+
error: classifyUsageFetchFailure('Muse', 'muse', probe.status, probe.retryAfter, options?.usageScope),
|
|
2186
|
+
};
|
|
2004
2187
|
}
|
|
2005
|
-
catch {
|
|
2006
|
-
|
|
2188
|
+
catch (err) {
|
|
2189
|
+
// A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
|
|
2190
|
+
// read like any other — staying silent here would hand the caller a stale
|
|
2191
|
+
// snapshot to render as confirmed (RUSH-3040).
|
|
2192
|
+
return { snapshot: null, error: usageUnreachableError('Muse', err) };
|
|
2007
2193
|
}
|
|
2008
2194
|
}
|
|
2009
2195
|
/** Resolve a Muse API key from env or auth.json without logging the value. */
|
|
@@ -2037,25 +2223,28 @@ function resolveMuseApiKey(base) {
|
|
|
2037
2223
|
}
|
|
2038
2224
|
/**
|
|
2039
2225
|
* Probe Meta Model API for rate-limit headers. Uses GET /v1/models (no token
|
|
2040
|
-
* spend).
|
|
2226
|
+
* spend). The 429 backoff is noted by the CALLER via
|
|
2227
|
+
* {@link classifyUsageFetchFailure}, not here, so a throttled read is recorded
|
|
2228
|
+
* exactly once regardless of which branch of `getMuseUsageInfo` observes it.
|
|
2041
2229
|
*/
|
|
2042
2230
|
async function probeMuseRateLimits(base) {
|
|
2043
|
-
if (usageRateLimitedUntil('muse'))
|
|
2044
|
-
return null;
|
|
2045
2231
|
const key = resolveMuseApiKey(base);
|
|
2046
2232
|
if (!key)
|
|
2047
|
-
return null;
|
|
2233
|
+
return { snapshot: null, hasKey: false, status: null, retryAfter: null, noHeaders: false };
|
|
2048
2234
|
try {
|
|
2049
2235
|
const response = await fetch('https://api.meta.ai/v1/models', {
|
|
2050
2236
|
headers: { Authorization: `Bearer ${key}` },
|
|
2051
2237
|
signal: AbortSignal.timeout(8_000),
|
|
2052
2238
|
});
|
|
2053
|
-
if (response.
|
|
2054
|
-
|
|
2055
|
-
|
|
2239
|
+
if (!response.ok) {
|
|
2240
|
+
return {
|
|
2241
|
+
snapshot: null,
|
|
2242
|
+
hasKey: true,
|
|
2243
|
+
status: response.status,
|
|
2244
|
+
retryAfter: response.headers.get('retry-after'),
|
|
2245
|
+
noHeaders: false,
|
|
2246
|
+
};
|
|
2056
2247
|
}
|
|
2057
|
-
if (!response.ok)
|
|
2058
|
-
return null;
|
|
2059
2248
|
const limitTokens = headerNumber(response.headers, 'x-ratelimit-limit-tokens');
|
|
2060
2249
|
const remainingTokens = headerNumber(response.headers, 'x-ratelimit-remaining-tokens');
|
|
2061
2250
|
const limitRequests = headerNumber(response.headers, 'x-ratelimit-limit-requests');
|
|
@@ -2083,18 +2272,25 @@ async function probeMuseRateLimits(base) {
|
|
|
2083
2272
|
windowMinutes: 1,
|
|
2084
2273
|
});
|
|
2085
2274
|
}
|
|
2086
|
-
if (windows.length === 0)
|
|
2087
|
-
return null;
|
|
2275
|
+
if (windows.length === 0) {
|
|
2276
|
+
return { snapshot: null, hasKey: true, status: response.status, retryAfter: null, noHeaders: true };
|
|
2277
|
+
}
|
|
2088
2278
|
return {
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2279
|
+
snapshot: {
|
|
2280
|
+
source: 'live',
|
|
2281
|
+
sourceLabel: 'Meta Model API rate limits',
|
|
2282
|
+
capturedAt: new Date(),
|
|
2283
|
+
windows,
|
|
2284
|
+
plan: 'Meta Model API',
|
|
2285
|
+
},
|
|
2286
|
+
hasKey: true,
|
|
2287
|
+
status: response.status,
|
|
2288
|
+
retryAfter: null,
|
|
2289
|
+
noHeaders: false,
|
|
2094
2290
|
};
|
|
2095
2291
|
}
|
|
2096
2292
|
catch {
|
|
2097
|
-
return null;
|
|
2293
|
+
return { snapshot: null, hasKey: true, status: null, retryAfter: null, noHeaders: false };
|
|
2098
2294
|
}
|
|
2099
2295
|
}
|
|
2100
2296
|
function headerNumber(headers, name) {
|
|
@@ -2333,7 +2529,7 @@ export function normalizeCursorUsageSummary(data) {
|
|
|
2333
2529
|
function readCursorCredentials(base) {
|
|
2334
2530
|
try {
|
|
2335
2531
|
const cfgPath = path.join(base, '.cursor', 'cli-config.json');
|
|
2336
|
-
const authPath = path.join(base, '.
|
|
2532
|
+
const authPath = path.join(base, '.cursor', 'auth.json');
|
|
2337
2533
|
if (!fs.existsSync(cfgPath) || !fs.existsSync(authPath))
|
|
2338
2534
|
return null;
|
|
2339
2535
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
@@ -2660,10 +2856,14 @@ async function refreshAntigravityAccessToken(refreshToken) {
|
|
|
2660
2856
|
}
|
|
2661
2857
|
/**
|
|
2662
2858
|
* POST :retrieveUserQuota against the Code Assist endpoints in order, returning
|
|
2663
|
-
* the first successful bucket list. null when every endpoint rejects
|
|
2664
|
-
* token, no quota API for the account) or the network fails
|
|
2859
|
+
* the first successful bucket list. `buckets: null` when every endpoint rejects
|
|
2860
|
+
* (expired token, no quota API for the account) or the network fails — `status`
|
|
2861
|
+
* carries the LAST rejection's HTTP status (or null when every attempt threw)
|
|
2862
|
+
* so the caller can classify the failure instead of it reading as silence.
|
|
2665
2863
|
*/
|
|
2666
2864
|
async function fetchAntigravityQuota(accessToken) {
|
|
2865
|
+
let lastStatus = null;
|
|
2866
|
+
let lastRetryAfter = null;
|
|
2667
2867
|
for (const url of ANTIGRAVITY_QUOTA_URLS) {
|
|
2668
2868
|
try {
|
|
2669
2869
|
const response = await fetch(url, {
|
|
@@ -2676,16 +2876,19 @@ async function fetchAntigravityQuota(accessToken) {
|
|
|
2676
2876
|
body: '{}',
|
|
2677
2877
|
signal: AbortSignal.timeout(5000),
|
|
2678
2878
|
});
|
|
2679
|
-
if (!response.ok)
|
|
2879
|
+
if (!response.ok) {
|
|
2880
|
+
lastStatus = response.status;
|
|
2881
|
+
lastRetryAfter = response.headers.get('retry-after');
|
|
2680
2882
|
continue;
|
|
2883
|
+
}
|
|
2681
2884
|
const data = (await response.json());
|
|
2682
|
-
return Array.isArray(data?.buckets) ? data.buckets : [];
|
|
2885
|
+
return { buckets: Array.isArray(data?.buckets) ? data.buckets : [], status: response.status, retryAfter: null };
|
|
2683
2886
|
}
|
|
2684
2887
|
catch {
|
|
2685
2888
|
continue;
|
|
2686
2889
|
}
|
|
2687
2890
|
}
|
|
2688
|
-
return null;
|
|
2891
|
+
return { buckets: null, status: lastStatus, retryAfter: lastRetryAfter };
|
|
2689
2892
|
}
|
|
2690
2893
|
/** Compact model tag for the inline bar — 'gemini-2.5-flash-lite' => '2.5FL'. */
|
|
2691
2894
|
export function antigravityModelShortLabel(modelId) {
|
|
@@ -2747,16 +2950,28 @@ async function getAntigravityUsageInfo(options) {
|
|
|
2747
2950
|
try {
|
|
2748
2951
|
const token = await loadAntigravityOauth(options?.home);
|
|
2749
2952
|
if (!token)
|
|
2750
|
-
return { snapshot: null, error:
|
|
2953
|
+
return { snapshot: null, error: usageNoCredentialError('Antigravity') };
|
|
2751
2954
|
let accessToken = normalizeString(token.access_token);
|
|
2752
2955
|
if ((!accessToken || antigravityTokenNeedsRefresh(token.expiry)) && token.refresh_token) {
|
|
2753
2956
|
accessToken = await refreshAntigravityAccessToken(token.refresh_token);
|
|
2754
2957
|
}
|
|
2755
2958
|
if (!accessToken)
|
|
2756
|
-
return { snapshot: null, error:
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2959
|
+
return { snapshot: null, error: usageExpiredCredentialError('Antigravity') };
|
|
2960
|
+
// Honour a live Retry-After rather than re-arming the penalty (see
|
|
2961
|
+
// usage-backoff.ts). No request at all while the window is open. Antigravity
|
|
2962
|
+
// previously had NO rate-limit backoff at all (RUSH-3040) — every refresh
|
|
2963
|
+
// re-hit a throttled endpoint.
|
|
2964
|
+
const throttledUntil = usageRateLimitedUntil('antigravity', Date.now(), options?.usageScope);
|
|
2965
|
+
if (throttledUntil) {
|
|
2966
|
+
return { snapshot: null, error: usageThrottledError('Antigravity', throttledUntil) };
|
|
2967
|
+
}
|
|
2968
|
+
const { buckets, status, retryAfter } = await fetchAntigravityQuota(accessToken);
|
|
2969
|
+
if (!buckets) {
|
|
2970
|
+
return {
|
|
2971
|
+
snapshot: null,
|
|
2972
|
+
error: classifyUsageFetchFailure('Antigravity', 'antigravity', status, retryAfter, options?.usageScope),
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2760
2975
|
const windows = normalizeAntigravityWindows(buckets);
|
|
2761
2976
|
if (windows.length === 0)
|
|
2762
2977
|
return { snapshot: null, error: null };
|
|
@@ -2770,7 +2985,10 @@ async function getAntigravityUsageInfo(options) {
|
|
|
2770
2985
|
error: null,
|
|
2771
2986
|
};
|
|
2772
2987
|
}
|
|
2773
|
-
catch {
|
|
2774
|
-
|
|
2988
|
+
catch (err) {
|
|
2989
|
+
// A thrown request (timeout, DNS, TLS, a malformed payload) is a failed
|
|
2990
|
+
// read like any other — staying silent here would hand the caller a stale
|
|
2991
|
+
// snapshot to render as confirmed (RUSH-3040).
|
|
2992
|
+
return { snapshot: null, error: usageUnreachableError('Antigravity', err) };
|
|
2775
2993
|
}
|
|
2776
2994
|
}
|