@phnx-labs/agents-cli 1.20.77 → 1.20.82

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 (212) hide show
  1. package/CHANGELOG.md +589 -0
  2. package/README.md +27 -25
  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 +74 -0
  12. package/dist/commands/doctor.js +718 -40
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +53 -8
  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/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +195 -11
  29. package/dist/commands/routines.js +16 -4
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +108 -34
  33. package/dist/commands/sessions-browser.d.ts +45 -3
  34. package/dist/commands/sessions-browser.js +118 -12
  35. package/dist/commands/sessions-export.d.ts +3 -0
  36. package/dist/commands/sessions-export.js +13 -10
  37. package/dist/commands/sessions-inject.d.ts +7 -4
  38. package/dist/commands/sessions-inject.js +18 -18
  39. package/dist/commands/sessions-picker.d.ts +15 -1
  40. package/dist/commands/sessions-picker.js +90 -7
  41. package/dist/commands/sessions-resume.d.ts +16 -0
  42. package/dist/commands/sessions-resume.js +14 -13
  43. package/dist/commands/sessions.d.ts +126 -1
  44. package/dist/commands/sessions.js +365 -42
  45. package/dist/commands/set.d.ts +15 -0
  46. package/dist/commands/set.js +79 -0
  47. package/dist/commands/setup-fleet.d.ts +21 -0
  48. package/dist/commands/setup-fleet.js +201 -0
  49. package/dist/commands/setup-secrets.d.ts +20 -0
  50. package/dist/commands/setup-secrets.js +223 -0
  51. package/dist/commands/setup.js +17 -3
  52. package/dist/commands/skills.js +1 -1
  53. package/dist/commands/ssh.js +9 -1
  54. package/dist/commands/sync.js +7 -2
  55. package/dist/commands/teams-picker.js +0 -1
  56. package/dist/commands/teams.d.ts +33 -1
  57. package/dist/commands/teams.js +208 -44
  58. package/dist/commands/versions.js +10 -5
  59. package/dist/commands/view.d.ts +1 -0
  60. package/dist/commands/view.js +11 -0
  61. package/dist/index.js +8 -13
  62. package/dist/lib/acp/harnesses.js +0 -7
  63. package/dist/lib/activity.d.ts +156 -0
  64. package/dist/lib/activity.js +282 -0
  65. package/dist/lib/agents.d.ts +9 -23
  66. package/dist/lib/agents.js +72 -100
  67. package/dist/lib/auto-pull.d.ts +16 -8
  68. package/dist/lib/auto-pull.js +23 -28
  69. package/dist/lib/browser/service.js +3 -0
  70. package/dist/lib/browser/types.d.ts +7 -0
  71. package/dist/lib/capabilities.js +6 -2
  72. package/dist/lib/crabbox/lease.js +2 -2
  73. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  74. package/dist/lib/crabbox/setup-copy.js +4 -4
  75. package/dist/lib/daemon.d.ts +26 -40
  76. package/dist/lib/daemon.js +63 -123
  77. package/dist/lib/devices/fleet-divergence.d.ts +101 -0
  78. package/dist/lib/devices/fleet-divergence.js +188 -0
  79. package/dist/lib/devices/fleet-inventory.d.ts +19 -0
  80. package/dist/lib/devices/fleet-inventory.js +57 -0
  81. package/dist/lib/devices/fleet.d.ts +1 -1
  82. package/dist/lib/devices/fleet.js +1 -1
  83. package/dist/lib/devices/health-report.d.ts +10 -2
  84. package/dist/lib/devices/health-report.js +32 -1
  85. package/dist/lib/doctor-diff.d.ts +20 -0
  86. package/dist/lib/doctor-diff.js +6 -1
  87. package/dist/lib/drift.d.ts +31 -11
  88. package/dist/lib/drift.js +58 -7
  89. package/dist/lib/events.d.ts +8 -1
  90. package/dist/lib/events.js +10 -1
  91. package/dist/lib/exec.js +35 -8
  92. package/dist/lib/feed-post.d.ts +28 -1
  93. package/dist/lib/feed-post.js +110 -2
  94. package/dist/lib/fleet/apply.js +8 -1
  95. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  96. package/dist/lib/fleet/auth-sync.js +25 -11
  97. package/dist/lib/fleet/remote-login.js +5 -0
  98. package/dist/lib/git.d.ts +27 -14
  99. package/dist/lib/git.js +138 -43
  100. package/dist/lib/hooks.d.ts +42 -0
  101. package/dist/lib/hooks.js +137 -93
  102. package/dist/lib/hosts/dispatch.d.ts +28 -1
  103. package/dist/lib/hosts/dispatch.js +33 -5
  104. package/dist/lib/hosts/option.js +2 -2
  105. package/dist/lib/hosts/passthrough.d.ts +22 -1
  106. package/dist/lib/hosts/passthrough.js +238 -13
  107. package/dist/lib/hosts/remote-cmd.js +2 -0
  108. package/dist/lib/hosts/run-target.d.ts +2 -0
  109. package/dist/lib/hosts/run-target.js +1 -0
  110. package/dist/lib/hq/floor.d.ts +1 -1
  111. package/dist/lib/hq/floor.js +6 -1
  112. package/dist/lib/mcp.js +3 -71
  113. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
  114. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  115. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  116. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  117. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  118. package/dist/lib/menubar/notify-desktop.js +41 -5
  119. package/dist/lib/merged-resources.d.ts +11 -0
  120. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  121. package/dist/lib/permissions.d.ts +0 -32
  122. package/dist/lib/permissions.js +4 -157
  123. package/dist/lib/picker.js +1 -1
  124. package/dist/lib/platform/process.d.ts +16 -5
  125. package/dist/lib/platform/process.js +54 -0
  126. package/dist/lib/plugins.d.ts +0 -8
  127. package/dist/lib/plugins.js +4 -110
  128. package/dist/lib/project-resources.js +4 -1
  129. package/dist/lib/refresh.d.ts +5 -3
  130. package/dist/lib/refresh.js +7 -5
  131. package/dist/lib/resources/commands.js +4 -1
  132. package/dist/lib/resources/mcp.js +0 -4
  133. package/dist/lib/resources/permissions.d.ts +1 -1
  134. package/dist/lib/resources/permissions.js +1 -5
  135. package/dist/lib/resources/rules.js +4 -1
  136. package/dist/lib/resources/skills.js +4 -1
  137. package/dist/lib/resources/subagents.js +4 -0
  138. package/dist/lib/resources/types.d.ts +1 -1
  139. package/dist/lib/routines.d.ts +20 -0
  140. package/dist/lib/routines.js +6 -0
  141. package/dist/lib/run-defaults.d.ts +1 -0
  142. package/dist/lib/run-defaults.js +9 -0
  143. package/dist/lib/runner.d.ts +0 -12
  144. package/dist/lib/runner.js +71 -29
  145. package/dist/lib/sandbox.js +0 -16
  146. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  147. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  148. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  149. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  150. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  151. package/dist/lib/secrets/agent.d.ts +18 -0
  152. package/dist/lib/secrets/agent.js +131 -17
  153. package/dist/lib/secrets/audit.d.ts +46 -0
  154. package/dist/lib/secrets/audit.js +56 -0
  155. package/dist/lib/secrets/bundles.d.ts +12 -9
  156. package/dist/lib/secrets/bundles.js +47 -31
  157. package/dist/lib/secrets/index.d.ts +2 -1
  158. package/dist/lib/secrets/index.js +8 -2
  159. package/dist/lib/secrets/remote.d.ts +75 -2
  160. package/dist/lib/secrets/remote.js +139 -5
  161. package/dist/lib/secrets/scope.d.ts +26 -0
  162. package/dist/lib/secrets/scope.js +29 -0
  163. package/dist/lib/secrets/session-store.d.ts +10 -0
  164. package/dist/lib/secrets/session-store.js +64 -11
  165. package/dist/lib/secrets/sync.js +3 -3
  166. package/dist/lib/session/active.d.ts +117 -26
  167. package/dist/lib/session/active.js +199 -53
  168. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  169. package/dist/lib/session/actor-sidecar.js +101 -0
  170. package/dist/lib/session/db.d.ts +41 -1
  171. package/dist/lib/session/db.js +173 -8
  172. package/dist/lib/session/digest.js +126 -21
  173. package/dist/lib/session/discover.d.ts +19 -2
  174. package/dist/lib/session/discover.js +32 -11
  175. package/dist/lib/session/hook-sessions.d.ts +9 -0
  176. package/dist/lib/session/hook-sessions.js +57 -8
  177. package/dist/lib/session/origin-machine.d.ts +18 -0
  178. package/dist/lib/session/origin-machine.js +34 -0
  179. package/dist/lib/session/pid-registry.d.ts +13 -0
  180. package/dist/lib/session/render.d.ts +2 -0
  181. package/dist/lib/session/render.js +1 -1
  182. package/dist/lib/session/state.d.ts +1 -0
  183. package/dist/lib/session/state.js +1 -1
  184. package/dist/lib/session/sync/config.js +2 -2
  185. package/dist/lib/session/types.d.ts +16 -0
  186. package/dist/lib/smart-launch.d.ts +86 -0
  187. package/dist/lib/smart-launch.js +172 -0
  188. package/dist/lib/staleness/detectors/commands.js +2 -2
  189. package/dist/lib/staleness/detectors/permissions.js +1 -45
  190. package/dist/lib/staleness/writers/commands.js +4 -5
  191. package/dist/lib/staleness/writers/hooks.js +1 -1
  192. package/dist/lib/startup/command-registry.d.ts +1 -2
  193. package/dist/lib/startup/command-registry.js +2 -4
  194. package/dist/lib/subagents-registry.js +5 -12
  195. package/dist/lib/subagents.d.ts +0 -10
  196. package/dist/lib/subagents.js +0 -12
  197. package/dist/lib/teams/agents.d.ts +0 -2
  198. package/dist/lib/teams/agents.js +10 -27
  199. package/dist/lib/teams/index.d.ts +1 -1
  200. package/dist/lib/teams/index.js +1 -1
  201. package/dist/lib/types.d.ts +17 -9
  202. package/dist/lib/usage.d.ts +28 -5
  203. package/dist/lib/usage.js +271 -8
  204. package/dist/lib/versions.js +7 -1
  205. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  206. package/dist/lib/watchdog/watchdog.js +25 -13
  207. package/package.json +1 -2
  208. package/dist/commands/check.d.ts +0 -15
  209. package/dist/commands/check.js +0 -180
  210. package/dist/commands/resources.d.ts +0 -5
  211. package/dist/lib/secrets/account-token.d.ts +0 -20
  212. package/dist/lib/secrets/account-token.js +0 -64
