@phnx-labs/agents-cli 1.22.114 → 1.22.115

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 (176) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +199 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
@@ -25,7 +25,11 @@ import { withActorEnv } from '../lib/hosts/dispatch.js';
25
25
  import { streamAgentsOnHost, passthroughSshOptions, } from '../lib/hosts/passthrough.js';
26
26
  import { buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, stripRoutingFlags, } from '../lib/hosts/remote-cmd.js';
27
27
  import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
28
- import { sshExec, SSH_OPTS } from '../lib/ssh-exec.js';
28
+ import { sshExec, SSH_OPTS, sshStreamWithArgs } from '../lib/ssh-exec.js';
29
+ import { getBrowserRuntimeDir } from '../lib/state.js';
30
+ import { buildSshInvocation, writeAskpassShim } from '../lib/devices/connect.js';
31
+ import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
32
+ import { resolveDeviceTarget } from '../lib/devices/resolve-target.js';
29
33
  import { flagValue } from '../lib/hosts/routing-flag.js';
30
34
  import { browserTaskPicker } from './browser-picker.js';
31
35
  import { assertRemoteControlAllowed, isFleetRemoteInvocation } from '../lib/browser/remote-control.js';
@@ -35,7 +39,7 @@ import { registerCommandGroups, setHelpSections } from '../lib/help.js';
35
39
  import { buildHar } from '../lib/browser/har.js';
36
40
  import { getCliVersion } from '../lib/version.js';
37
41
  import { runBrowserIPCStream } from '../lib/browser/stream.js';
38
- import { machineId } from '../lib/machine-id.js';
42
+ import { machineId, normalizeHost } from '../lib/machine-id.js';
39
43
  import { isArcRunning } from '../lib/browser/drivers/arc.js';
40
44
  import { resolveBrowserTarget } from '../lib/browser/resolve-target.js';
