@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.
Files changed (272) hide show
  1. package/CHANGELOG.md +503 -17
  2. package/README.md +76 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +201 -39
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon-test-harness.d.ts +42 -0
  14. package/dist/commands/daemon-test-harness.js +90 -0
  15. package/dist/commands/daemon.d.ts +21 -0
  16. package/dist/commands/daemon.js +147 -12
  17. package/dist/commands/device-config-test-harness.d.ts +11 -0
  18. package/dist/commands/device-config-test-harness.js +59 -0
  19. package/dist/commands/exec.js +76 -16
  20. package/dist/commands/focus.d.ts +14 -0
  21. package/dist/commands/focus.js +48 -5
  22. package/dist/commands/go.d.ts +3 -0
  23. package/dist/commands/go.js +16 -3
  24. package/dist/commands/inspect.js +2 -2
  25. package/dist/commands/lease.d.ts +1 -1
  26. package/dist/commands/lease.js +4 -3
  27. package/dist/commands/mailboxes.js +4 -3
  28. package/dist/commands/menubar.js +11 -4
  29. package/dist/commands/projects.d.ts +16 -1
  30. package/dist/commands/projects.js +58 -64
  31. package/dist/commands/resume.js +1 -1
  32. package/dist/commands/route.js +4 -4
  33. package/dist/commands/routines.js +3 -2
  34. package/dist/commands/run-broadcast.d.ts +1 -1
  35. package/dist/commands/run-broadcast.js +1 -1
  36. package/dist/commands/sessions-picker.d.ts +22 -0
  37. package/dist/commands/sessions-picker.js +48 -9
  38. package/dist/commands/sessions-trace.js +1 -1
  39. package/dist/commands/sessions.js +1 -0
  40. package/dist/commands/setup-secrets.js +23 -0
  41. package/dist/commands/share.d.ts +20 -5
  42. package/dist/commands/share.js +158 -53
  43. package/dist/commands/ssh.js +54 -7
  44. package/dist/commands/teams.d.ts +12 -0
  45. package/dist/commands/teams.js +44 -67
  46. package/dist/commands/traces.d.ts +2 -0
  47. package/dist/commands/traces.js +188 -0
  48. package/dist/commands/view.d.ts +1 -1
  49. package/dist/commands/view.js +17 -34
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +5 -0
  52. package/dist/lib/account-provider-registry.d.ts +9 -0
  53. package/dist/lib/account-provider-registry.js +20 -0
  54. package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
  55. package/dist/lib/accounting/account-pool-collect.js +71 -0
  56. package/dist/lib/accounting/account-pool.d.ts +46 -0
  57. package/dist/lib/accounting/account-pool.js +33 -0
  58. package/dist/lib/accounting/capacity.d.ts +24 -0
  59. package/dist/lib/accounting/capacity.js +30 -0
  60. package/dist/lib/accounting/rotate.d.ts +11 -18
  61. package/dist/lib/accounting/rotate.js +6 -24
  62. package/dist/lib/accounting/usage.d.ts +14 -3
  63. package/dist/lib/accounting/usage.js +64 -18
  64. package/dist/lib/agent-spec/agents.js +11 -9
  65. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  66. package/dist/lib/browser/drivers/ssh.js +36 -9
  67. package/dist/lib/browser/ipc.js +8 -3
  68. package/dist/lib/browser/profiles.d.ts +35 -101
  69. package/dist/lib/browser/profiles.js +130 -301
  70. package/dist/lib/browser/registry.d.ts +39 -0
  71. package/dist/lib/browser/registry.js +149 -0
  72. package/dist/lib/browser/remote-control.d.ts +8 -6
  73. package/dist/lib/browser/remote-control.js +8 -6
  74. package/dist/lib/browser/resolve-target.d.ts +68 -0
  75. package/dist/lib/browser/resolve-target.js +250 -0
  76. package/dist/lib/browser/runtime-state.d.ts +40 -16
  77. package/dist/lib/browser/runtime-state.js +92 -25
  78. package/dist/lib/browser/service.d.ts +28 -9
  79. package/dist/lib/browser/service.js +139 -62
  80. package/dist/lib/browser/task-index.d.ts +70 -0
  81. package/dist/lib/browser/task-index.js +180 -0
  82. package/dist/lib/byok-usage.js +2 -2
  83. package/dist/lib/claude-statusline.d.ts +29 -0
  84. package/dist/lib/claude-statusline.js +139 -0
  85. package/dist/lib/cloud/host.d.ts +1 -1
  86. package/dist/lib/cloud/host.js +1 -1
  87. package/dist/lib/cloud/rush.js +1 -1
  88. package/dist/lib/computer/computer-rpc.js +2 -2
  89. package/dist/lib/computer/download.d.ts +15 -15
  90. package/dist/lib/computer/download.js +35 -98
  91. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  92. package/dist/lib/computer/ssh-tunnel.js +12 -22
  93. package/dist/lib/config-keys.d.ts +2 -8
  94. package/dist/lib/config-keys.js +2 -13
  95. package/dist/lib/crabbox/cli.d.ts +1 -1
  96. package/dist/lib/crabbox/cli.js +2 -2
  97. package/dist/lib/crabbox/lease.d.ts +9 -0
  98. package/dist/lib/crabbox/lease.js +70 -15
  99. package/dist/lib/crabbox/runtimes.js +1 -1
  100. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  101. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  102. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  103. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  104. package/dist/lib/daemon/daemon.d.ts +3 -0
  105. package/dist/lib/daemon/daemon.js +212 -444
  106. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  107. package/dist/lib/daemon/device-probe-service.js +47 -0
  108. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  109. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  110. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  111. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  112. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  113. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  114. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  115. package/dist/lib/daemon/self-heal-service.js +48 -0
  116. package/dist/lib/daemon/service.d.ts +102 -0
  117. package/dist/lib/daemon/service.js +98 -0
  118. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  119. package/dist/lib/daemon/session-index-service.js +40 -0
  120. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  121. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  122. package/dist/lib/daemon/supervisor.d.ts +87 -0
  123. package/dist/lib/daemon/supervisor.js +298 -0
  124. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  125. package/dist/lib/daemon/watchdog-service.js +40 -0
  126. package/dist/lib/daemon-health.d.ts +15 -0
  127. package/dist/lib/daemon-health.js +29 -5
  128. package/dist/lib/daemon-services.d.ts +8 -1
  129. package/dist/lib/daemon-services.js +55 -0
  130. package/dist/lib/daemon-ticks.d.ts +13 -2
  131. package/dist/lib/daemon-ticks.js +20 -18
  132. package/dist/lib/device-config.d.ts +0 -2
  133. package/dist/lib/device-config.js +1 -23
  134. package/dist/lib/devices/registry.d.ts +1 -1
  135. package/dist/lib/devices/registry.js +1 -1
  136. package/dist/lib/devices/stats-cache.d.ts +20 -0
  137. package/dist/lib/devices/stats-cache.js +49 -5
  138. package/dist/lib/devices/worker-pick.d.ts +48 -0
  139. package/dist/lib/devices/worker-pick.js +113 -0
  140. package/dist/lib/exec.d.ts +67 -13
  141. package/dist/lib/exec.js +85 -29
  142. package/dist/lib/harness/adapters/cursor.js +8 -11
  143. package/dist/lib/helper-download.d.ts +108 -0
  144. package/dist/lib/helper-download.js +194 -0
  145. package/dist/lib/helper-versions.d.ts +52 -0
  146. package/dist/lib/helper-versions.js +58 -0
  147. package/dist/lib/hooks/cache.js +11 -0
  148. package/dist/lib/hooks/install.js +2 -2
  149. package/dist/lib/hosts/credentials.d.ts +1 -1
  150. package/dist/lib/hosts/credentials.js +1 -1
  151. package/dist/lib/hosts/dispatch.d.ts +7 -1
  152. package/dist/lib/hosts/dispatch.js +24 -5
  153. package/dist/lib/hosts/passthrough.d.ts +8 -15
  154. package/dist/lib/hosts/passthrough.js +19 -16
  155. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  156. package/dist/lib/hosts/providers/devices.js +1 -1
  157. package/dist/lib/hosts/reconnect.d.ts +179 -21
  158. package/dist/lib/hosts/reconnect.js +266 -45
  159. package/dist/lib/installations/migrate.d.ts +4 -5
  160. package/dist/lib/installations/migrate.js +6 -7
  161. package/dist/lib/installations/shims.d.ts +1 -1
  162. package/dist/lib/installations/shims.js +23 -13
  163. package/dist/lib/installations/store.js +6 -3
  164. package/dist/lib/installations/versions.js +73 -48
  165. package/dist/lib/linear-autoclose.d.ts +1 -1
  166. package/dist/lib/linear-autoclose.js +1 -1
  167. package/dist/lib/loop.d.ts +2 -2
  168. package/dist/lib/loop.js +1 -1
  169. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  170. package/dist/lib/menubar/download-menubar.js +60 -0
  171. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  172. package/dist/lib/menubar/install-menubar.js +79 -21
  173. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  174. package/dist/lib/menubar/notify-desktop.js +6 -6
  175. package/dist/lib/model-tiers.d.ts +1 -1
  176. package/dist/lib/project-probe.d.ts +1 -1
  177. package/dist/lib/project-probe.js +1 -1
  178. package/dist/lib/project-pull.d.ts +1 -1
  179. package/dist/lib/project-pull.js +2 -2
  180. package/dist/lib/projects.d.ts +1 -1
  181. package/dist/lib/projects.js +1 -1
  182. package/dist/lib/sandbox.js +6 -7
  183. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  184. package/dist/lib/secrets/download-keychain.js +70 -0
  185. package/dist/lib/secrets/install-helper.d.ts +20 -1
  186. package/dist/lib/secrets/install-helper.js +44 -3
  187. package/dist/lib/session/active.js +17 -1
  188. package/dist/lib/session/db.d.ts +11 -1
  189. package/dist/lib/session/db.js +138 -6
  190. package/dist/lib/session/discover.d.ts +21 -1
  191. package/dist/lib/session/discover.js +33 -5
  192. package/dist/lib/session/highlights.d.ts +49 -1
  193. package/dist/lib/session/highlights.js +95 -0
  194. package/dist/lib/session/host-link.d.ts +16 -2
  195. package/dist/lib/session/host-link.js +7 -1
  196. package/dist/lib/session/parse.js +33 -1
  197. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  198. package/dist/lib/session/remote/remote-list.js +19 -2
  199. package/dist/lib/session/remote/watch.js +11 -2
  200. package/dist/lib/session/session-cache.d.ts +53 -0
  201. package/dist/lib/session/session-cache.js +88 -0
  202. package/dist/lib/session/state.js +1 -1
  203. package/dist/lib/session/tool-calls.d.ts +2 -0
  204. package/dist/lib/session/tool-calls.js +1 -1
  205. package/dist/lib/session/trajectory-html.d.ts +5 -1
  206. package/dist/lib/session/trajectory-html.js +42 -22
  207. package/dist/lib/session/trajectory-text.js +9 -2
  208. package/dist/lib/session/trajectory.js +53 -4
  209. package/dist/lib/session/types.d.ts +15 -0
  210. package/dist/lib/sha256-asset.d.ts +26 -0
  211. package/dist/lib/sha256-asset.js +41 -0
  212. package/dist/lib/share/backend.d.ts +95 -0
  213. package/dist/lib/share/backend.js +139 -0
  214. package/dist/lib/share/delete.d.ts +6 -2
  215. package/dist/lib/share/delete.js +8 -9
  216. package/dist/lib/share/html.d.ts +22 -0
  217. package/dist/lib/share/html.js +88 -0
  218. package/dist/lib/share/provision.d.ts +18 -4
  219. package/dist/lib/share/provision.js +38 -4
  220. package/dist/lib/share/publish.d.ts +44 -15
  221. package/dist/lib/share/publish.js +58 -26
  222. package/dist/lib/share/worker-template.js +402 -25
  223. package/dist/lib/sqlite.js +16 -3
  224. package/dist/lib/ssh-exec.d.ts +65 -0
  225. package/dist/lib/ssh-exec.js +120 -4
  226. package/dist/lib/startup/command-registry.js +1 -1
  227. package/dist/lib/startup/dev-build.js +3 -3
  228. package/dist/lib/state.d.ts +1 -1
  229. package/dist/lib/state.js +7 -3
  230. package/dist/lib/subagents-registry.js +1 -1
  231. package/dist/lib/teams/agents.d.ts +14 -12
  232. package/dist/lib/teams/agents.js +188 -73
  233. package/dist/lib/teams/api.d.ts +6 -1
  234. package/dist/lib/teams/api.js +2 -0
  235. package/dist/lib/teams/scheduler.d.ts +7 -0
  236. package/dist/lib/teams/scheduler.js +26 -8
  237. package/dist/lib/terminal/index.d.ts +1 -1
  238. package/dist/lib/terminal/types.d.ts +1 -1
  239. package/dist/lib/terminal/types.js +1 -1
  240. package/dist/lib/tmux/orphan-reap.js +1 -1
  241. package/dist/lib/tmux/session.js +31 -0
  242. package/dist/lib/traces/backend.d.ts +29 -0
  243. package/dist/lib/traces/backend.js +46 -0
  244. package/dist/lib/traces/classify.d.ts +52 -0
  245. package/dist/lib/traces/classify.js +72 -0
  246. package/dist/lib/traces/config.d.ts +10 -0
  247. package/dist/lib/traces/config.js +2 -0
  248. package/dist/lib/traces/provision.d.ts +19 -0
  249. package/dist/lib/traces/provision.js +30 -0
  250. package/dist/lib/traces/sync.d.ts +150 -0
  251. package/dist/lib/traces/sync.js +459 -0
  252. package/dist/lib/traces/worker-template.d.ts +4 -0
  253. package/dist/lib/traces/worker-template.js +159 -0
  254. package/dist/lib/types.d.ts +18 -19
  255. package/dist/lib/types.js +14 -0
  256. package/dist/lib/usage-refresh.d.ts +1 -1
  257. package/dist/lib/usage-refresh.js +11 -8
  258. package/dist/lib/workflows.d.ts +1 -1
  259. package/package.json +4 -6
  260. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  261. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  262. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  263. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  264. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  265. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  266. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  267. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  268. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  269. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  270. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  271. package/dist/lib/usage-fleet.d.ts +0 -32
  272. 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