@@ -19,6 +19,7 @@ import { saveTask, updateTask, terminalPatch } from './tasks.js';
19
19
  import { followHostTask } from './progress.js';
20
20
  import { wrapHostCommandWithCredentials } from './credentials.js';
21
21
  import { hostKeyCheckingOpts } from '../devices/known-hosts.js';
22
+ import { toRemotePortable } from '../project-root.js';
22
23
  // Use $HOME (not ~) so the path is correct whether or not it's quoted and
23
24
  // regardless of the run's cwd. Task ids are 8 hex chars, so these paths are
24
25
  // injection-safe to interpolate unquoted into remote commands.
@@ -39,6 +40,21 @@ function homeRemainder(p) {
39
40
  return p.slice(6);
40
41
  return null;
41
42
  }
43
+ /**
44
+ * Derive the remote directory to mirror from the local cwd, for a host run the
45
+ * caller gave no `--cwd`/`--remote-cwd`.
46
+ *
47
+ * Without this a `--host` run lands in the remote `$HOME`, so an agent launched
48
+ * from a repo starts with no project context and the user has to `cd` by hand.
49
+ * Only a cwd under the LOCAL home is mirrored — that is the part with a
50
+ * meaningful remote analogue (`~/src/x` re-roots onto the remote home). A path
51
+ * outside home returns undefined: `/opt/thing` on this box says nothing about
52
+ * the target's filesystem, so the run keeps the remote home.
53
+ */
54
+ export function deriveMirroredCwd(localCwd) {
55
+ const portable = toRemotePortable(localCwd);
56
+ return homeRemainder(portable) === null ? undefined : portable;
57
+ }
42
58
  /**
43
59
  * Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
44
60
  *
@@ -46,15 +62,26 @@ function homeRemainder(p) {
46
62
  * the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
47
63
  * for that segment — the remote login shell expands it — and shell-quote the
48
64
  * remainder. Any other path (absolute or relative) is quoted verbatim.
65
+ *
66
+ * `mirror` marks a directory the caller DERIVED from the local cwd rather than
67
+ * one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
68
+ * the same home-relative path on both boxes is the common fleet layout, so
69
+ * mirroring lands the remote agent in the project instead of `$HOME`. It is a
70
+ * best-effort mirror by definition — the host may simply not have that checkout
71
+ * — so a missing directory falls back to the remote home instead of failing the
72
+ * run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
73
+ * directory, so a missing one must surface as a `cd` error.
49
74
  */
