@phnx-labs/agents-cli 1.22.116 → 1.22.118

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 (254) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.d.ts +18 -0
  9. package/dist/commands/exec.js +104 -12
  10. package/dist/commands/repo.js +13 -49
  11. package/dist/commands/route.js +5 -5
  12. package/dist/commands/routines.js +39 -12
  13. package/dist/commands/sessions-render.d.ts +1 -1
  14. package/dist/commands/sessions-render.js +0 -1
  15. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  16. package/dist/commands/sessions.test-fixture.js +0 -37
  17. package/dist/commands/setup-browser.d.ts +17 -12
  18. package/dist/commands/setup-browser.js +52 -87
  19. package/dist/commands/setup-preferences.d.ts +0 -17
  20. package/dist/commands/setup-preferences.js +18 -49
  21. package/dist/commands/setup-term.d.ts +4 -3
  22. package/dist/commands/setup-term.js +5 -4
  23. package/dist/commands/setup.js +11 -11
  24. package/dist/commands/ssh.js +10 -1
  25. package/dist/commands/sync.js +6 -44
  26. package/dist/commands/teams.js +0 -1
  27. package/dist/commands/update.d.ts +19 -0
  28. package/dist/commands/update.js +34 -6
  29. package/dist/commands/view.d.ts +12 -1
  30. package/dist/commands/view.js +47 -2
  31. package/dist/index.js +107 -26
  32. package/dist/lib/account-capabilities.js +0 -2
  33. package/dist/lib/account-catalog.d.ts +38 -0
  34. package/dist/lib/account-catalog.js +110 -26
  35. package/dist/lib/account-provider-registry.js +1 -1
  36. package/dist/lib/account-registry.d.ts +1 -1
  37. package/dist/lib/account-registry.js +36 -2
  38. package/dist/lib/accounting/rotate.js +22 -8
  39. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  40. package/dist/lib/accounting/usage-ingest.js +115 -45
  41. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  42. package/dist/lib/accounting/usage-sync.js +227 -66
  43. package/dist/lib/accounting/usage.d.ts +16 -3
  44. package/dist/lib/accounting/usage.js +26 -8
  45. package/dist/lib/accounts/slots.d.ts +8 -0
  46. package/dist/lib/accounts/slots.js +17 -0
  47. package/dist/lib/add-dir.js +0 -1
  48. package/dist/lib/agent-cli-commands.js +0 -1
  49. package/dist/lib/agent-spec/agents.d.ts +6 -7
  50. package/dist/lib/agent-spec/agents.js +6 -60
  51. package/dist/lib/auth-health.d.ts +69 -1
  52. package/dist/lib/auth-health.js +133 -4
  53. package/dist/lib/browser/context.d.ts +82 -0
  54. package/dist/lib/browser/context.js +88 -0
  55. package/dist/lib/browser/paths.d.ts +25 -0
  56. package/dist/lib/browser/paths.js +51 -0
  57. package/dist/lib/browser/record.d.ts +38 -0
  58. package/dist/lib/browser/record.js +57 -0
  59. package/dist/lib/browser/sessions-list.js +1 -2
  60. package/dist/lib/browser-client.d.ts +158 -0
  61. package/dist/lib/browser-client.js +234 -0
  62. package/dist/lib/channels/owner-sink.d.ts +1 -18
  63. package/dist/lib/channels/owner-sink.js +29 -82
  64. package/dist/lib/channels/providers/rush.d.ts +7 -7
  65. package/dist/lib/channels/providers/rush.js +127 -57
  66. package/dist/lib/claude-statusline.d.ts +1 -1
  67. package/dist/lib/claude-statusline.js +15 -3
  68. package/dist/lib/commands.js +1 -1
  69. package/dist/lib/crabbox/lease.js +1 -1
  70. package/dist/lib/crabbox/runtimes.js +1 -3
  71. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  72. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  73. package/dist/lib/daemon/auth-sync-service.js +37 -43
  74. package/dist/lib/daemon/daemon.d.ts +25 -14
  75. package/dist/lib/daemon/daemon.js +92 -154
  76. package/dist/lib/daemon/runner.d.ts +2 -2
  77. package/dist/lib/daemon/runner.js +32 -9
  78. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  79. package/dist/lib/daemon/self-update-service.js +40 -36
  80. package/dist/lib/daemon/service.d.ts +12 -8
  81. package/dist/lib/daemon/supervisor.d.ts +65 -54
  82. package/dist/lib/daemon/supervisor.js +136 -171
  83. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  84. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  85. package/dist/lib/daemon/usage-sync-service.js +46 -78
  86. package/dist/lib/daemon-health.d.ts +22 -5
  87. package/dist/lib/daemon-health.js +47 -3
  88. package/dist/lib/daemon-services.d.ts +1 -1
  89. package/dist/lib/daemon-services.js +0 -10
  90. package/dist/lib/daemon-ticks.js +4 -2
  91. package/dist/lib/devices/doctor-findings.js +8 -11
  92. package/dist/lib/devices/harness-inventory.js +14 -5
  93. package/dist/lib/exec-account-home.js +5 -0
  94. package/dist/lib/exec.js +0 -11
  95. package/dist/lib/feed/events.js +0 -1
  96. package/dist/lib/feed/tool-activity.js +5 -2
  97. package/dist/lib/fleet-shared-state.d.ts +45 -0
  98. package/dist/lib/fleet-shared-state.js +78 -9
  99. package/dist/lib/fs-atomic.js +25 -0
  100. package/dist/lib/harness-auth-capabilities.js +0 -1
  101. package/dist/lib/helper-versions.js +1 -1
  102. package/dist/lib/hooks/cache.d.ts +7 -0
  103. package/dist/lib/hooks/cache.js +27 -2
  104. package/dist/lib/hooks/install.js +7 -1
  105. package/dist/lib/hosts/ready.d.ts +38 -8
  106. package/dist/lib/hosts/ready.js +62 -14
  107. package/dist/lib/hosts/remote-cmd.js +1 -0
  108. package/dist/lib/installations/active-check.d.ts +27 -0
  109. package/dist/lib/installations/active-check.js +61 -0
  110. package/dist/lib/installations/migrate.js +6 -3
  111. package/dist/lib/installations/shims.js +9 -7
  112. package/dist/lib/installations/update.js +21 -15
  113. package/dist/lib/installations/versions.js +12 -1
  114. package/dist/lib/manifest.js +1 -1
  115. package/dist/lib/models.js +0 -2
  116. package/dist/lib/open-url.js +28 -54
  117. package/dist/lib/placement.d.ts +2 -0
  118. package/dist/lib/placement.js +5 -0
  119. package/dist/lib/profiles.js +0 -2
  120. package/dist/lib/refresh.js +1 -1
  121. package/dist/lib/routine-readiness.d.ts +35 -0
  122. package/dist/lib/routine-readiness.js +74 -16
  123. package/dist/lib/rules/compile.js +6 -0
  124. package/dist/lib/sandbox.js +0 -2
  125. package/dist/lib/secrets-policy.d.ts +89 -11
  126. package/dist/lib/secrets-policy.js +175 -28
  127. package/dist/lib/session/db.d.ts +1 -1
  128. package/dist/lib/session/db.js +65 -18
  129. package/dist/lib/session/discover.js +1 -283
  130. package/dist/lib/session/mirror.d.ts +4 -3
  131. package/dist/lib/session/mirror.js +4 -3
  132. package/dist/lib/session/parse.d.ts +0 -2
  133. package/dist/lib/session/parse.js +1 -138
  134. package/dist/lib/session/throughput.d.ts +1 -3
  135. package/dist/lib/session/throughput.js +0 -20
  136. package/dist/lib/session/types.d.ts +2 -2
  137. package/dist/lib/session/types.js +2 -2
  138. package/dist/lib/sessions-client.d.ts +60 -1
  139. package/dist/lib/sessions-client.js +227 -4
  140. package/dist/lib/setup-tool-status.js +8 -12
  141. package/dist/lib/signin-badge.d.ts +3 -2
  142. package/dist/lib/signin-badge.js +6 -3
  143. package/dist/lib/smart-launch.js +4 -1
  144. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  147. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  148. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  149. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  150. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  151. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  152. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  153. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  154. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  155. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  156. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  157. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  158. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  159. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  160. package/dist/lib/staleness/writers/sources.js +5 -0
  161. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  162. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  163. package/dist/lib/state.d.ts +6 -4
  164. package/dist/lib/state.js +7 -17
  165. package/dist/lib/teams/parsers.d.ts +1 -1
  166. package/dist/lib/teams/parsers.js +1 -136
  167. package/dist/lib/teams/placement-probe.js +50 -15
  168. package/dist/lib/teams/scheduler.d.ts +7 -0
  169. package/dist/lib/types.d.ts +10 -1
  170. package/dist/lib/types.js +1 -1
  171. package/dist/lib/view-types.d.ts +32 -0
  172. package/dist/lib/watchdog/read.js +0 -1
  173. package/dist/lib/watchdog/rotate.js +1 -2
  174. package/dist/lib/watchdog/runner.js +2 -2
  175. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  176. package/dist/lib/watchdog/watchdogTail.js +0 -31
  177. package/package.json +1 -1
  178. package/scripts/postinstall.js +8 -8
  179. package/dist/browser.d.ts +0 -2
  180. package/dist/browser.js +0 -17
  181. package/dist/commands/browser-picker.d.ts +0 -18
  182. package/dist/commands/browser-picker.js +0 -97
  183. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  184. package/dist/lib/browser/arc-discovery.js +0 -197
  185. package/dist/lib/browser/arc-dom.d.ts +0 -14
  186. package/dist/lib/browser/arc-dom.js +0 -121
  187. package/dist/lib/browser/caller-identity.d.ts +0 -40
  188. package/dist/lib/browser/caller-identity.js +0 -175
  189. package/dist/lib/browser/cdp.d.ts +0 -51
  190. package/dist/lib/browser/cdp.js +0 -254
  191. package/dist/lib/browser/chrome.d.ts +0 -141
  192. package/dist/lib/browser/chrome.js +0 -704
  193. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  194. package/dist/lib/browser/chromium-discovery.js +0 -96
  195. package/dist/lib/browser/devices.d.ts +0 -34
  196. package/dist/lib/browser/devices.js +0 -61
  197. package/dist/lib/browser/domain-skills.d.ts +0 -71
  198. package/dist/lib/browser/domain-skills.js +0 -195
  199. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  200. package/dist/lib/browser/drivers/arc.js +0 -276
  201. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  202. package/dist/lib/browser/drivers/firefox.js +0 -377
  203. package/dist/lib/browser/drivers/local.d.ts +0 -55
  204. package/dist/lib/browser/drivers/local.js +0 -266
  205. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  206. package/dist/lib/browser/drivers/ssh.js +0 -467
  207. package/dist/lib/browser/editor.d.ts +0 -3
  208. package/dist/lib/browser/editor.js +0 -50
  209. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  210. package/dist/lib/browser/ffmpeg.js +0 -184
  211. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  212. package/dist/lib/browser/firefox-discovery.js +0 -162
  213. package/dist/lib/browser/har.d.ts +0 -85
  214. package/dist/lib/browser/har.js +0 -77
  215. package/dist/lib/browser/hygiene.d.ts +0 -97
  216. package/dist/lib/browser/hygiene.js +0 -153
  217. package/dist/lib/browser/index.d.ts +0 -5
  218. package/dist/lib/browser/index.js +0 -5
  219. package/dist/lib/browser/input.d.ts +0 -7
  220. package/dist/lib/browser/input.js +0 -92
  221. package/dist/lib/browser/ipc.d.ts +0 -204
  222. package/dist/lib/browser/ipc.js +0 -1453
  223. package/dist/lib/browser/login-detection.d.ts +0 -87
  224. package/dist/lib/browser/login-detection.js +0 -274
  225. package/dist/lib/browser/profiles.d.ts +0 -422
  226. package/dist/lib/browser/profiles.js +0 -1157
  227. package/dist/lib/browser/refs.d.ts +0 -89
  228. package/dist/lib/browser/refs.js +0 -191
  229. package/dist/lib/browser/registry.d.ts +0 -70
  230. package/dist/lib/browser/registry.js +0 -252
  231. package/dist/lib/browser/remote-control.d.ts +0 -59
  232. package/dist/lib/browser/remote-control.js +0 -85
  233. package/dist/lib/browser/resolve-target.d.ts +0 -66
  234. package/dist/lib/browser/resolve-target.js +0 -264
  235. package/dist/lib/browser/runtime-state.d.ts +0 -291
  236. package/dist/lib/browser/runtime-state.js +0 -584
  237. package/dist/lib/browser/secret-ref.d.ts +0 -10
  238. package/dist/lib/browser/secret-ref.js +0 -14
  239. package/dist/lib/browser/service.d.ts +0 -812
  240. package/dist/lib/browser/service.js +0 -4693
  241. package/dist/lib/browser/stream.d.ts +0 -17
  242. package/dist/lib/browser/stream.js +0 -72
  243. package/dist/lib/browser/task-index.d.ts +0 -78
  244. package/dist/lib/browser/task-index.js +0 -199
  245. package/dist/lib/browser/types.d.ts +0 -647
  246. package/dist/lib/browser/types.js +0 -61
  247. package/dist/lib/browser/upload.d.ts +0 -29
  248. package/dist/lib/browser/upload.js +0 -298
  249. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  250. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  251. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  252. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  253. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  254. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -1,21 +1,38 @@
