@phnx-labs/agents-cli 1.22.46 → 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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- 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/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- 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 +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -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.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- 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 +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- 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 +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- 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/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- 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 +6 -11
- package/dist/lib/config-keys.js +18 -19
- 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/runner.js +15 -2
- 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 +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- 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 +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- 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 +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- 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 +35 -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/hosts/registry.js +7 -0
- 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 +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- 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/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- 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/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.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.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- 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/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- 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.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- 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-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- 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.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- 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.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- 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 +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- 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 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- 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';
|
|
11
|
-
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
12
|
+
import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, buildProfilePrunePlan, pruneProfiles, PRUNE_REASON_TEXT, identityLoopbackMismatch, } from '../lib/browser/runtime-state.js';
|
|
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')
|
|
@@ -330,8 +575,11 @@ function registerProfilesCommands(browser) {
|
|
|
330
575
|
.option('--electron', 'Treat this profile as an Electron desktop app: never call Target.createTarget; bind to the visible window using --target-filter or the skip-invisible heuristic')
|
|
331
576
|
.option('--target-filter <expr>', 'Pick the visible CDP page target when the app exposes more than one. Format: url:<substring> or title:<substring>')
|
|
332
577
|
.action(async (name, opts) => {
|
|
333
|
-
|
|
334
|
-
|
|
578
|
+
try {
|
|
579
|
+
assertRegistrableProfileName(name);
|
|
580
|
+
}
|
|
581
|
+
catch (err) {
|
|
582
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
335
583
|
process.exit(1);
|
|
336
584
|
}
|
|
337
585
|
if (!VALID_BROWSERS.includes(opts.browser)) {
|
|
@@ -370,22 +618,22 @@ function registerProfilesCommands(browser) {
|
|
|
370
618
|
height: DEFAULT_VIEWPORT.height,
|
|
371
619
|
};
|
|
372
620
|
if (opts.window) {
|
|
373
|
-
const
|
|
374
|
-
if (!
|
|
621
|
+
const size = parseWindowSize(String(opts.window));
|
|
622
|
+
if (!size) {
|
|
375
623
|
console.error(`--window must be WxH, e.g. ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}`);
|
|
376
624
|
process.exit(1);
|
|
377
625
|
}
|
|
378
|
-
viewport.width =
|
|
379
|
-
viewport.height =
|
|
626
|
+
viewport.width = size.width;
|
|
627
|
+
viewport.height = size.height;
|
|
380
628
|
}
|
|
381
629
|
if (opts.position) {
|
|
382
|
-
const
|
|
383
|
-
if (!
|
|
630
|
+
const pos = parseWindowPosition(String(opts.position));
|
|
631
|
+
if (!pos) {
|
|
384
632
|
console.error('--position must be X,Y, e.g. 80,80');
|
|
385
633
|
process.exit(1);
|
|
386
634
|
}
|
|
387
|
-
viewport.x =
|
|
388
|
-
viewport.y =
|
|
635
|
+
viewport.x = pos.x;
|
|
636
|
+
viewport.y = pos.y;
|
|
389
637
|
}
|
|
390
638
|
const profile = {
|
|
391
639
|
name,
|
|
@@ -399,14 +647,169 @@ function registerProfilesCommands(browser) {
|
|
|
399
647
|
chrome: opts.headless ? { headless: true } : undefined,
|
|
400
648
|
viewport,
|
|
401
649
|
};
|
|
402
|
-
await createProfile(profile
|
|
403
|
-
|
|
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()}.`);
|
|
404
655
|
// Warn (don't fail) if the declared secrets bundle doesn't exist yet — it
|
|
405
656
|
// may be created later, but a typo should surface now.
|
|
406
657
|
if (opts.secrets && !bundleExists(opts.secrets)) {
|
|
407
658
|
console.error(`warning: secrets bundle "${opts.secrets}" does not exist yet. Create it with: agents secrets create ${opts.secrets}`);
|
|
408
659
|
}
|
|
409
660
|
});
|
|
661
|
+
profiles
|
|
662
|
+
.command('edit <name>')
|
|
663
|
+
.description('Edit an existing profile in place (stays in the store it already lives in)')
|
|
664
|
+
// Declared only so the action can explain WHY the browser type is not
|
|
665
|
+
// editable. Without it commander rejects `-b` as an unknown option before
|
|
666
|
+
// the action runs, and the delete-and-recreate guidance never reaches anyone.
|
|
667
|
+
.option('-b, --browser <type>', 'Not editable — see the error for the delete-and-recreate path')
|
|
668
|
+
.option('-d, --description <text>', "Profile description (pass '' to clear)")
|
|
669
|
+
.option('-e, --endpoint <url>', 'Replace the endpoint list (repeatable)', collect, [])
|
|
670
|
+
.option('-s, --secrets <bundle>', 'Secrets bundle to inject')
|
|
671
|
+
.option('--headless', 'Run in headless mode')
|
|
672
|
+
.option('--no-headless', 'Run headed')
|
|
673
|
+
.option('--window <WxH>', 'Window size in CSS pixels')
|
|
674
|
+
.option('--position <X,Y>', 'Window position on screen, e.g. 80,80')
|
|
675
|
+
.option('--binary <path>', 'Absolute path to the browser/app binary')
|
|
676
|
+
.option('--electron', 'Treat this profile as an Electron desktop app')
|
|
677
|
+
.option('--no-electron', 'Stop treating it as an Electron app')
|
|
678
|
+
.option('--target-filter <expr>', "url:<substring> or title:<substring>; requires --electron (pass '' to clear)")
|
|
679
|
+
.option('--json', 'Output machine-readable JSON')
|
|
680
|
+
.action(async (name, opts) => {
|
|
681
|
+
// The browser type and the name are identity, not settings: both key the
|
|
682
|
+
// on-disk runtime dir and any live `<name>@<endpoint>` connection, so
|
|
683
|
+
// changing either orphans the cached browser data (and its logins).
|
|
684
|
+
if (opts.browser) {
|
|
685
|
+
console.error('browser type is not editable — it keys the on-disk profile cache. ' +
|
|
686
|
+
`Remove and recreate instead: agents browser profiles remove ${name} && agents browser profiles create ${name} -b <type>`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
if (opts.targetFilter) {
|
|
690
|
+
const parsed = parseTargetFilter(String(opts.targetFilter));
|
|
691
|
+
if (!parsed) {
|
|
692
|
+
console.error('--target-filter must be url:<substring> or title:<substring> (non-empty value, no leading whitespace)');
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const patch = {};
|
|
697
|
+
if (opts.description !== undefined)
|
|
698
|
+
patch.description = opts.description || undefined;
|
|
699
|
+
if (opts.endpoint && opts.endpoint.length > 0)
|
|
700
|
+
patch.endpoints = opts.endpoint;
|
|
701
|
+
if (opts.secrets !== undefined)
|
|
702
|
+
patch.secrets = opts.secrets;
|
|
703
|
+
if (opts.binary !== undefined)
|
|
704
|
+
patch.binary = opts.binary;
|
|
705
|
+
if (opts.targetFilter !== undefined)
|
|
706
|
+
patch.targetFilter = opts.targetFilter || undefined;
|
|
707
|
+
// commander maps --electron/--no-electron and --headless/--no-headless onto
|
|
708
|
+
// one boolean each, and leaves it undefined when neither was passed.
|
|
709
|
+
if (opts.electron !== undefined)
|
|
710
|
+
patch.electron = opts.electron || undefined;
|
|
711
|
+
if (opts.headless !== undefined || opts.window || opts.position) {
|
|
712
|
+
const current = await getProfile(name);
|
|
713
|
+
if (!current) {
|
|
714
|
+
console.error(`Profile "${name}" does not exist`);
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
if (opts.headless !== undefined) {
|
|
718
|
+
const chrome = { ...current.chrome };
|
|
719
|
+
if (opts.headless)
|
|
720
|
+
chrome.headless = true;
|
|
721
|
+
else
|
|
722
|
+
delete chrome.headless;
|
|
723
|
+
// `{ headless: undefined }` is truthy, so profileToConfig would persist
|
|
724
|
+
// a bare `chrome: {}` into the YAML and the change-detector would report
|
|
725
|
+
// a phantom edit on an already-headed profile. Drop the key entirely.
|
|
726
|
+
patch.chrome = Object.keys(chrome).length > 0 ? chrome : undefined;
|
|
727
|
+
}
|
|
728
|
+
if (opts.window || opts.position) {
|
|
729
|
+
const viewport = { ...(current.viewport ?? { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height }) };
|
|
730
|
+
if (opts.window) {
|
|
731
|
+
const size = parseWindowSize(String(opts.window));
|
|
732
|
+
if (!size) {
|
|
733
|
+
console.error('--window must be WxH, e.g. 1512x982');
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
viewport.width = size.width;
|
|
737
|
+
viewport.height = size.height;
|
|
738
|
+
}
|
|
739
|
+
if (opts.position) {
|
|
740
|
+
const pos = parseWindowPosition(String(opts.position));
|
|
741
|
+
if (!pos) {
|
|
742
|
+
console.error('--position must be X,Y, e.g. 80,80');
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
viewport.x = pos.x;
|
|
746
|
+
viewport.y = pos.y;
|
|
747
|
+
}
|
|
748
|
+
patch.viewport = viewport;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (Object.keys(patch).length === 0) {
|
|
752
|
+
console.error('Nothing to edit. Pass at least one field, e.g. -d "<description>".');
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
let result;
|
|
756
|
+
try {
|
|
757
|
+
result = await editProfile(name, patch);
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
if (opts.json) {
|
|
764
|
+
console.log(JSON.stringify({ ...result.profile, devices: result.devices, changed: result.changed }, null, 2));
|
|
765
|
+
}
|
|
766
|
+
else if (result.changed.length === 0) {
|
|
767
|
+
console.log(`No change: ${name} already had those values.`);
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
console.log(`Updated ${name} on ${machineId()}: ${result.changed.join(', ')}`);
|
|
771
|
+
}
|
|
772
|
+
if (patch.secrets && !bundleExists(patch.secrets)) {
|
|
773
|
+
console.error(`warning: secrets bundle "${patch.secrets}" does not exist yet. Create it with: agents secrets create ${patch.secrets}`);
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
profiles
|
|
777
|
+
.command('rename <from> <to>')
|
|
778
|
+
.description("Rename a profile, moving its browser data with it (logins survive)")
|
|
779
|
+
.option('--json', 'Output machine-readable JSON')
|
|
780
|
+
.action(async (from, to, opts) => {
|
|
781
|
+
let res;
|
|
782
|
+
try {
|
|
783
|
+
res = await renameProfile(from, to);
|
|
784
|
+
}
|
|
785
|
+
catch (err) {
|
|
786
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
if (opts.json) {
|
|
790
|
+
console.log(JSON.stringify({ from, to, ...res }, null, 2));
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
console.log(`Renamed ${from} -> ${to} on ${machineId()}`);
|
|
794
|
+
if (res.movedDirs.length > 0) {
|
|
795
|
+
console.log(` moved ${res.movedDirs.length} browser data dir${res.movedDirs.length === 1 ? '' : 's'} — logins preserved`);
|
|
796
|
+
}
|
|
797
|
+
if (res.repointedDefault) {
|
|
798
|
+
console.log(` browser.profile now points at ${to}`);
|
|
799
|
+
}
|
|
800
|
+
if (res.repointedViewer) {
|
|
801
|
+
console.log(` browser.viewer now points at ${to}`);
|
|
802
|
+
}
|
|
803
|
+
if (res.stalePins.length > 0) {
|
|
804
|
+
const devices = [...new Set(res.stalePins.map((p) => p.device))];
|
|
805
|
+
const one = devices.length === 1;
|
|
806
|
+
console.error(`warning: ${devices.join(', ')} still ${one ? 'pins' : 'pin'} "${from}" in ` +
|
|
807
|
+
`${one ? 'its' : 'their'} own device config. Fix with:`);
|
|
808
|
+
for (const pin of res.stalePins) {
|
|
809
|
+
console.error(` agents config set ${pin.key} ${to} --device ${pin.device}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
410
813
|
profiles
|
|
411
814
|
.command('show <name>')
|
|
412
815
|
.description('Show profile details')
|
|
@@ -485,8 +888,9 @@ function registerProfilesCommands(browser) {
|
|
|
485
888
|
}
|
|
486
889
|
});
|
|
487
890
|
profiles
|
|
488
|
-
.command('
|
|
489
|
-
.
|
|
891
|
+
.command('remove <name>')
|
|
892
|
+
.alias('delete')
|
|
893
|
+
.description('Remove a browser profile (drops YAML config + all cached runtime dirs)')
|
|
490
894
|
.option('--keep-cache', "Leave ~/.agents/.cache/browser/<name>* dirs in place (don't wipe chrome-data)")
|
|
491
895
|
.action(async (name, opts) => {
|
|
492
896
|
await deleteProfile(name);
|
|
@@ -519,24 +923,41 @@ function registerProfilesCommands(browser) {
|
|
|
519
923
|
});
|
|
520
924
|
profiles
|
|
521
925
|
.command('prune')
|
|
522
|
-
.description('Remove dead
|
|
926
|
+
.description('Remove dead profiles this device declares: browser not installed here, or never started')
|
|
523
927
|
.option('-n, --dry-run', 'Print what would be removed and exit without changing anything')
|
|
524
|
-
.option('--fleet', 'Also consider fleet-synced profiles (removing one removes it from EVERY machine)')
|
|
525
928
|
.option('--json', 'Output machine-readable JSON')
|
|
526
929
|
.action(async (opts) => {
|
|
527
|
-
const plan = await buildProfilePrunePlan(
|
|
930
|
+
const plan = await buildProfilePrunePlan();
|
|
528
931
|
if (!opts.dryRun)
|
|
529
932
|
await pruneProfiles(plan);
|
|
530
933
|
if (opts.json) {
|
|
531
934
|
console.log(JSON.stringify({ dryRun: !!opts.dryRun, ...plan }, null, 2));
|
|
532
935
|
return;
|
|
533
936
|
}
|
|
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.
|
|
940
|
+
const misfiled = plan.kept.filter((k) => k.misfiled);
|
|
941
|
+
const reportMisfiled = () => {
|
|
942
|
+
if (misfiled.length === 0)
|
|
943
|
+
return;
|
|
944
|
+
console.log('');
|
|
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:`);
|
|
948
|
+
for (const k of misfiled) {
|
|
949
|
+
console.log(` ${k.name} — ${k.why}`);
|
|
950
|
+
}
|
|
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.)');
|
|
953
|
+
};
|
|
534
954
|
if (plan.candidates.length === 0) {
|
|
535
955
|
console.log('Nothing to prune — every profile is in use, healthy, or protected.');
|
|
536
956
|
if (plan.kept.length > 0) {
|
|
537
957
|
for (const k of plan.kept)
|
|
538
958
|
console.log(` kept ${k.name} (${k.scope}) — ${k.why}`);
|
|
539
959
|
}
|
|
960
|
+
reportMisfiled();
|
|
540
961
|
return;
|
|
541
962
|
}
|
|
542
963
|
const verb = opts.dryRun ? 'Would remove' : 'Removed';
|
|
@@ -547,6 +968,7 @@ function registerProfilesCommands(browser) {
|
|
|
547
968
|
: '';
|
|
548
969
|
console.log(` ${c.name} (${c.scope}) — ${PRUNE_REASON_TEXT[c.reason]}${cache}`);
|
|
549
970
|
}
|
|
971
|
+
reportMisfiled();
|
|
550
972
|
if (opts.dryRun) {
|
|
551
973
|
console.log('');
|
|
552
974
|
console.log('Nothing was changed. Re-run without --dry-run to apply.');
|
|
@@ -554,16 +976,18 @@ function registerProfilesCommands(browser) {
|
|
|
554
976
|
});
|
|
555
977
|
setHelpSections(profiles, {
|
|
556
978
|
examples: `
|
|
557
|
-
#
|
|
979
|
+
# WHERE is the devices whose own file declares the name
|
|
558
980
|
agents browser profiles list
|
|
559
981
|
|
|
560
|
-
# Create one
|
|
982
|
+
# Create one on this device
|
|
983
|
+
agents browser profiles add work --browser chrome
|
|
561
984
|
agents browser profiles create work --browser chrome
|
|
562
985
|
|
|
563
|
-
#
|
|
564
|
-
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
|
|
565
989
|
|
|
566
|
-
# Clean up dead ones: check first, then apply
|
|
990
|
+
# Clean up dead ones this device declares: check first, then apply
|
|
567
991
|
agents browser profiles prune --dry-run
|
|
568
992
|
agents browser profiles prune
|
|
569
993
|
|
|
@@ -571,22 +995,24 @@ function registerProfilesCommands(browser) {
|
|
|
571
995
|
agents browser use work
|
|
572
996
|
`,
|
|
573
997
|
notes: `
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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.
|
|
577
1002
|
|
|
578
1003
|
In \`list\`, the \`*\` marker means "this machine's configured default"
|
|
579
1004
|
(\`agents browser start\` with no --profile). The auto-detected profile is
|
|
580
1005
|
named \`auto-chrome\`; \`default\` is only an ALIAS for whichever profile
|
|
581
1006
|
this machine is configured to use, resolved the same way by every command.
|
|
582
1007
|
|
|
583
|
-
\`prune\`
|
|
584
|
-
|
|
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\`.
|
|
585
1011
|
`,
|
|
586
1012
|
});
|
|
587
1013
|
profiles
|
|
588
1014
|
.command('doctor <name>')
|
|
589
|
-
.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')
|
|
590
1016
|
.action(async (name) => {
|
|
591
1017
|
const profile = await getProfile(name);
|
|
592
1018
|
if (!profile) {
|
|
@@ -594,6 +1020,29 @@ function registerProfilesCommands(browser) {
|
|
|
594
1020
|
process.exit(1);
|
|
595
1021
|
}
|
|
596
1022
|
const checks = [];
|
|
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';
|
|
1030
|
+
checks.push(misfiled.misfiled
|
|
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
|
+
}
|
|
597
1046
|
// 1. Binary exists for declared browser type, and is a real executable we
|
|
598
1047
|
// can drive — not a distro launcher script. findBrowserPath already
|
|
599
1048
|
// unwraps the known Chromium wrappers to their ELF; if it still hands
|
|
@@ -754,6 +1203,80 @@ function registerProfilesCommands(browser) {
|
|
|
754
1203
|
});
|
|
755
1204
|
}
|
|
756
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
|
+
});
|
|
757
1280
|
browser
|
|
758
1281
|
.command('remote-control [state]')
|
|
759
1282
|
.description("Allow or deny other fleet machines driving THIS machine's browser over `browser --device`. " +
|
|
@@ -794,6 +1317,7 @@ function registerTaskCommands(browser) {
|
|
|
794
1317
|
.command('stream')
|
|
795
1318
|
.description('Keep one process and daemon IPC socket open; read NDJSON requests from stdin and write NDJSON responses')
|
|
796
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)
|
|
797
1321
|
.action(async (opts) => {
|
|
798
1322
|
await runBrowserIPCStream({
|
|
799
1323
|
input: process.stdin,
|
|
@@ -829,6 +1353,7 @@ function registerTaskCommands(browser) {
|
|
|
829
1353
|
.option('--title <label>', 'Human label shown in `browser status` (defaults to first navigated host)')
|
|
830
1354
|
.option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
|
|
831
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)')
|
|
832
1357
|
.option('--fresh', 'Always open a new tab, skipping the reclaim of a tab an abandoned task is holding on that URL')
|
|
833
1358
|
.option('--no-skills', 'Skip auto-discovery of site-specific SKILL.md from ~/.agents/skills/browser/domain-skills/')
|
|
834
1359
|
.option('--record', 'Start recording right after the tab opens (shorthand for `agents browser record start` as a follow-up)')
|
|
@@ -836,9 +1361,11 @@ function registerTaskCommands(browser) {
|
|
|
836
1361
|
.option('--duration <sec>', 'Recording duration cap in seconds (with --record; default 60)', (v) => parseInt(v, 10))
|
|
837
1362
|
.option('--max-mb <mb>', 'Recording size cap in MB (with --record; default 25)', (v) => parseInt(v, 10))
|
|
838
1363
|
.action(async (opts) => {
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
1364
|
+
// Fast-fail copy of the consent gate, so a refused start never resolves or
|
|
1365
|
+
// auto-creates a profile. The AUTHORITATIVE gate is in the daemon
|
|
1366
|
+
// (BrowserService.start / resolveOrCreateTask, via
|
|
1367
|
+
// assertRemoteControlAllowedForRequest) — it has to be, because the page
|
|
1368
|
+
// verbs create a browser implicitly and never reach this command.
|
|
842
1369
|
try {
|
|
843
1370
|
assertRemoteControlAllowed();
|
|
844
1371
|
}
|
|
@@ -882,6 +1409,52 @@ function registerTaskCommands(browser) {
|
|
|
882
1409
|
process.exit(1);
|
|
883
1410
|
}
|
|
884
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
|
+
}
|
|
885
1458
|
// Grounded login guardrail: if the opening URL is a known login-gated
|
|
886
1459
|
// service and the resolved profile has no session for it, say so on stderr
|
|
887
1460
|
// and name a profile that IS logged in. Reads the profile's real cookie DB
|
|
@@ -917,18 +1490,27 @@ function registerTaskCommands(browser) {
|
|
|
917
1490
|
console.error(response.error);
|
|
918
1491
|
process.exit(1);
|
|
919
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
|
+
}
|
|
920
1504
|
// stdout: just the resolved name, one line, no decoration. Lets callers do:
|
|
921
1505
|
// export AGENTS_BROWSER_TASK=$(agents browser start --profile work)
|
|
922
1506
|
console.log(response.task);
|
|
923
1507
|
// stderr: human-friendly commentary so a TTY user still sees what happened.
|
|
924
1508
|
// Shell substitution captures stdout only, so $(...) stays clean.
|
|
1509
|
+
console.error(`Task "${response.task}" started on ${boundDevice} (profile: ${profileName}).`);
|
|
925
1510
|
if (opts.url && response.tabId) {
|
|
926
|
-
console.error(`
|
|
927
|
-
}
|
|
928
|
-
else {
|
|
929
|
-
console.error(`Started task "${response.task}"`);
|
|
1511
|
+
console.error(`Tab ${response.tabId}`);
|
|
930
1512
|
}
|
|
931
|
-
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.');
|
|
932
1514
|
console.error('Try: agents browser screenshot | agents browser console --level error');
|
|
933
1515
|
// Surface the matched domain-skill (if any) so an agent driving the
|
|
934
1516
|
// task picks up site-specific selectors and gotchas before it starts
|
|
@@ -965,6 +1547,7 @@ function registerTaskCommands(browser) {
|
|
|
965
1547
|
.command('done')
|
|
966
1548
|
.description('Complete a task and close its tabs (resolves from caller identity when --task is omitted)')
|
|
967
1549
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1550
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
968
1551
|
.action(async (opts) => {
|
|
969
1552
|
const task = resolveTaskName(opts);
|
|
970
1553
|
const response = await sendIPCRequest({
|
|
@@ -985,6 +1568,7 @@ function registerTaskCommands(browser) {
|
|
|
985
1568
|
.command('stop')
|
|
986
1569
|
.description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection)')
|
|
987
1570
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1571
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
988
1572
|
.option('-p, --profile <name>', 'Detach the whole profile (incl. composite "name@endpoint") instead of stopping a single task')
|
|
989
1573
|
.action(async (opts) => {
|
|
990
1574
|
if (opts.profile) {
|
|
@@ -1016,7 +1600,8 @@ function registerTaskCommands(browser) {
|
|
|
1016
1600
|
console.log(`Stopped task: ${response.task}`);
|
|
1017
1601
|
});
|
|
1018
1602
|
browser
|
|
1019
|
-
.command('
|
|
1603
|
+
.command('prune')
|
|
1604
|
+
.alias('gc')
|
|
1020
1605
|
.description('Close tabs for abandoned tasks — owning agent session exited, or idle past the window — and mark them done. ' +
|
|
1021
1606
|
'The same reaper the daemon already runs every 5 minutes; use this to run it now.')
|
|
1022
1607
|
.option('--dry-run', 'List what would be closed without closing anything')
|
|
@@ -1053,11 +1638,44 @@ function registerTaskCommands(browser) {
|
|
|
1053
1638
|
console.log(` ${c.task} (${c.profile}, ${c.reason})`);
|
|
1054
1639
|
}
|
|
1055
1640
|
});
|
|
1641
|
+
browser
|
|
1642
|
+
.command('show <url>')
|
|
1643
|
+
.description('Open a URL for a human to read: goes to browser.viewer (default: browser.profile), and binds no task')
|
|
1644
|
+
.option('--os-browser', 'Use the OS default handler instead of the configured viewer')
|
|
1645
|
+
.option('--json', 'Output machine-readable JSON')
|
|
1646
|
+
.action(async (url, opts) => {
|
|
1647
|
+
// The entry point external tools need. `navigate` binds a task, which the
|
|
1648
|
+
// abandoned-task reaper closes when the calling session ends — wrong for a
|
|
1649
|
+
// page a person is reading. This does not.
|
|
1650
|
+
const { showUrl, showFile } = await import('../lib/open-url.js');
|
|
1651
|
+
// `+` not `*` on the scheme body: `[a-z][a-z0-9+.-]*:` matches a Windows
|
|
1652
|
+
// drive letter (`C:\Users\me\plan.html`), so a real path was treated as a
|
|
1653
|
+
// URL. A scheme is at least two characters.
|
|
1654
|
+
const isLocalFile = !/^[a-z][a-z0-9+.-]+:/i.test(url);
|
|
1655
|
+
const outcome = isLocalFile
|
|
1656
|
+
? await showFile(path.resolve(url), { osBrowser: opts.osBrowser })
|
|
1657
|
+
: await showUrl(url, { osBrowser: opts.osBrowser });
|
|
1658
|
+
if (opts.json) {
|
|
1659
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
1660
|
+
}
|
|
1661
|
+
else if (outcome.via === 'none') {
|
|
1662
|
+
console.error(`Could not open a browser — open this yourself:\n ${url}`);
|
|
1663
|
+
}
|
|
1664
|
+
else if (outcome.via === 'profile') {
|
|
1665
|
+
console.log(`Shown in ${outcome.profile}: ${url}`);
|
|
1666
|
+
}
|
|
1667
|
+
else {
|
|
1668
|
+
console.log(`Opened in the OS default browser: ${url}`);
|
|
1669
|
+
}
|
|
1670
|
+
if (outcome.via === 'none')
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
});
|
|
1056
1673
|
browser
|
|
1057
1674
|
.command('navigate [url]')
|
|
1058
1675
|
.alias('goto')
|
|
1059
1676
|
.description('Navigate current tab to URL (creates a task and tab when none exist)')
|
|
1060
1677
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1678
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1061
1679
|
.option('-u, --url <url>', 'URL to navigate to (or pass it positionally)')
|
|
1062
1680
|
.option('-p, --profile <name>', 'Browser profile (optional if task is unique)')
|
|
1063
1681
|
.action(async (urlPos, opts) => {
|
|
@@ -1088,6 +1706,7 @@ function registerTaskCommands(browser) {
|
|
|
1088
1706
|
.command('add')
|
|
1089
1707
|
.description('Open URL in new tab (becomes current)')
|
|
1090
1708
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1709
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1091
1710
|
.requiredOption('--url <url>', 'URL to open in the new tab')
|
|
1092
1711
|
.option('-p, --profile <name>', 'Browser profile')
|
|
1093
1712
|
.action(async (opts) => {
|
|
@@ -1108,6 +1727,7 @@ function registerTaskCommands(browser) {
|
|
|
1108
1727
|
.command('focus <tabId>')
|
|
1109
1728
|
.description('Switch to tab (by ID, prefix, or URL substring)')
|
|
1110
1729
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1730
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1111
1731
|
.action(async (tabId, opts) => {
|
|
1112
1732
|
const task = resolveTaskName(opts);
|
|
1113
1733
|
const response = await sendIPCRequest({
|
|
@@ -1125,6 +1745,7 @@ function registerTaskCommands(browser) {
|
|
|
1125
1745
|
.command('close [tabId]')
|
|
1126
1746
|
.description('Close tab(s) — omit tabId to close all')
|
|
1127
1747
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1748
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1128
1749
|
.action(async (tabId, opts) => {
|
|
1129
1750
|
const task = resolveTaskName(opts);
|
|
1130
1751
|
const response = await sendIPCRequest({
|
|
@@ -1142,6 +1763,7 @@ function registerTaskCommands(browser) {
|
|
|
1142
1763
|
.command('tabs')
|
|
1143
1764
|
.description('List tabs open for the current task')
|
|
1144
1765
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1766
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1145
1767
|
.option('--json', 'Output machine-readable JSON')
|
|
1146
1768
|
.action(async (opts) => {
|
|
1147
1769
|
const task = resolveTaskName(opts);
|
|
@@ -1177,6 +1799,7 @@ function registerTaskCommands(browser) {
|
|
|
1177
1799
|
.command('screenshot')
|
|
1178
1800
|
.description('Take a screenshot — auto-saved per task; --output only needed when you want a specific path')
|
|
1179
1801
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1802
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1180
1803
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1181
1804
|
.option('-o, --output <path>', 'Specific output path (otherwise auto-saved under sessions/<task>/)')
|
|
1182
1805
|
.option('-q, --quality <mode>', 'compressed (JPEG, capped at ~100 KB — default) or raw (PNG, pixel-faithful)', 'compressed')
|
|
@@ -1197,17 +1820,22 @@ function registerTaskCommands(browser) {
|
|
|
1197
1820
|
console.error(response.error);
|
|
1198
1821
|
process.exit(1);
|
|
1199
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;
|
|
1200
1828
|
// stdout: just the path, so `P=$(agents browser screenshot)` works.
|
|
1201
|
-
console.log(
|
|
1829
|
+
console.log(savedPath);
|
|
1202
1830
|
// stderr: human commentary with size + dimensions, so an agent can
|
|
1203
1831
|
// see at a glance what was captured without `ls -l && file` round-trips.
|
|
1204
1832
|
const size = humanizeBytes(response.bytes);
|
|
1205
1833
|
const dims = response.width && response.height ? `${response.width}×${response.height}` : 'unknown size';
|
|
1206
|
-
console.error(`Saved screenshot to ${
|
|
1834
|
+
console.error(`Saved screenshot to ${savedPath} (${size}, ${dims})`);
|
|
1207
1835
|
// When auto-saving (no --output), surface the directory once so the
|
|
1208
1836
|
// agent doesn't have to dirname() the path or guess where files land.
|
|
1209
|
-
if (!opts.output &&
|
|
1210
|
-
const dir = path.dirname(
|
|
1837
|
+
if (!opts.output && savedPath) {
|
|
1838
|
+
const dir = path.dirname(savedPath);
|
|
1211
1839
|
console.error(`Tip: auto-saving to ${dir}. Pass --output <path> to choose a path.`);
|
|
1212
1840
|
}
|
|
1213
1841
|
});
|
|
@@ -1215,6 +1843,7 @@ function registerTaskCommands(browser) {
|
|
|
1215
1843
|
.command('pdf [output]')
|
|
1216
1844
|
.description('Export the current tab as PDF via CDP Page.printToPDF — auto-saved under sessions/<task>/ when [output] is omitted')
|
|
1217
1845
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1846
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1218
1847
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1219
1848
|
.action(async (output, opts) => {
|
|
1220
1849
|
const task = resolveTaskName(opts);
|
|
@@ -1241,6 +1870,7 @@ function registerTaskCommands(browser) {
|
|
|
1241
1870
|
.alias('eval')
|
|
1242
1871
|
.description('Evaluate JavaScript in current tab')
|
|
1243
1872
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1873
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1244
1874
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1245
1875
|
.option('-e, --expression <js>', 'JavaScript expression to evaluate (or pass it positionally)')
|
|
1246
1876
|
.option('-f, --file <path>', 'Path to a .js file whose contents will be evaluated')
|
|
@@ -1590,6 +2220,7 @@ function registerTaskCommands(browser) {
|
|
|
1590
2220
|
.command('refs')
|
|
1591
2221
|
.description('Get DOM refs for interactive elements')
|
|
1592
2222
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2223
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1593
2224
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1594
2225
|
.option('--all', 'Include non-interactive elements')
|
|
1595
2226
|
.option('-l, --limit <n>', 'Max elements (default 500)', '500')
|
|
@@ -1632,6 +2263,7 @@ function registerTaskCommands(browser) {
|
|
|
1632
2263
|
.command('click [ref]')
|
|
1633
2264
|
.description('Click an element by ref, or raw coordinates with --at X,Y')
|
|
1634
2265
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2266
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1635
2267
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1636
2268
|
.option('--at <x,y>', 'Click viewport coordinates (e.g. --at 320,540), bypassing ref resolution')
|
|
1637
2269
|
.action(async (ref, opts) => {
|
|
@@ -1674,6 +2306,7 @@ function registerTaskCommands(browser) {
|
|
|
1674
2306
|
.command('type <ref>')
|
|
1675
2307
|
.description('Type text into an element by ref')
|
|
1676
2308
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2309
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1677
2310
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1678
2311
|
.option('--text <text>', 'Text to type (use quotes for spaces/special chars)')
|
|
1679
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.')
|
|
@@ -1738,6 +2371,7 @@ function registerTaskCommands(browser) {
|
|
|
1738
2371
|
.command('press <key>')
|
|
1739
2372
|
.description('Press a key (Enter, Tab, Escape, etc)')
|
|
1740
2373
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2374
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1741
2375
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1742
2376
|
.action(async (key, opts) => {
|
|
1743
2377
|
const task = resolveTaskName(opts);
|
|
@@ -1757,6 +2391,7 @@ function registerTaskCommands(browser) {
|
|
|
1757
2391
|
.command('hover <ref>')
|
|
1758
2392
|
.description('Hover over an element by ref')
|
|
1759
2393
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2394
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1760
2395
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1761
2396
|
.action(async (ref, opts) => {
|
|
1762
2397
|
const task = resolveTaskName(opts);
|
|
@@ -1776,6 +2411,7 @@ function registerTaskCommands(browser) {
|
|
|
1776
2411
|
.command('scroll')
|
|
1777
2412
|
.description('Scroll the page by pixel amount (negatives scroll up/left)')
|
|
1778
2413
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2414
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1779
2415
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1780
2416
|
.option('--dx <n>', 'Horizontal pixels (negative = left)', (v) => parseInt(v, 10), 0)
|
|
1781
2417
|
.option('--dy <n>', 'Vertical pixels (negative = up)', (v) => parseInt(v, 10), 0)
|
|
@@ -1810,6 +2446,7 @@ function registerTaskCommands(browser) {
|
|
|
1810
2446
|
.command('upload')
|
|
1811
2447
|
.description('Upload file(s) — supports hidden file inputs, drag-drop targets, and OS chooser interception')
|
|
1812
2448
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2449
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1813
2450
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1814
2451
|
.option('-r, --ref <n>', 'Ref of the upload target element (file input or drop zone)', (v) => parseInt(v, 10))
|
|
1815
2452
|
.option('--trigger <n>', 'Ref of a button that opens the OS file chooser (Pattern C)', (v) => parseInt(v, 10))
|
|
@@ -1861,6 +2498,7 @@ function registerTaskCommands(browser) {
|
|
|
1861
2498
|
.command('viewport <width> <height>')
|
|
1862
2499
|
.description('Set viewport size')
|
|
1863
2500
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2501
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1864
2502
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1865
2503
|
.option('-m, --mobile', 'Enable mobile emulation')
|
|
1866
2504
|
.option('-s, --scale <factor>', 'Device scale factor', parseFloat)
|
|
@@ -1885,6 +2523,7 @@ function registerTaskCommands(browser) {
|
|
|
1885
2523
|
.command('device <device-name>')
|
|
1886
2524
|
.description('Emulate a device (iPhone 14, iPad, MacBook Pro)')
|
|
1887
2525
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2526
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1888
2527
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1889
2528
|
.action(async (deviceName, opts) => {
|
|
1890
2529
|
const task = resolveTaskName(opts);
|
|
@@ -1915,6 +2554,7 @@ function registerTaskCommands(browser) {
|
|
|
1915
2554
|
.command('console')
|
|
1916
2555
|
.description('Read console logs from a tab')
|
|
1917
2556
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2557
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1918
2558
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1919
2559
|
.option('-l, --level <level>', 'Filter by level (log, info, warn, error)')
|
|
1920
2560
|
.option('--clear', 'Clear logs after reading')
|
|
@@ -1945,6 +2585,7 @@ function registerTaskCommands(browser) {
|
|
|
1945
2585
|
.command('errors')
|
|
1946
2586
|
.description('Read page errors from a tab')
|
|
1947
2587
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2588
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1948
2589
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1949
2590
|
.option('--clear', 'Clear errors after reading')
|
|
1950
2591
|
.action(async (opts) => {
|
|
@@ -1977,6 +2618,7 @@ function registerTaskCommands(browser) {
|
|
|
1977
2618
|
.command('requests')
|
|
1978
2619
|
.description('Read captured network requests. --format har emits a HAR 1.2 JSON document.')
|
|
1979
2620
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2621
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
1980
2622
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1981
2623
|
.option('-f, --filter <text>', 'Filter URLs containing text')
|
|
1982
2624
|
.option('--clear', 'Clear requests after reading')
|
|
@@ -2022,6 +2664,7 @@ function registerTaskCommands(browser) {
|
|
|
2022
2664
|
.command('logs')
|
|
2023
2665
|
.description('Read merged rush-app + rush-cli JSONL logs for a task')
|
|
2024
2666
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2667
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2025
2668
|
.option('--source <name>', 'Source to scope to: rush-app or rush-cli (default both)')
|
|
2026
2669
|
.option('--lines <n>', 'Tail N entries (default 200; ignored when --since)', (v) => parseInt(v, 10))
|
|
2027
2670
|
.option('--since <when>', 'Absolute timestamp or relative offset (e.g. 5m, 2h, 1d)')
|
|
@@ -2060,6 +2703,7 @@ function registerTaskCommands(browser) {
|
|
|
2060
2703
|
.command('responsebody <url-pattern>')
|
|
2061
2704
|
.description('Wait for and read a response body by URL pattern')
|
|
2062
2705
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2706
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2063
2707
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
2064
2708
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
2065
2709
|
.option('--max-chars <n>', 'Max characters to return', parseInt)
|
|
@@ -2084,6 +2728,7 @@ function registerTaskCommands(browser) {
|
|
|
2084
2728
|
.command('wait')
|
|
2085
2729
|
.description('Wait for a condition')
|
|
2086
2730
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2731
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2087
2732
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
2088
2733
|
.option('--time <ms>', 'Wait for milliseconds')
|
|
2089
2734
|
.option('--selector <css>', 'Wait for CSS selector to appear')
|
|
@@ -2138,6 +2783,7 @@ function registerTaskCommands(browser) {
|
|
|
2138
2783
|
.command('download')
|
|
2139
2784
|
.description("Set the download directory for a task (defaults to the profile's downloads dir)")
|
|
2140
2785
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2786
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2141
2787
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
2142
2788
|
.option('-p, --path <dir>', "Download directory path (default: the profile's downloads dir)")
|
|
2143
2789
|
.action(async (opts) => {
|
|
@@ -2170,6 +2816,7 @@ function registerTaskCommands(browser) {
|
|
|
2170
2816
|
.command('start')
|
|
2171
2817
|
.description('Start recording — auto-saved under sessions/<task>/recordings/. Bounded by --fps, --duration, --max-mb.')
|
|
2172
2818
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2819
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2173
2820
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
2174
2821
|
.option('--fps <n>', 'Frames per second (1–30, default 5)', (v) => parseInt(v, 10))
|
|
2175
2822
|
.option('--duration <sec>', 'Hard duration cap in seconds (default 60)', (v) => parseInt(v, 10))
|
|
@@ -2197,6 +2844,7 @@ function registerTaskCommands(browser) {
|
|
|
2197
2844
|
.command('stop')
|
|
2198
2845
|
.description('Stop an in-progress recording')
|
|
2199
2846
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2847
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2200
2848
|
.action(async (opts) => {
|
|
2201
2849
|
const task = resolveTaskName(opts);
|
|
2202
2850
|
const response = await sendIPCRequest({ action: 'record-stop', task });
|
|
@@ -2213,6 +2861,7 @@ function registerTaskCommands(browser) {
|
|
|
2213
2861
|
.command('waitdownload')
|
|
2214
2862
|
.description('Wait for a download to complete')
|
|
2215
2863
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2864
|
+
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2216
2865
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
2217
2866
|
.action(async (opts) => {
|
|
2218
2867
|
const task = resolveTaskName(opts);
|