@phnx-labs/agents-cli 1.20.78 → 1.20.83

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 (183) hide show
  1. package/CHANGELOG.md +595 -0
  2. package/README.md +26 -26
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +66 -0
  12. package/dist/commands/doctor.js +596 -33
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/menubar.js +12 -0
  22. package/dist/commands/models.js +1 -1
  23. package/dist/commands/packages.js +6 -6
  24. package/dist/commands/permissions.js +1 -1
  25. package/dist/commands/profiles.js +1 -1
  26. package/dist/commands/pull.d.ts +1 -1
  27. package/dist/commands/pull.js +4 -4
  28. package/dist/commands/repo.d.ts +78 -0
  29. package/dist/commands/repo.js +187 -11
  30. package/dist/commands/routines.d.ts +8 -1
  31. package/dist/commands/routines.js +241 -77
  32. package/dist/commands/rules.js +13 -12
  33. package/dist/commands/secrets.d.ts +3 -1
  34. package/dist/commands/secrets.js +100 -32
  35. package/dist/commands/sessions-inject.d.ts +7 -4
  36. package/dist/commands/sessions-inject.js +18 -18
  37. package/dist/commands/sessions-picker.js +32 -3
  38. package/dist/commands/sessions-resume.d.ts +16 -0
  39. package/dist/commands/sessions-resume.js +14 -13
  40. package/dist/commands/sessions.d.ts +115 -0
  41. package/dist/commands/sessions.js +189 -35
  42. package/dist/commands/set.d.ts +15 -0
  43. package/dist/commands/set.js +79 -0
  44. package/dist/commands/setup-fleet.d.ts +21 -0
  45. package/dist/commands/setup-fleet.js +201 -0
  46. package/dist/commands/setup-secrets.d.ts +20 -0
  47. package/dist/commands/setup-secrets.js +223 -0
  48. package/dist/commands/setup.js +17 -3
  49. package/dist/commands/skills.js +1 -1
  50. package/dist/commands/sync.js +7 -2
  51. package/dist/commands/teams-picker.js +0 -1
  52. package/dist/commands/teams.d.ts +33 -1
  53. package/dist/commands/teams.js +208 -44
  54. package/dist/commands/versions.js +10 -5
  55. package/dist/commands/view.d.ts +1 -0
  56. package/dist/commands/view.js +11 -0
  57. package/dist/index.js +52 -8
  58. package/dist/lib/acp/harnesses.js +0 -7
  59. package/dist/lib/activity.d.ts +156 -0
  60. package/dist/lib/activity.js +282 -0
  61. package/dist/lib/agents.d.ts +9 -2
  62. package/dist/lib/agents.js +72 -63
  63. package/dist/lib/browser/service.js +3 -0
  64. package/dist/lib/browser/types.d.ts +7 -0
  65. package/dist/lib/capabilities.js +6 -2
  66. package/dist/lib/crabbox/lease.js +2 -2
  67. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  68. package/dist/lib/crabbox/setup-copy.js +4 -4
  69. package/dist/lib/daemon.d.ts +18 -0
  70. package/dist/lib/daemon.js +47 -6
  71. package/dist/lib/devices/fleet.d.ts +1 -1
  72. package/dist/lib/devices/fleet.js +1 -1
  73. package/dist/lib/doctor-diff.d.ts +20 -0
  74. package/dist/lib/doctor-diff.js +6 -1
  75. package/dist/lib/drift.d.ts +31 -11
  76. package/dist/lib/drift.js +58 -7
  77. package/dist/lib/events.d.ts +8 -1
  78. package/dist/lib/events.js +10 -1
  79. package/dist/lib/exec.d.ts +7 -0
  80. package/dist/lib/exec.js +69 -11
  81. package/dist/lib/feed-post.d.ts +28 -1
  82. package/dist/lib/feed-post.js +110 -2
  83. package/dist/lib/fleet/apply.js +8 -1
  84. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  85. package/dist/lib/fleet/auth-sync.js +25 -11
  86. package/dist/lib/fleet/remote-login.js +5 -0
  87. package/dist/lib/git.d.ts +14 -14
  88. package/dist/lib/git.js +41 -44
  89. package/dist/lib/hooks.d.ts +71 -0
  90. package/dist/lib/hooks.js +245 -95
  91. package/dist/lib/hosts/dispatch.d.ts +28 -1
  92. package/dist/lib/hosts/dispatch.js +33 -5
  93. package/dist/lib/hosts/option.js +2 -2
  94. package/dist/lib/hosts/passthrough.d.ts +22 -1
  95. package/dist/lib/hosts/passthrough.js +238 -13
  96. package/dist/lib/hosts/run-target.d.ts +2 -0
  97. package/dist/lib/hosts/run-target.js +1 -0
  98. package/dist/lib/hq/floor.d.ts +1 -1
  99. package/dist/lib/hq/floor.js +6 -1
  100. package/dist/lib/mcp.js +3 -71
  101. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  102. package/dist/lib/menubar/install-menubar.d.ts +24 -0
  103. package/dist/lib/menubar/install-menubar.js +52 -2
  104. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  105. package/dist/lib/menubar/notify-desktop.js +41 -5
  106. package/dist/lib/merged-resources.d.ts +11 -0
  107. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  108. package/dist/lib/permissions.d.ts +0 -32
  109. package/dist/lib/permissions.js +4 -157
  110. package/dist/lib/picker.js +1 -1
  111. package/dist/lib/platform/process.d.ts +16 -5
  112. package/dist/lib/platform/process.js +54 -0
  113. package/dist/lib/plugins.d.ts +0 -8
  114. package/dist/lib/plugins.js +4 -110
  115. package/dist/lib/project-resources.js +4 -1
  116. package/dist/lib/refresh.d.ts +5 -3
  117. package/dist/lib/refresh.js +7 -5
  118. package/dist/lib/resources/commands.js +4 -1
  119. package/dist/lib/resources/mcp.js +0 -4
  120. package/dist/lib/resources/permissions.d.ts +1 -1
  121. package/dist/lib/resources/permissions.js +1 -5
  122. package/dist/lib/resources/rules.js +4 -1
  123. package/dist/lib/resources/skills.js +4 -1
  124. package/dist/lib/resources/subagents.js +4 -0
  125. package/dist/lib/resources/types.d.ts +1 -1
  126. package/dist/lib/routines.d.ts +33 -0
  127. package/dist/lib/routines.js +122 -0
  128. package/dist/lib/run-defaults.d.ts +1 -0
  129. package/dist/lib/run-defaults.js +9 -0
  130. package/dist/lib/runner.d.ts +0 -12
  131. package/dist/lib/runner.js +49 -10
  132. package/dist/lib/scheduler.js +16 -4
  133. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  134. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  135. package/dist/lib/secrets/agent.d.ts +16 -0
  136. package/dist/lib/secrets/agent.js +105 -3
  137. package/dist/lib/secrets/audit.d.ts +46 -0
  138. package/dist/lib/secrets/audit.js +56 -0
  139. package/dist/lib/secrets/bundles.d.ts +11 -8
  140. package/dist/lib/secrets/bundles.js +31 -23
  141. package/dist/lib/secrets/index.d.ts +2 -1
  142. package/dist/lib/secrets/index.js +8 -2
  143. package/dist/lib/secrets/remote.d.ts +75 -2
  144. package/dist/lib/secrets/remote.js +139 -5
  145. package/dist/lib/secrets/sync.js +3 -3
  146. package/dist/lib/session/active.d.ts +128 -26
  147. package/dist/lib/session/active.js +192 -53
  148. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  149. package/dist/lib/session/actor-sidecar.js +101 -0
  150. package/dist/lib/session/db.d.ts +4 -1
  151. package/dist/lib/session/db.js +65 -8
  152. package/dist/lib/session/discover.d.ts +4 -0
  153. package/dist/lib/session/discover.js +13 -0
  154. package/dist/lib/session/hook-sessions.d.ts +9 -0
  155. package/dist/lib/session/hook-sessions.js +57 -8
  156. package/dist/lib/session/pid-registry.d.ts +13 -0
  157. package/dist/lib/session/render.d.ts +2 -0
  158. package/dist/lib/session/render.js +1 -1
  159. package/dist/lib/session/types.d.ts +16 -0
  160. package/dist/lib/staleness/detectors/commands.js +2 -2
  161. package/dist/lib/staleness/detectors/permissions.js +1 -45
  162. package/dist/lib/staleness/writers/commands.js +4 -5
  163. package/dist/lib/staleness/writers/hooks.js +1 -1
  164. package/dist/lib/startup/command-registry.d.ts +1 -2
  165. package/dist/lib/startup/command-registry.js +2 -4
  166. package/dist/lib/state.js +57 -1
  167. package/dist/lib/subagents-registry.js +5 -12
  168. package/dist/lib/subagents.d.ts +0 -10
  169. package/dist/lib/subagents.js +0 -12
  170. package/dist/lib/teams/agents.d.ts +0 -2
  171. package/dist/lib/teams/agents.js +10 -27
  172. package/dist/lib/teams/index.d.ts +1 -1
  173. package/dist/lib/teams/index.js +1 -1
  174. package/dist/lib/types.d.ts +17 -9
  175. package/dist/lib/usage.d.ts +28 -5
  176. package/dist/lib/usage.js +271 -8
  177. package/dist/lib/versions.js +7 -1
  178. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  179. package/dist/lib/watchdog/watchdog.js +25 -13
  180. package/package.json +1 -2
  181. package/dist/commands/check.d.ts +0 -15
  182. package/dist/commands/check.js +0 -180
  183. package/dist/commands/resources.d.ts +0 -5
