@phnx-labs/agents-cli 1.22.52 → 1.22.54

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 (180) hide show
  1. package/CHANGELOG.md +336 -0
  2. package/README.md +42 -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 +220 -174
  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 +26 -17
  16. package/dist/commands/fleet-capture.js +7 -0
  17. package/dist/commands/focus.d.ts +1 -0
  18. package/dist/commands/focus.js +4 -2
  19. package/dist/commands/go.d.ts +5 -4
  20. package/dist/commands/go.js +8 -7
  21. package/dist/commands/insights.js +9 -0
  22. package/dist/commands/monitors.js +85 -30
  23. package/dist/commands/output.js +8 -2
  24. package/dist/commands/repo.js +18 -0
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions-inject.js +8 -3
  27. package/dist/commands/sessions-picker.js +2 -1
  28. package/dist/commands/sessions.d.ts +20 -12
  29. package/dist/commands/sessions.js +94 -40
  30. package/dist/commands/setup-accounts.d.ts +8 -0
  31. package/dist/commands/setup-accounts.js +47 -0
  32. package/dist/commands/setup.d.ts +1 -1
  33. package/dist/commands/setup.js +11 -2
  34. package/dist/commands/share.d.ts +52 -3
  35. package/dist/commands/share.js +262 -18
  36. package/dist/commands/ssh.d.ts +7 -0
  37. package/dist/commands/ssh.js +53 -14
  38. package/dist/commands/status.js +14 -0
  39. package/dist/commands/sync.js +44 -0
  40. package/dist/commands/view.d.ts +3 -1
  41. package/dist/commands/view.js +5 -4
  42. package/dist/lib/account-registry.d.ts +15 -5
  43. package/dist/lib/account-registry.js +165 -53
  44. package/dist/lib/accounting/rotate.d.ts +20 -6
  45. package/dist/lib/accounting/rotate.js +38 -7
  46. package/dist/lib/accounting/usage.d.ts +37 -1
  47. package/dist/lib/accounting/usage.js +71 -6
  48. package/dist/lib/agent-spec/agents.d.ts +5 -2
  49. package/dist/lib/agent-spec/agents.js +25 -7
  50. package/dist/lib/analytics/mix-commands.js +12 -6
  51. package/dist/lib/answer-router.js +2 -1
  52. package/dist/lib/auth-mint.d.ts +150 -0
  53. package/dist/lib/auth-mint.js +434 -0
  54. package/dist/lib/browser/profiles.d.ts +18 -0
  55. package/dist/lib/browser/profiles.js +26 -1
  56. package/dist/lib/browser/registry.d.ts +44 -14
  57. package/dist/lib/browser/registry.js +141 -45
  58. package/dist/lib/browser/remote-control.d.ts +9 -7
  59. package/dist/lib/browser/remote-control.js +9 -7
  60. package/dist/lib/claude-account-token.d.ts +10 -0
  61. package/dist/lib/claude-account-token.js +14 -4
  62. package/dist/lib/config-drift.d.ts +37 -0
  63. package/dist/lib/config-drift.js +72 -0
  64. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  65. package/dist/lib/daemon/auth-sync-service.js +34 -0
  66. package/dist/lib/daemon/daemon.js +30 -4
  67. package/dist/lib/daemon/runner.js +10 -2
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/device-config.d.ts +3 -3
  71. package/dist/lib/device-config.js +8 -7
  72. package/dist/lib/devices/config-migration.js +147 -1
  73. package/dist/lib/devices/connect.d.ts +26 -0
  74. package/dist/lib/devices/connect.js +48 -1
  75. package/dist/lib/devices/device-docs.d.ts +35 -0
  76. package/dist/lib/devices/device-docs.js +163 -0
  77. package/dist/lib/devices/discovery-policy.d.ts +14 -2
  78. package/dist/lib/devices/discovery-policy.js +31 -21
  79. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  80. package/dist/lib/devices/doctor-findings.js +19 -1
  81. package/dist/lib/devices/registry.d.ts +11 -5
  82. package/dist/lib/devices/registry.js +46 -18
  83. package/dist/lib/exec.d.ts +88 -30
  84. package/dist/lib/exec.js +138 -34
  85. package/dist/lib/feed/feed.d.ts +10 -2
  86. package/dist/lib/feed/feed.js +35 -2
  87. package/dist/lib/feed-broadcast.js +1 -1
  88. package/dist/lib/fleet/apply.d.ts +11 -0
  89. package/dist/lib/fleet/apply.js +23 -3
  90. package/dist/lib/fleet/auth-sync.js +5 -3
  91. package/dist/lib/help.d.ts +9 -0
  92. package/dist/lib/help.js +29 -1
  93. package/dist/lib/hosts/dispatch.d.ts +4 -3
  94. package/dist/lib/hosts/dispatch.js +12 -8
  95. package/dist/lib/hosts/passthrough.d.ts +1 -10
  96. package/dist/lib/hosts/passthrough.js +1 -13
  97. package/dist/lib/hosts/providers/local.d.ts +9 -3
  98. package/dist/lib/hosts/providers/local.js +23 -12
  99. package/dist/lib/hosts/reconnect.d.ts +7 -4
  100. package/dist/lib/hosts/reconnect.js +29 -25
  101. package/dist/lib/hosts/registry.js +4 -1
  102. package/dist/lib/hosts/remote-os.js +3 -1
  103. package/dist/lib/installations/versions.js +9 -1
  104. package/dist/lib/linux-userns.d.ts +58 -0
  105. package/dist/lib/linux-userns.js +116 -0
  106. package/dist/lib/memory.d.ts +26 -0
  107. package/dist/lib/memory.js +80 -1
  108. package/dist/lib/monitors/config.d.ts +11 -0
  109. package/dist/lib/monitors/config.js +8 -0
  110. package/dist/lib/monitors/engine.js +8 -1
  111. package/dist/lib/monitors/state.d.ts +37 -1
  112. package/dist/lib/monitors/state.js +79 -4
  113. package/dist/lib/permissions-registry.d.ts +2 -0
  114. package/dist/lib/permissions-registry.js +116 -14
  115. package/dist/lib/permissions.d.ts +5 -3
  116. package/dist/lib/permissions.js +25 -27
  117. package/dist/lib/profiles.d.ts +8 -7
  118. package/dist/lib/profiles.js +12 -0
  119. package/dist/lib/project-key.d.ts +9 -0
  120. package/dist/lib/project-key.js +11 -0
  121. package/dist/lib/secrets/bundles.d.ts +35 -0
  122. package/dist/lib/secrets/bundles.js +78 -1
  123. package/dist/lib/secrets/push.d.ts +3 -8
  124. package/dist/lib/secrets/push.js +18 -14
  125. package/dist/lib/secrets/remote.d.ts +9 -18
  126. package/dist/lib/secrets/remote.js +11 -26
  127. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  128. package/dist/lib/secrets/reserved-sync.js +129 -0
  129. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  130. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  131. package/dist/lib/self-heal/registry.js +4 -0
  132. package/dist/lib/self-heal/types.d.ts +1 -1
  133. package/dist/lib/session/active.d.ts +10 -1
  134. package/dist/lib/session/active.js +8 -5
  135. package/dist/lib/session/actor-sidecar.d.ts +7 -0
  136. package/dist/lib/session/actor-sidecar.js +2 -0
  137. package/dist/lib/session/db.d.ts +39 -4
  138. package/dist/lib/session/db.js +168 -31
  139. package/dist/lib/session/discover.d.ts +32 -4
  140. package/dist/lib/session/discover.js +126 -37
  141. package/dist/lib/session/insights.d.ts +14 -0
  142. package/dist/lib/session/insights.js +25 -2
  143. package/dist/lib/session/linear.js +1 -1
  144. package/dist/lib/session/live-metadata.js +1 -0
  145. package/dist/lib/session/pid-registry.d.ts +7 -0
  146. package/dist/lib/session/prompt.d.ts +15 -0
  147. package/dist/lib/session/prompt.js +21 -0
  148. package/dist/lib/session/shell-programs.d.ts +17 -0
  149. package/dist/lib/session/shell-programs.js +21 -0
  150. package/dist/lib/session/state.js +2 -1
  151. package/dist/lib/session/stream-render.js +2 -1
  152. package/dist/lib/session/tool-calls.js +2 -5
  153. package/dist/lib/session/trajectory-html.js +2 -1
  154. package/dist/lib/session/trajectory.js +3 -12
  155. package/dist/lib/session/types.d.ts +25 -0
  156. package/dist/lib/session/types.js +10 -0
  157. package/dist/lib/share/publish.d.ts +53 -5
  158. package/dist/lib/share/publish.js +99 -17
  159. package/dist/lib/share/worker-template.js +594 -64
  160. package/dist/lib/startup/root-command.js +2 -1
  161. package/dist/lib/state.d.ts +24 -0
  162. package/dist/lib/state.js +318 -54
  163. package/dist/lib/sync-status.d.ts +4 -0
  164. package/dist/lib/sync-status.js +3 -0
  165. package/dist/lib/terminal/resolve.d.ts +7 -0
  166. package/dist/lib/terminal/resolve.js +41 -2
  167. package/dist/lib/traces/classify.js +24 -19
  168. package/dist/lib/traces/insights.d.ts +67 -0
  169. package/dist/lib/traces/insights.js +178 -0
  170. package/dist/lib/traces/phenotype.d.ts +67 -0
  171. package/dist/lib/traces/phenotype.js +437 -0
  172. package/dist/lib/traces/segments.d.ts +133 -0
  173. package/dist/lib/traces/segments.js +301 -0
  174. package/dist/lib/traces/sync.d.ts +33 -0
  175. package/dist/lib/traces/sync.js +11 -2
  176. package/dist/lib/types.d.ts +47 -1
  177. package/dist/lib/usage-refresh.js +2 -1
  178. package/dist/lib/view-types.d.ts +2 -0
  179. package/dist/lib/watchdog/runner.js +18 -4
  180. package/package.json +2 -1
