@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -1,55 +1,259 @@
1
1
  import chalk from 'chalk';
2
+ import { spawnSync } from 'node:child_process';
2
3
  import * as fs from 'fs';
3
4
  import * as path from 'path';
4
- import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, listProfilesWithScope, formatProfilesTable, editProfile, moveProfileScope, renameProfile, assertRegistrableProfileName, misfiledFleetProfile, } from '../lib/browser/profiles.js';
5
+ import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, formatProfilesTable, editProfile, renameProfile, assertRegistrableProfileName, isProfileLaunchableHere, } from '../lib/browser/profiles.js';
6
+ import { declaringDevices, migrateCentralBrowserProfiles, profileKind } from '../lib/browser/registry.js';
5
7
  import { resolveActor } from '../lib/actor.js';
6
8
  import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
7
9
  import { parseSecretRef } from '../lib/browser/secret-ref.js';
8
10
  import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
9
11
  import { findBrowserPath, getPortOccupant, isLauncherScript, listInstalledBrowsers } from '../lib/browser/chrome.js';
10
- import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, buildProfilePrunePlan, pruneProfiles, PRUNE_REASON_TEXT, } from '../lib/browser/runtime-state.js';
12
+ import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, buildProfilePrunePlan, pruneProfiles, PRUNE_REASON_TEXT, identityLoopbackMismatch, } from '../lib/browser/runtime-state.js';
11
13
  import { DEFAULT_VIEWPORT, parseWindowSize, parseWindowPosition } from '../lib/browser/devices.js';
12
14
  import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
13
15
  import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
14
16
  import { parseTargetFilter } from '../lib/browser/service.js';
15
- import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest, } from '../lib/browser/ipc.js';
17
+ import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendIPCRequest as sendRawIPCRequest, } from '../lib/browser/ipc.js';
18
+ import { bindTask, getTaskBinding, honorScreenshotOutput, REJECT_DEVICE_MESSAGE, resolveTaskRoute, unbindTask, unbindTasksForProfile, updateTaskBinding, } from '../lib/browser/task-index.js';
19
+ import { isSelfHost } from '../lib/devices/self-host.js';
20
+ import { resolveHost } from '../lib/hosts/registry.js';
21
+ import { sshTargetFor } from '../lib/hosts/types.js';
22
+ import { withActorEnv } from '../lib/hosts/dispatch.js';
23
+ import { streamAgentsOnHost, passthroughSshOptions, } from '../lib/hosts/passthrough.js';
24
+ import { buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, stripRoutingFlags, } from '../lib/hosts/remote-cmd.js';
25
+ import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
26
+ import { sshExec, SSH_OPTS } from '../lib/ssh-exec.js';
27
+ import { flagValue } from '../lib/hosts/routing-flag.js';
16
28
  import { browserTaskPicker } from './browser-picker.js';
17
- import { assertRemoteControlAllowed } from '../lib/browser/remote-control.js';
29
+ import { assertRemoteControlAllowed, isFleetRemoteInvocation } from '../lib/browser/remote-control.js';
18
30
  import { getConfigValue, setConfigValue, unsetConfigValue } from '../lib/device-config.js';
19
31
  import { isInteractiveTerminal } from './utils.js';
20
32
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
21
33
  import { buildHar } from '../lib/browser/har.js';
22
34
  import { getCliVersion } from '../lib/version.js';
23
35
  import { runBrowserIPCStream } from '../lib/browser/stream.js';
36
+ import { machineId } from '../lib/machine-id.js';
37
+ /**
38
+ * Task name inferred from the local task→device index when `--task` was
39
+ * omitted and this session owns exactly one open task. Set by the page-verb
40
+ * preAction hook; consumed by {@link resolveTaskName}.
41
+ */
42
+ let inferredTaskName;
24
43
  /**
25
44
  * Resolve which browser task a command targets. Order:
26
45
  * 1. `--task <name>` flag (explicit per-command override)
27
46
  * 2. `$AGENTS_BROWSER_TASK` (optional shell default)
28
- * 3. `undefined` the daemon resolves from the caller's identity
29
- * (session / launch id stamped in sendIPCRequest)
47
+ * 3. the local task-index match for this session (exactly one)
48
+ * 4. `undefined` the daemon resolves from the caller's identity
49
+ *
50
+ * `--device` on a page verb is rejected here so commander-parsed flags cannot
51
+ * silently re-route a later verb. Bind the device at `start`.
30
52
  *
31
53
  * `undefined` is valid: page verbs create a task when none resolves, and
32
54
  * done/stop report "nothing to close". Agents no longer need to type a handle
33
55
  * in the common case.
34
56
  */
35
57
  function resolveTaskName(opts) {
58
+ if (opts.device) {
59
+ const route = resolveTaskRoute({ device: opts.device });
60
+ console.error(route.kind === 'reject-device' ? route.message : REJECT_DEVICE_MESSAGE);
61
+ process.exit(1);
62
+ }
36
63
  if (opts.task)
37
64
  return opts.task;
38
65
  const fromEnv = process.env.AGENTS_BROWSER_TASK;
39
66
  if (fromEnv)
40
67
  return fromEnv;
41
- return undefined;
68
+ return inferredTaskName;
42
69
  }
43
70
  // `-t` is taken by `--tab` on most commands, so `--task` is long-form only.
44
71
  // The daemon resolves from caller identity when omitted.
45
72
  const TASK_OPTION_FLAG = '--task <name>';
46
73
  const TASK_OPTION_DESC = 'Task name (defaults to $AGENTS_BROWSER_TASK, else the caller\'s live task)';