50
- export function remoteCdPrefix(remoteCwd) {
75
+ export function remoteCdPrefix(remoteCwd, opts = {}) {
51
76
  if (!remoteCwd)
52
77
  return '';
53
78
  const rest = homeRemainder(remoteCwd);
54
79
  if (rest === '')
55
80
  return 'cd "$HOME" && ';
56
- if (rest !== null)
57
- return `cd "$HOME"/${shellQuote(rest)} && `;
81
+ if (rest !== null) {
82
+ const dir = `"$HOME"/${shellQuote(rest)}`;
83
+ return opts.mirror ? `{ cd ${dir} || cd "$HOME"; } && ` : `cd ${dir} && `;
84
+ }
58
85
  return `cd ${shellQuote(remoteCwd)} && `;
59
86
  }
60
87
  /**
@@ -190,7 +217,7 @@ async function launchDetached(host, target, opts) {
190
217
  // resolved actor provenance first so the detached remote run inherits it
191
218
  // instead of re-resolving from this box's SSH_CONNECTION (RUSH-2028).
192
219
  const invocation = ['agents', ...opts.forwardedArgs].map(shellQuote).join(' ');
193
- const cwd = remoteCdPrefix(opts.remoteCwd);
220
+ const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
194
221
  const actorExports = posixEnvExports(withActorEnv());
195
222
  const prelude = actorExports ? `${actorExports}; ` : '';
196
223
  let inner = `${prelude}${cwd}${invocation} > ${remoteLog} 2>&1; echo $? > ${remoteExit}`;
@@ -385,7 +412,7 @@ export async function runInteractiveOnHost(host, opts) {
385
412
  for (const w of warnings)
386
413
  process.stderr.write(`[hosts] warning: ${w}\n`);
387
414
  const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
388
- const cwd = remoteCdPrefix(opts.remoteCwd);
415
+ const cwd = remoteCdPrefix(opts.remoteCwd, { mirror: opts.mirrorCwd });
389
416
  // Forward actor provenance so the interactive remote run inherits it rather
390
417
  // than re-resolving from this box's SSH_CONNECTION (RUSH-2028).
391
418
  const actorExports = posixEnvExports(withActorEnv());
@@ -413,6 +440,7 @@ export async function dispatchToHost(host, opts) {
413
440
  return launchDetached(host, target, {
414
441
  forwardedArgs: buildRunForwardedArgs(opts),
415
442
  remoteCwd: opts.remoteCwd,
443
+ mirrorCwd: opts.mirrorCwd,
416
444
  follow: opts.follow,
417
445
  timeoutMs: opts.timeoutMs,
418
446
  agentLabel: opts.agent,
@@ -12,8 +12,8 @@
12
12
  /** Attach the standard `--host` flag family to a command and return it (chainable). */
13
13
  export function addHostOption(cmd) {
14
14
  return cmd
15
- .option('-H, --host <name>', 'Run this command on another machine over SSH instead of locally — a device, a registered host, or user@host. See `agents devices` / `agents hosts`.')
16
- .option('--device <name>', 'Alias of --host: run this command on a registered device (from `agents devices`).')
15
+ .option('-H, --host <name>', 'Run this command on another machine over SSH instead of locally — a device, a registered host, user@host, or `all` to fan out across every registered device. See `agents devices` / `agents hosts`.')
16
+ .option('--device <name>', 'Alias of --host: run this command on a registered device (from `agents devices`), or `all` to run it across the whole fleet.')
17
17
  .option('--remote-cwd <dir>', "Working directory on the host for --host runs. Resolves on the REMOTE host — pass a '$HOME'-relative path (single-quoted so your local shell doesn't expand it) or a valid remote absolute path; a local ~ expands here and won't exist there (/Users/you vs /home/you). No effect on 'teams add'.")
18
18
  .option('--no-tty', 'Force non-interactive output for --host runs even from a terminal.')
19
19
  .option('--any', 'With --host <cap> (a capability tag), pick any matching host instead of erroring when several match.');
@@ -16,8 +16,28 @@
16
16
  * table or, when `--host`/`--device` is present, exits with a clear
17
17
  * "not supported" message — never commander's raw `unknown option`.
18
18
  */
19
+ import { type DeviceRegistry } from '../devices/registry.js';
20
+ import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
21
+ /** Per-command remote behaviour. Absence from this map = not host-routable here. */
22
+ interface RemoteSpec {
23
+ /** Flags appended when running non-interactively (no local TTY / `--no-tty`). */
24
+ nonInteractive?: string[];
25
+ }
19
26
  /** Pull the value of `--host`/`-H`/`--remote-cwd` (any form) out of an argv. */
20
27
  export declare function flagValue(args: string[], long: string, short?: string): string | undefined;
28
+ /** Injectable dependencies for {@link runFleetPassthrough} — used by tests. */
29
+ export interface FleetPassthroughOptions {
30
+ /** Override the device registry loader (tests). */
31
+ loadDevices?: () => Promise<DeviceRegistry>;
32
+ /** Override the per-device runner (tests). Defaults to `runOnDevice`. */
33
+ runner?: typeof runOnDevice;
34
+ /** Override the local runner for the self device (tests). Defaults to `runLocalCommand`. */
35
+ localRunner?: typeof runLocalCommand;
36
+ /** Override this machine's id (tests). Defaults to `machineId()`. */
37
+ self?: string;
38
+ }
39
+ /** Run `agents <command> …` across every registered device and render the roster. */
40
+ export declare function runFleetPassthrough(command: string, allArgs: string[], spec: RemoteSpec, opts?: FleetPassthroughOptions): Promise<boolean>;
21
41
  /**
22
42
  * Route `agents <command> … --host <name>` to a remote if the command is
23
43
  * host-routable and a `--host` (or its `--device` alias) was given. Returns
@@ -30,4 +50,5 @@ export declare function flagValue(args: string[], long: string, short?: string):
30
50
  * @param command the resolved subcommand name (`process.argv`'s first non-flag).
31
51
  * @param allArgs `process.argv.slice(2)` — the command name followed by its args.
32
52
  */
33
- export declare function maybeRunOnHost(command: string, allArgs: string[]): Promise<boolean>;
53
+ export declare function maybeRunOnHost(command: string, allArgs: string[], opts?: FleetPassthroughOptions): Promise<boolean>;
54
+ export {};
@@ -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.
@@ -116,6 +116,8 @@ export const RUN_OPTION_FORWARDING = {
116
116
  tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
117
117
  copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
118
118
  authCheck: 'local-only', // --no-auth-check gates the local interactive login preflight; --host runs skip that preflight entirely
119
+ // Deprecated alias for --device auto; resolved on the launching box before SSH.
120
+ smart: 'local-only',
119
121
  };
120
122
  /** Actionable messages for value-aware rejections, keyed by attribute name. */
121
123
  export const RUN_OPTION_REJECT_MESSAGES = {
@@ -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.
@@ -8,6 +8,8 @@
8
8
  <string>com.phnx-labs.agents-menubar</string>
9
9
  <key>CFBundleName</key>
10
10
  <string>Agents Menu Bar</string>
11
+ <key>CFBundleIconFile</key>
12
+ <string>AppIcon</string>
11
13
  <key>CFBundlePackageType</key>
12
14
  <string>APPL</string>
13
15
  <key>CFBundleShortVersionString</key>