@@ -22,6 +22,12 @@ import type { AgentId } from '../types.js';
22
22
  */
23
23
  export declare function usageNoCredentialError(agent: string): string;
24
24
  export declare function usageExpiredCredentialError(agent: string): string;
25
+ /**
26
+ * Kimi-specific expired-credential wording. A normal Kimi launch refreshes its
27
+ * own OAuth access token, so the recovery action for an expired Kimi credential
28
+ * is to run Kimi once — not to re-auth through agents-cli (RUSH-3198).
29
+ */
30
+ export declare function usageExpiredKimiCredentialError(): string;
25
31
  export declare function usageRejectedError(agent: string, status: number): string;
26
32
  /**
27
33
  * Canonical phrase for the Anthropic setup-token scope gap (RUSH-2392).
@@ -39,6 +45,26 @@ export declare const USAGE_HEADLESS_SCOPE_MARKER = "usage unavailable (headless)
39
45
  export declare function usageHeadlessScopeError(agent?: string): string;
40
46
  /** True when an error string is the setup-token scope gap (RUSH-2392). */
41
47
  export declare function isUsageHeadlessScopeError(error: string | null | undefined): boolean;
48
+ /**
49
+ * Canonical phrase for a Claude account the usage reader holds no usable
50
+ * credential for. Distinct from {@link USAGE_HEADLESS_SCOPE_MARKER}, which
51
+ * means a setup-token WAS read and the endpoint refused its scope.
52
+ */
53
+ export declare const USAGE_NO_USAGE_CREDENTIAL_MARKER = "usage unavailable (no usage credential)";
54
+ /**
55
+ * Claude's own no-credential message. The shared
56
+ * {@link usageNoCredentialError} offers "sign in" as the remedy, which holds
57
+ * for Kimi/Droid/Cursor — their CLIs rotate a readable token on the next launch
58
+ * — and is false for Claude: the usage read deliberately never touches the
59
+ * interactive login (RUSH-1822), so an account that IS signed in reads as
60
+ * unreadable here and signing in again changes nothing. Naming only the second
61
+ * remedy would send the operator to `claude setup-token`, whose token then hits
62
+ * the `user:profile` scope gap (RUSH-2392) — the loop reported in #2987 — so
63
+ * this message states both constraints and that the account still runs.
64
+ */
65
+ export declare function usageNoClaudeUsageCredentialError(): string;
66
+ /** True when an error string is the Claude no-usage-credential state (#2987). */
67
+ export declare function isUsageNoUsageCredentialError(error: string | null | undefined): boolean;
42
68
  /**
43
69
  * Detect Anthropic's usage-endpoint scope denial: HTTP 403 whose body names
44
70
  * `user:profile` (or "scope requirement"). A bare 403 without that body stays
@@ -72,6 +98,16 @@ export declare function usageUnreachableError(agent: string, cause?: unknown): s
72
98
  export declare const USAGE_NO_RECENT_USAGE_MARKER = "no usage recorded yet";
73
99
  export declare const USAGE_BENIGN_STATE: unique symbol;
74
100
  export type UsageBenignState = 'no-recent-usage';
101
+ /**
102
+ * Sentinel `UsageInfo.error` for a read-only lookup whose cache held nothing
103
+ * (`getUsageInfoForIdentity`). No request was made and nothing failed — the
104
+ * daemon simply has not collected this account yet. It was an unclassified
105
+ * literal, so `classifyUsageErrorKind` fell through to `'rejected'` and
106
+ * `agents view` printed the generic "usage unavailable" for a cold cache,
107
+ * which reads as a failure the operator should chase (#2987). The string value
108
+ * is unchanged; callers that already compare against `'stale'` keep working.
109
+ */
110
+ export declare const USAGE_NOT_COLLECTED_MARKER = "stale";
75
111
  /**
76
112
  * Shared error-classification + 429 backoff for a networked usage fetch whose
77
113
  * only signal is an HTTP status (or none at all, on a network failure) —
@@ -91,7 +127,7 @@ export declare function classifyUsageFetchFailure(agent: string, agentId: 'antig
91
127
  * distinct causes (RUSH-3040). Matched against the canonical strings this file
92
128
  * constructs — never re-derive these prefixes at a call site.
93
129
  */
