@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.
- package/CHANGELOG.md +401 -17
- package/README.md +66 -32
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +206 -32
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/browser.js +501 -103
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +0 -20
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/exec.js +52 -15
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +4 -3
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +3 -2
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-trace.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.js +54 -7
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +17 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/accounting/usage.d.ts +14 -3
- package/dist/lib/accounting/usage.js +64 -18
- package/dist/lib/agent-spec/agents.js +11 -9
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.js +8 -3
- package/dist/lib/browser/profiles.d.ts +35 -101
- package/dist/lib/browser/profiles.js +130 -301
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +8 -6
- package/dist/lib/browser/remote-control.js +8 -6
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +40 -16
- package/dist/lib/browser/runtime-state.js +92 -25
- package/dist/lib/browser/service.d.ts +28 -9
- package/dist/lib/browser/service.js +139 -62
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +2 -8
- package/dist/lib/config-keys.js +2 -13
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +20 -18
- package/dist/lib/device-config.d.ts +0 -2
- package/dist/lib/device-config.js +1 -23
- package/dist/lib/devices/registry.d.ts +1 -1
- package/dist/lib/devices/registry.js +1 -1
- package/dist/lib/devices/stats-cache.d.ts +20 -0
- package/dist/lib/devices/stats-cache.js +49 -5
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +67 -13
- package/dist/lib/exec.js +85 -29
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +11 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +19 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +31 -7
- package/dist/lib/menubar/install-menubar.js +79 -21
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.js +17 -1
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +138 -6
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +33 -5
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.js +11 -2
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +88 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-html.d.ts +5 -1
- package/dist/lib/session/trajectory-html.js +42 -22
- package/dist/lib/session/trajectory-text.js +9 -2
- package/dist/lib/session/trajectory.js +53 -4
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +7 -3
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +18 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-refresh.d.ts +1 -1
- package/dist/lib/usage-refresh.js +11 -8
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +4 -6
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
package/dist/commands/browser.js
CHANGED
|
@@ -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,
|
|
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.
|
|
29
|
-
*
|
|
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
|
|
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', '
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
445
|
+
const allProfiles = await listProfiles();
|
|
240
446
|
const configuredDefault = getConfiguredDefaultProfileName();
|
|
241
447
|
if (opts.json) {
|
|
242
|
-
console.log(JSON.stringify(
|
|
448
|
+
console.log(JSON.stringify(allProfiles.map((profile) => ({
|
|
243
449
|
...profile,
|
|
244
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
.
|
|
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
|
|
406
|
-
|
|
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
|
-
`
|
|
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,
|
|
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}
|
|
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}
|
|
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('
|
|
679
|
-
.
|
|
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
|
|
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(
|
|
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
|
|
725
|
-
//
|
|
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}
|
|
733
|
-
`
|
|
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} —
|
|
949
|
+
console.log(` ${k.name} — ${k.why}`);
|
|
736
950
|
}
|
|
737
|
-
console.log('
|
|
738
|
-
console.log('(Prune never deletes these —
|
|
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
|
-
#
|
|
979
|
+
# WHERE is the devices whose own file declares the name
|
|
766
980
|
agents browser profiles list
|
|
767
981
|
|
|
768
|
-
# Create one
|
|
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
|
-
#
|
|
772
|
-
agents browser profiles
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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\`
|
|
792
|
-
|
|
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.
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
const
|
|
810
|
-
const
|
|
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: '
|
|
813
|
-
: {
|
|
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(`
|
|
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
|
|
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('
|
|
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(
|
|
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 ${
|
|
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 &&
|
|
1461
|
-
const dir = path.dirname(
|
|
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);
|