@phnx-labs/agents-cli 1.22.47 → 1.22.50
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 +503 -17
- package/README.md +76 -32
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +201 -39
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/browser.js +501 -103
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +0 -20
- package/dist/commands/daemon-test-harness.d.ts +42 -0
- package/dist/commands/daemon-test-harness.js +90 -0
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/device-config-test-harness.d.ts +11 -0
- package/dist/commands/device-config-test-harness.js +59 -0
- package/dist/commands/exec.js +76 -16
- 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/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +4 -3
- 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 +3 -2
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-trace.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +158 -53
- package/dist/commands/ssh.js +54 -7
- package/dist/commands/teams.d.ts +12 -0
- package/dist/commands/teams.js +44 -67
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +188 -0
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +17 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-provider-registry.d.ts +9 -0
- package/dist/lib/account-provider-registry.js +20 -0
- package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
- package/dist/lib/accounting/account-pool-collect.js +71 -0
- package/dist/lib/accounting/account-pool.d.ts +46 -0
- package/dist/lib/accounting/account-pool.js +33 -0
- package/dist/lib/accounting/capacity.d.ts +24 -0
- package/dist/lib/accounting/capacity.js +30 -0
- package/dist/lib/accounting/rotate.d.ts +11 -18
- package/dist/lib/accounting/rotate.js +6 -24
- package/dist/lib/accounting/usage.d.ts +14 -3
- package/dist/lib/accounting/usage.js +64 -18
- package/dist/lib/agent-spec/agents.js +11 -9
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.js +8 -3
- package/dist/lib/browser/profiles.d.ts +35 -101
- package/dist/lib/browser/profiles.js +130 -301
- 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 +8 -6
- package/dist/lib/browser/remote-control.js +8 -6
- 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 +40 -16
- package/dist/lib/browser/runtime-state.js +92 -25
- package/dist/lib/browser/service.d.ts +28 -9
- package/dist/lib/browser/service.js +139 -62
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- 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/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 +2 -8
- package/dist/lib/config-keys.js +2 -13
- 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/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 +20 -18
- package/dist/lib/device-config.d.ts +0 -2
- package/dist/lib/device-config.js +1 -23
- package/dist/lib/devices/registry.d.ts +1 -1
- package/dist/lib/devices/registry.js +1 -1
- package/dist/lib/devices/stats-cache.d.ts +20 -0
- package/dist/lib/devices/stats-cache.js +49 -5
- 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 +67 -13
- package/dist/lib/exec.js +85 -29
- 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 +11 -0
- package/dist/lib/hooks/install.js +2 -2
- 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 +19 -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/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/store.js +6 -3
- package/dist/lib/installations/versions.js +73 -48
- 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 +31 -7
- package/dist/lib/menubar/install-menubar.js +79 -21
- 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/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/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.js +17 -1
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +138 -6
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +33 -5
- 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/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.js +11 -2
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +88 -0
- 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-html.d.ts +5 -1
- package/dist/lib/session/trajectory-html.js +42 -22
- package/dist/lib/session/trajectory-text.js +9 -2
- package/dist/lib/session/trajectory.js +53 -4
- 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 +44 -15
- package/dist/lib/share/publish.js +58 -26
- package/dist/lib/share/worker-template.js +402 -25
- 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.js +1 -1
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +7 -3
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.d.ts +14 -12
- package/dist/lib/teams/agents.js +188 -73
- package/dist/lib/teams/api.d.ts +6 -1
- package/dist/lib/teams/api.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/teams/scheduler.js +26 -8
- package/dist/lib/terminal/index.d.ts +1 -1
- 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.js +31 -0
- 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 +459 -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 +18 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-refresh.d.ts +1 -1
- package/dist/lib/usage-refresh.js +11 -8
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +4 -6
- 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 -26
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
- 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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readAccountRegistry } from '../account-registry.js';
|
|
2
|
+
import { hasKeychainToken } from '../secrets/index.js';
|
|
3
|
+
import { getGlobalDefault, listInstalledVersions } from '../installations/versions.js';
|
|
4
|
+
import { collectRunCandidates } from './rotate.js';
|
|
5
|
+
import { registryPoolCandidates } from './account-pool.js';
|
|
6
|
+
/**
|
|
7
|
+
* Provider accounts whose credential is present on THIS device, as capability
|
|
8
|
+
* records. A registry entry with no local secret is skipped: including it would
|
|
9
|
+
* let `--strategy balanced` pick an account that then fails at spawn and exits the
|
|
10
|
+
* whole run (`resolveSpawnAccount` throws on a missing credential) instead of
|
|
11
|
+
* rotating to a healthy one.
|
|
12
|
+
*/
|
|
13
|
+
function localRegistryRecords() {
|
|
14
|
+
return Object.values(readAccountRegistry().accounts)
|
|
15
|
+
.filter((a) => hasKeychainToken(a.secretRef))
|
|
16
|
+
.map((a) => ({ name: a.name, provider: a.provider, auth: a.auth }));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* PURE: fold provider-account candidates into the native version-home list.
|
|
20
|
+
*
|
|
21
|
+
* A registry account runs in `runVersion` (the agent's default installed home);
|
|
22
|
+
* the token/key overrides that home's own login at exec, so any home works. It
|
|
23
|
+
* carries `providerAccount` so the run path routes it through the existing
|
|
24
|
+
* `--account` injection (`resolveSpawnAccount` → `accountEnv`). An account already
|
|
25
|
+
* covered by a native login is skipped by accountKey. With no installed version
|
|
26
|
+
* there is nowhere to execute a registry account, so the native list passes
|
|
27
|
+
* through unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export function foldRegistryCandidates(agent, inputs) {
|
|
30
|
+
const { native, records, runVersion } = inputs;
|
|
31
|
+
if (!runVersion)
|
|
32
|
+
return native;
|
|
33
|
+
const seen = new Set(native.filter((c) => c.accountKey).map((c) => c.accountKey));
|
|
34
|
+
const extra = registryPoolCandidates(records, agent)
|
|
35
|
+
.filter((r) => !seen.has(r.accountKey))
|
|
36
|
+
.map((r) => ({
|
|
37
|
+
agent,
|
|
38
|
+
version: runVersion,
|
|
39
|
+
accountKey: r.accountKey,
|
|
40
|
+
accountLabel: r.name,
|
|
41
|
+
email: null,
|
|
42
|
+
usageKey: null,
|
|
43
|
+
usageStatus: null,
|
|
44
|
+
usageSnapshot: null,
|
|
45
|
+
usageError: null,
|
|
46
|
+
usageMinutesToLimit: null,
|
|
47
|
+
plan: null,
|
|
48
|
+
signedIn: true,
|
|
49
|
+
authVerdict: null,
|
|
50
|
+
lastActive: null,
|
|
51
|
+
providerAccount: r.name,
|
|
52
|
+
}));
|
|
53
|
+
return [...native, ...extra];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Candidate list for the `agents run` strategy pick, account-centric: the usual
|
|
57
|
+
* native version-home candidates PLUS one candidate per provider account whose
|
|
58
|
+
* provider can authenticate `agent` and whose credential is present locally
|
|
59
|
+
* (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
|
|
60
|
+
*
|
|
61
|
+
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
|
|
62
|
+
* ONLY — the other consumers of {@link collectRunCandidates} (watchdog, session
|
|
63
|
+
* recovery, teams placement) keep calling it directly and are unaffected. The
|
|
64
|
+
* existing {@link pickBalancedCandidate} selects over the combined list, so the
|
|
65
|
+
* revoked / rate-limit / exhausted logic is unchanged.
|
|
66
|
+
*/
|
|
67
|
+
export async function collectRunCandidatesForRun(agent) {
|
|
68
|
+
const native = await collectRunCandidates(agent);
|
|
69
|
+
const runVersion = getGlobalDefault(agent) ?? listInstalledVersions(agent)[0];
|
|
70
|
+
return foldRegistryCandidates(agent, { native, records: localRegistryRecords(), runVersion });
|
|
71
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import type { AccountAuthKind } from '../account-provider-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* The provider-account side of the run-candidate pool (RUSH-3182).
|
|
5
|
+
*
|
|
6
|
+
* `--strategy balanced` used to enumerate only version-home native logins, so a
|
|
7
|
+
* setup-token / API-key account added via `agents accounts add` never balanced.
|
|
8
|
+
* This module turns the account registry into the extra candidates the run path
|
|
9
|
+
* folds in — see `collectRunCandidatesForRun` in `account-pool-collect.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Pure + dependency-light so the harness-capability filter is unit-tested in
|
|
12
|
+
* isolation with fixtures.
|
|
13
|
+
*/
|
|
14
|
+
/** A provider account record as stored in the registry (identity captured separately). */
|
|
15
|
+
export interface RegistryAccountRecord {
|
|
16
|
+
name: string;
|
|
17
|
+
provider: string;
|
|
18
|
+
auth: AccountAuthKind;
|
|
19
|
+
}
|
|
20
|
+
/** A registry account eligible to run one harness, ready to map to a candidate. */
|
|
21
|
+
export interface RegistryAccountInput {
|
|
22
|
+
/** Agent-scoped key so `(claude, X)` and `(codex, X)` stay distinct. */
|
|
23
|
+
accountKey: string;
|
|
24
|
+
email: string | null;
|
|
25
|
+
name: string;
|
|
26
|
+
provider: string;
|
|
27
|
+
auth: AccountAuthKind;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Which provider accounts can authenticate `agent`. An account is a candidate for
|
|
31
|
+
* harness H only when its provider adapter has an `envFor(H, kind)` mapping
|
|
32
|
+
* (`providerAuthenticatesHarness`, the same check the account UI uses) — so a
|
|
33
|
+
* Cursor key never enters Claude's pool and a Claude setup-token never enters
|
|
34
|
+
* Codex's. This is registry-driven, no per-harness `else if`: a harness gets a
|
|
35
|
+
* provider-account pool exactly when the adapter table maps a provider to it —
|
|
36
|
+
* today claude, codex, grok, cursor, opencode (plus gemini/antigravity). A harness
|
|
37
|
+
* with only a native OAuth login and no provider adapter — kimi — returns `[]`
|
|
38
|
+
* here and keeps balancing its native logins only.
|
|
39
|
+
*
|
|
40
|
+
* `accountKey` is synthetic here (`${agent}:name=${name}`) — a stable per-account
|
|
41
|
+
* key so balancing can include it immediately; the real agent-scoped identity
|
|
42
|
+
* (email / org uuid) is backfilled by identity capture and replaces it. `email`
|
|
43
|
+
* is null until then, which routes the account as usage-unverified but still a
|
|
44
|
+
* candidate, never excluded.
|
|
45
|
+
*/
|
|
46
|
+
export declare function registryPoolCandidates(records: RegistryAccountRecord[], agent: AgentId): RegistryAccountInput[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { providerAuthenticatesHarness } from '../account-provider-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which provider accounts can authenticate `agent`. An account is a candidate for
|
|
4
|
+
* harness H only when its provider adapter has an `envFor(H, kind)` mapping
|
|
5
|
+
* (`providerAuthenticatesHarness`, the same check the account UI uses) — so a
|
|
6
|
+
* Cursor key never enters Claude's pool and a Claude setup-token never enters
|
|
7
|
+
* Codex's. This is registry-driven, no per-harness `else if`: a harness gets a
|
|
8
|
+
* provider-account pool exactly when the adapter table maps a provider to it —
|
|
9
|
+
* today claude, codex, grok, cursor, opencode (plus gemini/antigravity). A harness
|
|
10
|
+
* with only a native OAuth login and no provider adapter — kimi — returns `[]`
|
|
11
|
+
* here and keeps balancing its native logins only.
|
|
12
|
+
*
|
|
13
|
+
* `accountKey` is synthetic here (`${agent}:name=${name}`) — a stable per-account
|
|
14
|
+
* key so balancing can include it immediately; the real agent-scoped identity
|
|
15
|
+
* (email / org uuid) is backfilled by identity capture and replaces it. `email`
|
|
16
|
+
* is null until then, which routes the account as usage-unverified but still a
|
|
17
|
+
* candidate, never excluded.
|
|
18
|
+
*/
|
|
19
|
+
export function registryPoolCandidates(records, agent) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const r of records) {
|
|
22
|
+
if (!providerAuthenticatesHarness(r.provider, r.auth, agent))
|
|
23
|
+
continue;
|
|
24
|
+
out.push({
|
|
25
|
+
accountKey: `${agent}:name=${r.name}`,
|
|
26
|
+
email: null,
|
|
27
|
+
name: r.name,
|
|
28
|
+
provider: r.provider,
|
|
29
|
+
auth: r.auth,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remaining-capacity weighting for account selection — a pure, dependency-free
|
|
3
|
+
* primitive shared by the version-home router (`rotate.ts`) and the account pool
|
|
4
|
+
* (`account-pool.ts`). Kept in its own module so either can import it without
|
|
5
|
+
* dragging in the heavy usage/secrets graph.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* How far from its projected cap an account must be to keep its FULL headroom
|
|
9
|
+
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
10
|
+
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PROJECTION_HORIZON_MIN = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
15
|
+
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
16
|
+
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
17
|
+
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
18
|
+
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
19
|
+
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
20
|
+
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
21
|
+
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
22
|
+
* weighted-random draw is not).
|
|
23
|
+
*/
|
|
24
|
+
export declare function capacityWeight(usedPercent: number | null, minutesToLimit: number | null): number;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remaining-capacity weighting for account selection — a pure, dependency-free
|
|
3
|
+
* primitive shared by the version-home router (`rotate.ts`) and the account pool
|
|
4
|
+
* (`account-pool.ts`). Kept in its own module so either can import it without
|
|
5
|
+
* dragging in the heavy usage/secrets graph.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* How far from its projected cap an account must be to keep its FULL headroom
|
|
9
|
+
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
10
|
+
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
11
|
+
*/
|
|
12
|
+
export const PROJECTION_HORIZON_MIN = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
15
|
+
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
16
|
+
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
17
|
+
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
18
|
+
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
19
|
+
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
20
|
+
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
21
|
+
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
22
|
+
* weighted-random draw is not).
|
|
23
|
+
*/
|
|
24
|
+
export function capacityWeight(usedPercent, minutesToLimit) {
|
|
25
|
+
const base = usedPercent === null ? 100 : Math.max(1, 100 - usedPercent);
|
|
26
|
+
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
27
|
+
return base;
|
|
28
|
+
const factor = Math.max(0, Math.min(1, minutesToLimit / PROJECTION_HORIZON_MIN));
|
|
29
|
+
return Math.max(1, base * factor);
|
|
30
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentId, RunStrategy } from '../types.js';
|
|
8
8
|
import type { FallbackEntry } from '../exec.js';
|
|
9
|
+
import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
|
|
9
10
|
import { type AccountInfo, type CredentialPresence } from '../agents.js';
|
|
10
11
|
import { type UsageSnapshot } from './usage.js';
|
|
11
12
|
import { type AuthVerdict } from '../auth-health.js';
|
|
@@ -49,6 +50,15 @@ export interface RotateCandidate {
|
|
|
49
50
|
*/
|
|
50
51
|
authVerdict: AuthVerdict | null;
|
|
51
52
|
lastActive: Date | null;
|
|
53
|
+
/**
|
|
54
|
+
* Set only for a candidate that comes from a provider account bundle rather
|
|
55
|
+
* than a native version-home login (RUSH-3182): the account name to inject via
|
|
56
|
+
* the `--account` spawn path (`resolveSpawnAccount` → `accountEnv`). Undefined
|
|
57
|
+
* for a native login, whose credential already lives in `version`'s home. The
|
|
58
|
+
* run path routes on this so a balanced pick of a setup-token / API-key account
|
|
59
|
+
* authenticates through the existing provider-account injection.
|
|
60
|
+
*/
|
|
61
|
+
providerAccount?: string;
|
|
52
62
|
}
|
|
53
63
|
export interface RotateResult {
|
|
54
64
|
/** The version picked for this run. */
|
|
@@ -212,24 +222,7 @@ export declare function checkRunAccountReadiness(agent: AgentId, version: string
|
|
|
212
222
|
* version so behavior stays predictable.
|
|
213
223
|
*/
|
|
214
224
|
export declare function pickBalancedCandidate(candidates: RotateCandidate[], nowMs?: number): RotateResult | null;
|
|
215
|
-
|
|
216
|
-
* How far from its projected cap an account must be to keep its FULL headroom
|
|
217
|
-
* weight. Inside this horizon the weight is scaled down linearly toward the
|
|
218
|
-
* floor, so an account racing toward its 5h cap loses priority before it maxes.
|
|
219
|
-
*/
|
|
220
|
-
export declare const PROJECTION_HORIZON_MIN = 30;
|
|
221
|
-
/**
|
|
222
|
-
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
223
|
-
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
224
|
-
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
225
|
-
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
226
|
-
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
227
|
-
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
228
|
-
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
229
|
-
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
230
|
-
* weighted-random draw is not).
|
|
231
|
-
*/
|
|
232
|
-
export declare function capacityWeight(usedPercent: number | null, minutesToLimit: number | null): number;
|
|
225
|
+
export { PROJECTION_HORIZON_MIN, capacityWeight };
|
|
233
226
|
/**
|
|
234
227
|
* Pick an available candidate. Prefers the configured pinned version when that
|
|
235
228
|
* version has usage available; otherwise routes to the candidate with the most
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import { PROJECTION_HORIZON_MIN, capacityWeight } from './capacity.js';
|
|
9
10
|
import { accountDisplayLabel, getAccountInfo, credentialPresence, ALL_AGENT_IDS, } from '../agents.js';
|
|
10
11
|
import { readMeta, writeMeta, getHelpersDir } from '../state.js';
|
|
11
12
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from '../installations/versions.js';
|
|
@@ -365,30 +366,11 @@ function preferVerified(pool, nowMs, choose, narrowing = 'any-verified') {
|
|
|
365
366
|
usageUnverified: !isUsageVerified(picked, nowMs),
|
|
366
367
|
};
|
|
367
368
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
export const PROJECTION_HORIZON_MIN = 30;
|
|
374
|
-
/**
|
|
375
|
-
* Weight one candidate by remaining routing capacity, deprioritized by how soon
|
|
376
|
-
* it is projected to cap. The base is weekly headroom (`max(1, 100 - used)`);
|
|
377
|
-
* an account with no live snapshot is treated as full-capacity (100) since there
|
|
378
|
-
* is no signal to deprioritize it. `minutesToLimit` (the daemon's burn-rate
|
|
379
|
-
* projection on the 5h session window) then scales that base: >= horizon (or
|
|
380
|
-
* unknown) keeps full weight, and closer-to-cap scales toward the floor of 1 —
|
|
381
|
-
* so a launch avoids an account projected to cap soon, not just a 100%-maxed
|
|
382
|
-
* one. Pure + exported so the deprioritization is unit-tested directly (a
|
|
383
|
-
* weighted-random draw is not).
|
|
384
|
-
*/
|
|
385
|
-
export function capacityWeight(usedPercent, minutesToLimit) {
|
|
386
|
-
const base = usedPercent === null ? 100 : Math.max(1, 100 - usedPercent);
|
|
387
|
-
if (minutesToLimit === null || !Number.isFinite(minutesToLimit))
|
|
388
|
-
return base;
|
|
389
|
-
const factor = Math.max(0, Math.min(1, minutesToLimit / PROJECTION_HORIZON_MIN));
|
|
390
|
-
return Math.max(1, base * factor);
|
|
391
|
-
}
|
|
369
|
+
// capacityWeight + PROJECTION_HORIZON_MIN moved to ./capacity.js (a pure,
|
|
370
|
+
// dependency-free module the account pool can import without this file's heavy
|
|
371
|
+
// graph). Imported at the top for internal use; re-exported here so existing
|
|
372
|
+
// importers keep resolving them from rotate.
|
|
373
|
+
export { PROJECTION_HORIZON_MIN, capacityWeight };
|
|
392
374
|
/**
|
|
393
375
|
* Pick one candidate from `sorted` using weights proportional to remaining
|
|
394
376
|
* routing capacity (see {@link capacityWeight}). Floor each weight at 1 so a
|
|
@@ -125,6 +125,8 @@ export interface UsageSnapshot {
|
|
|
125
125
|
capturedAt: Date | null;
|
|
126
126
|
windows: UsageWindow[];
|
|
127
127
|
plan?: string | null;
|
|
128
|
+
/** Action that makes an event-fed source emit a current reading. */
|
|
129
|
+
refreshHint?: string | null;
|
|
128
130
|
/**
|
|
129
131
|
* A refusal observed from a real harness run, independent of API windows.
|
|
130
132
|
* `session_limit` recovers on a clock (`resetsAt`). `out_of_credits` is a
|
|
@@ -306,6 +308,11 @@ export interface FormatUsageSummaryOpts {
|
|
|
306
308
|
* column width; single-agent and detail views leave this unset.
|
|
307
309
|
*/
|
|
308
310
|
maxWindows?: number;
|
|
311
|
+
/** Windows that must keep a visible slot even when the provider omits one. */
|
|
312
|
+
expectedWindows?: Array<{
|
|
313
|
+
key: string;
|
|
314
|
+
shortLabel: string;
|
|
315
|
+
}>;
|
|
309
316
|
/**
|
|
310
317
|
* The classified cause of `usageInfo.error` (RUSH-3040), from
|
|
311
318
|
* {@link classifyUsageErrorKind}. Lets the no-bars branch below name the
|
|
@@ -326,6 +333,8 @@ export interface FormatUsageSummaryOpts {
|
|
|
326
333
|
errorDetail?: string | null;
|
|
327
334
|
/** Benign state from {@link getUsageBenignState}; never sourced from `UsageInfo.error`. */
|
|
328
335
|
benignState?: UsageBenignState | null;
|
|
336
|
+
/** Provider-specific replacement for the generic no-local-event marker. */
|
|
337
|
+
noRecentUsageLabel?: string | null;
|
|
329
338
|
}
|
|
330
339
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
331
340
|
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: FormatUsageSummaryOpts): string;
|
|
@@ -552,7 +561,7 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
|
|
|
552
561
|
* a file-based setup-token and, when none is provisioned, gets `null` — it never
|
|
553
562
|
* reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
|
|
554
563
|
* to Anthropic's API is what gets it revoked; see the branch body and
|
|
555
|
-
* docs/
|
|
564
|
+
* docs/secrets.md). It is OFF by default so full-credential
|
|
556
565
|
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
|
|
557
566
|
* read the interactive login. Rush Cloud dispatch does not call this helper
|
|
558
567
|
* at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
|
|
@@ -588,6 +597,8 @@ export declare function readClaudeUsageCache(usageKey: string, cachePath?: strin
|
|
|
588
597
|
export declare function pruneExpiredClaudeUsageCacheEntry(usageKey: string, cachePath?: string, now?: Date): void;
|
|
589
598
|
/** Write a usage snapshot to the on-disk cache. */
|
|
590
599
|
export declare function writeClaudeUsageCache(usageKey: string, snapshot: UsageSnapshot, cachePath?: string): void;
|
|
600
|
+
/** Atomically merge partial native Claude windows into the current fresh row. */
|
|
601
|
+
export declare function mergeClaudeUsageCacheWindows(usageKey: string, snapshot: UsageSnapshot, cachePath?: string): void;
|
|
591
602
|
/**
|
|
592
603
|
* Persist a Claude tokens/credits exhaustion (`out of usage credits` / `monthly
|
|
593
604
|
* spend limit`) from a real run. Unlike a rate/session limit this does NOT reset
|
|
@@ -614,8 +625,8 @@ export declare function parseClaudeSessionLimitReset(text: string, nowMs?: numbe
|
|
|
614
625
|
* Returns true only when a valid access token can be obtained.
|
|
615
626
|
*/
|
|
616
627
|
export declare function isClaudeAuthValid(home?: string): Promise<boolean>;
|
|
617
|
-
/** Render a colored
|
|
618
|
-
export declare function renderBar(usedPercent: number, length: number
|
|
628
|
+
/** Render a proportional colored progress bar at one-eighth-cell resolution. */
|
|
629
|
+
export declare function renderBar(usedPercent: number, length: number): string;
|
|
619
630
|
/** Return a chalk color function based on the usage percentage threshold. */
|
|
620
631
|
export declare function getUsageColor(usedPercent: number): (text: string) => string;
|
|
621
632
|
/** Parse the latest billing info from Grok's unified log. */
|
|
@@ -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';
|
|
@@ -216,6 +217,7 @@ const COMPACT_BAR_LEN = 5;
|
|
|
216
217
|
const USAGE_BAR_LEN = 10;
|
|
217
218
|
const FULL = '\u2588';
|
|
218
219
|
const EMPTY = '\u2591';
|
|
220
|
+
const PARTIAL_BLOCKS = ['', '\u258F', '\u258E', '\u258D', '\u258C', '\u258B', '\u258A', '\u2589'];
|
|
219
221
|
/** Construct the benign no-local-log result without overloading `error`. */
|
|
220
222
|
export function usageNoRecentUsageInfo() {
|
|
221
223
|
return { snapshot: null, error: null, [USAGE_BENIGN_STATE]: 'no-recent-usage' };
|
|
@@ -498,11 +500,20 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
498
500
|
// multi-meter agent cannot force the whole table to wrap.
|
|
499
501
|
const selected = pickCompactUsageWindows(snapshot.windows, opts?.maxWindows);
|
|
500
502
|
const hidden = Math.max(0, snapshot.windows.filter((w) => w.key !== 'sonnet_week').length - selected.length);
|
|
501
|
-
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
|
+
}
|
|
502
512
|
const bar = renderCompactUsageBar(window.usedPercent);
|
|
503
513
|
const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
|
|
504
514
|
const reset = window.resetsAt ? chalk.dim(` (${formatResetHint(window.resetsAt)})`) : '';
|
|
505
|
-
|
|
515
|
+
const rendered = `${chalk.gray(`${shortLabel}:`)} ${bar} ${pct}${reset}`;
|
|
516
|
+
return index < windowsToRender.length - 1 ? padToWidth(rendered, 20) : rendered;
|
|
506
517
|
});
|
|
507
518
|
if (hidden > 0) {
|
|
508
519
|
windowParts.push(chalk.dim(`+${hidden}`));
|
|
@@ -510,6 +521,9 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
510
521
|
if (windowParts.length > 0) {
|
|
511
522
|
parts.push(windowParts.join(' '));
|
|
512
523
|
}
|
|
524
|
+
else if (snapshot.refreshHint) {
|
|
525
|
+
parts.push(chalk.dim(snapshot.refreshHint));
|
|
526
|
+
}
|
|
513
527
|
// The bars came from the cache and the live read that should have confirmed
|
|
514
528
|
// them failed, so they are the last thing we saw — not the current state.
|
|
515
529
|
// Drawing them unmarked is what let a 26h-old "48% used" read as fact.
|
|
@@ -528,7 +542,7 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
528
542
|
parts.push(chalk.dim(USAGE_HEADLESS_SCOPE_MARKER));
|
|
529
543
|
}
|
|
530
544
|
else if (opts?.benignState === 'no-recent-usage') {
|
|
531
|
-
parts.push(chalk.dim(USAGE_NO_RECENT_USAGE_MARKER));
|
|
545
|
+
parts.push(chalk.dim(opts.noRecentUsageLabel || USAGE_NO_RECENT_USAGE_MARKER));
|
|
532
546
|
}
|
|
533
547
|
else if (opts?.unavailable) {
|
|
534
548
|
// Signed-in account we could NOT fetch usage for (no live token in a reachable
|
|
@@ -1410,7 +1424,7 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1410
1424
|
* a file-based setup-token and, when none is provisioned, gets `null` — it never
|
|
1411
1425
|
* reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
|
|
1412
1426
|
* to Anthropic's API is what gets it revoked; see the branch body and
|
|
1413
|
-
* docs/
|
|
1427
|
+
* docs/secrets.md). It is OFF by default so full-credential
|
|
1414
1428
|
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
|
|
1415
1429
|
* read the interactive login. Rush Cloud dispatch does not call this helper
|
|
1416
1430
|
* at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
|
|
@@ -1443,7 +1457,7 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1443
1457
|
// interactive credential used programmatically, which Anthropic flags and
|
|
1444
1458
|
// revokes (the fleet-wide-logout class, RUSH-1822), and which violates the
|
|
1445
1459
|
// invariant that the interactive/rotating login is untouchable
|
|
1446
|
-
// (docs/
|
|
1460
|
+
// (docs/secrets.md). Report unprovisioned (-> probe
|
|
1447
1461
|
// token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
|
|
1448
1462
|
// setup-token via the mint-auth path to restore usage/probe for the account.
|
|
1449
1463
|
return null;
|
|
@@ -1604,6 +1618,32 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
|
|
|
1604
1618
|
/* best-effort cache write — lock busy or disk full */
|
|
1605
1619
|
}
|
|
1606
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
|
+
}
|
|
1607
1647
|
/** Read the entire usage cache file from disk. */
|
|
1608
1648
|
function readClaudeUsageCacheFile(cachePath) {
|
|
1609
1649
|
if (!fs.existsSync(cachePath)) {
|
|
@@ -1632,6 +1672,7 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1632
1672
|
return {
|
|
1633
1673
|
capturedAt: snapshot.capturedAt?.toISOString() || null,
|
|
1634
1674
|
plan: snapshot.plan ?? null,
|
|
1675
|
+
refreshHint: snapshot.refreshHint ?? null,
|
|
1635
1676
|
unavailable: snapshot.unavailable
|
|
1636
1677
|
? {
|
|
1637
1678
|
reason: snapshot.unavailable.reason,
|
|
@@ -1686,7 +1727,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1686
1727
|
// and `deriveUsageStatusFromSnapshot` still returns null for zero windows, so
|
|
1687
1728
|
// it can never read as a 0% bar or an "available" badge (the RUSH-2858
|
|
1688
1729
|
// property that made expired windows drop in the first place).
|
|
1689
|
-
if (windows.length === 0 && !unavailable && !snapshot.plan) {
|
|
1730
|
+
if (windows.length === 0 && !unavailable && !snapshot.plan && !snapshot.refreshHint) {
|
|
1690
1731
|
return null;
|
|
1691
1732
|
}
|
|
1692
1733
|
return {
|
|
@@ -1695,6 +1736,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1695
1736
|
capturedAt,
|
|
1696
1737
|
windows,
|
|
1697
1738
|
plan: snapshot.plan ?? null,
|
|
1739
|
+
refreshHint: snapshot.refreshHint ?? null,
|
|
1698
1740
|
unavailable,
|
|
1699
1741
|
};
|
|
1700
1742
|
}
|
|
@@ -1973,14 +2015,17 @@ function renderUsageBar(usedPercent) {
|
|
|
1973
2015
|
}
|
|
1974
2016
|
/** Render a compact usage bar for inline summaries. */
|
|
1975
2017
|
function renderCompactUsageBar(usedPercent) {
|
|
1976
|
-
return renderBar(usedPercent, COMPACT_BAR_LEN
|
|
1977
|
-
}
|
|
1978
|
-
/** Render a colored
|
|
1979
|
-
export function renderBar(usedPercent, length
|
|
1980
|
-
const
|
|
1981
|
-
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;
|
|
1982
2026
|
const color = getUsageColor(usedPercent);
|
|
1983
|
-
|
|
2027
|
+
const gauge = FULL.repeat(filled) + PARTIAL_BLOCKS[partial];
|
|
2028
|
+
return color(gauge) + chalk.dim(EMPTY.repeat(length - filled - (partial > 0 ? 1 : 0)));
|
|
1984
2029
|
}
|
|
1985
2030
|
/** Apply the appropriate color to a text string based on usage percentage. */
|
|
1986
2031
|
function colorUsage(text, usedPercent) {
|
|
@@ -2080,10 +2125,10 @@ async function getGrokUsageInfo(options) {
|
|
|
2080
2125
|
// (see readLatestGrokBilling).
|
|
2081
2126
|
const now = new Date();
|
|
2082
2127
|
const windows = match.windows.filter((window) => isCachedUsageWindowFresh(window, match.capturedAt, now));
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2128
|
+
const version = options?.cliVersion;
|
|
2129
|
+
const refreshHint = windows.length === 0
|
|
2130
|
+
? `run grok${version ? `@${version}` : ''} once to refresh usage`
|
|
2131
|
+
: null;
|
|
2087
2132
|
return {
|
|
2088
2133
|
snapshot: {
|
|
2089
2134
|
source: 'last_seen',
|
|
@@ -2091,6 +2136,7 @@ async function getGrokUsageInfo(options) {
|
|
|
2091
2136
|
capturedAt: match.capturedAt,
|
|
2092
2137
|
windows,
|
|
2093
2138
|
plan: match.subscriptionTier,
|
|
2139
|
+
refreshHint,
|
|
2094
2140
|
},
|
|
2095
2141
|
error: null,
|
|
2096
2142
|
};
|
|
@@ -2483,7 +2529,7 @@ export function normalizeCursorUsageSummary(data) {
|
|
|
2483
2529
|
function readCursorCredentials(base) {
|
|
2484
2530
|
try {
|
|
2485
2531
|
const cfgPath = path.join(base, '.cursor', 'cli-config.json');
|
|
2486
|
-
const authPath = path.join(base, '.
|
|
2532
|
+
const authPath = path.join(base, '.cursor', 'auth.json');
|
|
2487
2533
|
if (!fs.existsSync(cfgPath) || !fs.existsSync(authPath))
|
|
2488
2534
|
return null;
|
|
2489
2535
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
|
@@ -1504,12 +1504,9 @@ const CREDENTIAL_FILE_SEGMENTS = {
|
|
|
1504
1504
|
// Muse Code stores OAuth / API credentials at ~/.config/muse/auth.json
|
|
1505
1505
|
// (or META_API_KEY in the environment, which is not a file).
|
|
1506
1506
|
muse: [['.config', 'muse', 'auth.json']],
|
|
1507
|
-
//
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1510
|
-
// version home with no token of its own is genuinely logged out once runs pin
|
|
1511
|
-
// XDG_CONFIG_HOME per home (see buildExecEnv).
|
|
1512
|
-
cursor: [['.config', 'cursor', 'auth.json']],
|
|
1507
|
+
// With AGENT_CLI_CREDENTIAL_STORE=file, Cursor stores both OAuth tokens at
|
|
1508
|
+
// ~/.cursor/auth.json. cli-config.json holds account metadata only.
|
|
1509
|
+
cursor: [['.cursor', 'auth.json']],
|
|
1513
1510
|
};
|
|
1514
1511
|
/** Whether an agent's credential file exists under a given home. */
|
|
1515
1512
|
function credentialFileExistsUnder(agentId, home) {
|
|
@@ -2112,7 +2109,7 @@ export async function getAccountInfo(agentId, home) {
|
|
|
2112
2109
|
case 'cursor': {
|
|
2113
2110
|
// Cursor CLI keeps account metadata in ~/.cursor/cli-config.json
|
|
2114
2111
|
// (authInfo: { email, userId, authId }) and its OAuth tokens SEPARATELY
|
|
2115
|
-
// in ~/.
|
|
2112
|
+
// in ~/.cursor/auth.json ({ accessToken, refreshToken }). Presence
|
|
2116
2113
|
// of an access token is the signed-in signal; email/ids come from
|
|
2117
2114
|
// cli-config. authId is the OAuth subject (e.g. "google-oauth2|<n>") — the
|
|
2118
2115
|
// same value the usage endpoint keys on (see getCursorUsageInfo).
|
|
@@ -2126,7 +2123,7 @@ export async function getAccountInfo(agentId, home) {
|
|
|
2126
2123
|
const accountId = normalizeIdentityPart(authInfo?.authId ?? authInfo?.userId);
|
|
2127
2124
|
if (!email && !accountId)
|
|
2128
2125
|
return { ...empty, lastActive };
|
|
2129
|
-
const authPath = resolveAccountCredentialPath(base, '.
|
|
2126
|
+
const authPath = resolveAccountCredentialPath(base, '.cursor', 'auth.json');
|
|
2130
2127
|
let hasToken = false;
|
|
2131
2128
|
if (authPath) {
|
|
2132
2129
|
try {
|
|
@@ -2135,8 +2132,13 @@ export async function getAccountInfo(agentId, home) {
|
|
|
2135
2132
|
}
|
|
2136
2133
|
catch { /* unreadable token file */ }
|
|
2137
2134
|
}
|
|
2135
|
+
// cli-config survives logout and may describe a different keychain
|
|
2136
|
+
// account. Without this version's file token, its identity is stale
|
|
2137
|
+
// metadata rather than a usable managed login.
|
|
2138
|
+
if (!hasToken)
|
|
2139
|
+
return { ...empty, lastActive };
|
|
2138
2140
|
const accountKey = buildIdentityKey(agentId, [['user', accountId]]);
|
|
2139
|
-
return { ...empty, email, accountId, accountKey, signedIn:
|
|
2141
|
+
return { ...empty, email, accountId, accountKey, signedIn: true, lastActive };
|
|
2140
2142
|
}
|
|
2141
2143
|
catch { }
|
|
2142
2144
|
return { ...empty, lastActive };
|
|
@@ -16,13 +16,27 @@ export interface SSHConnection {
|
|
|
16
16
|
* strings differ (no `&` backgrounding, no `lsof`, `.exe` instead of `.app`).
|
|
17
17
|
*/
|
|
18
18
|
export type RemoteOs = 'windows' | 'posix';
|
|
19
|
+
export interface SSHConnectOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Attach to a browser another device already declared (identity-bearing or
|
|
22
|
+
* a fungible name we don't declare here). Do not launch a fresh remote
|
|
23
|
+
* chrome-data, and do not kill that browser when the tunnel closes — it
|
|
24
|
+
* holds the logins the caller is trying to reach.
|
|
25
|
+
*/
|
|
26
|
+
persistRemote?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Lifecycle for a registry-driven tunnel vs a profile the daemon launched. */
|
|
29
|
+
export declare function persistRemoteLifecycle(persistRemote: boolean): {
|
|
30
|
+
launchRemote: boolean;
|
|
31
|
+
killOnCleanup: boolean;
|
|
32
|
+
};
|
|
19
33
|
export declare function connectSSH(endpoint: string, profile: BrowserProfile,
|
|
20
34
|
/**
|
|
21
|
-
* Runtime key (`<profile>@<
|
|
35
|
+
* Runtime key (`<profile>@<device>`) this connection is stored under. It
|
|
22
36
|
* keys the tunnel's runtime record; `profile.name` is the bare, user-facing
|
|
23
37
|
* name and appears only in messages (RUSH-2709).
|
|
24
38
|
*/
|
|
25
|
-
key: ConnectionKey): Promise<SSHConnection>;
|
|
39
|
+
key: ConnectionKey, opts?: SSHConnectOptions): Promise<SSHConnection>;
|
|
26
40
|
/**
|
|
27
41
|
* Wrap a PowerShell script as a `-EncodedCommand` invocation. Base64 of the
|
|
28
42
|
* UTF-16LE bytes is a single quote-free token, so it rides through Node spawn
|