94
- export type UsageErrorKind = 'no-credential' | 'expired-credential' | 'rate-limited' | 'rejected' | 'headless-scope' | 'unreachable';
130
+ export type UsageErrorKind = 'no-credential' | 'no-usage-credential' | 'expired-credential' | 'rate-limited' | 'rejected' | 'headless-scope' | 'unreachable' | 'not-collected';
95
131
  /** Classify a `UsageInfo.error` string into its {@link UsageErrorKind}, or null when there is no error. */
96
132
  export declare function classifyUsageErrorKind(error: string | null | undefined): UsageErrorKind | null;
97
133
  /**
@@ -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,16 @@ 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';
133
176
  /**
134
177
  * Shared error-classification + 429 backoff for a networked usage fetch whose
135
178
  * only signal is an HTTP status (or none at all, on a network failure) —
@@ -155,8 +198,12 @@ export function classifyUsageFetchFailure(agent, agentId, status, retryAfterHead
155
198
  export function classifyUsageErrorKind(error) {
156
199
  if (!error)
157
200
  return null;
201
+ if (error === USAGE_NOT_COLLECTED_MARKER)
202
+ return 'not-collected';
158
203
  if (isUsageHeadlessScopeError(error))
159
204
  return 'headless-scope';
205
+ if (isUsageNoUsageCredentialError(error))
206
+ return 'no-usage-credential';
160
207
  if (error.startsWith('No readable '))
161
208
  return 'no-credential';
162
209
  if (error.includes('credential expired'))
@@ -342,7 +389,7 @@ export async function getUsageInfoForIdentity(input, opts) {
342
389
  // cache file holds every account without collision.
343
390
  if (!usageKey) {
344
391
  if (readOnly)
345
- return { snapshot: null, error: 'stale' };
392
+ return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
346
393
  return getUsageInfo(input.agentId, {
347
394
  home: input.home,
348
395
  cliVersion: input.cliVersion,
@@ -361,11 +408,12 @@ export async function getUsageInfoForIdentity(input, opts) {
361
408
  // it. A stale-or-absent snapshot is handled downstream by the router's own
362
409
  // freshness guard (`isUsageVerified` in rotate.ts), which routes around a
363
410
  // 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'`.
411
+ // stale snapshot here is safe, and an absent one reports
412
+ // {@link USAGE_NOT_COLLECTED_MARKER}.
365
413
  if (readOnly) {
366
414
  if (cached)
367
415
  return { snapshot: cached, error: null };
368
- return { snapshot: null, error: 'stale' };
416
+ return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
369
417
  }
370
418
  // Explicit refresh: block on the shared device collector.
371
419
  return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true);
@@ -460,15 +508,28 @@ function formatUsageErrorKindLabel(kind, detail) {
460
508
  switch (kind) {
461
509
  case 'no-credential':
462
510
  return 'sign in / provision token';
511
+ // Both of these are permanent for the account as configured, and both used
512
+ // to render as the generic bucket — which reads as a transient failure and
513
+ // sends operators back to `claude setup-token` for a remedy that cannot
514
+ // work (#2987). Name the state instead.
515
+ case 'no-usage-credential':
516
+ return USAGE_NO_USAGE_CREDENTIAL_MARKER;
517
+ case 'headless-scope':
518
+ return USAGE_HEADLESS_SCOPE_MARKER;
463
519
  case 'expired-credential':
520
+ // Kimi refreshes its own credential on a normal launch; the recovery hint
521
+ // is embedded in the error string so the label matches the exact action.
522
+ if (detail?.includes('run Kimi once'))
523
+ return 'run Kimi once';
464
524
  return 're-auth for usage';
525
+ case 'not-collected':
526
+ return 'usage pending';
465
527
  case 'rate-limited': {
466
528
  const retryHint = detail?.match(/not retrying for (.+)\.$/)?.[1] ?? null;
467
529
  return retryHint ? `rate-limited (retry ~${retryHint})` : 'rate-limited';
468
530
  }
469
531
  case 'rejected':
470
532
  case 'unreachable':
471
- case 'headless-scope':
472
533
  case null:
473
534
  case undefined:
474
535
  default:
@@ -766,7 +827,11 @@ async function getClaudeUsageInfo(options) {
766
827
  fileOnly: options?.fileOnly === true,
767
828
  });
768
829
  if (!oauth?.accessToken) {
769
- return { snapshot: null, error: usageNoCredentialError('Claude') };
830
+ // NOT the shared no-credential message: "sign in" is not a remedy here.
831
+ // The account this reads for is usually signed in already — the reader is
832
+ // forbidden from touching that login (RUSH-1822) — so the shared wording
833
+ // asked the operator to redo the one thing they had already done (#2987).
834
+ return { snapshot: null, error: usageNoClaudeUsageCredentialError() };
770
835
  }
771
836
  const requestedOrgId = normalizeString(options?.organizationId);
772
837
  const liveOrgId = normalizeString(oauth.organizationUuid);
@@ -887,7 +952,7 @@ async function getKimiUsageInfo(options) {
887
952
  }
888
953
  const expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
889
954
  if (expiresAt !== null && Date.now() / 1000 >= expiresAt) {
890
- return { snapshot: null, error: usageExpiredCredentialError('Kimi') };
955
+ return { snapshot: null, error: usageExpiredKimiCredentialError() };
891
956
  }
892
957
  // Honour a live Retry-After rather than re-arming the penalty (see
893
958
  // usage-backoff.ts). No request at all while the window is open.
@@ -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, {
@@ -1,3 +1,4 @@
1
+ import { addressabilityRecoveryHint } from './terminal/resolve.js';
1
2
  import { injectTargetFromReplyRail } from './session/inject.js';
2
3
  /**
3
4
  * Match free-text answer against question options.
@@ -123,7 +124,7 @@ export function resolveAnswerRoute(input) {
123
124
  return {
124
125
  kind: 'refuse',
125
126
  reason: 'Agent is parked on a question but has no addressable terminal (no tmux/iterm/pty rail). ' +
126
- 'Open its terminal and answer there, or run `agents sessions resume <id>` first.',
127
+ addressabilityRecoveryHint(session),
127
128
  };
128
129
  }
129
130
  // Open block but agent is still looping between tool calls — mailbox is correct.
@@ -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
+ };