@phnx-labs/agents-cli 1.22.53 → 1.22.55

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 (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -59,6 +59,14 @@ export function usageNoCredentialError(agent) {
59
59
  export function usageExpiredCredentialError(agent) {
60
60
  return `${agent} credential expired — re-auth this account (a usage read never refreshes it).`;
61
61
  }
62
+ /**
63
+ * Kimi-specific expired-credential wording. A normal Kimi launch refreshes its
64
+ * own OAuth access token, so the recovery action for an expired Kimi credential
65
+ * is to run Kimi once — not to re-auth through agents-cli (RUSH-3198).
66
+ */
67
+ export function usageExpiredKimiCredentialError() {
68
+ return `Kimi credential expired — run Kimi once to refresh it (a usage read never refreshes it).`;
69
+ }
62
70
  export function usageRejectedError(agent, status) {
63
71
  return status === 429
64
72
  ? `${agent} is rate-limiting the usage endpoint for this machine (HTTP 429).`
@@ -84,6 +92,31 @@ export function usageHeadlessScopeError(agent = 'Claude') {
84
92
  export function isUsageHeadlessScopeError(error) {
85
93
  return typeof error === 'string' && error.includes(USAGE_HEADLESS_SCOPE_MARKER);
86
94
  }
95
+ /**
96
+ * Canonical phrase for a Claude account the usage reader holds no usable
97
+ * credential for. Distinct from {@link USAGE_HEADLESS_SCOPE_MARKER}, which
98
+ * means a setup-token WAS read and the endpoint refused its scope.
99
+ */
100
+ export const USAGE_NO_USAGE_CREDENTIAL_MARKER = 'usage unavailable (no usage credential)';
101
+ /**
102
+ * Claude's own no-credential message. The shared
103
+ * {@link usageNoCredentialError} offers "sign in" as the remedy, which holds
104
+ * for Kimi/Droid/Cursor — their CLIs rotate a readable token on the next launch
105
+ * — and is false for Claude: the usage read deliberately never touches the
106
+ * interactive login (RUSH-1822), so an account that IS signed in reads as
107
+ * unreadable here and signing in again changes nothing. Naming only the second
108
+ * remedy would send the operator to `claude setup-token`, whose token then hits
109
+ * the `user:profile` scope gap (RUSH-2392) — the loop reported in #2987 — so
110
+ * this message states both constraints and that the account still runs.
111
+ */
112
+ export function usageNoClaudeUsageCredentialError() {
113
+ return (`Claude ${USAGE_NO_USAGE_CREDENTIAL_MARKER} — a usage read never uses your login ` +
114
+ '(RUSH-1822); a setup-token cannot read usage (RUSH-2392). The account still runs.');
115
+ }
116
+ /** True when an error string is the Claude no-usage-credential state (#2987). */
117
+ export function isUsageNoUsageCredentialError(error) {
118
+ return typeof error === 'string' && error.includes(USAGE_NO_USAGE_CREDENTIAL_MARKER);
119
+ }
87
120
  /**
88
121
  * Detect Anthropic's usage-endpoint scope denial: HTTP 403 whose body names
89
122
  * `user:profile` (or "scope requirement"). A bare 403 without that body stays
@@ -130,6 +163,35 @@ export function usageUnreachableError(agent, cause) {
130
163
  */
131
164
  export const USAGE_NO_RECENT_USAGE_MARKER = 'no usage recorded yet';
132
165
  export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
166
+ /**
167
+ * Sentinel `UsageInfo.error` for a read-only lookup whose cache held nothing
168
+ * (`getUsageInfoForIdentity`). No request was made and nothing failed — the
169
+ * daemon simply has not collected this account yet. It was an unclassified
170
+ * literal, so `classifyUsageErrorKind` fell through to `'rejected'` and
171
+ * `agents view` printed the generic "usage unavailable" for a cold cache,
172
+ * which reads as a failure the operator should chase (#2987). The string value
173
+ * is unchanged; callers that already compare against `'stale'` keep working.
174
+ */
175
+ export const USAGE_NOT_COLLECTED_MARKER = 'stale';
176
+ /**
177
+ * Human-facing form of a `UsageInfo.error` for a machine/JSON consumer
178
+ * (`agents view --json`'s `usageError`). Every error string this module
179
+ * constructs is already a full human sentence EXCEPT the internal
180
+ * {@link USAGE_NOT_COLLECTED_MARKER} (`'stale'`) sentinel, which a read-only
181
+ * lookup returns for a never-cached account when `--refresh` was not passed. That
182
+ * value is an internal cache signal, not an error message, and leaking it verbatim
183
+ * contradicts the field's "human-readable" contract (PHNX-3348). Map it to a
184
+ * plain-language, actionable string and pass every genuine error through
185
+ * unchanged. Returns `null` when there is no error.
186
+ */
187
+ export function usageErrorForDisplay(error) {
188
+ if (!error)
189
+ return null;
190
+ if (error === USAGE_NOT_COLLECTED_MARKER) {
191
+ return 'Usage not collected yet — run `agents view --refresh` to fetch it.';
192
+ }
193
+ return error;
194
+ }
133
195
  /**
134
196
  * Shared error-classification + 429 backoff for a networked usage fetch whose
135
197
  * only signal is an HTTP status (or none at all, on a network failure) —
@@ -155,8 +217,12 @@ export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHead
155
217
  export function classifyUsageErrorKind(error) {
156
218
  if (!error)
157
219
  return null;
220
+ if (error === USAGE_NOT_COLLECTED_MARKER)
221
+ return 'not-collected';
158
222
  if (isUsageHeadlessScopeError(error))
159
223
  return 'headless-scope';
224
+ if (isUsageNoUsageCredentialError(error))
225
+ return 'no-usage-credential';
160
226
  if (error.startsWith('No readable '))
161
227
  return 'no-credential';
162
228
  if (error.includes('credential expired'))
@@ -342,12 +408,13 @@ export async function getUsageInfoForIdentity(input, opts) {
342
408
  // cache file holds every account without collision.
343
409
  if (!usageKey) {
344
410
  if (readOnly)
345
- return { snapshot: null, error: 'stale' };
411
+ return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
346
412
  return getUsageInfo(input.agentId, {
347
413
  home: input.home,
348
414
  cliVersion: input.cliVersion,
349
415
  organizationId: input.info.organizationId,
350
416
  fileOnly: opts?.fileOnly,
417
+ allowInteractiveLogin: opts?.allowInteractiveLogin,
351
418
  });
352
419
  }
353
420
  const cached = readClaudeUsageCache(usageKey);
@@ -361,21 +428,24 @@ export async function getUsageInfoForIdentity(input, opts) {
361
428
  // it. A stale-or-absent snapshot is handled downstream by the router's own
362
429
  // freshness guard (`isUsageVerified` in rotate.ts), which routes around a
363
430
  // number it can't confirm rather than trusting an old one — so returning a
364
- // stale snapshot here is safe, and an absent one reports `'stale'`.
431
+ // stale snapshot here is safe, and an absent one reports
432
+ // {@link USAGE_NOT_COLLECTED_MARKER}.
365
433
  if (readOnly) {
366
434
  if (cached)
367
435
  return { snapshot: cached, error: null };
368
- return { snapshot: null, error: 'stale' };
436
+ return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
369
437
  }
370
438
  // Explicit refresh: block on the shared device collector.
371
- return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true);
439
+ return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true, {
440
+ allowInteractiveLogin: opts?.allowInteractiveLogin === true,
441
+ });
372
442
  }
373
443
  /**
374
444
  * Single-flight live usage fetch per usage key. Concurrent callers (view +
375
445
  * rotation, or two rows sharing an account) await the same promise rather than
376
446
  * opening duplicate HTTP requests that then time out and pile up.
377
447
  */
378
- async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
448
+ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly, opts) {
379
449
  const existing = inFlightLiveFetches.get(usageKey);
380
450
  if (existing)
381
451
  return existing;
@@ -398,6 +468,7 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
398
468
  // throttled account cannot park the whole provider (RUSH-3036).
399
469
  usageScope: usageKey,
400
470
  fileOnly,
471
+ allowInteractiveLogin: opts?.allowInteractiveLogin === true,
401
472
  });
402
473
  if (usage.snapshot) {
403
474
  if (!usage.snapshot.capturedAt || usage.snapshot.capturedAt.getTime() <= previousCapturedAt) {
@@ -460,15 +531,28 @@ function formatUsageErrorKindLabel(kind, detail) {
460
531
  switch (kind) {
461
532
  case 'no-credential':
462
533
  return 'sign in / provision token';
534
+ // Both of these are permanent for the account as configured, and both used
535
+ // to render as the generic bucket — which reads as a transient failure and
536
+ // sends operators back to `claude setup-token` for a remedy that cannot
537
+ // work (#2987). Name the state instead.
538
+ case 'no-usage-credential':
539
+ return USAGE_NO_USAGE_CREDENTIAL_MARKER;
540
+ case 'headless-scope':
541
+ return USAGE_HEADLESS_SCOPE_MARKER;
463
542
  case 'expired-credential':
543
+ // Kimi refreshes its own credential on a normal launch; the recovery hint
544
+ // is embedded in the error string so the label matches the exact action.
545
+ if (detail?.includes('run Kimi once'))
546
+ return 'run Kimi once';
464
547
  return 're-auth for usage';
548
+ case 'not-collected':
549
+ return 'usage pending';
465
550
  case 'rate-limited': {
466
551
  const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
467
552
  return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
468
553
  }
469
554
  case 'rejected':
470
555
  case 'unreachable':
471
- case 'headless-scope':
472
556
  case null:
473
557
  case undefined:
474
558
  default:
@@ -761,12 +845,24 @@ async function getClaudeUsageInfo(options) {
761
845
  // the interactive login (reading that ACL-bound token and firing it at the
762
846
  // usage API is what got it revoked — RUSH-1822). No setup-token => null =>
763
847
  // "usage pending". fileOnly additionally forbids the ACL keychain path.
848
+ //
849
+ // allowInteractiveLogin is the one sanctioned exception (USAGE-READ-1/2): a
850
+ // foreground human `agents view` on a `personal` device MAY fall through to
851
+ // the interactive login when no setup-token exists, because that login is the
852
+ // only credential carrying the `user:profile` scope the usage endpoint
853
+ // requires (the setup-token is user:inference → 403, RUSH-2392). It is unset
854
+ // for every background caller, so the RUSH-1822 guarantee is untouched there.
764
855
  const oauth = await loadClaudeOauth(options?.home, {
765
856
  accessTokenCache: true,
766
857
  fileOnly: options?.fileOnly === true,
858
+ allowInteractiveLogin: options?.allowInteractiveLogin === true,
767
859
  });
768
860
  if (!oauth?.accessToken) {
769
- return { snapshot: null, error: usageNoCredentialError('Claude') };
861
+ // NOT the shared no-credential message: "sign in" is not a remedy here.
862
+ // The account this reads for is usually signed in already — the reader is
863
+ // forbidden from touching that login (RUSH-1822) — so the shared wording
864
+ // asked the operator to redo the one thing they had already done (#2987).
865
+ return { snapshot: null, error: usageNoClaudeUsageCredentialError() };
770
866
  }
771
867
  const requestedOrgId = normalizeString(options?.organizationId);
772
868
  const liveOrgId = normalizeString(oauth.organizationUuid);
@@ -887,7 +983,7 @@ async function getKimiUsageInfo(options) {
887
983
  }
888
984
  const expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
889
985
  if (expiresAt !== null && Date.now() / 1000 >= expiresAt) {
890
- return { snapshot: null, error: usageExpiredCredentialError('Kimi') };
986
+ return { snapshot: null, error: usageExpiredKimiCredentialError() };
891
987
  }
892
988
  // Honour a live Retry-After rather than re-arming the penalty (see
893
989
  // usage-backoff.ts). No request at all while the window is open.
@@ -1450,7 +1546,7 @@ export async function loadClaudeOauth(home, opts) {
1450
1546
  // the source of truth if it has actually been revoked.
1451
1547
  return { accessToken: setupToken };
1452
1548
  }
1453
- // No provisioned setup-token: a read-only usage/health probe MUST NOT fall
1549
+ // No provisioned setup-token. A read-only usage/health probe MUST NOT fall
1454
1550
  // through to Claude Code's interactive login credential. The daemon's usage
1455
1551
  // (~60s) and auth-health (~3min) warms would otherwise read the ACL-bound
1456
1552
  // OAuth token and transmit it to api.anthropic.com/api/oauth/usage — an
@@ -1460,7 +1556,17 @@ export async function loadClaudeOauth(home, opts) {
1460
1556
  // (docs/secrets.md). Report unprovisioned (-> probe
1461
1557
  // token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
1462
1558
  // setup-token via the mint-auth path to restore usage/probe for the account.
1463
- return null;
1559
+ //
1560
+ // The single sanctioned exception (USAGE-READ-1/2): a foreground human
1561
+ // `agents view` on a `personal` device sets allowInteractiveLogin, and only
1562
+ // then do we fall through to the interactive-login read below — the one
1563
+ // credential carrying `user:profile`, which the usage endpoint requires. This
1564
+ // is a human running one command, not an unattended loop, so it is not the
1565
+ // revocation risk RUSH-1822 fixed. Every background caller leaves the flag
1566
+ // unset and still returns null here.
1567
+ if (opts?.allowInteractiveLogin !== true) {
1568
+ return null;
1569
+ }
1464
1570
  }
1465
1571
  // Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken)
1466
1572
  // legitimately read the interactive login to run/refresh Claude. Rush Cloud
@@ -376,8 +376,11 @@ export declare function resolveOpenCodeAccountId(base: string): string | undefin
376
376
  * this file is not authoritative, and probing the Keychain would raise an
377
377
  * authorization sheet per installed version on every `agents run` — the reason
378
378
  * rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
379
- * only store, so a token-less file is proof of signed-out. `platform` is a
380
- * parameter so both branches are testable on any host.
379
+ * only store, so a missing or token-less file is proof of signed-out — unless a
380
+ * Linux setup-token (`.claude/.oauth_token`) is present, which the shim exports
381
+ * as `CLAUDE_CODE_OAUTH_TOKEN` and can authenticate the run without
382
+ * `.credentials.json`. `platform` is a parameter so both branches are testable
383
+ * on any host.
381
384
  *
382
385
  * Sync, no Keychain, no network — safe on the `agents run` hot path.
383
386
  */
@@ -1897,14 +1897,29 @@ function museAuthEmail(value, depth = 0) {
1897
1897
  * this file is not authoritative, and probing the Keychain would raise an
1898
1898
  * authorization sheet per installed version on every `agents run` — the reason
1899
1899
  * rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
1900
- * only store, so a token-less file is proof of signed-out. `platform` is a
1901
- * parameter so both branches are testable on any host.
1900
+ * only store, so a missing or token-less file is proof of signed-out — unless a
1901
+ * Linux setup-token (`.claude/.oauth_token`) is present, which the shim exports
1902
+ * as `CLAUDE_CODE_OAUTH_TOKEN` and can authenticate the run without
1903
+ * `.credentials.json`. `platform` is a parameter so both branches are testable
1904
+ * on any host.
1902
1905
  *
1903
1906
  * Sync, no Keychain, no network — safe on the `agents run` hot path.
1904
1907
  */
1905
1908
  export function isClaudeCredentialFileBlank(base, platform = process.platform) {
1906
1909
  if (platform === 'darwin')
1907
1910
  return false;
1911
+ // A per-version setup-token is a real credential on Linux even when
1912
+ // `.credentials.json` was never written (the shim's `$CLAUDE_CONFIG_DIR/.oauth_token`
1913
+ // fallback). Treat it as signed-in so rotation does not skip a worker that
1914
+ // authenticates from an attached setup-token.
1915
+ try {
1916
+ const token = fs.readFileSync(path.join(base, '.claude', '.oauth_token'), 'utf-8').trim();
1917
+ if (token.length > 0)
1918
+ return false;
1919
+ }
1920
+ catch {
1921
+ /* absent — fall through to the credentials.json floor */
1922
+ }
1908
1923
  try {
1909
1924
  const raw = fs.readFileSync(path.join(base, '.claude', '.credentials.json'), 'utf-8');
1910
1925
  const oauth = JSON.parse(raw).claudeAiOauth;
@@ -1913,11 +1928,14 @@ export function isClaudeCredentialFileBlank(base, platform = process.platform) {
1913
1928
  const nonEmpty = (v) => typeof v === 'string' && v.trim().length > 0;
1914
1929
  return !nonEmpty(oauth.accessToken) && !nonEmpty(oauth.refreshToken);
1915
1930
  }
1916
- catch {
1917
- // No file (a Keychain-backed home, or never logged in here) or an
1918
- // unreadable/corrupt one: not positive evidence of a blank credential, so
1919
- // leave the existing signal alone rather than declaring a working install
1920
- // signed out.
1931
+ catch (err) {
1932
+ // Off macOS the file IS the store. Missing it means this home cannot
1933
+ // authenticate (a newly installed default with leftover `.claude.json`
1934
+ // oauthAccount is the PHNX-2685 false-healthy case). A corrupt file is
1935
+ // not positive evidence of a blank credential — leave the existing
1936
+ // signal alone rather than declaring a working install signed out.
1937
+ if (err.code === 'ENOENT')
1938
+ return true;
1921
1939
  return false;
1922
1940
  }
1923
1941
  }
@@ -73,7 +73,10 @@ export function registerMixCommands(parent) {
73
73
  .option('--days <n>', 'Days of history to include', '7')
74
74
  .option('--json', 'Emit JSON instead of tables')
75
75
  .action(function summary() {
76
- const o = this.opts();
76
+ // optsWithGlobals(): --json collides by name with the `insights` parent, so
77
+ // commander binds it to the parent and this.opts() never sees it. Merging
78
+ // ancestor opts is what the per-recipe leaves below already do.
79
+ const o = this.optsWithGlobals();
77
80
  renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
78
81
  });
79
82
  setHelpSections(mix, {
@@ -104,7 +107,8 @@ export function registerMixCommands(parent) {
104
107
  parent.command('recipes')
105
108
  .description('List baked mix-recipe ids')
106
109
  .option('--json', 'Emit JSON')
107
- .action((o) => {
110
+ .action(function recipes() {
111
+ const o = this.optsWithGlobals();
108
112
  const list = listRecipes();
109
113
  if (o.json) {
110
114
  console.log(JSON.stringify(list, null, 2));
@@ -122,7 +126,8 @@ export function registerMixCommands(parent) {
122
126
  .option('--days <n>', 'Days of history', '7')
123
127
  .option('--limit <n>', 'Max rows', '40')
124
128
  .option('--json', 'Emit JSON')
125
- .action((o) => {
129
+ .action(function query() {
130
+ const o = this.optsWithGlobals();
126
131
  const win = analyticsWindow(parseMixDays(o.days));
127
132
  const kind = o.kind && USAGE_KINDS.includes(o.kind)
128
133
  ? o.kind
@@ -165,8 +170,9 @@ export function registerMixCommands(parent) {
165
170
  .option('--days <n>', 'Days of history', '7')
166
171
  .option('--json', 'Emit JSON')
167
172
  .action(function recipeAction() {
168
- const parentOpts = this.parent?.opts?.();
169
- const o = { ...parentOpts, ...this.opts() };
173
+ // optsWithGlobals() merges the `insights` parent opts, so the name-colliding
174
+ // --json/--since reach this leaf (see the sibling commands above).
175
+ const o = this.optsWithGlobals();
170
176
  const win = analyticsWindow(parseMixDays(o.days));
171
177
  const section = runRecipe(id, win);
172
178
  if (o.json) {
@@ -187,7 +193,7 @@ export function registerMixCommands(parent) {
187
193
  .option('--days <n>', 'Days of history to include', '7')
188
194
  .option('--json', 'Emit JSON instead of tables')
189
195
  .action(function summary() {
190
- const o = this.opts();
196
+ const o = this.optsWithGlobals();
191
197
  renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
192
198
  });
193
199
  setHelpSections(trends, {
@@ -0,0 +1,150 @@
1
+ /**
2
+ * First-class setup-token mint + seed (PHNX-2364).
3
+ *
4
+ * Closes the mint-auth manual recipe: drive `claude setup-token` through the
5
+ * same injectable PTY driver `agents fleet login` uses, capture a well-formed
6
+ * `sk-ant-oat01-…` token (the #1767 ANSI-banner guard), and seed BOTH:
7
+ *
8
+ * 1. a named provider account (`agents accounts add` shape, policy never)
9
+ * 2. the reserved FILE-BASED `auth` bundle keyed per-account email, which
10
+ * usage/probe reads (`resolveClaudeSetupToken`)
11
+ *
12
+ * Native rotating OAuth is never copied. Only this non-rotating class is
13
+ * stored and optionally synced. Interactive mint is Claude-only; every other
14
+ * harness fails loud with the command that actually provisions it.
15
+ */
16
+ import type { AgentId } from './types.js';
17
+ import { type CredentialAccount } from './account-registry.js';
18
+ import { type DriveOptions, type PtyDriver } from './fleet/remote-login.js';
19
+ /** Well-formed Claude setup-token as it appears inside a TTY blob. */
20
+ export declare const CLAUDE_SETUP_TOKEN_CAPTURE_RE: RegExp;
21
+ export interface MintFlow {
22
+ harness: AgentId;
23
+ provider: string;
24
+ auth: 'setup-token';
25
+ /** Interactive mint argv after HOME=… <bin>. Null when stdin-seed only. */
26
+ mintArgs: string[] | null;
27
+ verificationUrlRegex: RegExp;
28
+ tokenCapture: RegExp;
29
+ }
30
+ /**
31
+ * Harnesses that expose an interactive setup-token mint. Native device-code
32
+ * login (codex/droid/kimi/grok) stays on `agents fleet login`; API keys stay
33
+ * on `agents accounts add`. Adding a harness here without a real mint command
34
+ * is a lying table — do not.
35
+ */
36
+ export declare const MINT_FLOWS: Record<string, MintFlow>;
37
+ export declare function listMintableHarnesses(): AgentId[];
38
+ export declare function getMintFlow(harnessRaw: string): MintFlow;
39
+ export declare function unmintableMessage(harness: string): string;
40
+ /** Strip CSI / Fe ANSI so a #1767 TTY blob can be scanned for a real token. */
41
+ export declare function stripAnsi(text: string): string;
42
+ /**
43
+ * Pull a single well-formed Claude setup-token out of a (possibly ANSI-wrapped)
44
+ * screen. Returns null when none is present. Two distinct tokens fail loud —
45
+ * guessing which one to seed is how a banner fragment becomes an auth header.
46
+ */
47
+ export declare function extractClaudeSetupToken(screen: string): string | null;
48
+ /** First https URL on the screen, trailing punctuation stripped. */
49
+ export declare function extractMintUrl(screen: string, flow: MintFlow): string | undefined;
50
+ export declare function isEmail(value: string): boolean;
51
+ /** Account-name slug of an email (`ada@example.com` → `ada-at-example.com`). */
52
+ export declare function accountNameFromEmail(email: string): string;
53
+ export declare function assertValidSetupToken(token: string): string;
54
+ export interface ResolveMintIdentityInput {
55
+ account?: string;
56
+ email?: string;
57
+ home?: string;
58
+ }
59
+ export interface ResolvedMintIdentity {
60
+ accountName: string;
61
+ email: string;
62
+ }
63
+ /**
64
+ * Resolve the named account + the email that keys the reserved `auth` bundle.
65
+ * `--account` that looks like an email is the email; a name needs `--email` or
66
+ * a locally signed-in `.claude.json`. Missing email fails loud — we must not
67
+ * fall back to a bare shared key (that is the multi-account mix-up).
68
+ */
69
+ export declare function resolveMintIdentity(input: ResolveMintIdentityInput): ResolvedMintIdentity;
70
+ /**
71
+ * Write (or rotate) the reserved FILE-BASED `auth` bundle's per-account key.
72
+ * Usage/probe ignores a keychain- or vault-backed bundle of this name, so a
73
+ * wrong backend fails loud instead of looking like a successful mint.
74
+ */
75
+ export declare function seedReservedAuthToken(email: string, token: string): {
76
+ key: string;
77
+ };
78
+ /**
79
+ * Create or rotate the named provider account that `agents run --account` and
80
+ * `agents accounts sync` consume. Existing account of a different kind fails
81
+ * loud rather than silently overwriting an API key with a setup-token.
82
+ */
83
+ export declare function seedNamedAccount(name: string, token: string, flow: MintFlow): CredentialAccount;
84
+ export interface MintDriveHooks {
85
+ driver?: PtyDriver;
86
+ openUrl?: (url: string) => Promise<void>;
87
+ /** Asked once the authorize URL is on screen, when `--code` was not given. */
88
+ readCode?: () => Promise<string | undefined>;
89
+ drive?: DriveOptions;
90
+ }
91
+ export interface DriveMintResult {
92
+ token: string;
93
+ url?: string;
94
+ sessionId: string;
95
+ }
96
+ export interface DriveSetupTokenMintOpts extends MintDriveHooks {
97
+ code?: string;
98
+ /** Suppress stdout progress so `--json` callers get a parseable blob. */
99
+ json?: boolean;
100
+ }
101
+ /**
102
+ * Drive `claude setup-token` in a PTY: scrape the authorize URL, open it,
103
+ * optionally paste `--code`, then capture the token with the #1767 guard.
104
+ * Tears the session down on the way out (success, timeout, or throw).
105
+ */
106
+ export declare function driveSetupTokenMint(command: string, flow: MintFlow, opts?: DriveSetupTokenMintOpts): Promise<DriveMintResult>;
107
+ export declare function buildMintCommand(flow: MintFlow, bin: string, home: string): string;
108
+ export declare function resolveMintInstallation(harness: AgentId): {
109
+ version: string;
110
+ bin: string;
111
+ home: string;
112
+ };
113
+ export interface MintAndSeedInput {
114
+ harness: string;
115
+ account?: string;
116
+ email?: string;
117
+ token?: string;
118
+ code?: string;
119
+ open?: boolean;
120
+ fleet?: boolean;
121
+ devices?: string[];
122
+ /** Suppress progress prints so `--json` stdout stays machine-parseable. */
123
+ json?: boolean;
124
+ hooks?: MintDriveHooks;
125
+ }
126
+ export interface FleetSyncRow {
127
+ device: string;
128
+ ok: boolean;
129
+ message: string;
130
+ }
131
+ export interface MintAndSeedResult {
132
+ harness: AgentId;
133
+ account: string;
134
+ email: string;
135
+ authBundleKey: string;
136
+ rotated: boolean;
137
+ fleet: FleetSyncRow[];
138
+ }
139
+ /**
140
+ * End-to-end mint: resolve identity, obtain a token (stdin or PTY drive),
141
+ * seed the named account + reserved auth bundle, optionally sync the fleet.
142
+ * Never returns or logs the token.
143
+ */
144
+ export declare function mintAndSeed(input: MintAndSeedInput): Promise<MintAndSeedResult>;
145
+ export declare function resolveSyncTargets(fleet: boolean, devices: string[]): Promise<string[]>;
146
+ /** True when a Claude setup-token is already seeded on this box (setup status). */
147
+ export declare function hasMintedSetupToken(): {
148
+ ready: boolean;
149
+ detail: string;
150
+ };