@@ -24,6 +24,9 @@ import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
24
24
  import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
25
25
  import { resolveRemoteOsSync } from './remote-os.js';
26
26
  import { machineId } from '../session/sync/config.js';
27
+ import { loadDevices } from '../devices/registry.js';
28
+ import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
29
+ import { platformGroupLabel } from '../devices/health-report.js';
27
30
  /**
28
31
  * First-class groups that run transparently on a remote via SSH when
29
32
  * `--host`/`--device` is present. Keep both canonical names and aliases
@@ -102,6 +105,7 @@ const OWN_HOST_COMMANDS = new Set([
102
105
  'harnesses',
103
106
  'sessions',
104
107
  'feed',
108
+ 'activity', // fans `--host`/`--device`/`--devices-all` out itself (feed-style)
105
109
  'computer',
106
110
  'secrets',
107
111
  'logs',
@@ -112,8 +116,15 @@ const OWN_HOST_COMMANDS = new Set([
112
116
  'apply', // `--device` scopes the fleet reconcile to one device (it targets devices itself)
113
117
  'monitors', // `--device` names the OWNER machine (pin-to-one), not a routing target
114
118
  ]);
115
- /** `--no-tty` is stripped like the routing flags but carries no value. */
116
- const STRIP_SPECS = [...HOST_ROUTING_SPECS, { long: 'no-tty', takesValue: false }];
119
+ /** `--no-tty` is stripped like the routing flags but carries no value. The plural
120
+ * fleet flags are stripped only when we handle the `all` sentinel ourselves;
121
+ * otherwise they fall through to command-level aggregators. */
122
+ const STRIP_SPECS = [
123
+ ...HOST_ROUTING_SPECS,
124
+ { long: 'no-tty', takesValue: false },
125
+ { long: 'hosts', takesValue: true },
126
+ { long: 'devices', takesValue: true },
127
+ ];
117
128
  /** Pull the value of `--host`/`-H`/`--remote-cwd` (any form) out of an argv. */
