@phnx-labs/agents-cli 1.22.94 → 1.22.96
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 +22 -2
- package/dist/commands/browser.js +32 -9
- package/dist/lib/browser/chrome.d.ts +17 -0
- package/dist/lib/browser/chrome.js +121 -16
- package/dist/lib/browser/chromium-discovery.d.ts +27 -0
- package/dist/lib/browser/chromium-discovery.js +96 -0
- package/dist/lib/browser/drivers/firefox.d.ts +103 -0
- package/dist/lib/browser/drivers/firefox.js +377 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +38 -3
- package/dist/lib/browser/firefox-discovery.d.ts +68 -0
- package/dist/lib/browser/firefox-discovery.js +162 -0
- package/dist/lib/browser/ipc.js +4 -0
- package/dist/lib/browser/profiles.d.ts +36 -2
- package/dist/lib/browser/profiles.js +200 -55
- package/dist/lib/browser/resolve-target.js +7 -1
- package/dist/lib/browser/service.d.ts +60 -3
- package/dist/lib/browser/service.js +545 -14
- package/dist/lib/browser/types.d.ts +51 -4
- package/dist/lib/daemon/usage-sync-service.js +12 -0
- package/dist/lib/open-url.js +6 -0
- package/dist/lib/types.d.ts +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.96
|
|
4
|
+
|
|
5
|
+
- **`agents browser tabs --all` shows every tab open in the profile browser, yours included (PHNX-4043).** Until now `tabs` listed only the task's own tabs, so an agent sharing an Arc Space, a Comet store, or a Firefox profile with you could not see what else was open. `--all` lists every top-level tab: OWNER names the agents-cli task that owns it, or `you` for a tab no task owns; `*` still marks the task's current tab. Read-only on every backend (Chromium `Target.getTargets`, the Arc Space's tabs, Firefox `browsingContext.getTree`). Source: `cli/src/lib/browser/service.ts` `profileTabs`, `cli/src/commands/browser.ts`.
|
|
6
|
+
|
|
7
|
+
- **Firefox automation over WebDriver BiDi (PHNX-4043).** `agents browser` now drives
|
|
8
|
+
Firefox, which dropped the Chrome DevTools Protocol in 129. Every Firefox profile in
|
|
9
|
+
`profiles.ini` is discovered read-only as a `firefox-<name>` browser profile
|
|
10
|
+
(`firefox-default`, `firefox-default-release`, …) pinned to that profile directory;
|
|
11
|
+
agents launch it headless with a debug port (or attach to a running one, failing loud
|
|
12
|
+
when a portless Firefox already holds the profile). Supported verbs: start, navigate,
|
|
13
|
+
tab add, tabs, evaluate, refs, click, fill/type, scroll, screenshot, done — with the
|
|
14
|
+
same same-task reopen semantics as the rest of the service. A trusted pointer click
|
|
15
|
+
goes through `input.performActions`. Network capture, upload, and PDF fail loud with a
|
|
16
|
+
structured error naming a Chromium-family profile. Source: `apps/cli/src/lib/browser/drivers/firefox.ts`,
|
|
17
|
+
`apps/cli/src/lib/browser/firefox-discovery.ts`, `apps/cli/src/lib/browser/service.ts`.
|
|
18
|
+
|
|
19
|
+
- **Comet's own profiles are browser profiles, and discovered profiles reach the whole fleet (PHNX-4042).** On a Mac, `agents browser profiles list` now shows one profile per entry in Comet's profile menu (`comet-work`, ...), read from Comet's `Local State` and pinned to Comet's real user-data dir and `--profile-directory`, next to the `arc-*` rows for Arc Spaces. Agents and you share one Comet window per profile: agents attach when it runs with remote debugging on the profile's port, launch Comet on your own store when it is not running (over a TCP port, so the instance outlives a daemon restart and the ownership guard can verify it), and fail loud with the relaunch command when it runs without a port. The daemon's fleet-sync tick publishes every discovered profile into this device's declaration, so other boxes list it with `WHERE=<device>` and `--profile arc-gmail` from a worker routes to the Mac that owns it. `AGENTS_COMET_DIR` points discovery at another store. Source: `cli/src/lib/browser/chromium-discovery.ts`, `cli/src/lib/browser/profiles.ts`, `cli/src/lib/browser/chrome.ts`, `cli/src/lib/browser/drivers/local.ts`, `cli/src/lib/daemon/usage-sync-service.ts`.
|
|
20
|
+
|
|
3
21
|
## 1.22.94
|
|
4
22
|
|
|
23
|
+
- **Agents drive your running Arc, one profile per Arc Space (PHNX-2399, macOS).** On a Mac with Arc, `agents browser profiles list` shows every Arc Space as a profile — `arc-gmail`, `arc-work`, `arc-dev`, named after the Space title — discovered read-only from Arc's `User Data/Local State` and `StorableSidebar.json`. A Space already carries its Arc profile's logins, so it is the browser profile agents pick with `--profile`; there is no separate Space flag and nothing to create. `agents browser use arc-gmail` makes it the default. Tabs are created, navigated, evaluated, and closed through Apple Events against the Arc you already have open — no debug port, no relaunch, never a second Arc. Agents only ever touch the tabs they created: durable task state addresses them by stable window, Space, and tab ids, a crash-safe marker intent guards creation, and closing a task removes only its own tabs. Creating a tab makes Arc select it for about 100 ms before the tab you had selected is restored; navigate, evaluate, and close are silent. Native refs, click, fill/type, scroll, and `tab focus` work; screenshots, promise evaluation, trusted input, network/console capture, uploads/downloads, and PDF fail with `ArcNativeCapabilityError` and point at a Chromium-family profile. Source: `cli/src/lib/browser/arc-discovery.ts`, `cli/src/lib/browser/drivers/arc.ts`, `cli/src/lib/browser/arc-dom.ts`, `cli/src/lib/browser/profiles.ts`, `cli/src/lib/browser/service.ts`, `cli/src/commands/browser.ts`.
|
|
24
|
+
|
|
25
|
+
## 1.22.93
|
|
26
|
+
|
|
5
27
|
- **An idle reminder is not a permission request — attention is classified from explicit harness evidence, and time-based signals expire (PHNX-3999).** The fleet feed was rendering Claude's `idle_prompt` Notification ("the turn ended and you have been idle") as a `permission` with Approve/Deny — a session whose whole transcript was "reply with exactly: pong" → "pong" got an approval banner — and the state engine labelled any tool call that had run quietly for two minutes a permission too. Now a `notification` block is a `permission` only when its recorded subtype is `permission_prompt` and the session's transcript corroborates it (no tool result or new turn stamped after the block, process alive); the feed-publish hook no longer publishes `idle_prompt` at all, an `idle_prompt` block left on disk yields no request, an `elicitation_dialog` is a question with no invented buttons, and a pending tool call reads as `working` however long it has run. One unresolved record per `host/session/generation` is resolved by later evidence — a transcript event after the block, or a dead process — never by dismissing the banner. What the CLI cannot confirm (a stale hook block with no transcript cursor to check, a notification with no recorded subtype, an older peer's elapsed-time `permission` claim) is the new `unverified` kind: the banner and the `feed watch` stream say "Could not verify request" with an Open terminal action only, and `agents feed answer` refuses it. `computeLiveSignals` now memoizes only the parsed transcript tail on mtime and re-classifies against the current clock every call, so the 30-minute prose-question decay expires on schedule instead of freezing until someone types; rows carry `lastEventMs`, the harness stamp on the last meaningful event. Source: `cli/src/lib/feed/attention.ts`, `cli/src/lib/session/state.ts`, `cli/src/lib/session/active.ts`, `cli/src/lib/feed/feed.ts`, `cli/src/lib/feed/answer.ts`, `cli/src/lib/daemon/attention-notify-service.ts`.
|
|
6
28
|
|
|
7
29
|
- **AGI Menu 1.1.1: three fixed tabs (PHNX-3999, PHNX-4036).** The `menubar` helper floor moves to 1.1.1, the first release cut from phnx-labs/agi-menu: the status item opens a popover with Sessions (working first, then confirmed requests), Projects (the tracker's projects → milestones with cycle chips → issues, expanding in place, from one `linear projects overview --json` call) and Settings, replacing the long dropdown. Fresh installs and `agents menubar setup` fetch it from the public `menubar/v1.1.1` release. Source: `cli/src/lib/helper-versions.ts`.
|
|
8
30
|
|
|
9
31
|
- **AGI Menu moved out — the menu-bar helper now lives in `phnx-labs/agi-menu` (PHNX-4036).** `cli/menubar/` (the SwiftPM source, its build/test scripts, and the build-text test) is gone from this repo, the same split AGI EXT got in RUSH-3189. Nothing user-facing changes: `agents menubar setup`/`enable` still download and verify the signed, notarized `MenubarHelper.app.zip` from the `menubar/v<floor>` release tag (`src/lib/helper-versions.ts`), at the same URL, with the same bundle identity and designated-requirement pin — that release is now cut by agi-menu's `scripts/release.sh`, which also uploads a `menubar-source.txt` provenance sidecar. Release tooling never builds the helper again: a new `scripts/stage-menubar-helper.sh` downloads the published `menubar/v<floor>` asset, verifies its sha256, and (on macOS) extracts it into `bin/MenubarHelper.app` behind `codesign --verify --deep --strict`, `spctl --assess`, and the DR-pin gate (`scripts/verify-menubar-helper.sh`, no longer a prepack gate); `--fetch-only` downloads and sha-verifies on any OS. `release-attestation-produce.sh --with-helpers` records the menubar helper-manifest row from that published asset (asset digest = sha256 of the zip, `source` = the sidecar's repo/commit/tag when the release carries one) instead of a fresh build, keyed on `src/lib/helper-versions.ts` as its input — a floor bump is what re-records it; a missing or corrupt release fails closed naming the agi-menu publish step. `remote-sign-mac.sh` stages the published helper on the home base instead of running `swift build`. The installer's dead `cli/menubar/dist/` source candidate is removed; `bin/MenubarHelper.app` (staged, or copied from an agi-menu checkout) remains the working-tree source. Source: `cli/scripts/stage-menubar-helper.sh`, `cli/scripts/release-manifest.sh`, `cli/scripts/release-attestation-produce.sh`, `cli/scripts/remote-sign-mac.sh`, `cli/src/lib/menubar/download-menubar.ts`.
|
|
10
32
|
|
|
11
|
-
- **Agents drive your running Arc, one profile per Arc Space (PHNX-2399, macOS).** On a Mac with Arc, `agents browser profiles list` shows every Arc Space as a profile — `arc-gmail`, `arc-work`, `arc-dev`, named after the Space title — discovered read-only from Arc's `User Data/Local State` and `StorableSidebar.json`. A Space already carries its Arc profile's logins, so it is the browser profile agents pick with `--profile`; there is no separate Space flag and nothing to create. `agents browser use arc-gmail` makes it the default. Tabs are created, navigated, evaluated, and closed through Apple Events against the Arc you already have open — no debug port, no relaunch, never a second Arc. Agents only ever touch the tabs they created: durable task state addresses them by stable window, Space, and tab ids, a crash-safe marker intent guards creation, and closing a task removes only its own tabs. Creating a tab makes Arc select it for about 100 ms before the tab you had selected is restored; navigate, evaluate, and close are silent. Native refs, click, fill/type, scroll, and `tab focus` work; screenshots, promise evaluation, trusted input, network/console capture, uploads/downloads, and PDF fail with `ArcNativeCapabilityError` and point at a Chromium-family profile. Source: `cli/src/lib/browser/arc-discovery.ts`, `cli/src/lib/browser/drivers/arc.ts`, `cli/src/lib/browser/arc-dom.ts`, `cli/src/lib/browser/profiles.ts`, `cli/src/lib/browser/service.ts`, `cli/src/commands/browser.ts`.
|
|
12
|
-
|
|
13
33
|
- **A Claude account-slot launch now runs in its own slot home, and worker slots come pre-onboarded (PHNX-3940 follow-up).** `agents run claude --device <worker>` picked an account slot and pinned `CLAUDE_CONFIG_DIR` to it, but the versioned alias (`~/.agents/.cache/shims/claude@<version>`) re-exported the shared version home unconditionally, so the run authenticated as the picked account (the setup-token env survived) while reading and writing config, onboarding state and history in whatever home the version carried — on yosemite-m1 that home belonged to another account and had never onboarded, so every dispatched run opened on Claude Code's theme picker. `buildExecEnv` now stamps a slot launch in `AGENTS_EXEC_HOME`; every shim that pins a config-dir env (claude, grok, opencode, kimi, muse, copilot — bare shim and direct alias, schema 32 / alias 21 — the daemon self-heal or `agents doctor --fix` regenerates on-disk copies) yields its pin to that slot through one shared `slotAwareConfigEnvBash` and consumes the marker before `exec`, so a nested launch never inherits its parent's slot and a bare `<agent>@<version>` from a terminal still gets the version home; the same override was live for all six, codex and cursor already yielded. A re-seed that still reads as unseeded (a slot config file that exists but cannot be parsed) is reported as an error each tick instead of a false "provisioned". Separately, `provisionWorkerSlot` seeded only the identity email into a slot's `.claude.json`, never `hasCompletedOnboarding` — a worker has no human to finish onboarding — so every provisioned slot re-onboarded; it now seeds both, and the daemon's `reconcileLocalWorkerSlots` re-provisions a durable claude slot that lacks either instead of skipping it as "already provisioned", so the slots provisioned before this converge on the next tick. Source: `cli/src/lib/harness/adapter.ts`, `cli/src/lib/harness/adapters/*.ts`, `cli/src/lib/exec.ts`, `cli/src/lib/installations/shims.ts`, `cli/src/lib/claude-account-token.ts`, `cli/src/lib/secrets-policy.ts`, `cli/docs/credential-management.md`.
|
|
14
34
|
|
|
15
35
|
## 1.22.92
|
package/dist/commands/browser.js
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
-
import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, formatProfilesTable, editProfile, renameProfile, assertRegistrableProfileName, isProfileLaunchableHere, isAttachOnlyProfile, resolveProfileDataDir, normalizeDataDir,
|
|
6
|
+
import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, formatProfilesTable, editProfile, renameProfile, assertRegistrableProfileName, isProfileLaunchableHere, isAttachOnlyProfile, resolveProfileDataDir, normalizeDataDir, persistDiscoveredProfile, } from '../lib/browser/profiles.js';
|
|
7
7
|
import { declaringDevices, migrateCentralBrowserProfiles, profileKind } from '../lib/browser/registry.js';
|
|
8
8
|
import { resolveActor } from '../lib/actor.js';
|
|
9
9
|
import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
|
|
@@ -442,7 +442,7 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
|
|
|
442
442
|
console.error(`Available profiles: ${all.map((profile) => profile.name).join(', ')}`);
|
|
443
443
|
return false;
|
|
444
444
|
}
|
|
445
|
-
await
|
|
445
|
+
await persistDiscoveredProfile(selectedName);
|
|
446
446
|
setConfigValue('browser.profile', selectedName);
|
|
447
447
|
const endpoint = Object.values(getEndpointPresets(target))[0]?.target ?? '';
|
|
448
448
|
console.log(`Default browser profile (this machine) is now "${selectedName}" (${target.browser}${endpoint ? `, ${endpoint}` : ''}).`);
|
|
@@ -503,7 +503,7 @@ function registerProfilesCommands(browser) {
|
|
|
503
503
|
console.log(JSON.stringify(allProfiles.map((profile) => ({
|
|
504
504
|
...profile,
|
|
505
505
|
devices: profile.devices,
|
|
506
|
-
kind: profile.arc ? 'identity' : profileKind(profile.name),
|
|
506
|
+
kind: profile.arc || profile.firefox ? 'identity' : profileKind(profile.name),
|
|
507
507
|
isConfiguredDefault: profile.name === configuredDefault,
|
|
508
508
|
})), null, 2));
|
|
509
509
|
return;
|
|
@@ -950,6 +950,11 @@ function registerProfilesCommands(browser) {
|
|
|
950
950
|
console.log(`Arc Space: ${profile.arc.spaceTitle} (${profile.arc.spaceId})`);
|
|
951
951
|
console.log(`Arc profile: ${profile.arc.profileName} (${profile.arc.profileId})`);
|
|
952
952
|
}
|
|
953
|
+
if (profile.firefox) {
|
|
954
|
+
console.log(`Firefox profile: ${profile.firefox.profileName}${profile.firefox.isDefault ? ' (default)' : ''}`);
|
|
955
|
+
if (profile.userDataDir)
|
|
956
|
+
console.log(`Profile directory: ${profile.userDataDir}`);
|
|
957
|
+
}
|
|
953
958
|
const presets = getEndpointPresets(profile);
|
|
954
959
|
const defaultName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
|
|
955
960
|
? profile.defaultEndpoint
|
|
@@ -975,7 +980,7 @@ function registerProfilesCommands(browser) {
|
|
|
975
980
|
console.log(`Headless: true`);
|
|
976
981
|
// Login state per known service: live session + account identity + whether
|
|
977
982
|
// login creds are declared in the profile's secrets bundle.
|
|
978
|
-
if (profile.arc)
|
|
983
|
+
if (profile.arc || profile.firefox)
|
|
979
984
|
return;
|
|
980
985
|
const active = await loginsForProfile(profile.name);
|
|
981
986
|
const accounts = await accountsForProfile(profile.name);
|
|
@@ -1119,11 +1124,17 @@ function registerProfilesCommands(browser) {
|
|
|
1119
1124
|
profile that is in use, the configured default, or the \`auto-chrome\`
|
|
1120
1125
|
profile a setup wizard created.
|
|
1121
1126
|
|
|
1122
|
-
On a Mac with Arc, every Arc Space is listed as a profile (arc-
|
|
1127
|
+
On a Mac with Arc, every Arc Space is listed as a profile (arc-gmail,
|
|
1123
1128
|
arc-work, ...) discovered read-only from Arc's own metadata — a Space
|
|
1124
1129
|
already carries its Arc profile's logins, so it IS the browser profile.
|
|
1125
1130
|
Agents attach to the running Arc through Apple Events and never launch or
|
|
1126
|
-
relaunch it.
|
|
1131
|
+
relaunch it. Comet's own profiles are listed the same way (comet-work, ...),
|
|
1132
|
+
pinned to Comet's real user-data dir, so agents and you share one Comet
|
|
1133
|
+
window per profile: agents attach when it is running with remote debugging,
|
|
1134
|
+
launch it on your store when it is not running, and fail loud with the
|
|
1135
|
+
relaunch command when it runs without a port. The daemon publishes every
|
|
1136
|
+
discovered profile into this device's declaration, so other fleet boxes list
|
|
1137
|
+
it with WHERE=<this device> and route to it. Point agents at Comet when a workflow needs screenshots,
|
|
1127
1138
|
downloads, network capture, or trusted input:
|
|
1128
1139
|
agents browser profiles create agents-comet --browser comet --attach-only
|
|
1129
1140
|
A CDP \`--attach-only\` profile attaches to a browser you
|
|
@@ -1142,7 +1153,7 @@ function registerProfilesCommands(browser) {
|
|
|
1142
1153
|
console.error(`Profile "${name}" not found`);
|
|
1143
1154
|
process.exit(1);
|
|
1144
1155
|
}
|
|
1145
|
-
if (profile.arc && !isFleetRemoteInvocation()) {
|
|
1156
|
+
if ((profile.arc || profile.firefox) && !isFleetRemoteInvocation()) {
|
|
1146
1157
|
const routed = resolveBrowserTarget(profile.name);
|
|
1147
1158
|
if (!routed.local && routed.commandDispatch) {
|
|
1148
1159
|
const result = await dispatchBrowserToDevice(routed.device, ['browser', 'profiles', 'doctor', name], 'capture');
|
|
@@ -1767,7 +1778,7 @@ function registerTaskCommands(browser) {
|
|
|
1767
1778
|
// Discovery is read-only for list/show/doctor. Starting is the explicit
|
|
1768
1779
|
// adoption point: persist only the agents-cli alias/native identity, never
|
|
1769
1780
|
// mutate Arc's own profile or Space data.
|
|
1770
|
-
await
|
|
1781
|
+
await persistDiscoveredProfile(profileName);
|
|
1771
1782
|
// A native endpoint is not CDP and cannot be tunnelled. Re-exec the
|
|
1772
1783
|
// complete command on the declaring owner, then bind its returned task
|
|
1773
1784
|
// locally so every later verb follows the same owner from task-index.
|
|
@@ -2224,15 +2235,17 @@ function registerTaskCommands(browser) {
|
|
|
2224
2235
|
});
|
|
2225
2236
|
browser
|
|
2226
2237
|
.command('tabs')
|
|
2227
|
-
.description('List tabs open for the current task')
|
|
2238
|
+
.description('List tabs open for the current task; --all shows every tab open in the profile browser, yours included')
|
|
2228
2239
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
2229
2240
|
.option(DEVICE_ON_PAGE_VERB_FLAG, DEVICE_ON_PAGE_VERB_DESC)
|
|
2241
|
+
.option('--all', "Every tab open in the profile browser, not only this task's: OWNER names the owning task, or \"you\" for a tab no task owns")
|
|
2230
2242
|
.option('--json', 'Output machine-readable JSON')
|
|
2231
2243
|
.action(async (opts) => {
|
|
2232
2244
|
const task = resolveTaskName(opts);
|
|
2233
2245
|
const response = await sendIPCRequest({
|
|
2234
2246
|
action: 'tab-list',
|
|
2235
2247
|
task,
|
|
2248
|
+
...(opts.all ? { all: true } : {}),
|
|
2236
2249
|
});
|
|
2237
2250
|
if (!response.ok) {
|
|
2238
2251
|
if (opts.json) {
|
|
@@ -2251,6 +2264,16 @@ function registerTaskCommands(browser) {
|
|
|
2251
2264
|
console.log('No tabs open');
|
|
2252
2265
|
return;
|
|
2253
2266
|
}
|
|
2267
|
+
if (opts.all) {
|
|
2268
|
+
console.log('TAB'.padEnd(12) + 'OWNER'.padEnd(18) + 'URL');
|
|
2269
|
+
console.log('-'.repeat(88));
|
|
2270
|
+
for (const t of response.tabs) {
|
|
2271
|
+
const owner = t.task ?? 'you';
|
|
2272
|
+
const current = t.current ? ' *' : '';
|
|
2273
|
+
console.log(t.id.slice(0, 11).padEnd(12) + owner.slice(0, 17).padEnd(18) + t.url.slice(0, 55) + current);
|
|
2274
|
+
}
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2254
2277
|
console.log('TAB'.padEnd(12) + 'URL');
|
|
2255
2278
|
console.log('-'.repeat(70));
|
|
2256
2279
|
for (const t of response.tabs) {
|
|
@@ -38,6 +38,23 @@ export interface LaunchResult {
|
|
|
38
38
|
port: number;
|
|
39
39
|
wsUrl: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The process that holds a Chromium user-data dir, read from the `SingletonLock`
|
|
43
|
+
* symlink Chromium keeps inside the dir on macOS and Linux (`<host>-<pid>`,
|
|
44
|
+
* PHNX-4042). A live pid means a browser already owns that store: a launch on it
|
|
45
|
+
* would only hand its arguments to the running instance and the requested debug
|
|
46
|
+
* port would never bind. Windows keeps no lock file, so this reports null there
|
|
47
|
+
* and `launchBrowser` catches the hand-off by the child's early exit instead.
|
|
48
|
+
*/
|
|
49
|
+
export declare function storeOccupant(userDataDir: string): {
|
|
50
|
+
pid: number;
|
|
51
|
+
} | null;
|
|
52
|
+
/**
|
|
53
|
+
* The relaunch that makes a browser holding `userDataDir` attachable: the
|
|
54
|
+
* ownership guard reads `--user-data-dir` off the running process, so the
|
|
55
|
+
* owner's normal launch (no flag) can never be verified.
|
|
56
|
+
*/
|
|
57
|
+
export declare function storeRelaunchCommand(browserType: BrowserType, port: number, userDataDir: string, profileDirectory?: string): string;
|
|
41
58
|
/**
|
|
42
59
|
* Resolve a browser-profile secrets bundle into an env map for the child, or an
|
|
43
60
|
* EMPTY map when the bundle is absent, locked, or otherwise unreadable — never a
|
|
@@ -24,6 +24,10 @@ const BROWSER_PATHS = {
|
|
|
24
24
|
brave: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
|
|
25
25
|
edge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
26
26
|
arc: ['/Applications/Arc.app/Contents/MacOS/Arc'],
|
|
27
|
+
firefox: [
|
|
28
|
+
'/Applications/Firefox.app/Contents/MacOS/firefox',
|
|
29
|
+
'/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox',
|
|
30
|
+
],
|
|
27
31
|
custom: [],
|
|
28
32
|
},
|
|
29
33
|
linux: {
|
|
@@ -34,6 +38,10 @@ const BROWSER_PATHS = {
|
|
|
34
38
|
edge: ['/usr/bin/microsoft-edge'],
|
|
35
39
|
// Arc has no Linux build (macOS + Windows only).
|
|
36
40
|
arc: [],
|
|
41
|
+
// `/usr/bin/firefox` on Ubuntu is the snap wrapper script: it execs the snap
|
|
42
|
+
// in place, so the pid survives and `--remote-debugging-port` works through
|
|
43
|
+
// it. The driver reads the real pid from the BiDi handshake anyway.
|
|
44
|
+
firefox: ['/usr/bin/firefox', '/snap/bin/firefox', '/usr/lib/firefox/firefox', '/opt/firefox/firefox'],
|
|
37
45
|
custom: [],
|
|
38
46
|
},
|
|
39
47
|
win32: {
|
|
@@ -63,6 +71,11 @@ const BROWSER_PATHS = {
|
|
|
63
71
|
// Arc ships a Windows build, but its install path is not yet verified here;
|
|
64
72
|
// leave unlisted (undetected) rather than guess a path that false-positives.
|
|
65
73
|
arc: [],
|
|
74
|
+
firefox: [
|
|
75
|
+
`${WIN_PROGRAMFILES}\\Mozilla Firefox\\firefox.exe`,
|
|
76
|
+
`${WIN_PROGRAMFILES_X86}\\Mozilla Firefox\\firefox.exe`,
|
|
77
|
+
`${WIN_LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
|
|
78
|
+
],
|
|
66
79
|
custom: [],
|
|
67
80
|
},
|
|
68
81
|
};
|
|
@@ -236,6 +249,43 @@ export function listInstalledBrowsers(platform = os.platform()) {
|
|
|
236
249
|
}
|
|
237
250
|
return found;
|
|
238
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The process that holds a Chromium user-data dir, read from the `SingletonLock`
|
|
254
|
+
* symlink Chromium keeps inside the dir on macOS and Linux (`<host>-<pid>`,
|
|
255
|
+
* PHNX-4042). A live pid means a browser already owns that store: a launch on it
|
|
256
|
+
* would only hand its arguments to the running instance and the requested debug
|
|
257
|
+
* port would never bind. Windows keeps no lock file, so this reports null there
|
|
258
|
+
* and `launchBrowser` catches the hand-off by the child's early exit instead.
|
|
259
|
+
*/
|
|
260
|
+
export function storeOccupant(userDataDir) {
|
|
261
|
+
let target;
|
|
262
|
+
try {
|
|
263
|
+
target = fs.readlinkSync(path.join(userDataDir, 'SingletonLock'));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const pid = Number(/-(\d+)$/.exec(target)?.[1]);
|
|
269
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
270
|
+
return null;
|
|
271
|
+
try {
|
|
272
|
+
process.kill(pid, 0);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
return error.code === 'EPERM' ? { pid } : null;
|
|
276
|
+
}
|
|
277
|
+
return { pid };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* The relaunch that makes a browser holding `userDataDir` attachable: the
|
|
281
|
+
* ownership guard reads `--user-data-dir` off the running process, so the
|
|
282
|
+
* owner's normal launch (no flag) can never be verified.
|
|
283
|
+
*/
|
|
284
|
+
export function storeRelaunchCommand(browserType, port, userDataDir, profileDirectory) {
|
|
285
|
+
const app = browserType === 'comet' ? 'Comet' : browserType;
|
|
286
|
+
const profileFlag = profileDirectory ? ` --profile-directory=${profileDirectory}` : '';
|
|
287
|
+
return `open -a ${app} --args --remote-debugging-port=${port} --user-data-dir=${userDataDir}${profileFlag}`;
|
|
288
|
+
}
|
|
239
289
|
/**
|
|
240
290
|
* Resolve a browser-profile secrets bundle into an env map for the child, or an
|
|
241
291
|
* EMPTY map when the bundle is absent, locked, or otherwise unreadable — never a
|
|
@@ -265,14 +315,25 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
265
315
|
// orphan reaper and `agents browser status` can label processes.
|
|
266
316
|
isElectron = false) {
|
|
267
317
|
const browserPath = findBrowserPath(browserType, customBinary);
|
|
318
|
+
// A profile discovered from the browser's OWN store (PHNX-4042) launches on
|
|
319
|
+
// that store — the owner's real user-data dir and profile directory — so the
|
|
320
|
+
// window agents open is the window the owner uses. Its Preferences are the
|
|
321
|
+
// owner's and are never rewritten. Anything else runs under a managed dir.
|
|
322
|
+
const ownerStore = options.userDataDir !== undefined;
|
|
268
323
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
269
|
-
const userDataDir = path.join(runtimeDir, 'chrome-data');
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
324
|
+
const userDataDir = options.userDataDir ?? path.join(runtimeDir, 'chrome-data');
|
|
325
|
+
if (!ownerStore) {
|
|
326
|
+
fs.mkdirSync(userDataDir, { recursive: true });
|
|
327
|
+
// Pre-launch Preferences pass: first-launch profile-name stamp, plus (for
|
|
328
|
+
// real browsers, not Electron apps) the session-cookie persistence pin.
|
|
329
|
+
// Electron apps manage their own storage and don't read Chromium's
|
|
330
|
+
// `session.*` prefs, so they get the name stamp only.
|
|
331
|
+
ensureProfilePreferences(userDataDir, profileName, !isElectron);
|
|
332
|
+
}
|
|
333
|
+
// The owner's store is attached over a TCP port rather than the daemon's
|
|
334
|
+
// private pipe: the instance outlives any one daemon and must stay reachable
|
|
335
|
+
// (and verifiable by the ownership guard) after a daemon restart.
|
|
336
|
+
const transport = ownerStore ? 'port' : 'pipe';
|
|
276
337
|
// Chromium on macOS coordinates instances via the SingletonLock file
|
|
277
338
|
// *inside* each user-data-dir. Direct binary spawn with a fresh
|
|
278
339
|
// --user-data-dir creates a fully independent process — the user's
|
|
@@ -288,8 +349,9 @@ isElectron = false) {
|
|
|
288
349
|
// profile never reaches this launcher (see connectLocal / isAttachOnlyProfile).
|
|
289
350
|
const viewport = options.viewport ?? { width: 1512, height: 982 };
|
|
290
351
|
const args = [
|
|
291
|
-
'--remote-debugging-pipe'
|
|
352
|
+
transport === 'pipe' ? '--remote-debugging-pipe' : `--remote-debugging-port=${port}`,
|
|
292
353
|
`--user-data-dir=${userDataDir}`,
|
|
354
|
+
...(options.profileDirectory ? [`--profile-directory=${options.profileDirectory}`] : []),
|
|
293
355
|
'--disable-background-timer-throttling',
|
|
294
356
|
'--disable-backgrounding-occluded-windows',
|
|
295
357
|
'--disable-renderer-backgrounding',
|
|
@@ -311,7 +373,7 @@ isElectron = false) {
|
|
|
311
373
|
// cookies survive, no ghost tabs — and the task flow creates its own tab
|
|
312
374
|
// over CDP anyway. Electron apps need their window to appear (the CDP
|
|
313
375
|
// driver binds to it), so they skip the flag.
|
|
314
|
-
...(isElectron ? [] : ['--no-startup-window']),
|
|
376
|
+
...(isElectron || ownerStore ? [] : ['--no-startup-window']),
|
|
315
377
|
...(options.headless ? ['--headless=new'] : []),
|
|
316
378
|
`--window-size=${viewport.width},${viewport.height}`,
|
|
317
379
|
...(viewport.x !== undefined && viewport.y !== undefined
|
|
@@ -324,26 +386,69 @@ isElectron = false) {
|
|
|
324
386
|
const env = { ...process.env, ...(await resolveProfileSecretsEnv(secrets)) };
|
|
325
387
|
const child = spawn(browserPath, args, {
|
|
326
388
|
detached: true,
|
|
327
|
-
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
|
|
389
|
+
stdio: transport === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
328
390
|
env,
|
|
329
391
|
});
|
|
330
392
|
child.unref();
|
|
331
393
|
child.stdout?.resume();
|
|
332
394
|
child.stderr?.resume();
|
|
333
395
|
const pid = child.pid;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
396
|
+
let wsUrl;
|
|
397
|
+
if (transport === 'pipe') {
|
|
398
|
+
const writePipe = child.stdio[3];
|
|
399
|
+
const readPipe = child.stdio[4];
|
|
400
|
+
if (!writePipe || !readPipe) {
|
|
401
|
+
throw new Error('Chrome failed to expose CDP pipe file descriptors');
|
|
402
|
+
}
|
|
403
|
+
wsUrl = registerPipeTransport({ read: readPipe, write: writePipe });
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
wsUrl = (await waitForDevToolsPort(port, profileName, child, { browserType, userDataDir, profileDirectory: options.profileDirectory })).wsUrl;
|
|
338
407
|
}
|
|
339
|
-
const wsUrl = registerPipeTransport({ read: readPipe, write: writePipe });
|
|
340
408
|
writeProfileRuntime(profileName, {
|
|
341
409
|
pid,
|
|
342
410
|
command: path.basename(browserPath),
|
|
343
411
|
userDataDir,
|
|
344
412
|
kind: isElectron ? 'electron' : 'browser',
|
|
345
413
|
});
|
|
346
|
-
return { pid, port: 0, wsUrl };
|
|
414
|
+
return { pid, port: transport === 'pipe' ? 0 : port, wsUrl };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Poll the DevTools endpoint of a browser just launched with
|
|
418
|
+
* `--remote-debugging-port` until it answers, or fail loud naming the pid.
|
|
419
|
+
* Chromium binds the port only after its profile has loaded, which on a real
|
|
420
|
+
* signed-in store takes a few seconds. A child that exits first never bound
|
|
421
|
+
* it: on an owner's store that is the singleton hand-off to a browser already
|
|
422
|
+
* open there (PHNX-4042), so the failure names that instance's relaunch
|
|
423
|
+
* instead of waiting out the deadline.
|
|
424
|
+
*/
|
|
425
|
+
async function waitForDevToolsPort(port, profileName, child, store, timeoutMs = 20_000) {
|
|
426
|
+
const pid = child.pid;
|
|
427
|
+
let exited;
|
|
428
|
+
child.once('exit', (code, signal) => {
|
|
429
|
+
exited = code !== null ? `code ${code}` : `signal ${signal}`;
|
|
430
|
+
});
|
|
431
|
+
const deadline = Date.now() + timeoutMs;
|
|
432
|
+
let lastError = '';
|
|
433
|
+
while (Date.now() < deadline) {
|
|
434
|
+
if (exited) {
|
|
435
|
+
const app = store.browserType === 'comet' ? 'Comet' : store.browserType;
|
|
436
|
+
throw new Error(`${app} (pid ${pid}) exited (${exited}) before serving the DevTools protocol on port ${port} ` +
|
|
437
|
+
`for profile "${profileName}". A ${app} already open on ${store.userDataDir} takes a new launch ` +
|
|
438
|
+
`as an argument hand-off and never binds the port; quit it, then relaunch it with remote debugging:\n` +
|
|
439
|
+
` ${storeRelaunchCommand(store.browserType, port, store.userDataDir, store.profileDirectory)}\n` +
|
|
440
|
+
`and retry. If none is open, the browser crashed on start.`);
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
return await discoverBrowserWsUrl(port, 'localhost', profileName);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
447
|
+
}
|
|
448
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`Browser for profile "${profileName}" (pid ${pid}) did not serve the DevTools protocol on ` +
|
|
451
|
+
`port ${port} within ${Math.round(timeoutMs / 1000)}s: ${lastError}`);
|
|
347
452
|
}
|
|
348
453
|
export async function attachToChrome(port) {
|
|
349
454
|
const { wsUrl } = await discoverBrowserWsUrl(port);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BrowserType } from './types.js';
|
|
2
|
+
export interface ChromiumNativeProfile {
|
|
3
|
+
browser: BrowserType;
|
|
4
|
+
/** The agents-cli profile name: `<browser>-<display-name-slug>`. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** The browser's user-data dir (its own, never a cache dir). */
|
|
7
|
+
userDataDir: string;
|
|
8
|
+
/** Profile directory basename inside the user-data dir. Authoritative id. */
|
|
9
|
+
profileDirectory: string;
|
|
10
|
+
/** Display-only name from Local State. */
|
|
11
|
+
displayName: string;
|
|
12
|
+
}
|
|
13
|
+
export type ChromiumDiscoveryResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
profiles: ChromiumNativeProfile[];
|
|
16
|
+
userDataDir: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
kind: 'unsupported' | 'not-installed' | 'invalid';
|
|
20
|
+
reason: string;
|
|
21
|
+
};
|
|
22
|
+
/** The browser's own user-data dir on this platform, or undefined where it does not ship. */
|
|
23
|
+
export declare function chromiumUserDataDir(browser: BrowserType): string | undefined;
|
|
24
|
+
export declare function discoverChromiumProfilesAt(browser: BrowserType, userDataDir: string): ChromiumDiscoveryResult;
|
|
25
|
+
export declare function discoverChromiumProfiles(browser: BrowserType): ChromiumDiscoveryResult;
|
|
26
|
+
/** Every browser whose native profiles agents-cli discovers on this platform. */
|
|
27
|
+
export declare function discoverableChromiumBrowsers(): BrowserType[];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only discovery of a Chromium-family browser's OWN profiles (PHNX-4042).
|
|
3
|
+
*
|
|
4
|
+
* Chromium keeps its profiles under one user-data dir: `Local State` lists them
|
|
5
|
+
* in `profile.info_cache` keyed by directory (`Default`, `Profile 1`, ...) with
|
|
6
|
+
* the display name the user sees in the profile menu. Each entry becomes one
|
|
7
|
+
* agents-cli profile (`comet-work`, ...) pinned to that dir and directory, so
|
|
8
|
+
* the owner and agents share ONE window per profile instead of agents spawning
|
|
9
|
+
* a rival instance under a cache dir. Nothing here writes to the browser's
|
|
10
|
+
* files.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as os from 'node:os';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
/** Browsers whose native profiles are discovered, and the env var that points tests at another user-data dir. */
|
|
16
|
+
const NATIVE_CHROMIUM = {
|
|
17
|
+
comet: { dirName: 'Comet', envOverride: 'AGENTS_COMET_DIR' },
|
|
18
|
+
};
|
|
19
|
+
/** The browser's own user-data dir on this platform, or undefined where it does not ship. */
|
|
20
|
+
export function chromiumUserDataDir(browser) {
|
|
21
|
+
const entry = NATIVE_CHROMIUM[browser];
|
|
22
|
+
if (!entry)
|
|
23
|
+
return undefined;
|
|
24
|
+
const override = process.env[entry.envOverride];
|
|
25
|
+
if (override)
|
|
26
|
+
return path.resolve(override);
|
|
27
|
+
if (process.platform === 'darwin') {
|
|
28
|
+
return path.join(os.homedir(), 'Library', 'Application Support', entry.dirName);
|
|
29
|
+
}
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
const local = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
32
|
+
return path.join(local, entry.dirName, 'User Data');
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function slugify(value) {
|
|
40
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
41
|
+
}
|
|
42
|
+
export function discoverChromiumProfilesAt(browser, userDataDir) {
|
|
43
|
+
const localState = path.join(userDataDir, 'Local State');
|
|
44
|
+
if (!fs.existsSync(localState)) {
|
|
45
|
+
return { ok: false, kind: 'not-installed', reason: `${browser} Local State not found: ${localState}` };
|
|
46
|
+
}
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(fs.readFileSync(localState, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
kind: 'invalid',
|
|
55
|
+
reason: error instanceof SyntaxError
|
|
56
|
+
? `${browser} Local State is not valid JSON`
|
|
57
|
+
: `Cannot read ${browser} Local State: ${error instanceof Error ? error.message : String(error)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(parsed) || !isRecord(parsed.profile) || !isRecord(parsed.profile.info_cache)) {
|
|
61
|
+
return { ok: false, kind: 'invalid', reason: `${browser} Local State has no profile.info_cache map` };
|
|
62
|
+
}
|
|
63
|
+
const rows = [];
|
|
64
|
+
for (const [profileDirectory, value] of Object.entries(parsed.profile.info_cache)) {
|
|
65
|
+
if (!isRecord(value) || typeof value.name !== 'string' || value.name.trim() === '') {
|
|
66
|
+
return { ok: false, kind: 'invalid', reason: `${browser} Local State profile ${JSON.stringify(profileDirectory)} has no non-empty name` };
|
|
67
|
+
}
|
|
68
|
+
rows.push({
|
|
69
|
+
browser,
|
|
70
|
+
name: `${browser}-${slugify(value.name) || 'profile'}`,
|
|
71
|
+
userDataDir,
|
|
72
|
+
profileDirectory,
|
|
73
|
+
displayName: value.name,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Two profiles with the same display name stay distinct by their directory.
|
|
77
|
+
const counts = new Map();
|
|
78
|
+
for (const row of rows)
|
|
79
|
+
counts.set(row.name, (counts.get(row.name) ?? 0) + 1);
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
userDataDir,
|
|
83
|
+
profiles: rows.map((row) => (counts.get(row.name) ?? 0) > 1 ? { ...row, name: `${row.name}-${slugify(row.profileDirectory)}` } : row),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function discoverChromiumProfiles(browser) {
|
|
87
|
+
const userDataDir = chromiumUserDataDir(browser);
|
|
88
|
+
if (!userDataDir) {
|
|
89
|
+
return { ok: false, kind: 'unsupported', reason: `${browser} profiles are not discovered on this platform` };
|
|
90
|
+
}
|
|
91
|
+
return discoverChromiumProfilesAt(browser, userDataDir);
|
|
92
|
+
}
|
|
93
|
+
/** Every browser whose native profiles agents-cli discovers on this platform. */
|
|
94
|
+
export function discoverableChromiumBrowsers() {
|
|
95
|
+
return Object.keys(NATIVE_CHROMIUM).filter((browser) => chromiumUserDataDir(browser) !== undefined);
|
|
96
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { BrowserProfile, ConnectionKey } from '../types.js';
|
|
2
|
+
/** How agents obtain a live Firefox — matches the CDP driver's launch/attach split. */
|
|
3
|
+
export interface FirefoxConnection {
|
|
4
|
+
bidi: FirefoxBiDiClient;
|
|
5
|
+
port: number;
|
|
6
|
+
/** The Firefox process pid we launched, or 0 when we attached to a running one. */
|
|
7
|
+
pid: number;
|
|
8
|
+
/** The BiDi session id from `session.new`. */
|
|
9
|
+
sessionId: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A verb Firefox-over-BiDi cannot serve (network capture, upload, pdf, trusted
|
|
13
|
+
* key input, …). Mirrors `ArcNativeCapabilityError`: the service throws it in
|
|
14
|
+
* the one place the capability is missing, and the message steers the caller to
|
|
15
|
+
* a Chromium-family profile that has it.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FirefoxCapabilityError extends Error {
|
|
18
|
+
readonly capability: string;
|
|
19
|
+
constructor(capability: string, message?: string);
|
|
20
|
+
}
|
|
21
|
+
/** A BiDi error frame carrying the protocol error + human message. */
|
|
22
|
+
export declare class FirefoxBiDiError extends Error {
|
|
23
|
+
readonly bidiError: string;
|
|
24
|
+
constructor(bidiError: string, message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Minimal WebDriver BiDi client: request/response keyed by the `id` field, with
|
|
28
|
+
* events drained and ignored. Deliberately shaped like `CDPClient` so the
|
|
29
|
+
* service treats a Firefox connection the same way it treats a CDP one. The
|
|
30
|
+
* large `maxPayload` matches CDPClient's, so a base64 screenshot of a
|
|
31
|
+
* content-rich page never trips the socket's decompressed-size cap.
|
|
32
|
+
*/
|
|
33
|
+
export declare class FirefoxBiDiClient {
|
|
34
|
+
private ws;
|
|
35
|
+
private nextId;
|
|
36
|
+
private pending;
|
|
37
|
+
private closed;
|
|
38
|
+
get isOpen(): boolean;
|
|
39
|
+
connect(url: string): Promise<void>;
|
|
40
|
+
private handleMessage;
|
|
41
|
+
private handleClose;
|
|
42
|
+
send<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>): Promise<T>;
|
|
43
|
+
close(): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The loud error raised when a Firefox already holds this profile but exposes no
|
|
47
|
+
* BiDi port, so agents cannot attach and must not launch a rival (Firefox is
|
|
48
|
+
* single-instance per profile dir). Mirrors `attachOnlyRequiredError` in
|
|
49
|
+
* `drivers/local.ts`: name the exact relaunch that makes the running instance
|
|
50
|
+
* attachable. Exported so the contract is unit-testable without a real Firefox.
|
|
51
|
+
*/
|
|
52
|
+
export declare function firefoxAttachRequiredError(profile: Pick<BrowserProfile, 'name'> & {
|
|
53
|
+
firefox?: {
|
|
54
|
+
profileName: string;
|
|
55
|
+
};
|
|
56
|
+
}, port: number, profileDir: string): Error;
|
|
57
|
+
/**
|
|
58
|
+
* Connect a Firefox profile over BiDi: attach if the port is already served,
|
|
59
|
+
* otherwise launch Firefox and wait for it. Fails loud when a Firefox holds the
|
|
60
|
+
* profile without a port.
|
|
61
|
+
*/
|
|
62
|
+
export declare function connectFirefox(profile: BrowserProfile, key: ConnectionKey, port: number, opts?: {
|
|
63
|
+
profileDir: string;
|
|
64
|
+
headless?: boolean;
|
|
65
|
+
}): Promise<FirefoxConnection>;
|
|
66
|
+
export declare function deserializeBidi(remote: unknown): unknown;
|
|
67
|
+
/** One top-level browsing context (a tab), flattened from `browsingContext.getTree`. */
|
|
68
|
+
export interface BiDiContext {
|
|
69
|
+
context: string;
|
|
70
|
+
url: string;
|
|
71
|
+
}
|
|
72
|
+
/** Every top-level tab currently open in this Firefox. */
|
|
73
|
+
export declare function bidiTopLevelContexts(bidi: FirefoxBiDiClient): Promise<BiDiContext[]>;
|
|
74
|
+
/** Open a fresh tab and return its context id. */
|
|
75
|
+
export declare function bidiCreateTab(bidi: FirefoxBiDiClient): Promise<string>;
|
|
76
|
+
/** Navigate a context and wait for the document to finish loading. */
|
|
77
|
+
export declare function bidiNavigate(bidi: FirefoxBiDiClient, context: string, url: string): Promise<void>;
|
|
78
|
+
/** Reload a context in place, waiting for load. */
|
|
79
|
+
export declare function bidiReload(bidi: FirefoxBiDiClient, context: string): Promise<void>;
|
|
80
|
+
/** Close a tab. */
|
|
81
|
+
export declare function bidiCloseTab(bidi: FirefoxBiDiClient, context: string): Promise<void>;
|
|
82
|
+
/** Bring a tab to the foreground (explicit focus only). */
|
|
83
|
+
export declare function bidiActivate(bidi: FirefoxBiDiClient, context: string): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate an expression in a context and return the deserialized value.
|
|
86
|
+
* `awaitPromise` mirrors the CDP evaluate contract; a thrown/rejected value
|
|
87
|
+
* surfaces as an Error rather than a silent undefined.
|
|
88
|
+
*/
|
|
89
|
+
export declare function bidiEvaluate(bidi: FirefoxBiDiClient, context: string, expression: string): Promise<unknown>;
|
|
90
|
+
/** Capture a screenshot of the viewport as raw bytes. `quality` is 0–1 for JPEG. */
|
|
91
|
+
export declare function bidiScreenshot(bidi: FirefoxBiDiClient, context: string, format: {
|
|
92
|
+
type: 'image/png';
|
|
93
|
+
} | {
|
|
94
|
+
type: 'image/jpeg';
|
|
95
|
+
quality: number;
|
|
96
|
+
}): Promise<Buffer>;
|
|
97
|
+
/**
|
|
98
|
+
* A real, trusted left click at viewport coordinates via `input.performActions`
|
|
99
|
+
* — the pointer path CDP `Input.dispatchMouseEvent` gives Chromium, which Arc
|
|
100
|
+
* (Apple Events) never had. The service resolves a ref to (x, y) first, exactly
|
|
101
|
+
* as the CDP click path does.
|
|
102
|
+
*/
|
|
103
|
+
export declare function bidiClickAt(bidi: FirefoxBiDiClient, context: string, x: number, y: number): Promise<void>;
|