@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.
Files changed (248) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.js +53 -1
  9. package/dist/commands/repo.js +13 -49
  10. package/dist/commands/route.js +5 -5
  11. package/dist/commands/routines.js +39 -12
  12. package/dist/commands/sessions-render.d.ts +1 -1
  13. package/dist/commands/sessions-render.js +0 -1
  14. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  15. package/dist/commands/sessions.test-fixture.js +0 -37
  16. package/dist/commands/setup-browser.d.ts +17 -12
  17. package/dist/commands/setup-browser.js +52 -87
  18. package/dist/commands/setup-preferences.d.ts +0 -17
  19. package/dist/commands/setup-preferences.js +18 -49
  20. package/dist/commands/setup.js +9 -9
  21. package/dist/commands/ssh.js +10 -1
  22. package/dist/commands/sync.js +6 -44
  23. package/dist/commands/teams.js +0 -1
  24. package/dist/commands/update.d.ts +19 -0
  25. package/dist/commands/update.js +34 -6
  26. package/dist/commands/view.d.ts +12 -1
  27. package/dist/commands/view.js +47 -2
  28. package/dist/index.js +107 -26
  29. package/dist/lib/account-capabilities.js +0 -2
  30. package/dist/lib/account-catalog.d.ts +38 -0
  31. package/dist/lib/account-catalog.js +110 -26
  32. package/dist/lib/account-provider-registry.js +1 -1
  33. package/dist/lib/account-registry.d.ts +1 -1
  34. package/dist/lib/account-registry.js +36 -2
  35. package/dist/lib/accounting/rotate.js +22 -8
  36. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  37. package/dist/lib/accounting/usage-ingest.js +115 -45
  38. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  39. package/dist/lib/accounting/usage-sync.js +227 -66
  40. package/dist/lib/accounting/usage.d.ts +16 -3
  41. package/dist/lib/accounting/usage.js +26 -8
  42. package/dist/lib/accounts/slots.d.ts +8 -0
  43. package/dist/lib/accounts/slots.js +17 -0
  44. package/dist/lib/add-dir.js +0 -1
  45. package/dist/lib/agent-cli-commands.js +0 -1
  46. package/dist/lib/agent-spec/agents.d.ts +6 -7
  47. package/dist/lib/agent-spec/agents.js +6 -60
  48. package/dist/lib/auth-health.d.ts +69 -1
  49. package/dist/lib/auth-health.js +133 -4
  50. package/dist/lib/browser/context.d.ts +82 -0
  51. package/dist/lib/browser/context.js +88 -0
  52. package/dist/lib/browser/paths.d.ts +25 -0
  53. package/dist/lib/browser/paths.js +51 -0
  54. package/dist/lib/browser/record.d.ts +38 -0
  55. package/dist/lib/browser/record.js +57 -0
  56. package/dist/lib/browser/sessions-list.js +1 -2
  57. package/dist/lib/browser-client.d.ts +158 -0
  58. package/dist/lib/browser-client.js +234 -0
  59. package/dist/lib/channels/owner-sink.d.ts +1 -18
  60. package/dist/lib/channels/owner-sink.js +29 -82
  61. package/dist/lib/channels/providers/rush.d.ts +7 -7
  62. package/dist/lib/channels/providers/rush.js +127 -57
  63. package/dist/lib/claude-statusline.d.ts +1 -1
  64. package/dist/lib/claude-statusline.js +15 -3
  65. package/dist/lib/commands.js +1 -1
  66. package/dist/lib/crabbox/lease.js +1 -1
  67. package/dist/lib/crabbox/runtimes.js +1 -3
  68. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  69. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  70. package/dist/lib/daemon/auth-sync-service.js +37 -43
  71. package/dist/lib/daemon/daemon.d.ts +25 -14
  72. package/dist/lib/daemon/daemon.js +92 -154
  73. package/dist/lib/daemon/runner.d.ts +2 -2
  74. package/dist/lib/daemon/runner.js +32 -9
  75. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  76. package/dist/lib/daemon/self-update-service.js +40 -36
  77. package/dist/lib/daemon/service.d.ts +12 -8
  78. package/dist/lib/daemon/supervisor.d.ts +65 -54
  79. package/dist/lib/daemon/supervisor.js +136 -171
  80. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  81. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  82. package/dist/lib/daemon/usage-sync-service.js +46 -78
  83. package/dist/lib/daemon-health.d.ts +22 -5
  84. package/dist/lib/daemon-health.js +47 -3
  85. package/dist/lib/daemon-services.d.ts +1 -1
  86. package/dist/lib/daemon-services.js +0 -10
  87. package/dist/lib/daemon-ticks.js +4 -2
  88. package/dist/lib/devices/doctor-findings.js +8 -11
  89. package/dist/lib/devices/harness-inventory.js +14 -5
  90. package/dist/lib/exec-account-home.js +5 -0
  91. package/dist/lib/exec.js +0 -11
  92. package/dist/lib/feed/events.js +0 -1
  93. package/dist/lib/feed/tool-activity.js +5 -2
  94. package/dist/lib/fleet-shared-state.d.ts +45 -0
  95. package/dist/lib/fleet-shared-state.js +78 -9
  96. package/dist/lib/fs-atomic.js +25 -0
  97. package/dist/lib/harness-auth-capabilities.js +0 -1
  98. package/dist/lib/helper-versions.js +1 -1
  99. package/dist/lib/hooks/cache.d.ts +7 -0
  100. package/dist/lib/hooks/cache.js +27 -2
  101. package/dist/lib/hooks/install.js +7 -1
  102. package/dist/lib/hosts/ready.d.ts +38 -8
  103. package/dist/lib/hosts/ready.js +62 -14
  104. package/dist/lib/installations/active-check.d.ts +27 -0
  105. package/dist/lib/installations/active-check.js +61 -0
  106. package/dist/lib/installations/migrate.js +6 -3
  107. package/dist/lib/installations/shims.js +9 -7
  108. package/dist/lib/installations/update.js +21 -15
  109. package/dist/lib/installations/versions.js +12 -1
  110. package/dist/lib/manifest.js +1 -1
  111. package/dist/lib/models.js +0 -2
  112. package/dist/lib/open-url.js +28 -54
  113. package/dist/lib/profiles.js +0 -2
  114. package/dist/lib/refresh.js +1 -1
  115. package/dist/lib/routine-readiness.d.ts +35 -0
  116. package/dist/lib/routine-readiness.js +74 -16
  117. package/dist/lib/rules/compile.js +6 -0
  118. package/dist/lib/sandbox.js +0 -2
  119. package/dist/lib/secrets-policy.d.ts +89 -11
  120. package/dist/lib/secrets-policy.js +175 -28
  121. package/dist/lib/session/db.d.ts +1 -1
  122. package/dist/lib/session/db.js +65 -18
  123. package/dist/lib/session/discover.js +1 -283
  124. package/dist/lib/session/mirror.d.ts +4 -3
  125. package/dist/lib/session/mirror.js +4 -3
  126. package/dist/lib/session/parse.d.ts +0 -2
  127. package/dist/lib/session/parse.js +1 -138
  128. package/dist/lib/session/throughput.d.ts +1 -3
  129. package/dist/lib/session/throughput.js +0 -20
  130. package/dist/lib/session/types.d.ts +2 -2
  131. package/dist/lib/session/types.js +2 -2
  132. package/dist/lib/sessions-client.d.ts +60 -1
  133. package/dist/lib/sessions-client.js +227 -4
  134. package/dist/lib/setup-tool-status.js +5 -10
  135. package/dist/lib/signin-badge.d.ts +3 -2
  136. package/dist/lib/signin-badge.js +6 -3
  137. package/dist/lib/smart-launch.js +4 -1
  138. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  139. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  140. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  141. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  142. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  143. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  144. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  147. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  148. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  149. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  150. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  151. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  152. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  153. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  154. package/dist/lib/staleness/writers/sources.js +5 -0
  155. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  156. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  157. package/dist/lib/state.d.ts +6 -4
  158. package/dist/lib/state.js +7 -17
  159. package/dist/lib/teams/parsers.d.ts +1 -1
  160. package/dist/lib/teams/parsers.js +1 -136
  161. package/dist/lib/teams/placement-probe.js +50 -15
  162. package/dist/lib/teams/scheduler.d.ts +7 -0
  163. package/dist/lib/types.d.ts +10 -1
  164. package/dist/lib/types.js +1 -1
  165. package/dist/lib/view-types.d.ts +32 -0
  166. package/dist/lib/watchdog/read.js +0 -1
  167. package/dist/lib/watchdog/rotate.js +1 -2
  168. package/dist/lib/watchdog/runner.js +2 -2
  169. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  170. package/dist/lib/watchdog/watchdogTail.js +0 -31
  171. package/package.json +1 -1
  172. package/scripts/postinstall.js +8 -8
  173. package/dist/browser.d.ts +0 -2
  174. package/dist/browser.js +0 -17
  175. package/dist/commands/browser-picker.d.ts +0 -18
  176. package/dist/commands/browser-picker.js +0 -97
  177. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  178. package/dist/lib/browser/arc-discovery.js +0 -197
  179. package/dist/lib/browser/arc-dom.d.ts +0 -14
  180. package/dist/lib/browser/arc-dom.js +0 -121
  181. package/dist/lib/browser/caller-identity.d.ts +0 -40
  182. package/dist/lib/browser/caller-identity.js +0 -175
  183. package/dist/lib/browser/cdp.d.ts +0 -51
  184. package/dist/lib/browser/cdp.js +0 -254
  185. package/dist/lib/browser/chrome.d.ts +0 -141
  186. package/dist/lib/browser/chrome.js +0 -704
  187. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  188. package/dist/lib/browser/chromium-discovery.js +0 -96
  189. package/dist/lib/browser/devices.d.ts +0 -34
  190. package/dist/lib/browser/devices.js +0 -61
  191. package/dist/lib/browser/domain-skills.d.ts +0 -71
  192. package/dist/lib/browser/domain-skills.js +0 -195
  193. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  194. package/dist/lib/browser/drivers/arc.js +0 -276
  195. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  196. package/dist/lib/browser/drivers/firefox.js +0 -377
  197. package/dist/lib/browser/drivers/local.d.ts +0 -55
  198. package/dist/lib/browser/drivers/local.js +0 -266
  199. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  200. package/dist/lib/browser/drivers/ssh.js +0 -467
  201. package/dist/lib/browser/editor.d.ts +0 -3
  202. package/dist/lib/browser/editor.js +0 -50
  203. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  204. package/dist/lib/browser/ffmpeg.js +0 -184
  205. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  206. package/dist/lib/browser/firefox-discovery.js +0 -162
  207. package/dist/lib/browser/har.d.ts +0 -85
  208. package/dist/lib/browser/har.js +0 -77
  209. package/dist/lib/browser/hygiene.d.ts +0 -97
  210. package/dist/lib/browser/hygiene.js +0 -153
  211. package/dist/lib/browser/index.d.ts +0 -5
  212. package/dist/lib/browser/index.js +0 -5
  213. package/dist/lib/browser/input.d.ts +0 -7
  214. package/dist/lib/browser/input.js +0 -92
  215. package/dist/lib/browser/ipc.d.ts +0 -204
  216. package/dist/lib/browser/ipc.js +0 -1453
  217. package/dist/lib/browser/login-detection.d.ts +0 -87
  218. package/dist/lib/browser/login-detection.js +0 -274
  219. package/dist/lib/browser/profiles.d.ts +0 -422
  220. package/dist/lib/browser/profiles.js +0 -1157
  221. package/dist/lib/browser/refs.d.ts +0 -89
  222. package/dist/lib/browser/refs.js +0 -191
  223. package/dist/lib/browser/registry.d.ts +0 -70
  224. package/dist/lib/browser/registry.js +0 -252
  225. package/dist/lib/browser/remote-control.d.ts +0 -59
  226. package/dist/lib/browser/remote-control.js +0 -85
  227. package/dist/lib/browser/resolve-target.d.ts +0 -66
  228. package/dist/lib/browser/resolve-target.js +0 -264
  229. package/dist/lib/browser/runtime-state.d.ts +0 -291
  230. package/dist/lib/browser/runtime-state.js +0 -584
  231. package/dist/lib/browser/secret-ref.d.ts +0 -10
  232. package/dist/lib/browser/secret-ref.js +0 -14
  233. package/dist/lib/browser/service.d.ts +0 -812
  234. package/dist/lib/browser/service.js +0 -4693
  235. package/dist/lib/browser/stream.d.ts +0 -17
  236. package/dist/lib/browser/stream.js +0 -72
  237. package/dist/lib/browser/task-index.d.ts +0 -78
  238. package/dist/lib/browser/task-index.js +0 -199
  239. package/dist/lib/browser/types.d.ts +0 -647
  240. package/dist/lib/browser/types.js +0 -61
  241. package/dist/lib/browser/upload.d.ts +0 -29
  242. package/dist/lib/browser/upload.js +0 -298
  243. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  244. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  245. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  246. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  247. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  248. 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 {};