1
1
  /**
2
- * Fleet distribution of identity-keyed Claude usage through the user repo.
2
+ * Fleet distribution of identity-keyed Claude usage over SSH (PHNX-4116).
3
3
  *
4
- * A headed device can read authoritative usage; a worker's setup-token cannot.
5
- * Headed daemons therefore publish one snapshot into their conflict-free
6
- * `~/.agents/devices/<device>/daemon-state.json`. The daemon's bounded Git
7
- * transport delivers those files automatically; workers merge newest-wins.
8
- * There is deliberately no device-to-device SSH in this module or its tick.
4
+ * A headed device can read authoritative usage; a worker's setup-token cannot
5
+ * (credential-management.md invariant 5: a worker never polls
6
+ * `/api/oauth/usage`). So the headed daemon's `usage-sync` tick dials every
7
+ * dialable peer in parallel with `agents __usage-ingest --reply`, sending this
8
+ * box's own daemon-state envelope on stdin; the peer merges the usage rows into
9
+ * its cache newest-wins and prints its own envelope back, which the headed box
10
+ * stores in `~/.agents/devices/<peer>/daemon-state.json` stamped `receivedAt`.
11
+ * Workers never initiate. A peer that times out is skipped this tick and never
12
+ * blocks another. Only usage rows, verdicts, and session digests move here —
13
+ * never a credential (invariant 7: roles never cross).
14
+ *
15
+ * This replaced a git exchange over the fleet-synced user repo: every daemon
16
+ * committed its own file each tick, so the shared store reached 1.1 GiB /
17
+ * 18,358 commits with 100% of the last 2,000 being `chore(devices): publish
18
+ * <device> daemon state`, and a worker's clone fell 10k commits behind with
19
+ * `git fetch timed out` on every tick — a valid setup-token could not be
20
+ * scheduled because a repo was bloated. Git now carries human-authored
21
+ * resources only; `devices/<device>/daemon-state.json` is untracked.
9
22
  */