118
129
  export function flagValue(args, long, short) {
119
130
  for (let i = 0; i < args.length; i++) {
@@ -155,6 +166,201 @@ async function resolveTargetHost(name, any) {
155
166
  assertValidSshTarget(name); // rejects injection / flag-smuggling before it reaches ssh
156
167
  return syntheticHost(name);
157
168
  }
169
+ /** Detect the `all` sentinel on any routing flag. */
170
+ function isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag) {
171
+ return (hostFlag?.toLowerCase() === 'all' ||
172
+ deviceFlag?.toLowerCase() === 'all' ||
173
+ hostsFlag?.toLowerCase() === 'all' ||
174
+ devicesFlag?.toLowerCase() === 'all');
175
+ }
176
+ /** Strip routing flags from the argv and ensure the per-device call emits JSON. */
177
+ function buildFleetForwardedArgs(allArgs) {
178
+ const stripped = stripRoutingFlags(allArgs, STRIP_SPECS);
179
+ if (!stripped.includes('--json'))
180
+ stripped.push('--json');
181
+ return stripped;
182
+ }
183
+ /** Parse stdout as JSON; on failure return an object describing the error. */
184
+ function safeJsonParse(stdout) {
185
+ try {
186
+ return JSON.parse(stdout);
187
+ }
188
+ catch {
189
+ return { parseError: 'invalid JSON', snippet: stdout.trim().slice(0, 200) };
190
+ }
191
+ }
192
+ /** One-line summary of a per-device `agents view [agent] --json` payload. */
193
+ function summarizeViewResult(forwarded, json) {
194
+ // After routing flags are stripped, the agent argument is the first token that
195
+ // is not a flag (e.g. `kimi` in `agents view kimi --json`).
196
+ const agentArg = forwarded.find((a, i) => i > 0 && !a.startsWith('-'));
197
+ // `agents view --json` returns an array; `agents view <agent> --json` returns
198
+ // a single object. Normalize to the per-agent shape.
199
+ const agent = agentArg
200
+ ? (Array.isArray(json) ? json[0] : json)
201
+ : undefined;
202
+ if (!agentArg) {
203
+ const rows = Array.isArray(json) ? json : [];
204
+ const count = rows.reduce((n, r) => n + (r.versions?.length ?? 0), 0);
205
+ return count === 0 ? 'no agents installed' : `${count} version${count === 1 ? '' : 's'}`;
206
+ }
207
+ if (!agent || !Array.isArray(agent.versions) || agent.versions.length === 0) {
208
+ return 'not installed';
209
+ }
210
+ const v = agent.versions.find((x) => x.isDefault) ?? agent.versions[0];
211
+ const parts = [chalk.cyan(String(v.version))];
212
+ if (v.signedIn) {
213
+ parts.push(chalk.green('active'));
214
+ if (v.email)
215
+ parts.push(chalk.gray(String(v.email)));
216
+ }
217
+ else {
218
+ parts.push(chalk.gray('signed out'));
219
+ }
220
+ return parts.join(' · ');
221
+ }
222
+ function formatCompactUsd(usd) {
223
+ if (usd >= 1000)
224
+ return `$${(usd / 1000).toFixed(1)}k`;
225
+ if (usd >= 1)
226
+ return `$${usd.toFixed(2)}`;
227
+ return `$${usd.toFixed(3)}`;
228
+ }
229
+ function formatCompactTokens(n) {
230
+ const abs = Math.abs(n);
231
+ if (abs >= 1e9)
232
+ return `${(n / 1e9).toFixed(1)}B`;
233
+ if (abs >= 1e6)
234
+ return `${(n / 1e6).toFixed(1)}M`;
235
+ if (abs >= 1e3)
236
+ return `${(n / 1e3).toFixed(1)}K`;
237
+ return String(n);
238
+ }
239
+ /** One-line summary of a per-device `agents output --json` payload. */
240
+ function summarizeOutputResult(json) {
241
+ const p = json;
242
+ const burn = p?.burn;
243
+ if (!burn || typeof burn !== 'object')
244
+ return 'ok';
245
+ const parts = [];
246
+ if (typeof burn.costUsd === 'number')
247
+ parts.push(`${formatCompactUsd(burn.costUsd)} burned`);
248
+ if (typeof burn.outputTokens === 'number')
249
+ parts.push(`${formatCompactTokens(burn.outputTokens)} output tokens`);
250
+ const commits = p?.output?.commits;
251
+ if (typeof commits === 'number' && commits > 0)
252
+ parts.push(`${commits} commits`);
253
+ return parts.length ? parts.join(' · ') : 'ok';
254
+ }
255
+ /** Best-effort summary of any per-device JSON payload. */
256
+ function summarizeResult(command, forwarded, json) {
257
+ if (command === 'view')
258
+ return summarizeViewResult(forwarded, json);
259
+ if (command === 'output')
260
+ return summarizeOutputResult(json);
261
+ return 'ok';
262
+ }
263
+ const GROUP_ORDER = ['macOS', 'Linux', 'Windows', 'Other'];
264
+ /** Render the grouped-by-OS fleet roster from per-device results. */
265
+ function renderFleetRoster(command, forwarded, results, self) {
266
+ const agentArg = forwarded[1];
267
+ const installed = results.filter((r) => r.status === 'ok').length;
268
+ const skipped = results.filter((r) => r.status === 'skipped').length;
269
+ const failed = results.filter((r) => r.status === 'failed').length;
270
+ const title = agentArg ? `${command} ${agentArg}` : command;
271
+ const summaryParts = [];
272
+ if (installed)
273
+ summaryParts.push(`${installed} installed`);
274
+ if (failed)
275
+ summaryParts.push(`${failed} unreachable`);
276
+ if (skipped)
277
+ summaryParts.push(`${skipped} skipped`);
278
+ console.log(chalk.bold(title) + chalk.gray(` · ${results.length} device${results.length === 1 ? '' : 's'}`));
279
+ console.log('');
280
+ const nameW = Math.max(6, ...results.map((r) => r.name.length));
281
+ const grouped = new Map();
282
+ for (const r of results) {
283
+ const g = platformGroupLabel(r.device.platform);
284
+ (grouped.get(g) ?? grouped.set(g, []).get(g)).push(r);
285
+ }
286
+ for (const group of GROUP_ORDER) {
287
+ const members = grouped.get(group);
288
+ if (!members || members.length === 0)
289
+ continue;
290
+ members.sort((a, b) => {
291
+ const aSelf = a.name.toLowerCase() === self.toLowerCase();
292
+ const bSelf = b.name.toLowerCase() === self.toLowerCase();
293
+ return (aSelf ? -1 : bSelf ? 1 : 0) || a.name.localeCompare(b.name);
294
+ });
295
+ console.log(chalk.bold(group));
296
+ for (const r of members) {
297
+ const isSelf = r.name.toLowerCase() === self.toLowerCase();
298
+ const prefix = isSelf ? chalk.cyan('▸') : ' ';
299
+ let glyph;
300
+ let text;
301
+ if (r.status === 'skipped') {
302
+ glyph = chalk.gray('○');
303
+ text = chalk.gray(String(r.reason ?? 'skipped'));
304
+ }
305
+ else if (r.status === 'failed') {
306
+ glyph = chalk.red('✕');
307
+ text = chalk.red(String(r.error ?? 'unreachable').split('\n')[0].slice(0, 80));
308
+ }
309
+ else {
310
+ glyph = chalk.green('●');
311
+ text = summarizeResult(command, forwarded, r.value);
312
+ }
313
+ const selfNote = isSelf ? chalk.cyan(' ← this machine') : '';
314
+ console.log(` ${prefix} ${r.name.padEnd(nameW)} ${glyph} ${text}${selfNote}`);
315
+ }
316
+ console.log('');
317
+ }
318
+ if (summaryParts.length) {
319
+ console.log(chalk.gray(summaryParts.join(' · ')));
320
+ }
321
+ }
322
+ /** Run `agents <command> …` across every registered device and render the roster. */
323
+ export async function runFleetPassthrough(command, allArgs, spec, opts = {}) {
324
+ const self = opts.self ?? machineId();
325
+ const registry = await (opts.loadDevices ?? loadDevices)();
326
+ const planned = planFleetTargets(registry);
327
+ const targets = planned.map((t) => ({
328
+ name: t.device.name,
329
+ device: t.device,
330
+ skip: t.skip,
331
+ }));
332
+ const forwarded = buildFleetForwardedArgs(allArgs);
333
+ const runner = opts.runner ?? runOnDevice;
334
+ const localRunner = opts.localRunner ?? runLocalCommand;
335
+ const results = await fanOutDevices(targets, async (target) => {
336
+ const cmd = ['agents', ...forwarded];
337
+ const isSelf = target.device.name.toLowerCase() === self.toLowerCase();
338
+ const res = isSelf ? localRunner(cmd) : runner(target.device, cmd);
339
+ if (res.code !== 0) {
340
+ const detail = (res.stderr || res.stdout || 'unreachable').trim().slice(0, 200);
341
+ throw new Error(detail || 'unreachable');
342
+ }
343
+ return safeJsonParse(res.stdout);
344
+ }, { perDeviceTimeoutMs: 120_000 });
345
+ // Map fan-out results back to typed results with device attached for rendering.
346
+ const typedResults = results.map((r, i) => ({
347
+ ...r,
348
+ device: targets[i].device,
349
+ }));
350
+ if (allArgs.includes('--json')) {
351
+ const out = {};
352
+ for (const r of typedResults) {
353
+ out[r.name] = r.status === 'ok' ? r.value : { error: r.error ?? r.reason ?? 'unknown' };
354
+ }
355
+ console.log(JSON.stringify(out, null, 2));
356
+ }
357
+ else {
358
+ renderFleetRoster(command, forwarded, typedResults, self);
359
+ }
360
+ const anyFailed = typedResults.some((r) => r.status === 'failed');
361
+ process.exitCode = anyFailed ? 1 : 0;
362
+ return true;
363
+ }
158
364
  /**
159
365
  * Route `agents <command> … --host <name>` to a remote if the command is
160
366
  * host-routable and a `--host` (or its `--device` alias) was given. Returns
@@ -167,11 +373,16 @@ async function resolveTargetHost(name, any) {
167
373
  * @param command the resolved subcommand name (`process.argv`'s first non-flag).
168
374
  * @param allArgs `process.argv.slice(2)` — the command name followed by its args.
169
375
  */
170
- export async function maybeRunOnHost(command, allArgs) {
376
+ export async function maybeRunOnHost(command, allArgs, opts) {
171
377
  const hostFlag = flagValue(allArgs, 'host', 'H');
172
378
  const deviceFlag = flagValue(allArgs, 'device');
379
+ const hostsFlag = flagValue(allArgs, 'hosts');
380
+ const devicesFlag = flagValue(allArgs, 'devices');
173
381
  const hostName = hostFlag ?? deviceFlag;
174
- if (!hostName)
382
+ const fleetAll = isFleetAllSentinel(hostFlag, deviceFlag, hostsFlag, devicesFlag);
383
+ // Proceed when any routing flag is present, including the plural fleet flags
384
+ // that may carry the `all` sentinel.
385
+ if (!hostName && !hostsFlag && !devicesFlag)
175
386
  return false;
176
387
  // Commands with their own richer --host semantics must reach local commander
177
388
  // BEFORE any single-target conflict gate. sessions/feed merge --host and
@@ -193,14 +404,18 @@ export async function maybeRunOnHost(command, allArgs) {
193
404
  return false;
194
405
  }
195
406
  }
196
- // `--hosts` is always a generic fleet flag bail for every command so the
197
- // local aggregator handles it. `--devices` is fan-out on most commands but
198
- // a placement flag on `routines` (which devices may run the routine), so
199
- // only exempt routines from the bail.
200
- if (allArgs.includes('--hosts'))
201
- return false;
202
- if (allArgs.includes('--devices') && command !== 'routines')
407
+ // `--hosts` / `--devices` are command-level fleet flags unless their value is
408
+ // the `all` sentinel, which this module fans out generically. On `routines`,
409
+ // a non-all `--devices` value is placement (which devices may run the routine).
410
+ if (allArgs.includes('--hosts') && hostsFlag?.toLowerCase() !== 'all')
203
411
  return false;
412
+ if (allArgs.includes('--devices')) {
413
+ if (devicesFlag === undefined)
414
+ return false; // malformed, let commander error
415
+ const isAll = devicesFlag.toLowerCase() === 'all';
416
+ if (!isAll && command !== 'routines')
417
+ return false;
418
+ }
204
419
  const spec = REMOTE_PASSTHROUGH[command];
205
420
  if (!spec) {
206
421
  // Flag was accepted (no raw commander "unknown option") but this group has
@@ -210,13 +425,23 @@ export async function maybeRunOnHost(command, allArgs) {
210
425
  process.exitCode = 1;
211
426
  return true;
212
427
  }
213
- // Single-target remote path only: reject a conflicting --host/--device pair
214
- // rather than silently preferring one (same rule as `agents run`).
428
+ // Reject a conflicting --host/--device pair before either the fleet fan-out
429
+ // or single-target path runs. Equal values (e.g. both `all`) are allowed.
215
430
  if (hostFlag && deviceFlag && hostFlag !== deviceFlag) {
216
431
  console.error(chalk.red('Conflicting --host/--device values — pass just one.'));
217
432
  process.exitCode = 1;
218
433
  return true;
219
434
  }
435
+ // Generic fleet fan-out for the `all` sentinel — before single-host resolution
436
+ // so `all` is never treated as a literal hostname.
437
+ if (fleetAll) {
438
+ return runFleetPassthrough(command, allArgs, spec, opts);
439
+ }
440
+ // After the bailouts and fleet fan-out above, the only remaining path is a
441
+ // single-target --host/--device. Guard for the type checker: plural non-all
442
+ // flags and bare flags were already handled.
443
+ if (!hostName)
444
+ return false;
220
445
  // Running against your own machine is just a local run — skip the SSH round-trip.
221
446
  // `machineId()` is the same self-identifier the device registry and session
222
447
  // sync use (lowercased short hostname); compare case-insensitively.
@@ -49,6 +49,8 @@ export interface HostPromptRun {
49
49
  sessionId?: string;
50
50
  /** Working directory on the host, already made remote-portable by the caller. */
51
51
  remoteCwd?: string;
52
+ /** `remoteCwd` was derived from the local cwd — mirror it, don't fail on it. */
53
+ mirrorCwd?: boolean;
52
54
  /** Stream progress and block until completion (default true). */
53
55
  follow?: boolean;
54
56
  timeoutMs?: number;
@@ -85,6 +85,7 @@ export async function dispatchPromptToHost(host, opts) {
85
85
  mode: opts.mode,
86
86
  model: opts.model,
87
87
  remoteCwd: opts.remoteCwd,
88
+ mirrorCwd: opts.mirrorCwd,
88
89
  sessionId: forcedSessionId,
89
90
  emitSessionId,
90
91
  name: opts.name,
@@ -2,7 +2,7 @@ import type { ActiveSession } from '../session/active.js';
2
2
  import type { OpenBlock } from '../feed.js';
3
3
  import type { AgentStatusDetail, TaskInfo } from '../teams/api.js';
4
4
  export type HqRoomKind = 'team' | 'machine' | 'lobby';
5
- export type HqAgentMood = 'working' | 'waiting' | 'blocked' | 'celebrating' | 'idle';
5
+ export type HqAgentMood = 'working' | 'waiting' | 'blocked' | 'celebrating' | 'done' | 'idle';
6
6
  export type HqAmbientKind = 'needs_input' | 'error' | 'pr' | 'team_active' | 'idle_scene';
7
7
  export interface HqAction {
8
8
  id: string;
@@ -28,6 +28,10 @@ function teammateName(session, teammatesById) {
28
28
  return session.agentId;
29
29
  }
30
30
  function moodForSession(session, hasOpenBlock) {
31
+ if (session.status === 'abandoned')
32
+ return 'blocked';
33
+ if (session.status === 'closed')
34
+ return 'done';
31
35
  if (session.status === 'input_required' || session.activity === 'waiting_input' || hasOpenBlock)
32
36
  return 'waiting';
33
37
  if (session.rateLimited)
@@ -175,6 +179,7 @@ export function buildHqFloor(input) {
175
179
  }
176
180
  }
177
181
  for (const room of rooms.values()) {
182
+ const roomAgents = agents.filter((agent) => agent.roomId === room.id);
178
183
  if (room.kind === 'team' && room.counts.running > 0) {
179
184
  ambientEvents.push({
180
185
  id: `team-active:${room.id}`,
@@ -184,7 +189,7 @@ export function buildHqFloor(input) {
184
189
  intensity: 'low',
185
190
  });
186
191
  }
187
- if (room.counts.agents > 0 && room.counts.running === 0 && room.counts.needsInput === 0) {
192
+ if (roomAgents.length > 0 && roomAgents.every((agent) => agent.mood === 'idle')) {
188
193
  ambientEvents.push({
189
194
  id: `idle:${room.id}`,
190
195
  kind: 'idle_scene',
package/dist/lib/mcp.js CHANGED
@@ -18,7 +18,6 @@ import { getBinaryPath, getVersionHomePath } from './versions.js';
18
18
  import { IS_WINDOWS, execFileShellSpec } from './platform/index.js';
19
19
  import { AGENTS, getMcpConfigPathForHome, getProjectMcpConfigPath } from './agents.js';
20
20
  import { isCapable } from './capabilities.js';
21
- import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
22
21
  /**
23
22
  * Parse an MCP server config from a YAML file.
24
23
  */
@@ -401,7 +400,7 @@ export async function registerMcpCommandToTargets(targets, name, commandSpec, sc
401
400
  function registerMcpCommand(agentId, name, commandSpec, scope, transport, options = {}) {
402
401
  try {
403
402
  validateMcpServerName(name);
404
- if (agentId === 'hermes' || agentId === 'forge') {
403
+ if (agentId === 'hermes') {
405
404
  const server = {
406
405
  name,
407
406
  path: '',
@@ -413,12 +412,7 @@ function registerMcpCommand(agentId, name, commandSpec, scope, transport, option
413
412
  : { command: commandSpec.command, args: commandSpec.args }),
414
413
  },
415
414
  };
416
- if (agentId === 'hermes') {
417
- installMcpToHermesConfig(server, options.home || os.homedir());
418
- }
419
- else {
420
- installMcpToForgeConfig(server, options.home || os.homedir());
421
- }
415
+ installMcpToHermesConfig(server, options.home || os.homedir());
422
416
  return { success: true };
423
417
  }
424
418
  const bin = options.binary || AGENTS[agentId].cliCommand;
@@ -436,31 +430,6 @@ function registerMcpCommand(agentId, name, commandSpec, scope, transport, option
436
430
  return { success: false, error: err.message };
437
431
  }
438
432
  }
439
- /**
440
- * Install MCP server to Gemini config file.
441
- */
442
- function installMcpToGeminiConfig(server, versionHome) {
443
- const configPath = path.join(versionHome, '.gemini', 'settings.json');
444
- updateGeminiSettings(configPath, (config) => {
445
- setGeminiAutoUpdateDisabled(config);
446
- if (!config.mcpServers || typeof config.mcpServers !== 'object') {
447
- config.mcpServers = {};
448
- }
449
- const mcpServers = config.mcpServers;
450
- if (server.config.transport === 'stdio') {
451
- mcpServers[server.name] = {
452
- command: server.config.command,
453
- args: server.config.args || [],
454
- env: server.config.env || {},
455
- };
456
- }
457
- else {
458
- mcpServers[server.name] = {
459
- url: server.config.url,
460
- };
461
- }
462
- });
463
- }
464
433
  /**
465
434
  * Install MCP server to Cursor config file.
466
435
  */
@@ -574,31 +543,6 @@ function installMcpToHermesConfig(server, versionHome) {
574
543
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
575
544
  fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
576
545
  }
577
- function installMcpToForgeConfig(server, versionHome) {
578
- const configPath = path.join(versionHome, '.forge', '.mcp.json');
579
- let config = {};
580
- if (fs.existsSync(configPath)) {
581
- config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
582
- }
583
- if (!config.mcpServers || typeof config.mcpServers !== 'object' || Array.isArray(config.mcpServers)) {
584
- config.mcpServers = {};
585
- }
586
- const mcpServers = config.mcpServers;
587
- if (server.config.transport === 'stdio') {
588
- mcpServers[server.name] = {
589
- command: server.config.command,
590
- args: server.config.args || [],
591
- ...(server.config.env && { env: server.config.env }),
592
- };
593
- }
594
- else {
595
- mcpServers[server.name] = {
596
- url: server.config.url,
597
- };
598
- }
599
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
600
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
601
- }
602
546
  function installMcpToOpenCodeConfig(server, versionHome) {
603
547
  const configPath = path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
604
548
  let config = {};
@@ -643,10 +587,8 @@ function writeMcpConfigSupportsAgent(agentId) {
643
587
  switch (agentId) {
644
588
  case 'claude':
645
589
  case 'cursor':
646
- case 'gemini':
647
590
  case 'kimi':
648
591
  case 'droid':
649
- case 'forge':
650
592
  case 'openclaw':
651
593
  case 'codex':
652
594
  case 'grok':
@@ -672,10 +614,8 @@ export function writeMcpConfig(agentId, configPath, servers, mode = 'overwrite')
672
614
  switch (agentId) {
673
615
  case 'claude':
674
616
  case 'cursor':
675
- case 'gemini':
676
617
  case 'kimi':
677
- case 'droid':
678
- case 'forge': {
618
+ case 'droid': {
679
619
  let config = {};
680
620
  if (fs.existsSync(configPath)) {
681
621
  try {
@@ -888,10 +828,6 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
888
828
  installMcpViaCodex(binaryPath, server, versionHome);
889
829
  handled = true;
890
830
  }
891
- else if (agentId === 'gemini') {
892
- installMcpToGeminiConfig(server, versionHome);
893
- handled = true;
894
- }
895
831
  else if (agentId === 'cursor') {
896
832
  installMcpToCursorConfig(server, versionHome);
897
833
  handled = true;
@@ -943,10 +879,6 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
943
879
  installMcpToHermesConfig(server, versionHome);
944
880
  handled = true;
945
881
  }
946
- else if (agentId === 'forge') {
947
- installMcpToForgeConfig(server, versionHome);
948
- handled = true;
949
- }
950
882
  // Project-layer servers also get merged into the agent's project-level
951
883
  // config (e.g., .mcp.json, .codex/config.toml) so the CLI discovers them
952
884
  // when run inside the repo.
@@ -124,6 +124,28 @@ export declare function disableMenubarService(): void;
124
124
  * Best-effort — never throws into startup.
125
125
  */
126
126
  export declare function installMenubarLaunchAgentOnUpgrade(): void;
127
+ /** A live MenubarHelper process: its pid and the executable it is running. */
128
+ export interface MenubarProcess {
129
+ pid: number;
130
+ executable: string;
131
+ }
132
+ /**
133
+ * Split the live MenubarHelper processes into the installed bundle's own
134
+ * (`running`) and every other copy (`foreign`).
135
+ *
136
+ * `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
137
+ * the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
138
+ * still reported a healthy `running: yes` — the paste was dead and nothing said
139
+ * so. A foreign copy is the thing to look for, so name it.
140
+ *
141
+ * Identity comes from `comm` (the resolved executable), never from a substring
142
+ * of the command line: matching the latter flags any shell that merely mentions
143
+ * MenubarHelper. `command` is consulted only to drop `--notify` one-shots.
144
+ */
145
+ export declare function classifyMenubarProcesses(commOutput: string, commandOutput: string, installedExec: string): {
146
+ running: boolean;
147
+ foreign: MenubarProcess[];
148
+ };
127
149
  export interface MenubarStatus {
128
150
  platform: string;
129
151
  source: string | null;
@@ -133,6 +155,8 @@ export interface MenubarStatus {
133
155
  stale: boolean;
134
156
  serviceInstalled: boolean;
135
157
  running: boolean;
158
+ /** Live MenubarHelper processes that are NOT the installed bundle. */
159
+ foreignInstances: MenubarProcess[];
136
160
  disabledByUser: boolean;
137
161
  }
138
162
  export declare function getMenubarStatus(): MenubarStatus;
@@ -441,12 +441,61 @@ export function installMenubarLaunchAgentOnUpgrade() {
441
441
  /* never block startup on the menu bar */
442
442
  }
443
443
  }
444
+ /** Parse `ps -axo pid=,<field>=` into pid -> field. The field is the rest of the
445
+ * line, so a path containing spaces (App Support does) survives intact. */
446
+ function parsePsLines(psOutput) {
447
+ const out = new Map();
448
+ for (const line of psOutput.split('\n')) {
449
+ const m = /^\s*(\d+)\s+(.+)$/.exec(line);
450
+ if (m)
451
+ out.set(Number(m[1]), m[2]);
452
+ }
453
+ return out;
454
+ }
455
+ /**
456
+ * Split the live MenubarHelper processes into the installed bundle's own
457
+ * (`running`) and every other copy (`foreign`).
458
+ *
459
+ * `pgrep -f MenubarHelper` conflated the two, so a stray dev build could hold
460
+ * the global Cmd-Shift-V chord (RegisterEventHotKey is first-come) while status
461
+ * still reported a healthy `running: yes` — the paste was dead and nothing said
462
+ * so. A foreign copy is the thing to look for, so name it.
463
+ *
464
+ * Identity comes from `comm` (the resolved executable), never from a substring
465
+ * of the command line: matching the latter flags any shell that merely mentions
466
+ * MenubarHelper. `command` is consulted only to drop `--notify` one-shots.
467
+ */
468
+ export function classifyMenubarProcesses(commOutput, commandOutput, installedExec) {
469
+ const commands = parsePsLines(commandOutput);
470
+ const foreign = [];
471
+ let running = false;
472
+ for (const [pid, executable] of parsePsLines(commOutput)) {
473
+ if (path.basename(executable) !== 'MenubarHelper')
474
+ continue;
475
+ // `--notify` is a one-shot that posts a notification and exits; it runs the
476
+ // installed binary but never claims the status item or the chords.
477
+ if ((commands.get(pid) || '').includes('--notify'))
478
+ continue;
479
+ if (executable === installedExec)
480
+ running = true;
481
+ else
482
+ foreign.push({ pid, executable });
483
+ }
484
+ return { running, foreign };
485
+ }
444
486
  export function getMenubarStatus() {
445
487
  const dest = installedAppPath();
446
488
  let running = false;
489
+ let foreignInstances = [];
447
490
  if (onDarwin()) {
448
- const r = spawnSync('pgrep', ['-f', 'MenubarHelper'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' });
449
- running = r.status === 0 && (r.stdout || '').trim().length > 0;
491
+ const ps = (format) => spawnSync('ps', ['-axo', format], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf-8' });
492
+ const comm = ps('pid=,comm=');
493
+ const command = ps('pid=,command=');
494
+ if (comm.status === 0 && command.status === 0) {
495
+ const c = classifyMenubarProcesses(comm.stdout || '', command.stdout || '', installedExecutablePath());
496
+ running = c.running;
497
+ foreignInstances = c.foreign;
498
+ }
450
499
  }
451
500
  const serviceInstalled = menubarServiceInstalled();
452
501
  return {
@@ -458,6 +507,7 @@ export function getMenubarStatus() {
458
507
  stale: onDarwin() && serviceInstalled && menubarSetupStale(),
459
508
  serviceInstalled,
460
509
  running,
510
+ foreignInstances,
461
511
  disabledByUser: menubarDisabledByUser(),
462
512
  };
463
513
  }
@@ -16,6 +16,7 @@
16
16
  * 'error' listener — without it Node re-throws ENOENT as an uncaught exception and
17
17
  * takes the whole daemon down (the exact crash overdue.test.ts guards).
18
18
  */
19
+ import { type ChildProcess } from 'child_process';
19
20
  export interface DesktopNotification {
20
21
  /** Bold first line. */
21
22
  title: string;
@@ -36,6 +37,20 @@ export interface DesktopNotification {
36
37
  export declare function buildMenubarNotifyArgs(n: DesktopNotification): string[];
37
38
  /** AppleScript for the osascript degradation path. Exported for tests. */
38
39
  export declare function buildOsascriptNotifyArgs(n: DesktopNotification): string[];
40
+ /**
41
+ * Spawn one detached, best-effort notifier process with a bounded lifetime.
42
+ *
43
+ * The child is detached + unref'd so it never blocks the daemon, but — unlike a
44
+ * pure fire-and-forget — it is supervised: a watchdog SIGKILLs it after
45
+ * `timeoutMs` so a stalled notifier can never linger (the pile-up this fixes).
46
+ * The common path (a sub-second post + self-exit) clears the watchdog on the
47
+ * child's own 'exit', so the kill only ever fires for a genuinely hung child.
48
+ *
49
+ * `timeoutMs` is injectable so the bounded-lifetime behaviour is testable against
50
+ * a real long-running child without a multi-second wait. Returns the child so a
51
+ * caller/test can observe it; callers in this module ignore it.
52
+ */
53
+ export declare function spawnDetachedQuiet(command: string, args: string[], timeoutMs?: number): ChildProcess;
39
54
  /**
40
55
  * Fire a native desktop notification, branded via the MenubarHelper companion on
41
56
  * macOS. Best-effort — any failure is swallowed so a notification hiccup never