74
+ const DEVICE_ON_PAGE_VERB_FLAG = '--device <name>';
75
+ const DEVICE_ON_PAGE_VERB_DESC = 'Not valid here — bind the device at `agents browser start --device`';
76
+ const TASK_ROUTED_COMMANDS = new Set([
77
+ 'stream',
78
+ 'done',
79
+ 'stop',
80
+ 'navigate',
81
+ 'goto',
82
+ 'tab',
83
+ 'tabs',
84
+ 'screenshot',
85
+ 'pdf',
86
+ 'evaluate',
87
+ 'refs',
88
+ 'click',
89
+ 'type',
90
+ 'press',
91
+ 'hover',
92
+ 'scroll',
93
+ 'upload',
94
+ 'set',
95
+ 'console',
96
+ 'errors',
97
+ 'requests',
98
+ 'logs',
99
+ 'responsebody',
100
+ 'wait',
101
+ 'download',
102
+ 'record',
103
+ 'waitdownload',
104
+ ]);
105
+ function commandPath(actionCommand) {
106
+ const parts = [];
107
+ let current = actionCommand;
108
+ while (current) {
109
+ const name = current.name();
110
+ if (name === 'agents' || name === 'browser')
111
+ break;
112
+ parts.unshift(name);
113
+ current = current.parent ?? null;
114
+ }
115
+ return parts;
116
+ }
117
+ function callerSessionId() {
118
+ return process.env.AGENT_SESSION_ID || process.env.AGENTS_SESSION_ID;
119
+ }
120
+ function callerLaunchId() {
121
+ return process.env.AGENT_LAUNCH_ID;
122
+ }
123
+ function browserForwardedArgv() {
124
+ const raw = process.argv.slice(2);
125
+ const withCommand = raw[0] === 'browser' ? raw : ['browser', ...raw];
126
+ return stripRoutingFlags(withCommand, HOST_ROUTING_SPECS);
127
+ }
128
+ async function dispatchBrowserToDevice(device, forwardedArgs, mode) {
129
+ const host = await resolveHost(device);
130
+ if (!host) {
131
+ throw new Error(`Unknown device "${device}". Next: agents devices list`);
132
+ }
133
+ const target = sshTargetFor(host);
134
+ const remoteOs = resolveRemoteOsSync(host.name);
135
+ const env = withActorEnv({ AGENTS_FLEET_REMOTE: '1' });
136
+ const remoteCmd = buildRemoteAgentsInvocation(forwardedArgs, undefined, remoteOs, env);
137
+ if (mode === 'stream') {
138
+ const code = streamAgentsOnHost(host, forwardedArgs, {
139
+ interactive: !!process.stdout.isTTY && !process.argv.includes('--no-tty'),
140
+ extraEnv: { AGENTS_FLEET_REMOTE: '1' },
141
+ remoteOs,
142
+ target,
143
+ });
144
+ return { code, stdout: '', stderr: '' };
145
+ }
146
+ const sshOpts = passthroughSshOptions(host, false);
147
+ const result = sshExec(target, remoteCmd, {
148
+ multiplex: sshOpts.multiplex,
149
+ extraSshArgs: sshOpts.extraSshArgs,
150
+ });
151
+ return {
152
+ code: result.code ?? 1,
153
+ stdout: result.stdout,
154
+ stderr: result.stderr,
155
+ };
156
+ }
157
+ async function pullRemoteFile(device, remotePath, localPath) {
158
+ const host = await resolveHost(device);
159
+ if (!host) {
160
+ throw new Error(`Unknown device "${device}". Next: agents devices list`);
161
+ }
162
+ const target = sshTargetFor(host);
163
+ fs.mkdirSync(path.dirname(path.resolve(localPath)), { recursive: true });
164
+ const sshOpts = passthroughSshOptions(host, false);
165
+ const copied = spawnSync('scp', [...sshOpts.extraSshArgs, ...SSH_OPTS, `${target}:${remotePath}`, path.resolve(localPath)], { encoding: 'utf8' });
166
+ if (copied.status !== 0) {
167
+ throw new Error(`Failed to copy screenshot from ${device}:${remotePath}: ${(copied.stderr || copied.stdout || '').trim()}`);
168
+ }
169
+ }
170
+ function syncTaskIndex(request, response) {
171
+ if (!response.ok)
172
+ return;
173
+ if (request.action === 'stop' && request.profile && !request.task) {
174
+ unbindTasksForProfile(request.profile);
175
+ return;
176
+ }
177
+ if (request.action === 'gc') {
178
+ if (!request.dryRun) {
179
+ for (const closed of response.reaped?.closed ?? []) {
180
+ unbindTask(closed.task);
181
+ }
182
+ }
183
+ return;
184
+ }
185
+ const name = response.task ?? request.task ?? request.taskName;
186
+ if (!name)
187
+ return;
188
+ if (request.action === 'done' || (request.action === 'stop' && request.task)) {
189
+ unbindTask(name);
190
+ return;
191
+ }
192
+ if (request.action === 'start') {
193
+ const existing = getTaskBinding(name);
194
+ if (!existing) {
195
+ bindTask(name, {
196
+ device: machineId(),
197
+ profile: request.profile,
198
+ url: request.url,
199
+ sessionId: request.sessionId ?? callerSessionId(),
200
+ launchId: request.launchId ?? callerLaunchId(),
201
+ createdAt: Date.now(),
202
+ });
203
+ }
204
+ else if (request.url) {
205
+ updateTaskBinding(name, { url: request.url });
206
+ }
207
+ return;
208
+ }
209
+ if (request.url) {
210
+ const existing = getTaskBinding(name);
211
+ if (existing) {
212
+ updateTaskBinding(name, { url: request.url });
213
+ }
214
+ else {
215
+ bindTask(name, {
216
+ device: machineId(),
217
+ profile: request.profile,
218
+ url: request.url,
219
+ sessionId: request.sessionId ?? callerSessionId(),
220
+ launchId: request.launchId ?? callerLaunchId(),
221
+ createdAt: Date.now(),
222
+ });
223
+ }
224
+ return;
225
+ }
226
+ if (!getTaskBinding(name)) {
227
+ bindTask(name, {
228
+ device: machineId(),
229
+ profile: request.profile,
230
+ sessionId: request.sessionId ?? callerSessionId(),
231
+ launchId: request.launchId ?? callerLaunchId(),
232
+ createdAt: Date.now(),
233
+ });
234
+ }
235
+ }
236
+ async function sendIPCRequest(request, opts) {
237
+ const response = await sendRawIPCRequest(request, opts);
238
+ syncTaskIndex(request, response);
239
+ return response;
240
+ }
241
+ function assertDeviceDeclaresProfile(device, profileName) {
242
+ const declared = declaringDevices(profileName);
243
+ if (declared.includes(device))
244
+ return;
245
+ const where = declared.length > 0
246
+ ? `Declared on: ${declared.join(', ')}.`
247
+ : 'No device declares that profile.';
248
+ throw new Error(`Device "${device}" does not declare browser profile "${profileName}". ${where}\n` +
249
+ `Next: run \`agents browser profiles add ${profileName} --browser <b>\` on ${device}.`);
250
+ }
47
251
  // Help groups — surfaces the actual mental model an agent follows
48
252
  // ("open a session / drive the page / capture evidence / rare extras")
49
253
  // instead of an alphabetical dump. Everything not listed falls into a
50
254
  // trailing "Other commands" section automatically.