10
- import { isHeadedDeviceRole, listConfiguredDeviceRoles, selfConfiguredDeviceRole } from '../device-config.js';
11
- import { readFleetSharedDeviceStates, updateFleetSharedDeviceStateAsync, } from '../fleet-shared-state.js';
23
+ import { isHeadedDeviceRole, selfConfiguredDeviceRole } from '../device-config.js';
24
+ import { FLEET_SHARED_STATE_VERSION, parseFleetSharedDeviceStateEnvelope, readFleetSharedDeviceStates, readOwnFleetSharedDeviceState, storePeerFleetSharedDeviceState, updateFleetSharedDeviceStateAsync, } from '../fleet-shared-state.js';
12
25
  import { getUserAgentsDir } from '../state.js';
13
26
  import { machineId, normalizeHost } from '../session/sync/config.js';
14
27
  import { exportClaudeUsageCacheRows, ingestPeerClaudeUsageRows, } from './usage.js';
15
- /** Cadence of the usage-sync git tick — the trust window for a `sync` snapshot. */
28
+ /** Cadence of the usage-sync tick — the trust window for a `sync` snapshot. */
16
29
  export const USAGE_SYNC_INTERVAL_MS = 15 * 60_000;
17
30
  /** Re-publish an unchanged windowed row at least this often so workers' 15-min trust does not lapse. */
18
31
  export const USAGE_PUBLISH_HEARTBEAT_MS = 10 * 60_000;
