@phnx-labs/agents-cli 1.22.117 → 1.22.118
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 +235 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -1
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup.js +9 -9
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +5 -10
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebDriver BiDi transport for Firefox (PHNX-4043).
|
|
3
|
-
*
|
|
4
|
-
* Firefox dropped the Chrome DevTools Protocol in 129, so agents drive it over
|
|
5
|
-
* WebDriver BiDi — a JSON-RPC protocol on a WebSocket at
|
|
6
|
-
* `ws://127.0.0.1:<port>/session`. The shape mirrors the CDP driver
|
|
7
|
-
* (`drivers/local.ts`): launch or attach, hand back a live client the service
|
|
8
|
-
* routes every `bidi`-backend action through.
|
|
9
|
-
*
|
|
10
|
-
* Launch: `firefox --remote-debugging-port <port> --profile <dir> --no-remote
|
|
11
|
-
* [--headless]`. `--remote-debugging-port` is what turns BiDi on; `--no-remote`
|
|
12
|
-
* forces a fresh instance bound to that profile rather than handing the URL to
|
|
13
|
-
* an already-running Firefox. When a Firefox already holds the profile without a
|
|
14
|
-
* debug port we cannot launch a rival (Firefox is single-instance per profile),
|
|
15
|
-
* so we fail loud with the exact relaunch — the same contract
|
|
16
|
-
* `attachOnlyRequiredError` states for Chromium.
|
|
17
|
-
*/
|
|
18
|
-
import { spawn } from 'node:child_process';
|
|
19
|
-
import * as fs from 'node:fs';
|
|
20
|
-
import * as path from 'node:path';
|
|
21
|
-
import WSWebSocket from 'ws';
|
|
22
|
-
import { findBrowserPath } from '../chrome.js';
|
|
23
|
-
import { writeProfileRuntime, isProcessAlive } from '../runtime-state.js';
|
|
24
|
-
/**
|
|
25
|
-
* A verb Firefox-over-BiDi cannot serve (network capture, upload, pdf, trusted
|
|
26
|
-
* key input, …). Mirrors `ArcNativeCapabilityError`: the service throws it in
|
|
27
|
-
* the one place the capability is missing, and the message steers the caller to
|
|
28
|
-
* a Chromium-family profile that has it.
|
|
29
|
-
*/
|
|
30
|
-
export class FirefoxCapabilityError extends Error {
|
|
31
|
-
capability;
|
|
32
|
-
constructor(capability, message) {
|
|
33
|
-
super(message ??
|
|
34
|
-
`Firefox (WebDriver BiDi) does not support ${capability}. ` +
|
|
35
|
-
`Use a Chromium-family profile (chrome/comet/chromium/brave/edge) for that capability.`);
|
|
36
|
-
this.capability = capability;
|
|
37
|
-
this.name = 'FirefoxCapabilityError';
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/** A BiDi error frame carrying the protocol error + human message. */
|
|
41
|
-
class FirefoxBiDiError extends Error {
|
|
42
|
-
bidiError;
|
|
43
|
-
constructor(bidiError, message) {
|
|
44
|
-
super(`Firefox BiDi ${bidiError}: ${message}`);
|
|
45
|
-
this.bidiError = bidiError;
|
|
46
|
-
this.name = 'FirefoxBiDiError';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Minimal WebDriver BiDi client: request/response keyed by the `id` field, with
|
|
51
|
-
* events drained and ignored. Deliberately shaped like `CDPClient` so the
|
|
52
|
-
* service treats a Firefox connection the same way it treats a CDP one. The
|
|
53
|
-
* large `maxPayload` matches CDPClient's, so a base64 screenshot of a
|
|
54
|
-
* content-rich page never trips the socket's decompressed-size cap.
|
|
55
|
-
*/
|
|
56
|
-
export class FirefoxBiDiClient {
|
|
57
|
-
ws = null;
|
|
58
|
-
nextId = 0;
|
|
59
|
-
pending = new Map();
|
|
60
|
-
closed = false;
|
|
61
|
-
get isOpen() {
|
|
62
|
-
return this.ws !== null && this.ws.readyState === WSWebSocket.OPEN;
|
|
63
|
-
}
|
|
64
|
-
async connect(url) {
|
|
65
|
-
await new Promise((resolve, reject) => {
|
|
66
|
-
this.ws = new WSWebSocket(url, { maxPayload: 256 * 1024 * 1024 });
|
|
67
|
-
this.ws.once('open', () => resolve());
|
|
68
|
-
this.ws.once('error', () => reject(new Error('WebSocket error')));
|
|
69
|
-
});
|
|
70
|
-
this.ws.on('message', (data) => this.handleMessage(String(data)));
|
|
71
|
-
this.ws.on('close', () => this.handleClose());
|
|
72
|
-
}
|
|
73
|
-
handleMessage(raw) {
|
|
74
|
-
let msg;
|
|
75
|
-
try {
|
|
76
|
-
msg = JSON.parse(raw);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// BiDi frames: type 'success' | 'error' carry an `id`; type 'event' does not.
|
|
82
|
-
const id = typeof msg.id === 'number' ? msg.id : undefined;
|
|
83
|
-
if (id === undefined)
|
|
84
|
-
return; // event — nothing subscribes yet.
|
|
85
|
-
const call = this.pending.get(id);
|
|
86
|
-
if (!call)
|
|
87
|
-
return;
|
|
88
|
-
this.pending.delete(id);
|
|
89
|
-
if (msg.type === 'error') {
|
|
90
|
-
call.reject(new FirefoxBiDiError(String(msg.error ?? 'unknown error'), String(msg.message ?? 'no message')));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
call.resolve(msg.result ?? {});
|
|
94
|
-
}
|
|
95
|
-
handleClose() {
|
|
96
|
-
this.closed = true;
|
|
97
|
-
for (const [, call] of this.pending) {
|
|
98
|
-
call.reject(new Error('Firefox BiDi connection closed'));
|
|
99
|
-
}
|
|
100
|
-
this.pending.clear();
|
|
101
|
-
}
|
|
102
|
-
send(method, params = {}) {
|
|
103
|
-
if (!this.isOpen) {
|
|
104
|
-
throw new Error('Firefox BiDi connection not open — the browser was likely closed externally. ' +
|
|
105
|
-
'Run `agents browser stop --profile <name>` (or restart the daemon) and try again.');
|
|
106
|
-
}
|
|
107
|
-
const id = ++this.nextId;
|
|
108
|
-
return new Promise((resolve, reject) => {
|
|
109
|
-
this.pending.set(id, { resolve: resolve, reject });
|
|
110
|
-
this.ws.send(JSON.stringify({ id, method, params }));
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
close() {
|
|
114
|
-
if (this.closed)
|
|
115
|
-
return;
|
|
116
|
-
this.closed = true;
|
|
117
|
-
try {
|
|
118
|
-
this.ws?.close();
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
/* already closing */
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const BIDI_CONNECT_ATTEMPTS = 120; // 120 × 250ms ≈ 30s — cold snap Firefox first-run is slow.
|
|
126
|
-
const BIDI_CONNECT_INTERVAL_MS = 250;
|
|
127
|
-
function bidiUrl(port) {
|
|
128
|
-
return `ws://127.0.0.1:${port}/session`;
|
|
129
|
-
}
|
|
130
|
-
/** Open a BiDi socket + session against a port, or null when nothing is there yet. */
|
|
131
|
-
async function tryOpenSession(port) {
|
|
132
|
-
const bidi = new FirefoxBiDiClient();
|
|
133
|
-
try {
|
|
134
|
-
await bidi.connect(bidiUrl(port));
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
try {
|
|
140
|
-
const result = await bidi.send('session.new', { capabilities: {} });
|
|
141
|
-
return { bidi, sessionId: String(result.sessionId ?? '') };
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
bidi.close();
|
|
145
|
-
// A live BiDi endpoint that refuses session.new is a real, surfaced failure
|
|
146
|
-
// (e.g. a session already exists) — never swallow it as "nothing listening".
|
|
147
|
-
throw err;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* The loud error raised when a Firefox already holds this profile but exposes no
|
|
152
|
-
* BiDi port, so agents cannot attach and must not launch a rival (Firefox is
|
|
153
|
-
* single-instance per profile dir). Mirrors `attachOnlyRequiredError` in
|
|
154
|
-
* `drivers/local.ts`: name the exact relaunch that makes the running instance
|
|
155
|
-
* attachable. Exported so the contract is unit-testable without a real Firefox.
|
|
156
|
-
*/
|
|
157
|
-
export function firefoxAttachRequiredError(profile, port, profileDir) {
|
|
158
|
-
return new Error(`Profile "${profile.name}" is a Firefox profile and nothing is serving WebDriver BiDi on ` +
|
|
159
|
-
`ws://127.0.0.1:${port}/session. A Firefox is already running on this profile directory ` +
|
|
160
|
-
`without a debug port, and Firefox is single-instance per profile — agents will not launch ` +
|
|
161
|
-
`a second one. Quit that Firefox, then relaunch it with remote debugging on this port:\n` +
|
|
162
|
-
` firefox --remote-debugging-port ${port} --profile ${profileDir} --no-remote\n` +
|
|
163
|
-
`and retry. Or close it entirely and let agents launch it headless for you.`);
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Whether a Firefox is currently holding `profileDir`. Firefox writes a `lock`
|
|
167
|
-
* symlink (POSIX: `<dir>/lock` → `<ip>:+<pid>`; macOS: `.parentlock`; Windows:
|
|
168
|
-
* `parent.lock`) while the profile is open. We only treat it as held when the
|
|
169
|
-
* lock resolves to a live pid, so a stale lock from a crash never wedges launch.
|
|
170
|
-
*/
|
|
171
|
-
function firefoxHoldsProfile(profileDir) {
|
|
172
|
-
const link = path.join(profileDir, 'lock');
|
|
173
|
-
try {
|
|
174
|
-
const target = fs.readlinkSync(link); // e.g. "127.0.0.1:+3685166"
|
|
175
|
-
const pid = Number(target.split('+').pop());
|
|
176
|
-
if (Number.isFinite(pid) && pid > 0)
|
|
177
|
-
return isProcessAlive(pid);
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
/* no POSIX lock symlink */
|
|
181
|
-
}
|
|
182
|
-
// macOS/Windows lock files are plain files, not symlinks with a pid. Their mere
|
|
183
|
-
// presence is a weak signal (they can be stale), so we do not block on them —
|
|
184
|
-
// a launch that collides fails loud below via the connect timeout instead.
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Connect a Firefox profile over BiDi: attach if the port is already served,
|
|
189
|
-
* otherwise launch Firefox and wait for it. Fails loud when a Firefox holds the
|
|
190
|
-
* profile without a port.
|
|
191
|
-
*/
|
|
192
|
-
export async function connectFirefox(profile, key, port, opts = { profileDir: '' }) {
|
|
193
|
-
const profileDir = opts.profileDir || profile.userDataDir || '';
|
|
194
|
-
if (!profileDir) {
|
|
195
|
-
throw new Error(`Firefox profile "${profile.name}" has no profile directory to launch with.`);
|
|
196
|
-
}
|
|
197
|
-
// 1. Already serving BiDi on the port → attach (pid 0, we did not launch it).
|
|
198
|
-
const attached = await tryOpenSession(port);
|
|
199
|
-
if (attached) {
|
|
200
|
-
return { bidi: attached.bidi, port, pid: 0, sessionId: attached.sessionId };
|
|
201
|
-
}
|
|
202
|
-
// 2. A Firefox holds the profile but exposes no port → fail loud (never a rival).
|
|
203
|
-
if (firefoxHoldsProfile(profileDir)) {
|
|
204
|
-
throw firefoxAttachRequiredError(profile, port, profileDir);
|
|
205
|
-
}
|
|
206
|
-
// 3. Launch our own headless Firefox bound to this profile + port.
|
|
207
|
-
const binary = findBrowserPath('firefox', profile.binary);
|
|
208
|
-
const headless = opts.headless ?? profile.chrome?.headless ?? true;
|
|
209
|
-
const args = [
|
|
210
|
-
'--remote-debugging-port',
|
|
211
|
-
String(port),
|
|
212
|
-
'--profile',
|
|
213
|
-
profileDir,
|
|
214
|
-
'--no-remote',
|
|
215
|
-
...(headless ? ['--headless'] : []),
|
|
216
|
-
];
|
|
217
|
-
const child = spawn(binary, args, {
|
|
218
|
-
detached: true,
|
|
219
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
220
|
-
env: { ...process.env },
|
|
221
|
-
});
|
|
222
|
-
child.unref();
|
|
223
|
-
const pid = child.pid ?? 0;
|
|
224
|
-
writeProfileRuntime(key, {
|
|
225
|
-
pid,
|
|
226
|
-
port,
|
|
227
|
-
command: path.basename(binary),
|
|
228
|
-
userDataDir: profileDir,
|
|
229
|
-
kind: 'browser',
|
|
230
|
-
});
|
|
231
|
-
for (let i = 0; i < BIDI_CONNECT_ATTEMPTS; i++) {
|
|
232
|
-
// A Firefox handed the URL to an already-running instance exits almost
|
|
233
|
-
// immediately without ever opening the port — detect that and fail loud
|
|
234
|
-
// rather than waiting out the full timeout.
|
|
235
|
-
if (pid && !isProcessAlive(pid) && i > 4) {
|
|
236
|
-
throw firefoxAttachRequiredError(profile, port, profileDir);
|
|
237
|
-
}
|
|
238
|
-
const session = await tryOpenSession(port);
|
|
239
|
-
if (session) {
|
|
240
|
-
return { bidi: session.bidi, port, pid, sessionId: session.sessionId };
|
|
241
|
-
}
|
|
242
|
-
await new Promise((r) => setTimeout(r, BIDI_CONNECT_INTERVAL_MS));
|
|
243
|
-
}
|
|
244
|
-
try {
|
|
245
|
-
if (pid)
|
|
246
|
-
process.kill(pid, 'SIGKILL');
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
/* already gone */
|
|
250
|
-
}
|
|
251
|
-
throw new Error(`Firefox for profile "${profile.name}" never exposed WebDriver BiDi on ` +
|
|
252
|
-
`ws://127.0.0.1:${port}/session within ${(BIDI_CONNECT_ATTEMPTS * BIDI_CONNECT_INTERVAL_MS) / 1000}s. ` +
|
|
253
|
-
`Check that Firefox is installed (\`${binary}\`) and that the profile directory is not locked.`);
|
|
254
|
-
}
|
|
255
|
-
export function deserializeBidi(remote) {
|
|
256
|
-
if (!remote || typeof remote !== 'object')
|
|
257
|
-
return remote;
|
|
258
|
-
const rv = remote;
|
|
259
|
-
switch (rv.type) {
|
|
260
|
-
case 'undefined':
|
|
261
|
-
return undefined;
|
|
262
|
-
case 'null':
|
|
263
|
-
return null;
|
|
264
|
-
case 'string':
|
|
265
|
-
case 'boolean':
|
|
266
|
-
return rv.value;
|
|
267
|
-
case 'number': {
|
|
268
|
-
// BiDi encodes ±Infinity / NaN as strings.
|
|
269
|
-
if (rv.value === 'Infinity')
|
|
270
|
-
return Infinity;
|
|
271
|
-
if (rv.value === '-Infinity')
|
|
272
|
-
return -Infinity;
|
|
273
|
-
if (rv.value === 'NaN')
|
|
274
|
-
return NaN;
|
|
275
|
-
return rv.value;
|
|
276
|
-
}
|
|
277
|
-
case 'bigint':
|
|
278
|
-
return typeof rv.value === 'string' ? BigInt(rv.value) : rv.value;
|
|
279
|
-
case 'date':
|
|
280
|
-
return rv.value;
|
|
281
|
-
case 'array':
|
|
282
|
-
case 'set':
|
|
283
|
-
return Array.isArray(rv.value) ? rv.value.map(deserializeBidi) : [];
|
|
284
|
-
case 'object':
|
|
285
|
-
case 'map': {
|
|
286
|
-
const out = {};
|
|
287
|
-
if (Array.isArray(rv.value)) {
|
|
288
|
-
for (const pair of rv.value) {
|
|
289
|
-
const rawKey = pair[0];
|
|
290
|
-
const key = typeof rawKey === 'string' ? rawKey : String(deserializeBidi(rawKey));
|
|
291
|
-
out[key] = deserializeBidi(pair[1]);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return out;
|
|
295
|
-
}
|
|
296
|
-
default:
|
|
297
|
-
// node / regexp / window / … — no JS value to hand back.
|
|
298
|
-
return undefined;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
/** Every top-level tab currently open in this Firefox. */
|
|
302
|
-
export async function bidiTopLevelContexts(bidi) {
|
|
303
|
-
const tree = await bidi.send('browsingContext.getTree', {});
|
|
304
|
-
return (tree.contexts ?? []).map((c) => ({ context: c.context, url: c.url }));
|
|
305
|
-
}
|
|
306
|
-
/** Open a fresh tab and return its context id. */
|
|
307
|
-
export async function bidiCreateTab(bidi) {
|
|
308
|
-
const created = await bidi.send('browsingContext.create', { type: 'tab' });
|
|
309
|
-
return created.context;
|
|
310
|
-
}
|
|
311
|
-
/** Navigate a context and wait for the document to finish loading. */
|
|
312
|
-
export async function bidiNavigate(bidi, context, url) {
|
|
313
|
-
await bidi.send('browsingContext.navigate', { context, url, wait: 'complete' });
|
|
314
|
-
}
|
|
315
|
-
/** Reload a context in place, waiting for load. */
|
|
316
|
-
export async function bidiReload(bidi, context) {
|
|
317
|
-
await bidi.send('browsingContext.reload', { context, wait: 'complete' });
|
|
318
|
-
}
|
|
319
|
-
/** Close a tab. */
|
|
320
|
-
export async function bidiCloseTab(bidi, context) {
|
|
321
|
-
await bidi.send('browsingContext.close', { context });
|
|
322
|
-
}
|
|
323
|
-
/** Bring a tab to the foreground (explicit focus only). */
|
|
324
|
-
export async function bidiActivate(bidi, context) {
|
|
325
|
-
await bidi.send('browsingContext.activate', { context });
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Evaluate an expression in a context and return the deserialized value.
|
|
329
|
-
* `awaitPromise` mirrors the CDP evaluate contract; a thrown/rejected value
|
|
330
|
-
* surfaces as an Error rather than a silent undefined.
|
|
331
|
-
*/
|
|
332
|
-
export async function bidiEvaluate(bidi, context, expression) {
|
|
333
|
-
const result = await bidi.send('script.evaluate', {
|
|
334
|
-
expression,
|
|
335
|
-
target: { context },
|
|
336
|
-
awaitPromise: true,
|
|
337
|
-
resultOwnership: 'none',
|
|
338
|
-
});
|
|
339
|
-
if (result.type === 'exception') {
|
|
340
|
-
const ex = result.exceptionDetails;
|
|
341
|
-
const value = ex?.exception ? deserializeBidi(ex.exception) : undefined;
|
|
342
|
-
throw new Error(ex?.text || (typeof value === 'string' ? value : 'evaluate failed'));
|
|
343
|
-
}
|
|
344
|
-
return deserializeBidi(result.result);
|
|
345
|
-
}
|
|
346
|
-
/** Capture a screenshot of the viewport as raw bytes. `quality` is 0–1 for JPEG. */
|
|
347
|
-
export async function bidiScreenshot(bidi, context, format) {
|
|
348
|
-
const shot = await bidi.send('browsingContext.captureScreenshot', {
|
|
349
|
-
context,
|
|
350
|
-
origin: 'viewport',
|
|
351
|
-
format,
|
|
352
|
-
});
|
|
353
|
-
return Buffer.from(shot.data, 'base64');
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* A real, trusted left click at viewport coordinates via `input.performActions`
|
|
357
|
-
* — the pointer path CDP `Input.dispatchMouseEvent` gives Chromium, which Arc
|
|
358
|
-
* (Apple Events) never had. The service resolves a ref to (x, y) first, exactly
|
|
359
|
-
* as the CDP click path does.
|
|
360
|
-
*/
|
|
361
|
-
export async function bidiClickAt(bidi, context, x, y) {
|
|
362
|
-
await bidi.send('input.performActions', {
|
|
363
|
-
context,
|
|
364
|
-
actions: [
|
|
365
|
-
{
|
|
366
|
-
type: 'pointer',
|
|
367
|
-
id: 'mouse',
|
|
368
|
-
parameters: { pointerType: 'mouse' },
|
|
369
|
-
actions: [
|
|
370
|
-
{ type: 'pointerMove', x: Math.round(x), y: Math.round(y) },
|
|
371
|
-
{ type: 'pointerDown', button: 0 },
|
|
372
|
-
{ type: 'pointerUp', button: 0 },
|
|
373
|
-
],
|
|
374
|
-
},
|
|
375
|
-
],
|
|
376
|
-
});
|
|
377
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { CDPClient } from '../cdp.js';
|
|
2
|
-
import type { BrowserProfile, ConnectionKey } from '../types.js';
|
|
3
|
-
interface LocalConnection {
|
|
4
|
-
cdp: CDPClient;
|
|
5
|
-
port: number;
|
|
6
|
-
pid: number;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Arc is single-instance: relaunching the Arc binary with a fresh
|
|
10
|
-
* `--user-data-dir` does not create a second debuggable process — macOS routes
|
|
11
|
-
* the launch to the running Arc, which was started with no debug port, so the
|
|
12
|
-
* spawn produces a stray window and no CDP endpoint (issue #2779). agents
|
|
13
|
-
* browser therefore ATTACHES to the user's running Arc and never launches its
|
|
14
|
-
* own. When the running Arc exposes no CDP endpoint on the profile's port, we
|
|
15
|
-
* fail loud with the one relaunch that fixes it rather than silently spawning a
|
|
16
|
-
* duplicate. Exported so the contract is unit-testable without a real Arc.
|
|
17
|
-
*/
|
|
18
|
-
export declare function arcAttachRequiredError(profileName: string, port: number): Error;
|
|
19
|
-
/**
|
|
20
|
-
* The loud error an ATTACH-ONLY profile raises when nothing debuggable is on its
|
|
21
|
-
* port (PHNX-3967). Same contract as {@link arcAttachRequiredError}, generalized
|
|
22
|
-
* to any browser: agents attach to a browser the user already started and NEVER
|
|
23
|
-
* spawn a rival window, so instead of falling through to `launchBrowser` (which
|
|
24
|
-
* would produce the second, logged-out dock tile this ticket set out to end) we
|
|
25
|
-
* fail loud with the exact relaunch that makes the canonical instance
|
|
26
|
-
* attachable — pinned to the profile's DURABLE `--user-data-dir` so the one-time
|
|
27
|
-
* sign-in persists. Exported so the contract is unit-testable without a browser.
|
|
28
|
-
*/
|
|
29
|
-
export declare function attachOnlyRequiredError(profile: Pick<BrowserProfile, 'name' | 'browser' | 'userDataDir'>, port: number): Error;
|
|
30
|
-
/**
|
|
31
|
-
* The loud error an attach-only profile raises when a browser IS serving CDP on
|
|
32
|
-
* its port but is a FOREIGN instance — its `--user-data-dir` is not the profile's
|
|
33
|
-
* durable dir (PHNX-3967). This closes the port-squat: a logged-out `/tmp/...`
|
|
34
|
-
* Comet answering CDP on the canonical port passes the browser-FAMILY identity
|
|
35
|
-
* check, so without an ownership check agents would drive it as if it were the
|
|
36
|
-
* credentialed browser. Rather than adopt the squatter we reject it and name the
|
|
37
|
-
* fix. Exported for unit testing.
|
|
38
|
-
*/
|
|
39
|
-
export declare function foreignInstanceError(profile: Pick<BrowserProfile, 'name' | 'browser' | 'userDataDir'>, port: number, runningDataDir: string, pid: number): Error;
|
|
40
|
-
/**
|
|
41
|
-
* The loud error a profile pinned to the owner's own store (PHNX-4042) raises
|
|
42
|
-
* when a browser already holds that store without a debug port. Launching on it
|
|
43
|
-
* would not open a second browser: Chromium hands the arguments to the running
|
|
44
|
-
* instance and exits, the port never binds, and the agent would wait out a
|
|
45
|
-
* timeout while the owner sees a stray window. Exported for unit testing.
|
|
46
|
-
*/
|
|
47
|
-
export declare function storeInUseError(profile: Pick<BrowserProfile, 'name' | 'browser' | 'userDataDir' | 'profileDirectory'>, port: number, pid: number): Error;
|
|
48
|
-
export declare function connectLocal(endpoint: string, profile: BrowserProfile,
|
|
49
|
-
/**
|
|
50
|
-
* Runtime key (`<profile>@<endpoint>`) the launched browser's user-data-dir,
|
|
51
|
-
* pid, and port files are stored under. Separate from `profile.name`, which
|
|
52
|
-
* stays the bare user-facing name and appears only in messages (RUSH-2709).
|
|
53
|
-
*/
|
|
54
|
-
key: ConnectionKey): Promise<LocalConnection>;
|
|
55
|
-
export {};
|