51
255
  const BROWSER_HELP_GROUPS = [
52
- { title: 'Session lifecycle', names: ['use', 'start', 'done', 'status', 'gc'] },
256
+ { title: 'Session lifecycle', names: ['use', 'start', 'done', 'status', 'prune'] },
53
257
  { title: 'Fast action loop', names: ['stream'] },
54
258
  {
55
259
  title: 'Drive the page',
@@ -86,8 +290,10 @@ export function registerBrowserCommand(program) {
86
290
  # Keep one process and daemon socket warm for repeated actions
87
291
  agents browser stream --task "$AGENTS_BROWSER_TASK"
88
292
 
89
- # Drive another machine's browser (needs its consent see remote-control)
90
- agents browser start --device zion
293
+ # Bind a device at start; later verbs resolve it from the task
294
+ agents browser start --task post --device zion --url https://x.com/
295
+ agents browser type --task post --ref @e3 "hello"
296
+ agents browser screenshot --task post
91
297
 
92
298
  # Browse a task-heavy profile's captures: one row per task, not per file
93
299
  agents browser sessions
@@ -99,7 +305,7 @@ export function registerBrowserCommand(program) {
99
305
  agents browser done
100
306
 
101
307
  # Close tabs the daemon's own reaper would have caught on its next 5-min tick
102
- agents browser gc --dry-run
308
+ agents browser prune --dry-run
103
309
  `,
104
310
  notes: `
105
311
  Most agent workflows should use the 'browser' skill instead of raw subcommands.
@@ -233,27 +439,28 @@ function registerProfilesCommands(browser) {
233
439
  profiles
234
440
  .command('list')
235
441
  .alias('ls')
236
- .description('List all browser profiles, with the store each lives in (local / fleet)')
237
- .option('--json', 'Output machine-readable JSON')
442
+ .description('List all browser profiles and the devices declaring each one (WHERE)')
443
+ .option('--json', 'Output machine-readable JSON (includes devices + kind)')
238
444
  .action(async (opts) => {
239
- const scoped = await listProfilesWithScope();
445
+ const allProfiles = await listProfiles();
240
446
  const configuredDefault = getConfiguredDefaultProfileName();
241
447
  if (opts.json) {
242
- console.log(JSON.stringify(scoped.map(({ profile, scope }) => ({
448
+ console.log(JSON.stringify(allProfiles.map((profile) => ({
243
449
  ...profile,
244
- scope,
450
+ devices: profile.devices,
451
+ kind: profileKind(profile.name),
245
452
  isConfiguredDefault: profile.name === configuredDefault,
246
453
  })), null, 2));
247
454
  return;
248
455
  }
249
- if (scoped.length === 0) {
456
+ if (allProfiles.length === 0) {
250
457
  console.log('No browser profiles configured.');
251
458
  console.log('Create one with: agents browser profiles create <name> --browser chrome');
252
459
  return;
253
460
  }
254
461
  // Rendering lives in lib/browser/profiles.ts so the column widths and the
255
462
  // default-marking rules are unit-tested rather than eyeballed (RUSH-2710).
256
- for (const line of formatProfilesTable(scoped, configuredDefault))
463
+ for (const line of formatProfilesTable(allProfiles, configuredDefault))
257
464
  console.log(line);
258
465
  });
259
466
  profiles
@@ -284,6 +491,44 @@ function registerProfilesCommands(browser) {
284
491
  }
285
492
  console.log('Pick your default with: agents browser use <name>');
286
493
  });
494
+ profiles
495
+ .command('claim [name]')
496
+ .description('Move leftover central browser: profiles into this device\'s declaration file. Only profiles this machine can host are claimed; the rest stay central. Run on the machine that actually has the browser.')
497
+ .option('--json', 'Output machine-readable JSON')
498
+ .action(async (name, opts) => {
499
+ const canHostHere = (config) => isProfileLaunchableHere({
500
+ name: '_',
501
+ browser: config.browser,
502
+ binary: config.binary,
503
+ endpoints: config.endpoints,
504
+ });
505
+ let result;
506
+ try {
507
+ result = migrateCentralBrowserProfiles(canHostHere, name);
508
+ }
509
+ catch (err) {
510
+ console.error(err instanceof Error ? err.message : String(err));
511
+ process.exit(1);
512
+ }
513
+ if (opts.json) {
514
+ console.log(JSON.stringify({ ...result, device: machineId() }, null, 2));
515
+ return;
516
+ }
517
+ if (result.claimed.length === 0 && result.skipped.length === 0) {
518
+ console.log('No leftover central browser profiles to claim.');
519
+ return;
520
+ }
521
+ for (const claimed of result.claimed) {
522
+ console.log(`Claimed ${claimed} on ${machineId()}`);
523
+ }
524
+ for (const skipped of result.skipped) {
525
+ console.log(`Skipped ${skipped}: this machine cannot host it (browser/binary isn't installed here). ` +
526
+ `Run \`agents browser profiles claim ${skipped}\` on the machine that has that browser.`);
527
+ }
528
+ if (result.claimed.length === 0) {
529
+ console.log('No leftover central profiles this machine can host.');
530
+ }
531
+ });
287
532
  configureBrowserUseCommand(profiles.command('use [name]'));
288
533
  configureBrowserUseCommand(profiles.command('set-default [name]', { hidden: true }), true);
289
534
  profiles
@@ -317,9 +562,9 @@ function registerProfilesCommands(browser) {
317
562
  const VALID_BROWSERS = ['chrome', 'comet', 'chromium', 'brave', 'edge', 'arc', 'custom'];
318
563
  profiles
319
564
  .command('create <name>')
320
- .description('Create a new browser profile (machine-local unless --fleet)')
565
+ .alias('add')
566
+ .description('Create a new browser profile on this device')
321
567
  .requiredOption('-b, --browser <type>', `Browser type: ${VALID_BROWSERS.join(', ')}`)
322
- .option('--fleet', 'Store in the synced agents.yaml so every machine sees it. Default is machine-local: a profile pins an OS-specific binary path and a locally chosen port, so a synced copy is wrong on every other box.')
323
568
  .option('-e, --endpoint <url>', 'CDP endpoint URL (repeatable; auto-assigned if omitted)', collect, [])
324
569
  .option('-s, --secrets <bundle>', 'Secrets bundle to inject')
325
570
  .option('-d, --description <text>', 'Profile description')
@@ -402,8 +647,11 @@ function registerProfilesCommands(browser) {
402
647
  chrome: opts.headless ? { headless: true } : undefined,
403
648
  viewport,
404
649
  };
405
- await createProfile(profile, { fleet: !!opts.fleet });
406
- console.log(`Created profile: ${name} (${opts.fleet ? 'fleet-synced — visible on every machine' : 'machine-local'})`);
650
+ await createProfile(profile);
651
+ const port = extractConfiguredPort(profile);
652
+ console.log(port !== undefined
653
+ ? `Added "${name}" on ${machineId()} (port ${port}).`
654
+ : `Added "${name}" on ${machineId()}.`);
407
655
  // Warn (don't fail) if the declared secrets bundle doesn't exist yet — it
408
656
  // may be created later, but a typo should surface now.
409
657
  if (opts.secrets && !bundleExists(opts.secrets)) {
@@ -435,7 +683,7 @@ function registerProfilesCommands(browser) {
435
683
  // changing either orphans the cached browser data (and its logins).
436
684
  if (opts.browser) {
437
685
  console.error('browser type is not editable — it keys the on-disk profile cache. ' +
438
- `Delete and recreate instead: agents browser profiles delete ${name} && agents browser profiles create ${name} -b <type>`);
686
+ `Remove and recreate instead: agents browser profiles remove ${name} && agents browser profiles create ${name} -b <type>`);
439
687
  process.exit(1);
440
688
  }
441
689
  if (opts.targetFilter) {
@@ -513,17 +761,13 @@ function registerProfilesCommands(browser) {
513
761
  process.exit(1);
514
762
  }
515
763
  if (opts.json) {
516
- console.log(JSON.stringify({ ...result.profile, scope: result.scope, changed: result.changed }, null, 2));
764
+ console.log(JSON.stringify({ ...result.profile, devices: result.devices, changed: result.changed }, null, 2));
517
765
  }
518
766
  else if (result.changed.length === 0) {
519
767
  console.log(`No change: ${name} already had those values.`);
520
768
  }
521
769
  else {
522
- console.log(`Updated ${name} (${result.scope}): ${result.changed.join(', ')}`);
523
- }
524
- if (result.fleetCopyLeftStale) {
525
- console.error(`warning: "${name}" is machine-local by rule, so the edit was written to this machine only. ` +
526
- `The fleet-synced copy still holds the old values on every other box.`);
770
+ console.log(`Updated ${name} on ${machineId()}: ${result.changed.join(', ')}`);
527
771
  }
528
772
  if (patch.secrets && !bundleExists(patch.secrets)) {
529
773
  console.error(`warning: secrets bundle "${patch.secrets}" does not exist yet. Create it with: agents secrets create ${patch.secrets}`);
@@ -546,7 +790,7 @@ function registerProfilesCommands(browser) {
546
790
  console.log(JSON.stringify({ from, to, ...res }, null, 2));
547
791
  return;
548
792
  }
549
- console.log(`Renamed ${from} -> ${to} (${res.scope})`);
793
+ console.log(`Renamed ${from} -> ${to} on ${machineId()}`);
550
794
  if (res.movedDirs.length > 0) {
551
795
  console.log(` moved ${res.movedDirs.length} browser data dir${res.movedDirs.length === 1 ? '' : 's'} — logins preserved`);
552
796
  }
@@ -566,37 +810,6 @@ function registerProfilesCommands(browser) {
566
810
  }
567
811
  }
568
812
  });
569
- profiles
570
- .command('scope <name> <scope>')
571
- .description('Move a profile between the fleet-synced store and this machine (local|fleet)')
572
- .option('--json', 'Output machine-readable JSON')
573
- .action(async (name, scope, opts) => {
574
- if (scope !== 'local' && scope !== 'fleet') {
575
- console.error('scope must be `local` or `fleet`');
576
- process.exit(1);
577
- }
578
- let moved;
579
- try {
580
- moved = await moveProfileScope(name, scope);
581
- }
582
- catch (err) {
583
- console.error(err instanceof Error ? err.message : String(err));
584
- process.exit(1);
585
- }
586
- if (opts.json) {
587
- console.log(JSON.stringify({ profile: name, ...moved }, null, 2));
588
- return;
589
- }
590
- if (moved.from === moved.to) {
591
- console.log(`${name} is already ${scope}.`);
592
- return;
593
- }
594
- console.log(`Moved ${name}: ${moved.from} -> ${moved.to}`);
595
- if (scope === 'local') {
596
- console.error(`note: ${name} is no longer visible on any other machine. ` +
597
- `Run this ON the machine that owns the browser — the surviving copy is this one.`);
598
- }
599
- });
600
813
  profiles
601
814
  .command('show <name>')
602
815
  .description('Show profile details')
@@ -675,8 +888,9 @@ function registerProfilesCommands(browser) {
675
888
  }
676
889
  });
677
890
  profiles
678
- .command('delete <name>')
679
- .description('Delete a browser profile (drops YAML config + all cached runtime dirs)')
891
+ .command('remove <name>')
892
+ .alias('delete')
893
+ .description('Remove a browser profile (drops YAML config + all cached runtime dirs)')
680
894
  .option('--keep-cache', "Leave ~/.agents/.cache/browser/<name>* dirs in place (don't wipe chrome-data)")
681
895
  .action(async (name, opts) => {
682
896
  await deleteProfile(name);
@@ -709,33 +923,33 @@ function registerProfilesCommands(browser) {
709
923
  });
710
924
  profiles
711
925
  .command('prune')
712
- .description('Remove dead machine-local profiles: browser not installed here, or never started')
926
+ .description('Remove dead profiles this device declares: browser not installed here, or never started')
713
927
  .option('-n, --dry-run', 'Print what would be removed and exit without changing anything')
714
- .option('--fleet', 'Also consider fleet-synced profiles (removing one removes it from EVERY machine)')
715
928
  .option('--json', 'Output machine-readable JSON')
716
929
  .action(async (opts) => {
717
- const plan = await buildProfilePrunePlan({ includeFleet: opts.fleet });
930
+ const plan = await buildProfilePrunePlan();
718
931
  if (!opts.dryRun)
719
932
  await pruneProfiles(plan);
720
933
  if (opts.json) {
721
934
  console.log(JSON.stringify({ dryRun: !!opts.dryRun, ...plan }, null, 2));
722
935
  return;
723
936
  }
724
- // A misfiled fleet profile is never a prune candidate (deleting a fleet
725
- // entry deletes it everywhere), so without this it would only ever surface
726
- // in --json. It is the one kept-reason a user has to act on.
937
+ // A misfiled profile is never a prune candidate the entry belongs to the
938
+ // device that declared it, not to this one — so without this it would only
939
+ // ever surface in --json. It is the one kept-reason a user has to act on.
727
940
  const misfiled = plan.kept.filter((k) => k.misfiled);
728
941
  const reportMisfiled = () => {
729
942
  if (misfiled.length === 0)
730
943
  return;
731
944
  console.log('');
732
- console.log(`${misfiled.length} fleet profile${misfiled.length === 1 ? '' : 's'} ${misfiled.length === 1 ? 'is' : 'are'} misfiled — ` +
733
- `fleet-synced but bound to a local port, so the name means a different browser on every machine:`);
945
+ console.log(`${misfiled.length} profile${misfiled.length === 1 ? '' : 's'} ${misfiled.length === 1 ? 'is' : 'are'} misfiled — ` +
946
+ `declared on another device but bound to a loopback port, so the name resolves to ` +
947
+ `this machine's own browser rather than the declaring device's:`);
734
948
  for (const k of misfiled) {
735
- console.log(` ${k.name} — agents browser profiles scope ${k.name} local`);
949
+ console.log(` ${k.name} — ${k.why}`);
736
950
  }
737
- console.log('Run that on the machine that owns each browser. Nothing was moved for you.');
738
- console.log('(Prune never deletes these — removing a fleet entry removes it everywhere.)');
951
+ console.log('Re-declare each on the machine that owns the browser. Nothing was moved for you.');
952
+ console.log('(Prune never deletes these — the declaring device owns the entry.)');
739
953
  };
740
954
  if (plan.candidates.length === 0) {
741
955
  console.log('Nothing to prune — every profile is in use, healthy, or protected.');
@@ -762,16 +976,18 @@ function registerProfilesCommands(browser) {
762
976
  });
763
977
  setHelpSections(profiles, {
764
978
  examples: `
765
- # What exists here, and whether each is this machine's or the whole fleet's
979
+ # WHERE is the devices whose own file declares the name
766
980
  agents browser profiles list
767
981
 
768
- # Create one machine-local by default
982
+ # Create one on this device
983
+ agents browser profiles add work --browser chrome
769
984
  agents browser profiles create work --browser chrome
770
985
 
771
- # Create one every machine should see (e.g. a remote ssh:// endpoint)
772
- agents browser profiles create shared-remote --browser chrome --fleet
986
+ # Claim leftover central profiles on the machine that hosts the browser
987
+ agents browser profiles claim
988
+ agents browser profiles claim comet-local
773
989
 
774
- # Clean up dead ones: check first, then apply
990
+ # Clean up dead ones this device declares: check first, then apply
775
991
  agents browser profiles prune --dry-run
776
992
  agents browser profiles prune
777
993
 
@@ -779,22 +995,24 @@ function registerProfilesCommands(browser) {
779
995
  agents browser use work
780
996
  `,
781
997
  notes: `
782
- Profiles are machine-local by default. A profile pins an OS-specific binary
783
- path and a locally chosen CDP port, so a fleet-synced copy is wrong on every
784
- other machine pass --fleet only when the profile really is fleet config.
998
+ A device declares its own browsers in its own \`devices/<machine>/agents.yaml\`.
999
+ A name declared by exactly one device is identity-bearing; a name declared
1000
+ by several is fungible. Leftover central \`browser:\` entries are claimed
1001
+ with \`agents browser profiles claim\` on the machine that hosts the browser.
785
1002
 
786
1003
  In \`list\`, the \`*\` marker means "this machine's configured default"
787
1004
  (\`agents browser start\` with no --profile). The auto-detected profile is
788
1005
  named \`auto-chrome\`; \`default\` is only an ALIAS for whichever profile
789
1006
  this machine is configured to use, resolved the same way by every command.
790
1007
 
791
- \`prune\` never removes a profile that is in use, the configured default, the
792
- auto-detected \`auto-chrome\`, or (without --fleet) a fleet-synced one.
1008
+ \`prune\` only considers profiles this device declares. It never removes a
1009
+ profile that is in use, the configured default, or the auto-detected
1010
+ \`auto-chrome\`.
793
1011
  `,
794
1012
  });
795
1013
  profiles
796
1014
  .command('doctor <name>')
797
- .description('Diagnose a browser profile: binary, port, user-data-dir, onboarding state')
1015
+ .description('Diagnose a browser profile: where it is declared, binary, port, user-data-dir, onboarding state')
798
1016
  .action(async (name) => {
799
1017
  const profile = await getProfile(name);
800
1018
  if (!profile) {
@@ -802,15 +1020,29 @@ function registerProfilesCommands(browser) {
802
1020
  process.exit(1);
803
1021
  }
804
1022
  const checks = [];
805
- // 0. Store scope. A fleet-synced profile whose endpoint is loopback cdp://
806
- // means a DIFFERENT browser on every machine the name lies everywhere
807
- // but here. Checked first because it invalidates the reading of every
808
- // check below on any other box.
809
- const scoped = (await listProfilesWithScope()).find((p) => p.profile.name === name);
810
- const misfiled = scoped ? misfiledFleetProfile(profile, scoped.scope) : { misfiled: false };
1023
+ // 0. Declaration topology. An identity-bearing profile declared by a
1024
+ // different device cannot be diagnosed through a loopback CDP endpoint
1025
+ // on this one that is the original comet-local bug. Checked first
1026
+ // because it invalidates every local check.
1027
+ const misfiled = identityLoopbackMismatch(profile);
1028
+ const kind = profileKind(profile.name);
1029
+ const where = profile.devices.length > 0 ? profile.devices.join(', ') : 'no device';
811
1030
  checks.push(misfiled.misfiled
812
- ? { label: 'scope', ok: false, detail: misfiled.why }
813
- : { label: 'scope', ok: true, detail: scoped ? `stored ${scoped.scope}` : 'stored local' });
1031
+ ? { label: 'where', ok: false, detail: misfiled.why }
1032
+ : {
1033
+ label: 'where',
1034
+ ok: true,
1035
+ detail: kind
1036
+ ? `${kind}, declared on ${where}`
1037
+ : `declared on ${where}`,
1038
+ });
1039
+ if (misfiled.misfiled) {
1040
+ for (const c of checks) {
1041
+ const marker = c.ok ? 'OK ' : 'FAIL';
1042
+ console.log(`${marker} ${c.label.padEnd(15)} ${c.detail}`);
1043
+ }
1044
+ process.exit(1);
1045
+ }
814
1046
  // 1. Binary exists for declared browser type, and is a real executable we
815
1047
  // can drive — not a distro launcher script. findBrowserPath already
816
1048
  // unwraps the known Chromium wrappers to their ELF; if it still hands
@@ -971,6 +1203,80 @@ function registerProfilesCommands(browser) {
971
1203
  });
972
1204
  }
973
1205
  function registerTaskCommands(browser) {
1206
+ browser.hook('preAction', async (_thisCommand, actionCommand) => {
1207
+ inferredTaskName = undefined;
1208
+ if (isFleetRemoteInvocation())
1209
+ return;
1210
+ const pathNames = commandPath(actionCommand);
1211
+ const top = pathNames[0];
1212
+ if (!top || !TASK_ROUTED_COMMANDS.has(top))
1213
+ return;
1214
+ if (pathNames.length === 1 && top === 'start')
1215
+ return;
1216
+ const opts = actionCommand.opts();
1217
+ const deviceFlag = opts.device ?? flagValue(process.argv.slice(2), 'device', 'D');
1218
+ const taskFlag = opts.task ?? flagValue(process.argv.slice(2), 'task');
1219
+ // Reject --device on every later verb, including `stop --profile`.
1220
+ if (deviceFlag) {
1221
+ console.error(REJECT_DEVICE_MESSAGE);
1222
+ process.exit(1);
1223
+ }
1224
+ if (top === 'stop' && opts.profile && !taskFlag)
1225
+ return;
1226
+ const route = resolveTaskRoute({
1227
+ task: taskFlag ?? process.env.AGENTS_BROWSER_TASK,
1228
+ sessionId: callerSessionId(),
1229
+ launchId: callerLaunchId(),
1230
+ });
1231
+ if (route.kind !== 'proceed') {
1232
+ console.error(route.message);
1233
+ process.exit(1);
1234
+ }
1235
+ inferredTaskName = route.task;
1236
+ if (isSelfHost(route.device) || !route.task)
1237
+ return;
1238
+ const forwarded = browserForwardedArgv();
1239
+ if (route.task && !forwarded.includes('--task')) {
1240
+ forwarded.push('--task', route.task);
1241
+ }
1242
+ const outputPath = top === 'screenshot'
1243
+ ? actionCommand.opts().output ??
1244
+ flagValue(process.argv.slice(2), 'output', 'o')
1245
+ : undefined;
1246
+ if (top === 'screenshot' && outputPath) {
1247
+ const withoutOutput = stripRoutingFlags(forwarded, [
1248
+ { long: 'output', short: 'o', takesValue: true },
1249
+ ]);
1250
+ const captured = await dispatchBrowserToDevice(route.device, withoutOutput, 'capture');
1251
+ if (captured.code !== 0) {
1252
+ process.stderr.write(captured.stderr);
1253
+ process.exit(captured.code);
1254
+ }
1255
+ const remotePath = captured.stdout
1256
+ .split('\n')
1257
+ .map((line) => line.trim())
1258
+ .find((line) => line.length > 0);
1259
+ if (!remotePath) {
1260
+ console.error(`Remote screenshot on ${route.device} produced no path.`);
1261
+ process.exit(1);
1262
+ }
1263
+ try {
1264
+ await pullRemoteFile(route.device, remotePath, outputPath);
1265
+ }
1266
+ catch (err) {
1267
+ console.error(err instanceof Error ? err.message : String(err));
1268
+ process.exit(1);
1269
+ }
1270
+ const saved = path.resolve(outputPath);
1271
+ console.log(saved);
1272
+ process.exit(0);
1273
+ }
1274
+ const result = await dispatchBrowserToDevice(route.device, forwarded, 'stream');
1275
+ if (result.code === 0 && (top === 'done' || top === 'stop') && route.task) {
1276
+ unbindTask(route.task);
1277
+ }
1278
+ process.exit(result.code);
1279
+ });
974
1280
  browser
975
1281
  .command('remote-control [state]')
976
1282
  .description("Allow or deny other fleet machines driving THIS machine's browser over `browser --device`. " +
@@ -1011,6 +1317,7 @@ function registerTaskCommands(browser) {
1011
1317
  .command('stream')
1012
1318
  .description('Keep one process and daemon IPC socket open; read NDJSON requests from stdin and write NDJSON responses')
1013
1319
  .option(TASK_OPTION_FLAG, 'Default task for requests that omit `task` (defaults to $AGENTS_BROWSER_TASK)')
1320
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1014
1321
  .action(async (opts) => {
1015
1322
  await runBrowserIPCStream({
1016
1323
  input: process.stdin,
@@ -1046,6 +1353,7 @@ function registerTaskCommands(browser) {
1046
1353
  .option('--title <label>', 'Human label shown in `browser status` (defaults to first navigated host)')
1047
1354
  .option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
1048
1355
  .option('-u, --url <url>', 'Open URL in first tab')
1356
+ .option('--device <name>', 'Device that hosts this task; later verbs resolve it from --task (not valid on page verbs)')
1049
1357
  .option('--fresh', 'Always open a new tab, skipping the reclaim of a tab an abandoned task is holding on that URL')
1050
1358
  .option('--no-skills', 'Skip auto-discovery of site-specific SKILL.md from ~/.agents/skills/browser/domain-skills/')
1051
1359
  .option('--record', 'Start recording right after the tab opens (shorthand for `agents browser record start` as a follow-up)')
@@ -1101,6 +1409,52 @@ function registerTaskCommands(browser) {
1101
1409
  process.exit(1);
1102
1410
  }
1103
1411
  }
1412
+ const deviceName = opts.device;
1413
+ if (deviceName) {
1414
+ const lowered = deviceName.toLowerCase();
1415
+ if (lowered === 'all' || lowered === 'auto') {
1416
+ console.error(`--device ${deviceName} is not valid on \`agents browser start\`: a task lives on one device.\n` +
1417
+ `Next: agents browser start --task <name> --device <device>`);
1418
+ process.exit(1);
1419
+ }
1420
+ try {
1421
+ assertDeviceDeclaresProfile(deviceName, profileName);
1422
+ }
1423
+ catch (err) {
1424
+ console.error(err instanceof Error ? err.message : String(err));
1425
+ process.exit(1);
1426
+ }
1427
+ if (!isSelfHost(deviceName) && !isFleetRemoteInvocation()) {
1428
+ try {
1429
+ const result = await dispatchBrowserToDevice(deviceName, browserForwardedArgv(), 'capture');
1430
+ process.stdout.write(result.stdout);
1431
+ process.stderr.write(result.stderr);
1432
+ if (result.code !== 0)
1433
+ process.exit(result.code);
1434
+ const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
1435
+ if (!taskName) {
1436
+ console.error(`Remote start on ${deviceName} produced no task name.`);
1437
+ process.exit(1);
1438
+ }
1439
+ bindTask(taskName, {
1440
+ device: deviceName,
1441
+ profile: profileName,
1442
+ url: opts.url,
1443
+ sessionId: callerSessionId(),
1444
+ launchId: callerLaunchId(),
1445
+ createdAt: Date.now(),
1446
+ });
1447
+ if (!result.stderr.includes(`started on ${deviceName}`)) {
1448
+ console.error(`Task "${taskName}" started on ${deviceName} (profile: ${profileName}).`);
1449
+ }
1450
+ }
1451
+ catch (err) {
1452
+ console.error(err instanceof Error ? err.message : String(err));
1453
+ process.exit(1);
1454
+ }
1455
+ return;
1456
+ }
1457
+ }
1104
1458
  // Grounded login guardrail: if the opening URL is a known login-gated
1105
1459
  // service and the resolved profile has no session for it, say so on stderr
1106
1460
  // and name a profile that IS logged in. Reads the profile's real cookie DB
@@ -1136,18 +1490,27 @@ function registerTaskCommands(browser) {
1136
1490
  console.error(response.error);
1137
1491
  process.exit(1);
1138
1492
  }
1493
+ const boundDevice = deviceName && isSelfHost(deviceName) ? deviceName : machineId();
1494
+ if (response.task) {
1495
+ bindTask(response.task, {
1496
+ device: boundDevice,
1497
+ profile: profileName,
1498
+ url: opts.url,
1499
+ sessionId: callerSessionId(),
1500
+ launchId: callerLaunchId(),
1501
+ createdAt: Date.now(),
1502
+ });
1503
+ }
1139
1504
  // stdout: just the resolved name, one line, no decoration. Lets callers do:
1140
1505
  // export AGENTS_BROWSER_TASK=$(agents browser start --profile work)
1141
1506
  console.log(response.task);
1142
1507
  // stderr: human-friendly commentary so a TTY user still sees what happened.
1143
1508
  // Shell substitution captures stdout only, so $(...) stays clean.
1509
+ console.error(`Task "${response.task}" started on ${boundDevice} (profile: ${profileName}).`);
1144
1510
  if (opts.url && response.tabId) {
1145
- console.error(`Started task "${response.task}" with tab ${response.tabId}`);
1146
- }
1147
- else {
1148
- console.error(`Started task "${response.task}"`);
1511
+ console.error(`Tab ${response.tabId}`);
1149
1512
  }
1150
- console.error('Tip: page verbs resolve this task from your session — --task only when running two at once.');
1513
+ console.error('Tip: page verbs resolve this task from --task; --device is only valid on start.');
1151
1514
  console.error('Try: agents browser screenshot | agents browser console --level error');
1152
1515
  // Surface the matched domain-skill (if any) so an agent driving the
1153
1516
  // task picks up site-specific selectors and gotchas before it starts
@@ -1184,6 +1547,7 @@ function registerTaskCommands(browser) {
1184
1547
  .command('done')
1185
1548
  .description('Complete a task and close its tabs (resolves from caller identity when --task is omitted)')
1186
1549
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1550
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1187
1551
  .action(async (opts) => {
1188
1552
  const task = resolveTaskName(opts);
1189
1553
  const response = await sendIPCRequest({
@@ -1204,6 +1568,7 @@ function registerTaskCommands(browser) {
1204
1568
  .command('stop')
1205
1569
  .description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection)')
1206
1570
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1571
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1207
1572
  .option('-p, --profile <name>', 'Detach the whole profile (incl. composite "name@endpoint") instead of stopping a single task')
1208
1573
  .action(async (opts) => {
1209
1574
  if (opts.profile) {
@@ -1235,7 +1600,8 @@ function registerTaskCommands(browser) {
1235
1600
  console.log(`Stopped task: ${response.task}`);
1236
1601
  });
1237
1602
  browser
1238
- .command('gc')
1603
+ .command('prune')
1604
+ .alias('gc')
1239
1605
  .description('Close tabs for abandoned tasks — owning agent session exited, or idle past the window — and mark them done. ' +
1240
1606
  'The same reaper the daemon already runs every 5 minutes; use this to run it now.')
1241
1607
  .option('--dry-run', 'List what would be closed without closing anything')
@@ -1309,6 +1675,7 @@ function registerTaskCommands(browser) {
1309
1675
  .alias('goto')
1310
1676
  .description('Navigate current tab to URL (creates a task and tab when none exist)')
1311
1677
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1678
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1312
1679
  .option('-u, --url <url>', 'URL to navigate to (or pass it positionally)')
1313
1680
  .option('-p, --profile <name>', 'Browser profile (optional if task is unique)')
1314
1681
  .action(async (urlPos, opts) => {
@@ -1339,6 +1706,7 @@ function registerTaskCommands(browser) {
1339
1706
  .command('add')
1340
1707
  .description('Open URL in new tab (becomes current)')
1341
1708
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1709
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1342
1710
  .requiredOption('--url <url>', 'URL to open in the new tab')
1343
1711
  .option('-p, --profile <name>', 'Browser profile')
1344
1712
  .action(async (opts) => {
@@ -1359,6 +1727,7 @@ function registerTaskCommands(browser) {
1359
1727
  .command('focus <tabId>')
1360
1728
  .description('Switch to tab (by ID, prefix, or URL substring)')
1361
1729
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1730
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1362
1731
  .action(async (tabId, opts) => {
1363
1732
  const task = resolveTaskName(opts);
1364
1733
  const response = await sendIPCRequest({
@@ -1376,6 +1745,7 @@ function registerTaskCommands(browser) {
1376
1745
  .command('close [tabId]')
1377
1746
  .description('Close tab(s) — omit tabId to close all')
1378
1747
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1748
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1379
1749
  .action(async (tabId, opts) => {
1380
1750
  const task = resolveTaskName(opts);
1381
1751
  const response = await sendIPCRequest({
@@ -1393,6 +1763,7 @@ function registerTaskCommands(browser) {
1393
1763
  .command('tabs')
1394
1764
  .description('List tabs open for the current task')
1395
1765
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1766
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1396
1767
  .option('--json', 'Output machine-readable JSON')
1397
1768
  .action(async (opts) => {
1398
1769
  const task = resolveTaskName(opts);
@@ -1428,6 +1799,7 @@ function registerTaskCommands(browser) {
1428
1799
  .command('screenshot')
1429
1800
  .description('Take a screenshot — auto-saved per task; --output only needed when you want a specific path')
1430
1801
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1802
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1431
1803
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1432
1804
  .option('-o, --output <path>', 'Specific output path (otherwise auto-saved under sessions/<task>/)')
1433
1805
  .option('-q, --quality <mode>', 'compressed (JPEG, capped at ~100 KB — default) or raw (PNG, pixel-faithful)', 'compressed')
@@ -1448,17 +1820,22 @@ function registerTaskCommands(browser) {
1448
1820
  console.error(response.error);
1449
1821
  process.exit(1);
1450
1822
  }
1823
+ // RUSH-3086: the daemon sandboxes writes to the runtime dir and silently
1824
+ // ignores `-o` outside it. Honor the requested path here.
1825
+ const savedPath = response.path
1826
+ ? honorScreenshotOutput(opts.output, response.path)
1827
+ : response.path;
1451
1828
  // stdout: just the path, so `P=$(agents browser screenshot)` works.
1452
- console.log(response.path);
1829
+ console.log(savedPath);
1453
1830
  // stderr: human commentary with size + dimensions, so an agent can
1454
1831
  // see at a glance what was captured without `ls -l && file` round-trips.
1455
1832
  const size = humanizeBytes(response.bytes);
1456
1833
  const dims = response.width && response.height ? `${response.width}×${response.height}` : 'unknown size';
1457
- console.error(`Saved screenshot to ${response.path} (${size}, ${dims})`);
1834
+ console.error(`Saved screenshot to ${savedPath} (${size}, ${dims})`);
1458
1835
  // When auto-saving (no --output), surface the directory once so the
1459
1836
  // agent doesn't have to dirname() the path or guess where files land.
1460
- if (!opts.output && response.path) {
1461
- const dir = path.dirname(response.path);
1837
+ if (!opts.output && savedPath) {
1838
+ const dir = path.dirname(savedPath);
1462
1839
  console.error(`Tip: auto-saving to ${dir}. Pass --output <path> to choose a path.`);
1463
1840
  }
1464
1841
  });
@@ -1466,6 +1843,7 @@ function registerTaskCommands(browser) {
1466
1843
  .command('pdf [output]')
1467
1844
  .description('Export the current tab as PDF via CDP Page.printToPDF — auto-saved under sessions/<task>/ when [output] is omitted')
1468
1845
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1846
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1469
1847
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1470
1848
  .action(async (output, opts) => {
1471
1849
  const task = resolveTaskName(opts);
@@ -1492,6 +1870,7 @@ function registerTaskCommands(browser) {
1492
1870
  .alias('eval')
1493
1871
  .description('Evaluate JavaScript in current tab')
1494
1872
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1873
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1495
1874
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1496
1875
  .option('-e, --expression <js>', 'JavaScript expression to evaluate (or pass it positionally)')
1497
1876
  .option('-f, --file <path>', 'Path to a .js file whose contents will be evaluated')
@@ -1841,6 +2220,7 @@ function registerTaskCommands(browser) {
1841
2220
  .command('refs')
1842
2221
  .description('Get DOM refs for interactive elements')
1843
2222
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2223
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1844
2224
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1845
2225
  .option('--all', 'Include non-interactive elements')
1846
2226
  .option('-l, --limit <n>', 'Max elements (default 500)', '500')
@@ -1883,6 +2263,7 @@ function registerTaskCommands(browser) {
1883
2263
  .command('click [ref]')
1884
2264
  .description('Click an element by ref, or raw coordinates with --at X,Y')
1885
2265
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2266
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1886
2267
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1887
2268
  .option('--at <x,y>', 'Click viewport coordinates (e.g. --at 320,540), bypassing ref resolution')
1888
2269
  .action(async (ref, opts) => {
@@ -1925,6 +2306,7 @@ function registerTaskCommands(browser) {
1925
2306
  .command('type <ref>')
1926
2307
  .description('Type text into an element by ref')
1927
2308
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2309
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1928
2310
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1929
2311
  .option('--text <text>', 'Text to type (use quotes for spaces/special chars)')
1930
2312
  .option('--secret <ref>', 'Resolve <bundle>/<KEY> from `agents secrets` and type the value. The secret is resolved in-process and never printed to stdout or the transcript. Alternative to --text.')
@@ -1989,6 +2371,7 @@ function registerTaskCommands(browser) {
1989
2371
  .command('press <key>')
1990
2372
  .description('Press a key (Enter, Tab, Escape, etc)')
1991
2373
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2374
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
1992
2375
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1993
2376
  .action(async (key, opts) => {
1994
2377
  const task = resolveTaskName(opts);
@@ -2008,6 +2391,7 @@ function registerTaskCommands(browser) {
2008
2391
  .command('hover <ref>')
2009
2392
  .description('Hover over an element by ref')
2010
2393
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2394
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2011
2395
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2012
2396
  .action(async (ref, opts) => {
2013
2397
  const task = resolveTaskName(opts);
@@ -2027,6 +2411,7 @@ function registerTaskCommands(browser) {
2027
2411
  .command('scroll')
2028
2412
  .description('Scroll the page by pixel amount (negatives scroll up/left)')
2029
2413
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2414
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2030
2415
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2031
2416
  .option('--dx <n>', 'Horizontal pixels (negative = left)', (v) => parseInt(v, 10), 0)
2032
2417
  .option('--dy <n>', 'Vertical pixels (negative = up)', (v) => parseInt(v, 10), 0)
@@ -2061,6 +2446,7 @@ function registerTaskCommands(browser) {
2061
2446
  .command('upload')
2062
2447
  .description('Upload file(s) — supports hidden file inputs, drag-drop targets, and OS chooser interception')
2063
2448
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2449
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2064
2450
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2065
2451
  .option('-r, --ref <n>', 'Ref of the upload target element (file input or drop zone)', (v) => parseInt(v, 10))
2066
2452
  .option('--trigger <n>', 'Ref of a button that opens the OS file chooser (Pattern C)', (v) => parseInt(v, 10))
@@ -2112,6 +2498,7 @@ function registerTaskCommands(browser) {
2112
2498
  .command('viewport <width> <height>')
2113
2499
  .description('Set viewport size')
2114
2500
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2501
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2115
2502
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2116
2503
  .option('-m, --mobile', 'Enable mobile emulation')
2117
2504
  .option('-s, --scale <factor>', 'Device scale factor', parseFloat)
@@ -2136,6 +2523,7 @@ function registerTaskCommands(browser) {
2136
2523
  .command('device <device-name>')
2137
2524
  .description('Emulate a device (iPhone 14, iPad, MacBook Pro)')
2138
2525
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2526
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2139
2527
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2140
2528
  .action(async (deviceName, opts) => {
2141
2529
  const task = resolveTaskName(opts);
@@ -2166,6 +2554,7 @@ function registerTaskCommands(browser) {
2166
2554
  .command('console')
2167
2555
  .description('Read console logs from a tab')
2168
2556
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2557
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2169
2558
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2170
2559
  .option('-l, --level <level>', 'Filter by level (log, info, warn, error)')
2171
2560
  .option('--clear', 'Clear logs after reading')
@@ -2196,6 +2585,7 @@ function registerTaskCommands(browser) {
2196
2585
  .command('errors')
2197
2586
  .description('Read page errors from a tab')
2198
2587
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2588
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2199
2589
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2200
2590
  .option('--clear', 'Clear errors after reading')
2201
2591
  .action(async (opts) => {
@@ -2228,6 +2618,7 @@ function registerTaskCommands(browser) {
2228
2618
  .command('requests')
2229
2619
  .description('Read captured network requests. --format har emits a HAR 1.2 JSON document.')
2230
2620
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2621
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2231
2622
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2232
2623
  .option('-f, --filter <text>', 'Filter URLs containing text')
2233
2624
  .option('--clear', 'Clear requests after reading')
@@ -2273,6 +2664,7 @@ function registerTaskCommands(browser) {
2273
2664
  .command('logs')
2274
2665
  .description('Read merged rush-app + rush-cli JSONL logs for a task')
2275
2666
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2667
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2276
2668
  .option('--source <name>', 'Source to scope to: rush-app or rush-cli (default both)')
2277
2669
  .option('--lines <n>', 'Tail N entries (default 200; ignored when --since)', (v) => parseInt(v, 10))
2278
2670
  .option('--since <when>', 'Absolute timestamp or relative offset (e.g. 5m, 2h, 1d)')
@@ -2311,6 +2703,7 @@ function registerTaskCommands(browser) {
2311
2703
  .command('responsebody <url-pattern>')
2312
2704
  .description('Wait for and read a response body by URL pattern')
2313
2705
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2706
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2314
2707
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2315
2708
  .option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
2316
2709
  .option('--max-chars <n>', 'Max characters to return', parseInt)
@@ -2335,6 +2728,7 @@ function registerTaskCommands(browser) {
2335
2728
  .command('wait')
2336
2729
  .description('Wait for a condition')
2337
2730
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2731
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2338
2732
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2339
2733
  .option('--time <ms>', 'Wait for milliseconds')
2340
2734
  .option('--selector <css>', 'Wait for CSS selector to appear')
@@ -2389,6 +2783,7 @@ function registerTaskCommands(browser) {
2389
2783
  .command('download')
2390
2784
  .description("Set the download directory for a task (defaults to the profile's downloads dir)")
2391
2785
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2786
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2392
2787
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2393
2788
  .option('-p, --path <dir>', "Download directory path (default: the profile's downloads dir)")
2394
2789
  .action(async (opts) => {
@@ -2421,6 +2816,7 @@ function registerTaskCommands(browser) {
2421
2816
  .command('start')
2422
2817
  .description('Start recording — auto-saved under sessions/<task>/recordings/. Bounded by --fps, --duration, --max-mb.')
2423
2818
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2819
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2424
2820
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
2425
2821
  .option('--fps <n>', 'Frames per second (1–30, default 5)', (v) => parseInt(v, 10))
2426
2822
  .option('--duration <sec>', 'Hard duration cap in seconds (default 60)', (v) => parseInt(v, 10))
@@ -2448,6 +2844,7 @@ function registerTaskCommands(browser) {
2448
2844
  .command('stop')
2449
2845
  .description('Stop an in-progress recording')
2450
2846
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2847
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2451
2848
  .action(async (opts) => {
2452
2849
  const task = resolveTaskName(opts);
2453
2850
  const response = await sendIPCRequest({ action: 'record-stop', task });
@@ -2464,6 +2861,7 @@ function registerTaskCommands(browser) {
2464
2861
  .command('waitdownload')
2465
2862
  .description('Wait for a download to complete')
2466
2863
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
2864
+ .option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
2467
2865
  .option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
2468
2866
  .action(async (opts) => {
2469
2867
  const task = resolveTaskName(opts);