32
+ /** Per-peer SSH budget for one exchange; a peer past it is skipped this tick. */
33
+ export const USAGE_EXCHANGE_PEER_TIMEOUT_MS = 20_000;
34
+ /** Marks the start of the JSON reply on the peer's stdout, so login-shell noise before it is ignored. */
35
+ export const FLEET_STATE_REPLY_MARKER = '@@AGENTS_FLEET_STATE@@';
19
36
  function usageMeterSignature(row) {
20
37
  return JSON.stringify({
21
38
  windows: row.windows,
@@ -28,7 +45,7 @@ function usageMeterSignature(row) {
28
45
  /**
29
46
  * Keep the previously published `capturedAt` when meters have not moved and
30
47
  * the last publish is still inside the heartbeat. Stops statusline re-renders
31
- * from dirtying the shared store (and taking the git lock) every few seconds.
48
+ * from rewriting the own-state file every few seconds.
32
49
  */
33
50
  export function mergeUsageRowsForPublish(previous, next, nowMs = Date.now()) {
34
51
  if (!previous)
@@ -47,7 +64,7 @@ export function mergeUsageRowsForPublish(previous, next, nowMs = Date.now()) {
47
64
  }
48
65
  return out;
49
66
  }
50
- /** Publish this headed device's stable usage snapshot into its owned store file. */
67
+ /** Publish this headed device's stable usage snapshot into its own state file. */
51
68
  export async function publishUsageSnapshotToSharedStore(options = {}) {
52
69
  const result = {
53
70
  published: false,
@@ -84,68 +101,212 @@ export async function publishUsageSnapshotToSharedStore(options = {}) {
84
101
  return result;
85
102
  }
86
103
  /**
87
- * Publish a changed snapshot immediately and exchange the shared repo so
88
- * workers see it without waiting for the 15-minute tick. No-ops when the
89
- * serialized store is unchanged (statusline re-renders with the same windows).
104
+ * Refresh every field this device owns in its own state file: the usage
105
+ * snapshot (headed only), the session digests, and the reserved-auth readiness
106
+ * verdict. Run before the envelope is sent (the headed tick) or printed back
107
+ * (`__usage-ingest --reply`) so a peer always receives current fields. Each
108
+ * publisher is independent: one failing is reported in `errors` and the others
109
+ * still land. All three writers are the async, non-blocking variants
110
+ * (`updateFleetSharedDeviceStateAsync` underneath).
90
111
  */
91
- export async function pushUsageSnapshotNow(options = {}) {
92
- const published = await publishUsageSnapshotToSharedStore(options);
93
- if (!published.changed)
94
- return { published };
95
- const { syncFleetSharedStateRepo } = await import('../fleet-shared-repo-sync.js');
96
- const transport = await syncFleetSharedStateRepo({
97
- userAgentsDir: options.userAgentsDir,
98
- device: options.device,
99
- timeoutMs: options.timeoutMs,
100
- lockPath: options.lockPath,
101
- });
102
- return { published, transport };
112
+ export async function publishOwnFleetState(options = {}) {
113
+ const errors = [];
114
+ const usage = await publishUsageSnapshotToSharedStore(options);
115
+ if (usage.error)
116
+ errors.push(`usage: ${usage.error}`);
117
+ let mirror = { changed: false, count: 0, error: null };
118
+ try {
119
+ const { publishSessionMirrorToSharedStore } = await import('../session/mirror.js');
120
+ const published = await publishSessionMirrorToSharedStore({ userAgentsDir: options.userAgentsDir, device: options.device });
121
+ mirror = { changed: published.changed, count: published.count, error: published.error };
122
+ }
123
+ catch (err) {
124
+ mirror = { changed: false, count: 0, error: err.message };
125
+ }
126
+ if (mirror.error)
127
+ errors.push(`sessions: ${mirror.error}`);
128
+ let auth = { status: 'unknown', error: null };
129
+ try {
130
+ const { publishReservedAuthVerdict } = await import('../secrets-policy.js');
131
+ const verdict = await publishReservedAuthVerdict({ userAgentsDir: options.userAgentsDir, localName: options.device });
132
+ auth = { status: verdict.status, error: verdict.error };
133
+ }
134
+ catch (err) {
135
+ auth = { status: 'unknown', error: err.message };
136
+ }
137
+ if (auth.error)
138
+ errors.push(`auth: ${auth.error}`);
139
+ return { usage, mirror, auth, errors };
140
+ }
141
+ /** This device's exchange envelope, from its own state file (or usage-only from the cache). */
142
+ export function buildFleetStatePayload(options = {}) {
143
+ const device = options.device ?? machineId();
144
+ if (options.usageOnly) {
145
+ const state = { version: FLEET_SHARED_STATE_VERSION, device };
146
+ if (isHeadedDeviceRole(options.role ?? selfConfiguredDeviceRole())) {
147
+ const rows = exportClaudeUsageCacheRows(options.cachePath);
148
+ if (Object.keys(rows).length > 0)
149
+ state.usage = { rows };
150
+ }
151
+ return { v: 2, state };
152
+ }
153
+ const state = readOwnFleetSharedDeviceState(device, options.userAgentsDir);
154
+ return { v: 2, state, ...(options.errors?.length ? { errors: options.errors } : {}) };
103
155
  }
104
- function capturedAtMs(row) {
105
- if (!row.capturedAt)
106
- return null;
107
- const ms = Date.parse(row.capturedAt);
108
- return Number.isFinite(ms) ? ms : null;
156
+ /** Serialize the reply as the peer prints it: marker line, then one JSON line. */
157
+ export function formatFleetStateReply(payload) {
158
+ return `${FLEET_STATE_REPLY_MARKER}\n${JSON.stringify(payload)}\n`;
109
159
  }
110
160
  /**
111
- * Read headed publishers from the local user-repo checkout and merge one
112
- * newest-wins batch into the worker cache. No network or subprocess is touched.
161
+ * Parse what a peer sent — a v2 exchange envelope, or the legacy v1 bare-rows
162
+ * envelope from an older headed peer. Throws a clear message on anything else;
163
+ * the caller decides whether that is exit code 2 (the receiver) or a per-peer
164
+ * error (the headed tick).
113
165
  */
114
- export function consumeUsageSnapshotsFromSharedStore(options = {}) {
115
- const result = { sources: [], merged: 0, skipped: null, errors: [] };
116
- if ((options.role ?? selfConfiguredDeviceRole()) !== 'worker') {
117
- result.skipped = 'this device is not a worker';
118
- return result;
166
+ export function parseFleetStateExchangeInput(raw) {
167
+ let parsed;
168
+ try {
169
+ parsed = JSON.parse(raw);
119
170
  }
120
- const read = readFleetSharedDeviceStates(options.userAgentsDir ?? getUserAgentsDir());
121
- result.errors.push(...read.errors);
122
- const self = normalizeHost(options.device ?? machineId());
123
- const roles = options.roles ?? listConfiguredDeviceRoles(read.states.map((state) => state.device));
124
- const rows = {};
125
- for (const state of read.states) {
126
- if (normalizeHost(state.device) === self || !isHeadedDeviceRole(roles[state.device]) || !state.usage)
171
+ catch {
172
+ throw new Error('malformed JSON payload');
173
+ }
174
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
175
+ throw new Error('unrecognized usage-sync payload shape');
176
+ }
177
+ const record = parsed;
178
+ if (record.v === 2) {
179
+ const state = parseFleetSharedDeviceStateEnvelope(record.state);
180
+ const errors = Array.isArray(record.errors) ? record.errors.filter((e) => typeof e === 'string') : undefined;
181
+ return { v: 2, state, ...(errors?.length ? { errors } : {}) };
182
+ }
183
+ if (record.v === 1) {
184
+ const rows = record.rows;
185
+ // `typeof [] === 'object'` — an array is NOT a rows map.
186
+ if (typeof rows !== 'object' || rows === null || Array.isArray(rows)) {
187
+ throw new Error('unrecognized usage-sync payload shape');
188
+ }
189
+ return { v: 1, rows: rows };
190
+ }
191
+ throw new Error('unrecognized usage-sync payload shape');
192
+ }
193
+ /** Extract the reply envelope from a peer's stdout (text before the marker is login-shell noise). */
194
+ export function parseFleetStateReply(stdout) {
195
+ const idx = stdout.lastIndexOf(FLEET_STATE_REPLY_MARKER);
196
+ if (idx === -1) {
197
+ throw new Error(stdout.trim()
198
+ ? 'no reply envelope on stdout (peer agents-cli predates `__usage-ingest --reply`?)'
199
+ : 'empty reply (peer agents-cli predates `__usage-ingest --reply`?)');
200
+ }
201
+ const body = stdout.slice(idx + FLEET_STATE_REPLY_MARKER.length).trim();
202
+ const payload = parseFleetStateExchangeInput(body);
203
+ if (payload.v !== 2)
204
+ throw new Error('reply is not a v2 exchange envelope');
205
+ return payload;
206
+ }
207
+ /**
208
+ * Rows as the receiver stores them: provenance `sync` (so the trust window is
209
+ * `USAGE_SYNC_TRUST_MS`, not the local-capture bar) and the poller pinned to the
210
+ * device that actually read the endpoint, defaulting to the sender.
211
+ */
212
+ function stampSyncRows(rows, sender) {
213
+ const out = {};
214
+ for (const [identity, incoming] of Object.entries(rows)) {
215
+ if (!incoming || !Array.isArray(incoming.windows) || incoming.windows.length === 0)
127
216
  continue;
128
- result.sources.push(state.device);
129
- for (const [identity, incoming] of Object.entries(state.usage.rows)) {
130
- if (!incoming || !Array.isArray(incoming.windows) || incoming.windows.length === 0)
131
- continue;
132
- const stamped = {
133
- ...incoming,
134
- freshnessSource: 'sync',
135
- pollerDevice: incoming.pollerDevice ?? state.device,
136
- };
137
- const current = rows[identity];
138
- if (!current) {
139
- rows[identity] = stamped;
140
- continue;
217
+ out[identity] = { ...incoming, freshnessSource: 'sync', pollerDevice: incoming.pollerDevice ?? sender };
218
+ }
219
+ return out;
220
+ }
221
+ /**
222
+ * Take a peer's envelope in: store it as that peer's file (stamped `receivedAt`)
223
+ * and merge its usage rows into this box's cache newest-wins. The same step runs
224
+ * on both ends — the worker on the pushed envelope, the headed box on the reply.
225
+ * Both writes take their file lock asynchronously: this runs on the daemon tick
226
+ * once per peer, and a `sleepSync` lock there freezes every other service.
227
+ */
228
+ export async function applyPeerFleetState(state, options = {}) {
229
+ const self = normalizeHost(options.device ?? machineId());
230
+ if (normalizeHost(state.device) === self) {
231
+ throw new Error(`peer envelope names this device (${state.device}); refusing to overwrite the own state file`);
232
+ }
233
+ const receivedAt = options.receivedAt ?? Date.now();
234
+ const write = await storePeerFleetSharedDeviceState(state, options.userAgentsDir ?? getUserAgentsDir(), receivedAt);
235
+ const merged = state.usage
236
+ ? await ingestPeerClaudeUsageRows(stampSyncRows(state.usage.rows, state.device), options.cachePath)
237
+ : 0;
238
+ return { path: write.path, merged, receivedAt };
239
+ }
240
+ /** The remote command one exchange runs on a peer, in the peer's shell dialect. */
241
+ export async function buildFleetStateExchangeCommand(os) {
242
+ const { buildRemoteAgentsInvocation, buildWindowsStdinAgentsCommand, remoteShellFor } = await import('../hosts/remote-cmd.js');
243
+ // The Windows `agents.ps1` shim does not forward ssh-piped stdin, so the
244
+ // payload goes through a temp file and `--from <path>` there.
245
+ return remoteShellFor(os) === 'powershell'
246
+ ? buildWindowsStdinAgentsCommand(['__usage-ingest', '--reply'])
247
+ : buildRemoteAgentsInvocation(['__usage-ingest', '--reply'], undefined, os);
248
+ }
249
+ async function sshDial(timeoutMs) {
250
+ const { sshExecAsync } = await import('../ssh-exec.js');
251
+ const { sshTargetFor, deviceIdentityArgs } = await import('../devices/connect.js');
252
+ return (peer, remoteCmd, input) => sshExecAsync(sshTargetFor(peer), remoteCmd, {
253
+ input,
254
+ timeoutMs,
255
+ extraSshArgs: deviceIdentityArgs(peer),
256
+ });
257
+ }
258
+ async function defaultPeers(self) {
259
+ const { isDialableDevice, loadDevicesSync } = await import('../devices/registry.js');
260
+ return Object.values(loadDevicesSync())
261
+ .filter((d) => normalizeHost(d.name) !== self && isDialableDevice(d))
262
+ .sort((a, b) => a.name.localeCompare(b.name));
263
+ }
264
+ /**
265
+ * The headed publisher's fan-out: dial every peer in parallel, each under its
266
+ * own timeout, and apply each reply as it arrives. Never throws for a peer — a
267
+ * timeout, an old CLI with no `--reply`, or a reply naming the wrong device is
268
+ * that peer's `error` and the others are unaffected. A non-headed device skips:
269
+ * workers never initiate (they only answer), so a worker's tick is a no-op here.
270
+ */
271
+ export async function exchangeFleetStateWithPeers(options = {}) {
272
+ const device = options.device ?? machineId();
273
+ const self = normalizeHost(device);
274
+ const peers = options.peers ?? await defaultPeers(self);
275
+ if (peers.length === 0)
276
+ return { skipped: 'no dialable peer in the device registry', outcomes: [] };
277
+ const timeoutMs = options.timeoutMs ?? USAGE_EXCHANGE_PEER_TIMEOUT_MS;
278
+ const dial = options.dial ?? await sshDial(timeoutMs);
279
+ const payload = options.payload ?? buildFleetStatePayload({ device, userAgentsDir: options.userAgentsDir });
280
+ const input = JSON.stringify(payload);
281
+ const outcomes = await Promise.all(peers.map(async (peer) => {
282
+ const outcome = { device: peer.name, delivered: false, receivedAt: null, merged: 0, error: null, peerErrors: [] };
283
+ try {
284
+ const remoteCmd = await buildFleetStateExchangeCommand(peer.shell === 'powershell' ? 'windows' : undefined);
285
+ const res = await dial(peer, remoteCmd, input);
286
+ if (res.timedOut)
287
+ throw new Error(`timed out after ${Math.round(timeoutMs / 1000)}s`);
288
+ if (res.code !== 0) {
289
+ const detail = res.stderr.trim().split('\n').pop() ?? '';
290
+ throw new Error(`exited ${res.code ?? 'without a status'}${detail ? `: ${detail}` : ''}`);
291
+ }
292
+ const reply = parseFleetStateReply(res.stdout);
293
+ if (normalizeHost(reply.state.device) !== normalizeHost(peer.name)) {
294
+ throw new Error(`reply names device '${reply.state.device}', expected '${peer.name}'`);
141
295
  }
142
- const incomingMs = capturedAtMs(stamped);
143
- const currentMs = capturedAtMs(current);
144
- if (incomingMs !== null && (currentMs === null || incomingMs > currentMs))
145
- rows[identity] = stamped;
296
+ const applied = await applyPeerFleetState(reply.state, {
297
+ device,
298
+ userAgentsDir: options.userAgentsDir,
299
+ cachePath: options.cachePath,
300
+ });
301
+ outcome.delivered = true;
302
+ outcome.receivedAt = applied.receivedAt;
303
+ outcome.merged = applied.merged;
304
+ outcome.peerErrors = reply.errors ?? [];
146
305
  }
147
- }
148
- result.sources.sort();
149
- result.merged = ingestPeerClaudeUsageRows(rows, options.cachePath);
150
- return result;
306
+ catch (err) {
307
+ outcome.error = err.message;
308
+ }
309
+ return outcome;
310
+ }));
311
+ return { skipped: null, outcomes };
151
312
  }
@@ -494,6 +494,17 @@ export interface FormatUsageSummaryOpts {
494
494
  export declare function viewUsageSummaryOptions(agentId: AgentId, signedIn: boolean, usageInfo: UsageInfo | undefined, maxWindows: number | undefined, version?: string): FormatUsageSummaryOpts;
495
495
  /** Format a one-line usage summary with compact bars for inline display. */
496
496
  export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: FormatUsageSummaryOpts): string;
497
+ /**
498
+ * The snapshot's windows that are still LIVE — reset time not yet passed. A
499
+ * window past its `resetsAt` has rolled over: its `usedPercent` is a reading of
500
+ * the PREVIOUS period, not a live throttle (PHNX-4116). Both the throttle
501
+ * verdict ({@link deriveUsageStatusFromSnapshot}) and the displayed
502
+ * `usedPercent` (`devices/harness-inventory.ts`) MUST select from this SAME set,
503
+ * or a maxed-then-reset account reads `available` yet shows a stale 99% next to
504
+ * its "ready" verdict (#3705). Callers still narrow further (dropping
505
+ * `sonnet_week`, the model sub-limit); this is only the reset-rollover cut.
506
+ */
507
+ export declare function liveUsageWindows(snapshot: UsageSnapshot, now?: number): UsageWindow[];
497
508
  /**
498
509
  * Derive an account's real throttle state from its live usage windows — the
499
510
  * single signal both the `agents view` badge and run-rotation eligibility share
@@ -513,7 +524,7 @@ export declare function formatUsageSummary(plan: string | null, snapshot: UsageS
513
524
  * The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
514
525
  * one model, not the account, so it shouldn't flip the whole row to throttled.
515
526
  */
516
- export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot | null | undefined): 'available' | 'rate_limited' | null;
527
+ export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot | null | undefined, now?: number): 'available' | 'rate_limited' | null;
517
528
  /** A prior sample of one window's utilization, for burn-rate projection. */
518
529
  interface UsagePriorSample {
519
530
  /** Epoch ms the prior snapshot was captured. */
@@ -768,7 +779,9 @@ export declare function exportClaudeUsageCacheRows(cachePath?: string): Record<s
768
779
  * a fresher row another peer (or, on a headed receiver, the local status-line)
769
780
  * already wrote. An incoming row with no `capturedAt` cannot prove it is newer, so
770
781
  * it never displaces an existing timestamped row. Returns the count updated.
771
- * Locked + atomic like every other cache writer.
782
+ * Locked + atomic like every other cache writer, but through the ASYNC lock: it
783
+ * runs on the daemon's usage-sync tick once per peer reply (PHNX-4116), where
784
+ * the sync lock's `sleepSync` would freeze every other service under contention.
772
785
  *
773
786
  * Deliberately NOT role-gated on the receiver. "Consume only on worker/unmarked"
774
787
  * is a SENDER-side optimization (don't waste a push on a headed peer that reads
@@ -778,7 +791,7 @@ export declare function exportClaudeUsageCacheRows(cachePath?: string): Record<s
778
791
  * runs now shows a usage bar), so gating here on the receiver's own — laggier —
779
792
  * view of its role would only reject legitimate data.
780
793
  */
781
- export declare function ingestPeerClaudeUsageRows(rows: Record<string, CachedUsageSnapshot>, cachePath?: string): number;
794
+ export declare function ingestPeerClaudeUsageRows(rows: Record<string, CachedUsageSnapshot>, cachePath?: string): Promise<number>;
782
795
  /**
783
796
  * Persist a Claude tokens/credits exhaustion (`out of usage credits` / `monthly
784
797
  * spend limit`) from a real run. Unlike a rate/session limit this does NOT reset
@@ -24,7 +24,7 @@ import { resolveClaudeSetupToken } from '../claude-account-token.js';
24
24
  import { formatBackoffRemaining, noteUsageRateLimited, usageRateLimitedUntil, } from '../usage-backoff.js';
25
25
  import { getCacheDir } from '../state.js';
26
26
  import { mapBounded } from '../concurrency.js';
27
- import { atomicWriteFileSync, ensureLockTarget, withFileLock } from '../fs-atomic.js';
27
+ import { atomicWriteFileSync, ensureLockTarget, withFileLock, withFileLockAsync } from '../fs-atomic.js';
28
28
  import { withRefreshLease } from '../refresh-coordinator.js';
29
29
  import { padToWidth } from '../session/width.js';
30
30
  const execFileAsync = promisify(execFile);
@@ -723,6 +723,19 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
723
723
  }
724
724
  return parts.join(' ');
725
725
  }
726
+ /**
727
+ * The snapshot's windows that are still LIVE — reset time not yet passed. A
728
+ * window past its `resetsAt` has rolled over: its `usedPercent` is a reading of
729
+ * the PREVIOUS period, not a live throttle (PHNX-4116). Both the throttle
730
+ * verdict ({@link deriveUsageStatusFromSnapshot}) and the displayed
731
+ * `usedPercent` (`devices/harness-inventory.ts`) MUST select from this SAME set,
732
+ * or a maxed-then-reset account reads `available` yet shows a stale 99% next to
733
+ * its "ready" verdict (#3705). Callers still narrow further (dropping
734
+ * `sonnet_week`, the model sub-limit); this is only the reset-rollover cut.
735
+ */
736
+ export function liveUsageWindows(snapshot, now = Date.now()) {
737
+ return snapshot.windows.filter((window) => !(window.resetsAt && window.resetsAt.getTime() <= now));
738
+ }
726
739
  /**
727
740
  * Derive an account's real throttle state from its live usage windows — the
728
741
  * single signal both the `agents view` badge and run-rotation eligibility share
@@ -742,7 +755,7 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
742
755
  * The model-specific `sonnet_week` sub-limit is excluded: hitting it throttles
743
756
  * one model, not the account, so it shouldn't flip the whole row to throttled.
744
757
  */
745
- export function deriveUsageStatusFromSnapshot(snapshot) {
758
+ export function deriveUsageStatusFromSnapshot(snapshot, now = Date.now()) {
746
759
  if (!snapshot)
747
760
  return null;
748
761
  if (snapshot.unavailable) {
@@ -750,14 +763,17 @@ export function deriveUsageStatusFromSnapshot(snapshot) {
750
763
  // clears it. session_limit blocks only until its reset time.
751
764
  if (snapshot.unavailable.reason === 'out_of_credits')
752
765
  return 'rate_limited';
753
- if (snapshot.unavailable.resetsAt && snapshot.unavailable.resetsAt.getTime() > Date.now()) {
766
+ if (snapshot.unavailable.resetsAt && snapshot.unavailable.resetsAt.getTime() > now) {
754
767
  return 'rate_limited';
755
768
  }
756
769
  }
757
770
  if (snapshot.windows.length === 0)
758
771
  return null;
759
- const blocking = snapshot.windows.filter((window) => window.key !== 'sonnet_week');
760
- const windows = blocking.length > 0 ? blocking : snapshot.windows;
772
+ const live = liveUsageWindows(snapshot, now);
773
+ if (live.length === 0)
774
+ return 'available';
775
+ const blocking = live.filter((window) => window.key !== 'sonnet_week');
776
+ const windows = blocking.length > 0 ? blocking : live;
761
777
  const maxUsed = Math.max(...windows.map((window) => window.usedPercent));
762
778
  return maxUsed >= 100 ? 'rate_limited' : 'available';
763
779
  }
@@ -1897,7 +1913,9 @@ function parseCapturedAtMs(capturedAt) {
1897
1913
  * a fresher row another peer (or, on a headed receiver, the local status-line)
1898
1914
  * already wrote. An incoming row with no `capturedAt` cannot prove it is newer, so
1899
1915
  * it never displaces an existing timestamped row. Returns the count updated.
1900
- * Locked + atomic like every other cache writer.
1916
+ * Locked + atomic like every other cache writer, but through the ASYNC lock: it
1917
+ * runs on the daemon's usage-sync tick once per peer reply (PHNX-4116), where
1918
+ * the sync lock's `sleepSync` would freeze every other service under contention.
1901
1919
  *
1902
1920
  * Deliberately NOT role-gated on the receiver. "Consume only on worker/unmarked"
1903
1921
  * is a SENDER-side optimization (don't waste a push on a headed peer that reads
@@ -1907,14 +1925,14 @@ function parseCapturedAtMs(capturedAt) {
1907
1925
  * runs now shows a usage bar), so gating here on the receiver's own — laggier —
1908
1926
  * view of its role would only reject legitimate data.
1909
1927
  */
1910
- export function ingestPeerClaudeUsageRows(rows, cachePath = getClaudeUsageCachePath()) {
1928
+ export async function ingestPeerClaudeUsageRows(rows, cachePath = getClaudeUsageCachePath()) {
1911
1929
  const incoming = Object.entries(rows).filter(([, row]) => row && Array.isArray(row.windows) && row.windows.length > 0);
1912
1930
  if (incoming.length === 0)
1913
1931
  return 0;
1914
1932
  let merged = 0;
1915
1933
  try {
1916
1934
  ensureLockTarget(cachePath, '{}');
1917
- withFileLock(cachePath, () => {
1935
+ await withFileLockAsync(cachePath, () => {
1918
1936
  const cache = readClaudeUsageCacheFile(cachePath);
1919
1937
  for (const [key, row] of incoming) {
1920
1938
  const prior = cache[key];
@@ -2,6 +2,14 @@ import type { AgentId, DeviceAccountSlot, Meta } from '../types.js';
2
2
  export type { DeviceAccountSlot };
3
3
  export declare function slotDir(harness: AgentId, accountId: string): string;
4
4
  export declare function readSlots(meta: Pick<Meta, 'deviceAccounts'>): Record<string, DeviceAccountSlot>;
5
+ /**
6
+ * Remove slot records from THIS box's device doc (`deviceAccounts.slots`).
7
+ * Symmetric with {@link recordSlot}. The slot DIRECTORY is not touched — a
8
+ * caller that also wants the on-disk home gone removes it separately; a stale
9
+ * worker slot keeps its dir because `.claude/projects` holds transcripts
10
+ * (PHNX-4116). A no-op for an empty list or an id with no record.
11
+ */
12
+ export declare function dropSlots(accountIds: readonly string[]): void;
5
13
  export declare function recordSlot(accountId: string, slot: DeviceAccountSlot): void;
6
14
  export interface SlotProjection {
7
15
  accountId: string;
@@ -35,6 +35,23 @@ export function slotDir(harness, accountId) {
35
35
  export function readSlots(meta) {
36
36
  return { ...(meta.deviceAccounts?.slots ?? {}) };
37
37
  }
38
+ /**
39
+ * Remove slot records from THIS box's device doc (`deviceAccounts.slots`).
40
+ * Symmetric with {@link recordSlot}. The slot DIRECTORY is not touched — a
41
+ * caller that also wants the on-disk home gone removes it separately; a stale
42
+ * worker slot keeps its dir because `.claude/projects` holds transcripts
43
+ * (PHNX-4116). A no-op for an empty list or an id with no record.
44
+ */
45
+ export function dropSlots(accountIds) {
46
+ if (accountIds.length === 0)
47
+ return;
48
+ updateMeta((current) => {
49
+ const slots = { ...current.deviceAccounts?.slots };
50
+ for (const id of accountIds)
51
+ delete slots[id];
52
+ return { ...current, deviceAccounts: { ...current.deviceAccounts, slots } };
53
+ });
54
+ }
38
55
  export function recordSlot(accountId, slot) {
39
56
  if (slot.accountId !== accountId) {
40
57
  throw new Error(`recordSlot accountId mismatch: key '${accountId}' vs slot.accountId '${slot.accountId}'.`);
@@ -32,7 +32,6 @@ export const ADD_DIR_STRATEGY = {
32
32
  grok: 'grok-sandbox',
33
33
  // No multi-root CLI surface today (single --dir / project path).
34
34
  opencode: 'none',
35
- gemini: 'none',
36
35
  openclaw: 'none',
37
36
  copilot: 'none',
38
37
  amp: 'none',
@@ -11,7 +11,6 @@
11
11
  export const AGENT_CLI_COMMANDS = [
12
12
  'claude',
13
13
  'codex',
14
- 'gemini',
15
14
  'cursor-agent',
16
15
  'opencode',
17
16
  'openclaw',
@@ -89,9 +89,9 @@ export declare const ALL_AGENT_IDS: AgentId[];
89
89
  * import (runner.ts already imports from routines.ts).
90
90
  *
91
91
  * This is a curated subset of AGENT_COMMANDS, not "every harness that can run
92
- * headlessly". Expanding it is a product change (gemini is hard-deprecated and
93
- * is deliberately absent). Argv itself is baked from AGENT_COMMANDS in
94
- * daemon/runner.ts bakeRoutineArgv — do not reintroduce a second token table.
92
+ * headlessly". Expanding it is a product change. Argv itself is baked from
93
+ * AGENT_COMMANDS in daemon/runner.ts bakeRoutineArgv — do not reintroduce a
94
+ * second token table.
95
95
  */
96
96
  export declare const ROUTINE_AGENT_IDS: readonly string[];
97
97
  /** Agents that can receive managed installs, imports, and resource sync writes. */
@@ -222,7 +222,7 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
222
222
  */
223
223
  export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
224
224
  /** Agents whose local credential formats expose enough state for account selection. */
225
- export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "cursor", "grok", "antigravity", "kimi", "droid", "opencode", "muse"];
225
+ export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "cursor", "grok", "antigravity", "kimi", "droid", "opencode", "muse"];
226
226
  /** Whether agents-cli can determine this agent's per-version sign-in state. */
227
227
  export declare function supportsAccountInspection(agentId: AgentId): boolean;
228
228
  /**
@@ -573,9 +573,8 @@ export declare function stripJsonComments(content: string): string;
573
573
  *
574
574
  * All three MCP path resolvers read `MCP_TARGETS`, so the file the writer
575
575
  * targets, the file the parser reads, and the file the staleness detector
576
- * watches cannot drift apart. An agent with no MCP target (the hard-deprecated
577
- * `gemini`) keeps the historical settings.json default so old configs still
578
- * parse.
576
+ * watches cannot drift apart. An agent with no MCP target keeps the historical
577
+ * settings.json default so old configs still parse.
579
578
  */
580
579
  export declare function getUserMcpConfigPath(agentId: AgentId): string;
581
580
  /**