41
45
  /**
@@ -179,6 +183,158 @@ export function remoteStartTaskName(stdout, explicit) {
179
183
  }
180
184
  return trimmed.split('\n').map((line) => line.trim()).find(Boolean);
181
185
  }
186
+ /**
187
+ * Fetch an absolute remote file into a private local path, bounded.
188
+ *
189
+ * Bounded for real, not by a pre-flight `stat`: the byte budget is enforced on the
190
+ * stream as it arrives (`sshExecRawStream` + `cat`), so a file that grows between
191
+ * a size check and the copy — or a peer that lies about the size — still cannot
192
+ * spend more than the budget. `scp` could not do this; it would transfer
193
+ * everything before this process got a chance to refuse.
194
+ *
195
+ * The local file is written 0600 inside a 0700 directory, because a capture can
196
+ * show anything that was on the peer's screen and a world-readable temp path
197
+ * would publish it to every local user.
198
+ */
199
+ /**
200
+ * The command that streams one file's RAW BYTES to stdout on the device's shell.
201
+ *
202
+ * Platform-correct, not one command with a portable-looking name. On Windows
203
+ * `cat` is an alias for `Get-Content`, which is a TEXT reader: it decodes the
204
+ * file, splits it into lines, and re-encodes on output — so a screenshot arrives
205
+ * corrupted rather than merely reordered. The .NET path opens a `FileStream` and
206
+ * copies it into the raw stdout handle, bypassing PowerShell's object/text
207
+ * pipeline entirely, which is the only way binary survives.
208
+ */
209
+ export function remoteReadFileArgv(remotePath, shell) {
210
+ if (shell === 'powershell') {
211
+ // Single-quoted inside the script with pwsh's own doubling, because this whole
212
+ // script is ONE argv token to the outer powershell.
213
+ const literal = remotePath.replace(/'/g, "''");
214
+ return ['powershell', '-NoProfile', '-Command', [
215
+ '$ErrorActionPreference=' + "'Stop';",
216
+ `$in=[System.IO.File]::OpenRead('${literal}');`,
217
+ 'try{$out=[System.Console]::OpenStandardOutput();$in.CopyTo($out);$out.Flush()}finally{$in.Dispose()}',
218
+ ].join(' ')];
219
+ }
220
+ // `--` so a path that begins with a dash is a path, not an option.
221
+ return ['cat', '--', remotePath];
222
+ }
223
+ /**
224
+ * Largest capture `browser show --device` will pull. A capture is a screenshot,
225
+ * a PDF or a short recording; anything past this is not something a viewer should
226
+ * stream over ssh, and the cap is what makes the transfer BOUNDED rather than
227
+ * "however big the peer's file happens to be".
228
+ */
229
+ export const REMOTE_VIEW_MAX_BYTES = 64 * 1024 * 1024;
230
+ /** Write a whole buffer, looping until it is all out. */
231
+ function writeFully(handle, chunk) {
232
+ let offset = 0;
233
+ while (offset < chunk.length) {
234
+ // `writeSync` may write FEWER bytes than asked (a signal, a pipe boundary),
235
+ // and ignoring the return value silently truncated the file mid-capture.
236
+ const wrote = fs.writeSync(handle, chunk, offset, chunk.length - offset);
237
+ if (wrote <= 0)
238
+ throw new Error('local write made no progress');
239
+ offset += wrote;
240
+ }
241
+ }
242
+ /**
243
+ * Fetch an absolute remote file into a private local path, bounded.
244
+ *
245
+ * Auth is the CANONICAL device path, not a hand-rolled ssh: `buildSshInvocation`
246
+ * supplies the askpass shim for a password-auth box, `-i`/`IdentitiesOnly` for an
247
+ * explicit identity file, and the managed known-hosts pinning. Building the ssh
248
+ * args here instead meant a password-auth device could not be reached at all and
249
+ * an explicit `identityFile` was silently ignored.
250
+ *
251
+ * Bounded for real, not by a pre-flight `stat`: the byte budget is enforced on the
252
+ * stream as it arrives, so a file that grows between a size check and the copy — or
253
+ * a peer that misreports its size — still cannot spend more than the budget.
254
+ *
255
+ * The local file is written 0600 inside a 0700 directory, because a capture can
256
+ * show anything that was on the peer's screen and a world-readable temp path
257
+ * would publish it to every local user.
258
+ */
259
+ export async function fetchRemoteFileForViewing(device, remotePath, localPath, opts = {}) {
260
+ const posixAbsolute = path.posix.isAbsolute(remotePath);
261
+ const windowsAbsolute = /^[A-Za-z]:[\\/]/.test(remotePath) || /^\\\\/.test(remotePath);
262
+ if (!posixAbsolute && !windowsAbsolute) {
263
+ throw new Error(`Remote path must be absolute: got "${remotePath}".\n`
264
+ + 'This machine cannot resolve the peer\'s working directory, so a relative path\n'
265
+ + 'would name a different file there than you meant here.');
266
+ }
267
+ // `resolveDeviceTarget` is the SAME resolver `agents ssh` uses, so it yields the
268
+ // full device profile — platform, shell and auth — that `buildSshInvocation`
269
+ // needs. `resolveHost` returns a hosts-registry `Host`, which carries none of
270
+ // that: with it, a password-auth device could not be reached and an explicit
271
+ // identityFile was silently dropped.
272
+ const resolved = await resolveDeviceTarget(device);
273
+ if (!resolved)
274
+ throw new Error(`Unknown device "${device}". Next: agents devices list`);
275
+ const profile = resolveDeviceProfile(resolved);
276
+ const maxBytes = opts.maxBytes ?? REMOTE_VIEW_MAX_BYTES;
277
+ // A POSIX device cannot read a Windows path and vice versa; saying so here beats
278
+ // a confusing shell error from the peer.
279
+ if (profile.shell === 'powershell' && posixAbsolute && !windowsAbsolute) {
280
+ throw new Error(`"${remotePath}" is a POSIX path but ${device} runs PowerShell. Pass the Windows path (e.g. C:\\Users\\…).`);
281
+ }
282
+ if (profile.shell !== 'powershell' && windowsAbsolute) {
283
+ throw new Error(`"${remotePath}" is a Windows path but ${device} is a POSIX host. Pass the absolute POSIX path.`);
284
+ }
285
+ const resolvedLocal = path.resolve(localPath);
286
+ const dir = path.dirname(resolvedLocal);
287
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
288
+ fs.chmodSync(dir, 0o700);
289
+ const handle = fs.openSync(resolvedLocal, 'w', 0o600);
290
+ let written = 0;
291
+ let overflow = false;
292
+ const controller = new AbortController();
293
+ try {
294
+ // `argv: true` is what makes a path containing a space or a quote survive —
295
+ // the same exact-token delivery `agents ssh --argv` exposes.
296
+ const { args, env } = buildSshInvocation(profile, remoteReadFileArgv(remotePath, profile.shell), writeAskpassShim(), {}, { argv: true });
297
+ const result = await sshStreamWithArgs({
298
+ args,
299
+ env,
300
+ timeoutMs: opts.timeoutMs ?? 120_000,
301
+ signal: controller.signal,
302
+ onStdout: (chunk) => {
303
+ if (overflow)
304
+ return;
305
+ if (written + chunk.length > maxBytes) {
306
+ overflow = true;
307
+ controller.abort();
308
+ return;
309
+ }
310
+ writeFully(handle, chunk);
311
+ written += chunk.length;
312
+ },
313
+ });
314
+ if (overflow) {
315
+ throw new Error(`Refusing to pull ${device}:${remotePath}: larger than the ${maxBytes}-byte view budget.`);
316
+ }
317
+ if (result.timedOut)
318
+ throw new Error(`Timed out pulling ${device}:${remotePath}.`);
319
+ if (result.code !== 0) {
320
+ const detail = result.stderr.toString('utf8').trim();
321
+ throw new Error(`Failed to read ${device}:${remotePath}${detail ? `: ${detail}` : ` (ssh exited ${result.code})`}`);
322
+ }
323
+ if (written === 0)
324
+ throw new Error(`${device}:${remotePath} is empty or was not readable.`);
325
+ }
326
+ catch (error) {
327
+ // Never leave a partial file behind for a viewer to open as if it were whole.
328
+ try {
329
+ fs.unlinkSync(resolvedLocal);
330
+ }
331
+ catch { /* nothing to clean */ }
332
+ throw error;
333
+ }
334
+ finally {
335
+ fs.closeSync(handle);
336
+ }
337
+ }
182
338
  async function pullRemoteFile(device, remotePath, localPath) {
183
339
  const host = await resolveHost(device);
184
340
  if (!host) {
@@ -2071,8 +2227,14 @@ function registerTaskCommands(browser) {
2071
2227
  });
2072
2228
  browser
2073
2229
  .command('show <url>')
2074
- .description('Open a URL for a human to read: goes to browser.viewer (default: browser.profile), and binds no task')
2230
+ .description('Open a URL or local file for a human to read: goes to browser.viewer (default: browser.profile), and binds no task')
2075
2231
  .option('--os-browser', 'Use the OS default handler instead of the configured viewer')
2232
+ // `--device` here names WHERE THE FILE IS, not where to open it. `show` is the
2233
+ // one browser verb narrowly exempt from the task-routing refusal
2234
+ // (`REJECT_DEVICE_MESSAGE`) because it binds no task and has nothing to route:
2235
+ // it is a local viewer, so the flag can only mean "the source lives there".
2236
+ // Every other verb still refuses `--device` — a task is bound at `start`.
2237
+ .option('--device <name>', 'fetch the file from this device, then view it locally (absolute path only)')
2076
2238
  .option('--json', 'Output machine-readable JSON')
2077
2239
  .action(async (url, opts) => {
2078
2240
  // The entry point external tools need. `navigate` binds a task, which the
@@ -2083,20 +2245,48 @@ function registerTaskCommands(browser) {
2083
2245
  // drive letter (`C:\Users\me\plan.html`), so a real path was treated as a
2084
2246
  // URL. A scheme is at least two characters.
2085
2247
  const isLocalFile = !/^[a-z][a-z0-9+.-]+:/i.test(url);
2086
- const outcome = isLocalFile
2087
- ? await showFile(path.resolve(url), { osBrowser: opts.osBrowser })
2248
+ let localTarget = isLocalFile ? path.resolve(url) : undefined;
2249
+ let fetched;
2250
+ if (opts.device) {
2251
+ // A URL is already reachable from here, so `--device` with one is a
2252
+ // contradiction rather than a no-op: it says the source is remote while
2253
+ // naming something that is not a file on that peer.
2254
+ if (!isLocalFile) {
2255
+ console.error(`--device names the device holding a FILE, so it cannot be combined with a URL.\n Drop --device to open ${url} here, or pass an absolute path on ${opts.device}.`);
2256
+ process.exit(1);
2257
+ }
2258
+ // The peer's path, verbatim — NOT path.resolve()'d, which would rewrite it
2259
+ // against this machine's cwd and name a different file there.
2260
+ const remotePath = url;
2261
+ const dir = path.join(getBrowserRuntimeDir(), 'remote-views');
2262
+ fetched = path.join(dir, `${normalizeHost(opts.device)}-${Date.now()}-${path.basename(remotePath) || 'capture'}`);
2263
+ try {
2264
+ await fetchRemoteFileForViewing(opts.device, remotePath, fetched);
2265
+ }
2266
+ catch (error) {
2267
+ console.error(error.message);
2268
+ process.exit(1);
2269
+ }
2270
+ localTarget = fetched;
2271
+ }
2272
+ const outcome = localTarget
2273
+ ? await showFile(localTarget, { osBrowser: opts.osBrowser })
2088
2274
  : await showUrl(url, { osBrowser: opts.osBrowser });
2275
+ // The fetched copy is what a viewer actually opened, so it is what the
2276
+ // caller is told about — pointing at the peer's path would name a file that
2277
+ // is not on this machine.
2278
+ const shown = fetched ?? url;
2089
2279
  if (opts.json) {
2090
- console.log(JSON.stringify(outcome, null, 2));
2280
+ console.log(JSON.stringify({ ...outcome, ...(fetched ? { source: `${opts.device}:${url}`, local: fetched } : {}) }, null, 2));
2091
2281
  }
2092
2282
  else if (outcome.via === 'none') {
2093
- console.error(`Could not open a browser — open this yourself:\n ${url}`);
2283
+ console.error(`Could not open a browser — open this yourself:\n ${shown}`);
2094
2284
  }
2095
2285
  else if (outcome.via === 'profile') {
2096
- console.log(`Shown in ${outcome.profile}: ${url}`);
2286
+ console.log(`Shown in ${outcome.profile}: ${shown}`);
2097
2287
  }
2098
2288
  else {
2099
- console.log(`Opened in the OS default browser: ${url}`);
2289
+ console.log(`Opened in the OS default browser: ${shown}`);
2100
2290
  }
2101
2291
  if (outcome.via === 'none')
2102
2292
  process.exit(1);
@@ -18,9 +18,8 @@
18
18
  * Read-only always: doctor diagnoses, it never mutates. To fix the gaps it
19
19
  * finds — install missing resources, repair Claude-invalid plugin manifests,
20
20
  * refresh stale plugins, reconcile drift, repair hook runtime shims — run
21
- * `agents sync <agent>@all` (the one fixer, a superset of the old `doctor
22
- * --fix`). Run `agents prune cleanup` to act on orphan readouts. `--fix` is a
23
- * deprecated no-op that now errors with a pointer to `agents sync`.
21
+ * `agents sync <agent>@all` (the one fixer). Run `agents prune cleanup` to act
22
+ * on orphan readouts.
24
23
  */
25
24
  import type { Command } from 'commander';
26
25
  import { type FleetInventory } from '../lib/devices/fleet-divergence.js';
@@ -1251,7 +1251,6 @@ export function registerDoctorCommand(program) {
1251
1251
  .description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
1252
1252
  .option('--json', 'Output machine-readable JSON')
1253
1253
  .option('--diff', 'In target mode, include unified diffs for divergent files')
1254
- .option('--fix', '(deprecated) doctor is diagnose-only — this errors with a pointer to `agents sync`, the one fixer')
1255
1254
  .option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,workflows,memory)')
1256
1255
  .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
1257
1256
  .option('--adopt <agent>', "Take over the agent's native launcher that shadows the shim (symlink it to the version-managed shim; reversible with --release)")
@@ -1381,16 +1380,6 @@ export function registerDoctorCommand(program) {
1381
1380
  }
1382
1381
  return;
1383
1382
  }
1384
- // `doctor --fix` moved to `agents sync` — doctor is diagnose-only now.
1385
- // Keep the flag for one release as a signpost: it never heals, it points at
1386
- // the one fixer and exits non-zero so scripts and muscle memory get a clear
1387
- // redirect instead of a silent no-op.
1388
- if (opts.fix) {
1389
- const where = target ? `agents sync ${target}` : 'agents sync <agent>@all';
1390
- console.error(chalk.yellow('`agents doctor --fix` has moved to `agents sync`.'));
1391
- console.error(chalk.gray(` doctor now only diagnoses. To fix, run: ${where}`));
1392
- process.exit(1);
1393
- }
1394
1383
  if (!target) {
1395
1384
  // Singleflight + short-TTL cache for the bare `doctor --json` overview.
1396
1385
  // This overview probes every host CLI, every agent's sign-in, and every
@@ -42,13 +42,11 @@ export interface EventsOptions {
42
42
  limit?: string;
43
43
  json?: boolean;
44
44
  follow?: boolean;
45
- /** @deprecated Prefer --include ops / --exclude activity */
46
- audit?: boolean;
47
45
  include?: string;
48
46
  exclude?: string;
49
47
  }
50
48
  /** Add the one canonical event-reader option surface to a command or alias. */
51
- export declare function addEventsReadOptions(command: Command, includeAuditFlag?: boolean): Command;
49
+ export declare function addEventsReadOptions(command: Command): Command;
52
50
  /** Canonical reader used by `agents events`, `agents events audit`, and `agents logs`. */
53
51
  export declare function runEventsCommand(options: EventsOptions, forceAudit?: boolean): Promise<void>;
54
52
  /** Shared by `events stats` and the `logs stats` alias. */
@@ -195,7 +195,7 @@ function registerEmitSubcommand(events) {
195
195
  });
196
196
  }
197
197
  /** Add the one canonical event-reader option surface to a command or alias. */
198
- export function addEventsReadOptions(command, includeAuditFlag = true) {
198
+ export function addEventsReadOptions(command) {
199
199
  command
200
200
  .option('--include <families>', `Only these families (comma-sep): ${EVENT_FAMILIES.join(', ')}`)
201
201
  .option('--exclude <families>', `Drop these families (comma-sep): ${EVENT_FAMILIES.join(', ')}`)
@@ -211,9 +211,6 @@ export function addEventsReadOptions(command, includeAuditFlag = true) {
211
211
  .option('--limit <n>', 'Max records to show; 0 for no cap (default 50)', '50')
212
212
  .option('--json', 'Output raw records as JSON')
213
213
  .option('-f, --follow', "Tail today's operational log live");
214
- if (includeAuditFlag) {
215
- command.option('--audit', 'Deprecated: operational events only — prefer --include ops or --exclude activity');
216
- }
217
214
  return command;
218
215
  }
219
216
  /** Canonical reader used by `agents events`, `agents events audit`, and `agents logs`. */
@@ -242,11 +239,11 @@ export async function runEventsCommand(options, forceAudit = false) {
242
239
  process.exitCode = 2;
243
240
  return;
244
241
  }
245
- // --audit / forceAudit = ops-only when no family flags. Families own the
246
- // source selection via applyFamilies — never override includeActivity after.
242
+ // forceAudit (`events audit` / `logs`) = ops-only when no family flags. Families
243
+ // own the source selection via applyFamilies — never override includeActivity after.
247
244
  const includeActivity = (includeFamilies !== undefined || excludeFamilies !== undefined)
248
245
  ? true
249
- : !(forceAudit || options.audit);
246
+ : !forceAudit;
250
247
  const fetched = readUnifiedEvents({
251
248
  startDate,
252
249
  eventTypes: options.event && options.event.length ? options.event : undefined,
@@ -53,7 +53,6 @@ interface ExecCommandActionOptions {
53
53
  * @deprecated Hidden alias for `--device auto`. Resolved before host dispatch.
54
54
  * Remove after one release.
55
55
  */
56
- smart?: boolean;
57
56
  acp?: boolean;
58
57
  yes?: boolean;
59
58
  loop?: boolean;
@@ -660,8 +660,6 @@ export function registerRunCommand(program) {
660
660
  // `--on` and `--computer` are hidden aliases of `--device` — same behavior.
661
661
  runCmd.addOption(new Option('--on <name>', 'Alias of --device.').hideHelp());
662
662
  runCmd.addOption(new Option('--computer <name>', 'Alias of --device.').hideHelp());
663
- // Deprecated one-release alias: `agents run … --smart` → treat as `--device auto`.
664
- runCmd.addOption(new Option('--smart', 'Deprecated: use --device auto (affinity host pick).').hideHelp());
665
663
  // Internal: the `--device` dispatch forwards this so the REMOTE run prints its
666
664
  // resolved session id as a one-line stdout sentinel (hosts/session-marker.ts),
667
665
  // letting the launcher relate the remote-created session back to itself for
@@ -1226,7 +1224,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1226
1224
  const defaultPlacement = bareInteractiveRunDefaultsToDeviceAuto(options, { prompt, devicePickerRequested }, { tty: isInteractiveTerminal(), json: options.json });
1227
1225
  if (defaultPlacement)
1228
1226
  options.device = 'auto';
1229
- // --device auto (and deprecated --smart): live fleet pick.
1227
+ // --device auto: live fleet pick.
1230
1228
  // Harness is always the agent the user typed — never auto-picked.
1231
1229
  // Placement failure propagates; an automatic request never becomes local.
1232
1230
  {
@@ -1252,9 +1250,6 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
1252
1250
  console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --device ${machineId()}`));
1253
1251
  process.exit(1);
1254
1252
  }
1255
- if (!options.quiet && result.deprecationSmart) {
1256
- process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
1257
- }
1258
1253
  if (!options.quiet && result.banner) {
1259
1254
  const { hostLabel, deviceHint, acctNote } = result.banner;
1260
1255
  process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
@@ -2064,7 +2059,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
2064
2059
  import('../lib/run-defaults.js'),
2065
2060
  import('../lib/mcp.js'),
2066
2061
  import('../lib/capabilities.js'),
2067
- import('../lib/share/config.js'),
2062
+ import('../lib/share-runtime.js'),
2068
2063
  ]);
2069
2064
  bootMark('run-deps:imported');
2070
2065
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
@@ -1,10 +1,51 @@
1
- import { machineId } from '../lib/machine-id.js';
2
1
  import { setHelpSections } from '../lib/help.js';
3
- import { watchFleetFeed, watchLocalFeed } from '../lib/feed/watch.js';
2
+ import { streamFeedFromHub, waitForHub } from '../lib/feed/hub-server.js';
3
+ import { ensureDaemonStarted } from '../lib/daemon/daemon.js';
4
+ /**
5
+ * Attach to the daemon's shared collector, starting the daemon if it is not up.
6
+ *
7
+ * There is deliberately no in-process fallback: running `watchFleetFeed` here
8
+ * would give this reader its own ssh child per peer, which is exactly the
9
+ * per-caller fan-out the hub exists to collapse (see `lib/feed/hub.ts`). So a
10
+ * missing hub is answered by starting its owner and WAITING for it.
11
+ *
12
+ * The wait is what makes that honest. `ensureDaemonStarted()` returns once the
13
+ * process is spawned, long before it has loaded its services and bound the
14
+ * socket, so retrying immediately raced the bind and reported a daemon that was
15
+ * about to be perfectly healthy as unavailable. The final error is the one from
16
+ * the last real attempt, not the first.
17
+ */
18
+ async function attachToHub(signal, emit, scope) {
19
+ try {
20
+ await streamFeedFromHub({ signal, emit, scope });
21
+ return;
22
+ }
23
+ catch (first) {
24
+ if (signal.aborted)
25
+ return;
26
+ ensureDaemonStarted();
27
+ if (!await waitForHub()) {
28
+ throw new Error(`the shared feed stream did not come up: ${first.message}\n`
29
+ + 'Next: agents daemon status, then agents daemon services enable feed-stream');
30
+ }
31
+ try {
32
+ await streamFeedFromHub({ signal, emit, scope });
33
+ }
34
+ catch (second) {
35
+ throw new Error(`the shared feed stream is unavailable: ${second.message}\n`
36
+ + 'Next: agents daemon status, then agents daemon services enable feed-stream');
37
+ }
38
+ }
39
+ }
4
40
  export function registerFeedWatchCommand(parent) {
5
- const command = parent.command('watch').description('Stream the canonical agent, attention, and activity projection as NDJSON')
41
+ const command = parent.command('watch').description('Stream the canonical agent, attention, tool, and activity projection as NDJSON')
6
42
  .option('--json', 'Emit versioned NDJSON envelopes').option('--local', 'Watch only this machine');
7
- setHelpSections(command, { examples: `agents feed watch --json\nagents feed watch --json --local`, notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect.' });
43
+ setHelpSections(command, {
44
+ examples: `agents feed watch --json\nagents feed watch --json --local`,
45
+ notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect. '
46
+ + 'The fleet stream is served by the daemon\'s shared collector, so several readers cost one connection per peer; '
47
+ + '--local is the per-machine stream the collector itself subscribes to over ssh.',
48
+ });
8
49
  command.action(async (_opts, invoked) => {
9
50
  const opts = invoked.optsWithGlobals();
10
51
  if (!opts.json)
@@ -15,10 +56,13 @@ export function registerFeedWatchCommand(parent) {
15
56
  process.once('SIGINT', stop);
16
57
  process.once('SIGTERM', stop);
17
58
  try {
18
- if (opts.local)
19
- await watchLocalFeed({ scope: machineId(), signal: controller.signal, emit });
20
- else
21
- await watchFleetFeed({ signal: controller.signal, emit });
59
+ // BOTH scopes go through the daemon's shared collectors. `--local` used to
60
+ // run its own `watchLocalFeed`, so every observing box's ssh subscription
61
+ // built a separate activity cursor set, tool watcher and setup subscription
62
+ // on the SAME peer. The local collector is a distinct hub from the fleet one
63
+ // (`feed-stream-service.ts`) and its watcher dials no peer at all, so a
64
+ // local reader can never trigger a fan-out, recursively or otherwise.
65
+ await attachToHub(controller.signal, emit, opts.local ? 'local' : 'fleet');
22
66
  }
23
67
  finally {
24
68
  process.off('SIGINT', stop);
@@ -136,7 +136,7 @@ export function registerFocusCommand(program) {
136
136
  # Resolve latest on yosemite-s0, then pick from that version's sessions
137
137
  agents sessions focus claude@latest --device yosemite-s0
138
138
 
139
- # Attach only — refuse if nothing is joinable (old sessions go)
139
+ # Attach only — refuse if nothing is joinable
140
140
  agents sessions focus a1b2c3d4 --attach-only
141
141
 
142
142
  # Pick from live sessions on this machine only
@@ -1,23 +1,6 @@
1
- /**
2
- * `agents sessions go [id]` — DEPRECATED alias for `agents sessions focus --attach-only`.
3
- *
4
- * `go` was "attach or refuse" (never fork/resume). `focus --attach-only` is exactly
5
- * that behavior, so `go` now prints a deprecation notice and delegates to `focusAction`.
6
- *
7
- * This file still owns the shared reach engine that `focus` imports:
8
- * - `gatherLiveTargets` / `pickLiveTarget` / `buildLivePool` — live-session discovery + picker
9
- * - `jumpTo` — the side-effecting jump: attach the already-running terminal
10
- * local tmux -> attach (switch-client when already inside tmux)
11
- * local Ghostty -> focus its tab (Cmd+<n> via System Events; tab # from ghostty-tabs)
12
- * remote tmux -> ssh -tt + tmux attach (pane->session resolved on the remote)
13
- * otherwise -> hand off to the `UnreachableFallback` (attach-only refuses; focus resumes)
14
- * - `refuseFallback` — the attach-only fallback (remote -> login shell; local -> refuse)
15
- */
16
- import type { Command } from 'commander';
17
1
  import { type ActiveSession } from '../lib/session/active.js';
18
2
  import type { SessionMeta } from '../lib/session/types.js';
19
3
  import { type LiveStatusFilter } from './sessions.js';
20
- export declare function registerGoCommand(program: Command): void;
21
4
  /**
22
5
  * Scope a live-session pool by device and live status. Pure so the `focus`
23
6
  * device/status filters are unit-testable without touching the sweep. `hosts`
@@ -1,10 +1,6 @@
1
1
  /**
2
- * `agents sessions go [id]` — DEPRECATED alias for `agents sessions focus --attach-only`.
3
- *
4
- * `go` was "attach or refuse" (never fork/resume). `focus --attach-only` is exactly
5
- * that behavior, so `go` now prints a deprecation notice and delegates to `focusAction`.
6
- *
7
- * This file still owns the shared reach engine that `focus` imports:
2
+ * The shared live-session reach engine that `sessions focus` / `resume --attach-only`
3
+ * import ("attach or refuse", never fork/resume):
8
4
  * - `gatherLiveTargets` / `pickLiveTarget` / `buildLivePool` — live-session discovery + picker
9
5
  * - `jumpTo` — the side-effecting jump: attach the already-running terminal
10
6
  * local tmux -> attach (switch-client when already inside tmux)
@@ -26,7 +22,6 @@ import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive, matche
26
22
  import { buildPreview } from './sessions-picker.js';
27
23
  import { multiItemPicker } from '../lib/picker.js';
28
24
  import { isPromptCancelled } from './utils.js';
29
- import { focusAction } from './focus.js';
30
25
  import { machineId } from '../lib/session/sync/config.js';
31
26
  import { attachTmux, runTmux } from '../lib/tmux/binary.js';
32
27
  import { ensureSessionHookRepaired, paneExitStatus, teardownIfAgentExited } from '../lib/tmux/session.js';
@@ -36,18 +31,6 @@ import { connectionEndedNotice } from '../lib/hosts/reconnect.js';
36
31
  import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
37
32
  import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
38
33
  const execFileAsync = promisify(execFile);
39
- export function registerGoCommand(program) {
40
- program
41
- .command('go', { hidden: true })
42
- .argument('[id]', 'Short/full session id to jump to; omit for an interactive picker')
43
- .option('--local', 'Only this machine (skip the cross-host sweep)')
44
- .description('Deprecated — use `agents sessions resume --attach-only` instead.')
45
- .action(async (id, opts) => {
46
- console.warn(chalk.yellow('`agents sessions go` is deprecated — use `agents sessions resume --attach-only` instead:'));
47
- console.warn(chalk.gray(` agents sessions resume ${id ?? '<id>'} --attach-only`));
48
- await focusAction(id, { local: opts.local, attachOnly: true });
49
- });
50
- }
51
34
  /**
52
35
  * Scope a live-session pool by device and live status. Pure so the `focus`
53
36
  * device/status filters are unit-testable without touching the sweep. `hosts`
@@ -263,7 +263,7 @@ export function registerLogsCommand(program) {
263
263
  .action((id, opts) => runLogs(id, opts));
264
264
  addEventsReadOptions(logsCmd
265
265
  .command('audit')
266
- .description('Alias for `agents events --audit`'), false)
266
+ .description('Alias for `agents events audit`'))
267
267
  .addHelpText('after', `
268
268
  Examples:
269
269
  agents logs audit Recent activity across everything