- * How far from its projected cap an account must be to keep its FULL headroom
370
- * weight. Inside this horizon the weight is scaled down linearly toward the
371
- * floor, so an account racing toward its 5h cap loses priority before it maxes.
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/credential-management.md). It is OFF by default so full-credential
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 block-character progress bar. */
618
- export declare function renderBar(usedPercent: number, length: number, minimumVisible?: number): string;
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 windowParts = selected.map((window) => {
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
- return `${chalk.gray(`${window.shortLabel}:`)} ${bar} ${pct}${reset}`;
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/credential-management.md). It is OFF by default so full-credential
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/credential-management.md). Report unprovisioned (-> probe
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, usedPercent > 0 ? 1 : 0);
1977
- }
1978
- /** Render a colored block-character progress bar. */
1979
- export function renderBar(usedPercent, length, minimumVisible = 0) {
1980
- const rounded = Math.round((usedPercent / 100) * length);
1981
- const filled = Math.max(minimumVisible, Math.max(0, Math.min(length, rounded)));
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
- return color(FULL.repeat(filled)) + chalk.dim(EMPTY.repeat(length - filled));
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
- // NOTE: an empty `windows` here (all windows expired) still returns a real
2084
- // snapshot, not the no-recent-usage marker — the plan/tier is still valid
2085
- // and callers (e.g. `formatUsageSummary`) render "no bars" correctly from
2086
- // an empty array. The marker above is only for "nothing was ever recorded".
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, '.config', 'cursor', 'auth.json');
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
- // Cursor's OAuth token — the login gate — is at $XDG_CONFIG_HOME/cursor/auth.json
1508
- // (~/.config/cursor/auth.json by default). cli-config.json holds only account
1509
- // metadata; the token file is what a real launch authenticates from, so a
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 ~/.config/cursor/auth.json ({ accessToken, refreshToken }). Presence
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, '.config', 'cursor', 'auth.json');
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: hasToken || !!email, lastActive };
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>@<endpoint>`) this connection is stored under. It
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