@phnx-labs/agents-cli 1.22.93 → 1.22.95
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 +20 -0
- package/dist/commands/browser.d.ts +1 -0
- package/dist/commands/browser.js +156 -11
- package/dist/lib/browser/arc-discovery.d.ts +60 -0
- package/dist/lib/browser/arc-discovery.js +187 -0
- package/dist/lib/browser/arc-dom.d.ts +14 -0
- package/dist/lib/browser/arc-dom.js +121 -0
- 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/arc.d.ts +57 -0
- package/dist/lib/browser/drivers/arc.js +281 -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/profiles.d.ts +39 -1
- package/dist/lib/browser/profiles.js +239 -9
- package/dist/lib/browser/refs.d.ts +2 -0
- package/dist/lib/browser/refs.js +2 -1
- package/dist/lib/browser/resolve-target.d.ts +2 -0
- package/dist/lib/browser/resolve-target.js +14 -0
- package/dist/lib/browser/runtime-state.js +9 -1
- package/dist/lib/browser/service.d.ts +86 -3
- package/dist/lib/browser/service.js +1045 -42
- package/dist/lib/browser/types.d.ts +85 -2
- package/dist/lib/daemon/usage-sync-service.js +12 -0
- package/dist/lib/open-url.js +6 -0
- package/dist/lib/types.d.ts +20 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.95
|
|
4
|
+
|
|
5
|
+
- **Firefox automation over WebDriver BiDi (PHNX-4043).** `agents browser` now drives
|
|
6
|
+
Firefox, which dropped the Chrome DevTools Protocol in 129. Every Firefox profile in
|
|
7
|
+
`profiles.ini` is discovered read-only as a `firefox-<name>` browser profile
|
|
8
|
+
(`firefox-default`, `firefox-default-release`, …) pinned to that profile directory;
|
|
9
|
+
agents launch it headless with a debug port (or attach to a running one, failing loud
|
|
10
|
+
when a portless Firefox already holds the profile). Supported verbs: start, navigate,
|
|
11
|
+
tab add, tabs, evaluate, refs, click, fill/type, scroll, screenshot, done — with the
|
|
12
|
+
same same-task reopen semantics as the rest of the service. A trusted pointer click
|
|
13
|
+
goes through `input.performActions`. Network capture, upload, and PDF fail loud with a
|
|
14
|
+
structured error naming a Chromium-family profile. Source: `apps/cli/src/lib/browser/drivers/firefox.ts`,
|
|
15
|
+
`apps/cli/src/lib/browser/firefox-discovery.ts`, `apps/cli/src/lib/browser/service.ts`.
|
|
16
|
+
|
|
17
|
+
- **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`.
|
|
18
|
+
|
|
19
|
+
## 1.22.94
|
|
20
|
+
|
|
21
|
+
- **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`.
|
|
22
|
+
|
|
3
23
|
## 1.22.93
|
|
4
24
|
|
|
5
25
|
- **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`.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type BrowserProfile } from '../lib/browser/profiles.js';
|
|
3
3
|
import { listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
4
|
+
export declare function remoteStartTaskName(stdout: string, explicit?: string): string | undefined;
|
|
4
5
|
/**
|
|
5
6
|
* The fleet browser hub this box drives by default (`browser.device`), or
|
|
6
7
|
* undefined to drive locally. It is one fleet-synced value in the central
|
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, } from '../lib/browser/profiles.js';
|
|
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';
|
|
@@ -36,6 +36,8 @@ import { buildHar } from '../lib/browser/har.js';
|
|
|
36
36
|
import { getCliVersion } from '../lib/version.js';
|
|
37
37
|
import { runBrowserIPCStream } from '../lib/browser/stream.js';
|
|
38
38
|
import { machineId } from '../lib/machine-id.js';
|
|
39
|
+
import { isArcRunning } from '../lib/browser/drivers/arc.js';
|
|
40
|
+
import { resolveBrowserTarget } from '../lib/browser/resolve-target.js';
|
|
39
41
|
/**
|
|
40
42
|
* Task name inferred from the local task→device index when `--task` was
|
|
41
43
|
* omitted and this session owns exactly one open task. Set by the page-verb
|
|
@@ -161,6 +163,22 @@ async function dispatchBrowserToDevice(device, forwardedArgs, mode) {
|
|
|
161
163
|
stderr: result.stderr,
|
|
162
164
|
};
|
|
163
165
|
}
|
|
166
|
+
export function remoteStartTaskName(stdout, explicit) {
|
|
167
|
+
if (explicit)
|
|
168
|
+
return explicit;
|
|
169
|
+
const trimmed = stdout.trim();
|
|
170
|
+
if (!trimmed)
|
|
171
|
+
return undefined;
|
|
172
|
+
try {
|
|
173
|
+
const decoded = JSON.parse(trimmed);
|
|
174
|
+
if (typeof decoded.task === 'string' && decoded.task.length > 0)
|
|
175
|
+
return decoded.task;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Human output is one task name on the first non-empty line.
|
|
179
|
+
}
|
|
180
|
+
return trimmed.split('\n').map((line) => line.trim()).find(Boolean);
|
|
181
|
+
}
|
|
164
182
|
async function pullRemoteFile(device, remotePath, localPath) {
|
|
165
183
|
const host = await resolveHost(device);
|
|
166
184
|
if (!host) {
|
|
@@ -424,6 +442,7 @@ export async function runBrowserUse(name, opts, interactive = isInteractiveTermi
|
|
|
424
442
|
console.error(`Available profiles: ${all.map((profile) => profile.name).join(', ')}`);
|
|
425
443
|
return false;
|
|
426
444
|
}
|
|
445
|
+
await persistDiscoveredProfile(selectedName);
|
|
427
446
|
setConfigValue('browser.profile', selectedName);
|
|
428
447
|
const endpoint = Object.values(getEndpointPresets(target))[0]?.target ?? '';
|
|
429
448
|
console.log(`Default browser profile (this machine) is now "${selectedName}" (${target.browser}${endpoint ? `, ${endpoint}` : ''}).`);
|
|
@@ -484,7 +503,7 @@ function registerProfilesCommands(browser) {
|
|
|
484
503
|
console.log(JSON.stringify(allProfiles.map((profile) => ({
|
|
485
504
|
...profile,
|
|
486
505
|
devices: profile.devices,
|
|
487
|
-
kind: profileKind(profile.name),
|
|
506
|
+
kind: profile.arc || profile.firefox ? 'identity' : profileKind(profile.name),
|
|
488
507
|
isConfiguredDefault: profile.name === configuredDefault,
|
|
489
508
|
})), null, 2));
|
|
490
509
|
return;
|
|
@@ -927,6 +946,15 @@ function registerProfilesCommands(browser) {
|
|
|
927
946
|
console.log(`Target filter: ${profile.targetFilter}`);
|
|
928
947
|
if (profile.description)
|
|
929
948
|
console.log(`Description: ${profile.description}`);
|
|
949
|
+
if (profile.arc) {
|
|
950
|
+
console.log(`Arc Space: ${profile.arc.spaceTitle} (${profile.arc.spaceId})`);
|
|
951
|
+
console.log(`Arc profile: ${profile.arc.profileName} (${profile.arc.profileId})`);
|
|
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
|
+
}
|
|
930
958
|
const presets = getEndpointPresets(profile);
|
|
931
959
|
const defaultName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
|
|
932
960
|
? profile.defaultEndpoint
|
|
@@ -952,6 +980,8 @@ function registerProfilesCommands(browser) {
|
|
|
952
980
|
console.log(`Headless: true`);
|
|
953
981
|
// Login state per known service: live session + account identity + whether
|
|
954
982
|
// login creds are declared in the profile's secrets bundle.
|
|
983
|
+
if (profile.arc || profile.firefox)
|
|
984
|
+
return;
|
|
955
985
|
const active = await loginsForProfile(profile.name);
|
|
956
986
|
const accounts = await accountsForProfile(profile.name);
|
|
957
987
|
const lines = [];
|
|
@@ -975,7 +1005,7 @@ function registerProfilesCommands(browser) {
|
|
|
975
1005
|
profiles
|
|
976
1006
|
.command('remove <name>')
|
|
977
1007
|
.alias('delete')
|
|
978
|
-
.description('Remove
|
|
1008
|
+
.description('Remove the agents-cli profile alias and cached runtime dirs (never deletes native Arc data)')
|
|
979
1009
|
.option('--keep-cache', "Leave ~/.agents/.cache/browser/<name>* dirs in place (don't wipe chrome-data)")
|
|
980
1010
|
.action(async (name, opts) => {
|
|
981
1011
|
await deleteProfile(name);
|
|
@@ -1094,12 +1124,20 @@ function registerProfilesCommands(browser) {
|
|
|
1094
1124
|
profile that is in use, the configured default, or the \`auto-chrome\`
|
|
1095
1125
|
profile a setup wizard created.
|
|
1096
1126
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1127
|
+
On a Mac with Arc, every Arc Space is listed as a profile (arc-gmail,
|
|
1128
|
+
arc-work, ...) discovered read-only from Arc's own metadata — a Space
|
|
1129
|
+
already carries its Arc profile's logins, so it IS the browser profile.
|
|
1130
|
+
Agents attach to the running Arc through Apple Events and never launch or
|
|
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,
|
|
1138
|
+
downloads, network capture, or trusted input:
|
|
1101
1139
|
agents browser profiles create agents-comet --browser comet --attach-only
|
|
1102
|
-
|
|
1140
|
+
A CDP \`--attach-only\` profile attaches to a browser you
|
|
1103
1141
|
already started with remote debugging and fails loud with the relaunch
|
|
1104
1142
|
command otherwise, pinned to a durable --user-data-dir so the sign-in
|
|
1105
1143
|
survives quit+relaunch. A foreign browser squatting the canonical port is
|
|
@@ -1115,6 +1153,17 @@ function registerProfilesCommands(browser) {
|
|
|
1115
1153
|
console.error(`Profile "${name}" not found`);
|
|
1116
1154
|
process.exit(1);
|
|
1117
1155
|
}
|
|
1156
|
+
if ((profile.arc || profile.firefox) && !isFleetRemoteInvocation()) {
|
|
1157
|
+
const routed = resolveBrowserTarget(profile.name);
|
|
1158
|
+
if (!routed.local && routed.commandDispatch) {
|
|
1159
|
+
const result = await dispatchBrowserToDevice(routed.device, ['browser', 'profiles', 'doctor', name], 'capture');
|
|
1160
|
+
process.stdout.write(result.stdout);
|
|
1161
|
+
process.stderr.write(result.stderr);
|
|
1162
|
+
if (result.code !== 0)
|
|
1163
|
+
process.exit(result.code);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1118
1167
|
const checks = [];
|
|
1119
1168
|
// 0. Declaration topology. An identity-bearing profile declared by a
|
|
1120
1169
|
// different device cannot be diagnosed through a loopback CDP endpoint
|
|
@@ -1139,6 +1188,31 @@ function registerProfilesCommands(browser) {
|
|
|
1139
1188
|
}
|
|
1140
1189
|
process.exit(1);
|
|
1141
1190
|
}
|
|
1191
|
+
if (profile.arc) {
|
|
1192
|
+
checks.push({
|
|
1193
|
+
label: 'space',
|
|
1194
|
+
ok: true,
|
|
1195
|
+
detail: `${profile.arc.spaceTitle} (${profile.arc.spaceId})`,
|
|
1196
|
+
});
|
|
1197
|
+
checks.push({
|
|
1198
|
+
label: 'arc-profile',
|
|
1199
|
+
ok: true,
|
|
1200
|
+
detail: `${profile.arc.profileName} (${profile.arc.profileId})`,
|
|
1201
|
+
});
|
|
1202
|
+
const running = await isArcRunning();
|
|
1203
|
+
checks.push({
|
|
1204
|
+
label: 'Arc process',
|
|
1205
|
+
ok: running,
|
|
1206
|
+
detail: running ? 'running; native automation attaches without launching or restarting Arc' : 'not running — start Arc normally',
|
|
1207
|
+
});
|
|
1208
|
+
const allOk = checks.every((check) => check.ok);
|
|
1209
|
+
for (const check of checks) {
|
|
1210
|
+
console.log(`${check.ok ? 'OK ' : 'FAIL'} ${check.label.padEnd(15)} ${check.detail}`);
|
|
1211
|
+
}
|
|
1212
|
+
if (!allOk)
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1142
1216
|
// 1. Binary exists for declared browser type, and is a real executable we
|
|
1143
1217
|
// can drive — not a distro launcher script. findBrowserPath already
|
|
1144
1218
|
// unwraps the known Chromium wrappers to their ELF; if it still hands
|
|
@@ -1358,8 +1432,41 @@ function registerTaskCommands(browser) {
|
|
|
1358
1432
|
}
|
|
1359
1433
|
if (top === 'stop' && opts.profile && !taskFlag)
|
|
1360
1434
|
return;
|
|
1435
|
+
// A native Arc endpoint cannot be represented by an SSH-forwarded socket.
|
|
1436
|
+
// For a cold profile-scoped page verb, create the task on the declaring
|
|
1437
|
+
// owner first and bind that owner locally; the ordinary task router below
|
|
1438
|
+
// then forwards this complete verb and every subsequent verb to that host.
|
|
1439
|
+
if (!taskFlag && opts.profile) {
|
|
1440
|
+
try {
|
|
1441
|
+
const profileName = (await resolveProfileRef(opts.profile)) ?? opts.profile;
|
|
1442
|
+
const target = resolveBrowserTarget(profileName);
|
|
1443
|
+
if (!target.local && target.commandDispatch) {
|
|
1444
|
+
const started = await dispatchBrowserToDevice(target.device, ['browser', 'start', '--profile', profileName, '--json'], 'capture');
|
|
1445
|
+
if (started.code !== 0) {
|
|
1446
|
+
process.stdout.write(started.stdout);
|
|
1447
|
+
process.stderr.write(started.stderr);
|
|
1448
|
+
process.exit(started.code);
|
|
1449
|
+
}
|
|
1450
|
+
const task = remoteStartTaskName(started.stdout);
|
|
1451
|
+
if (!task)
|
|
1452
|
+
throw new Error(`Remote start on ${target.device} produced no task name.`);
|
|
1453
|
+
bindTask(task, {
|
|
1454
|
+
device: target.device,
|
|
1455
|
+
profile: profileName,
|
|
1456
|
+
sessionId: callerSessionId(),
|
|
1457
|
+
launchId: callerLaunchId(),
|
|
1458
|
+
createdAt: Date.now(),
|
|
1459
|
+
});
|
|
1460
|
+
inferredTaskName = task;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
catch (err) {
|
|
1464
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1361
1468
|
const route = resolveTaskRoute({
|
|
1362
|
-
task: taskFlag ?? process.env.AGENTS_BROWSER_TASK,
|
|
1469
|
+
task: inferredTaskName ?? taskFlag ?? process.env.AGENTS_BROWSER_TASK,
|
|
1363
1470
|
sessionId: callerSessionId(),
|
|
1364
1471
|
launchId: callerLaunchId(),
|
|
1365
1472
|
hub: defaultBrowserHub(),
|
|
@@ -1545,7 +1652,7 @@ function registerTaskCommands(browser) {
|
|
|
1545
1652
|
process.stderr.write(result.stderr);
|
|
1546
1653
|
if (result.code !== 0)
|
|
1547
1654
|
process.exit(result.code);
|
|
1548
|
-
const taskName =
|
|
1655
|
+
const taskName = remoteStartTaskName(result.stdout, opts.task);
|
|
1549
1656
|
if (!taskName) {
|
|
1550
1657
|
console.error(`Remote start on ${hub} produced no task name.`);
|
|
1551
1658
|
process.exit(1);
|
|
@@ -1614,7 +1721,7 @@ function registerTaskCommands(browser) {
|
|
|
1614
1721
|
process.stderr.write(result.stderr);
|
|
1615
1722
|
if (result.code !== 0)
|
|
1616
1723
|
process.exit(result.code);
|
|
1617
|
-
const taskName =
|
|
1724
|
+
const taskName = remoteStartTaskName(result.stdout, opts.task);
|
|
1618
1725
|
if (!taskName) {
|
|
1619
1726
|
console.error(`Remote start on ${deviceName} produced no task name.`);
|
|
1620
1727
|
process.exit(1);
|
|
@@ -1668,6 +1775,44 @@ function registerTaskCommands(browser) {
|
|
|
1668
1775
|
console.error(`Create one with: agents browser profiles create ${profileName} --browser <chrome|comet|chromium|brave|edge|arc|custom>`);
|
|
1669
1776
|
process.exit(1);
|
|
1670
1777
|
}
|
|
1778
|
+
// Discovery is read-only for list/show/doctor. Starting is the explicit
|
|
1779
|
+
// adoption point: persist only the agents-cli alias/native identity, never
|
|
1780
|
+
// mutate Arc's own profile or Space data.
|
|
1781
|
+
await persistDiscoveredProfile(profileName);
|
|
1782
|
+
// A native endpoint is not CDP and cannot be tunnelled. Re-exec the
|
|
1783
|
+
// complete command on the declaring owner, then bind its returned task
|
|
1784
|
+
// locally so every later verb follows the same owner from task-index.
|
|
1785
|
+
if (!isFleetRemoteInvocation()) {
|
|
1786
|
+
let routed;
|
|
1787
|
+
try {
|
|
1788
|
+
routed = resolveBrowserTarget(profileName, { endpointName: opts.endpoint });
|
|
1789
|
+
}
|
|
1790
|
+
catch (err) {
|
|
1791
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
if (!routed.local && routed.commandDispatch) {
|
|
1795
|
+
const result = await dispatchBrowserToDevice(routed.device, browserForwardedArgv(), 'capture');
|
|
1796
|
+
process.stdout.write(result.stdout);
|
|
1797
|
+
process.stderr.write(result.stderr);
|
|
1798
|
+
if (result.code !== 0)
|
|
1799
|
+
process.exit(result.code);
|
|
1800
|
+
const taskName = remoteStartTaskName(result.stdout, opts.task);
|
|
1801
|
+
if (!taskName) {
|
|
1802
|
+
console.error(`Remote start on ${routed.device} produced no task name.`);
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
bindTask(taskName, {
|
|
1806
|
+
device: routed.device,
|
|
1807
|
+
profile: profileName,
|
|
1808
|
+
url: opts.url,
|
|
1809
|
+
sessionId: callerSessionId(),
|
|
1810
|
+
launchId: callerLaunchId(),
|
|
1811
|
+
createdAt: Date.now(),
|
|
1812
|
+
});
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1671
1816
|
// Pre-check the endpoint name too — same fail-fast rationale.
|
|
1672
1817
|
if (opts.endpoint) {
|
|
1673
1818
|
const presets = getEndpointPresets(profile);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface ArcSpace {
|
|
2
|
+
/** Stable UUID used for every native operation. */
|
|
3
|
+
id: string;
|
|
4
|
+
/** Display-only title. */
|
|
5
|
+
title: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ArcProfile {
|
|
8
|
+
/** Arc's profile directory basename: the stable profile identity. */
|
|
9
|
+
profileId: string;
|
|
10
|
+
/** Display-only profile name from Local State. */
|
|
11
|
+
displayName: string;
|
|
12
|
+
spaces: ArcSpace[];
|
|
13
|
+
}
|
|
14
|
+
export type ArcDiscoveryResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
profiles: ArcProfile[];
|
|
17
|
+
userDataDir: string;
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
kind: 'unsupported' | 'not-installed' | 'invalid';
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
interface SidebarSpace {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
profileId: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Arc's application-support root on this Mac. Arc keeps its Chromium `Local
|
|
30
|
+
* State` under `<root>/User Data/` and its own sidebar model (Spaces and their
|
|
31
|
+
* profile bindings) at `<root>/StorableSidebar.json` — two different levels,
|
|
32
|
+
* so discovery is rooted here rather than at the Chromium user-data dir.
|
|
33
|
+
* `AGENTS_ARC_DIR` points tests and non-default installs at another root.
|
|
34
|
+
*/
|
|
35
|
+
export declare function arcSupportDir(): string | undefined;
|
|
36
|
+
export declare function parseLocalStateProfiles(localStatePath: string): Map<string, string> | {
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function parseSidebarSpaces(sidebarPath: string): SidebarSpace[] | {
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function discoverArcProfilesAt(arcDir: string): ArcDiscoveryResult;
|
|
43
|
+
export declare function discoverArcProfiles(): ArcDiscoveryResult;
|
|
44
|
+
/** One Arc Space, flattened with the Arc profile it belongs to. */
|
|
45
|
+
export interface ArcSpaceProfile {
|
|
46
|
+
/** The agents-cli profile name: `arc-<space-title-slug>`. */
|
|
47
|
+
name: string;
|
|
48
|
+
profileId: string;
|
|
49
|
+
profileName: string;
|
|
50
|
+
spaceId: string;
|
|
51
|
+
spaceTitle: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Flatten discovery into one agents-cli profile per Space (the concept agents
|
|
55
|
+
* already know), named `arc-<space-title>`. Two Spaces with the same title get
|
|
56
|
+
* the first six characters of their stable Space id appended, so the names
|
|
57
|
+
* stay distinct without inventing a second selector.
|
|
58
|
+
*/
|
|
59
|
+
export declare function arcSpaceProfiles(result: ArcDiscoveryResult): ArcSpaceProfile[];
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** Read-only Arc profile/Space discovery from Arc's native metadata. */
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Arc's application-support root on this Mac. Arc keeps its Chromium `Local
|
|
10
|
+
* State` under `<root>/User Data/` and its own sidebar model (Spaces and their
|
|
11
|
+
* profile bindings) at `<root>/StorableSidebar.json` — two different levels,
|
|
12
|
+
* so discovery is rooted here rather than at the Chromium user-data dir.
|
|
13
|
+
* `AGENTS_ARC_DIR` points tests and non-default installs at another root.
|
|
14
|
+
*/
|
|
15
|
+
export function arcSupportDir() {
|
|
16
|
+
const override = process.env.AGENTS_ARC_DIR;
|
|
17
|
+
if (override)
|
|
18
|
+
return path.resolve(override);
|
|
19
|
+
if (process.platform !== 'darwin')
|
|
20
|
+
return undefined;
|
|
21
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Arc');
|
|
22
|
+
}
|
|
23
|
+
export function parseLocalStateProfiles(localStatePath) {
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(fs.readFileSync(localStatePath, 'utf8'));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
error: error instanceof SyntaxError
|
|
31
|
+
? 'Arc Local State is not valid JSON'
|
|
32
|
+
: `Cannot read Arc Local State: ${error instanceof Error ? error.message : String(error)}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (!isRecord(parsed) || !isRecord(parsed.profile) || !isRecord(parsed.profile.info_cache)) {
|
|
36
|
+
return { error: 'Arc Local State has no profile.info_cache map' };
|
|
37
|
+
}
|
|
38
|
+
const profiles = new Map();
|
|
39
|
+
for (const [profileId, value] of Object.entries(parsed.profile.info_cache)) {
|
|
40
|
+
if (profileId === '__ARC_SYSTEM_PROFILE')
|
|
41
|
+
continue;
|
|
42
|
+
if (!isRecord(value) || typeof value.name !== 'string' || value.name.trim() === '') {
|
|
43
|
+
return { error: `Arc Local State profile ${JSON.stringify(profileId)} has no non-empty name` };
|
|
44
|
+
}
|
|
45
|
+
profiles.set(profileId, value.name);
|
|
46
|
+
}
|
|
47
|
+
return profiles;
|
|
48
|
+
}
|
|
49
|
+
function parseProfileId(value, spaceId) {
|
|
50
|
+
if (!isRecord(value)) {
|
|
51
|
+
return { error: `Arc Space ${JSON.stringify(spaceId)} has no profile mapping` };
|
|
52
|
+
}
|
|
53
|
+
if (value.default === true && value.custom === undefined)
|
|
54
|
+
return 'Default';
|
|
55
|
+
if (!isRecord(value.custom)) {
|
|
56
|
+
return { error: `Arc Space ${JSON.stringify(spaceId)} has an unknown profile mapping` };
|
|
57
|
+
}
|
|
58
|
+
const entries = Object.values(value.custom);
|
|
59
|
+
if (entries.length !== 1 || !isRecord(entries[0])) {
|
|
60
|
+
return { error: `Arc Space ${JSON.stringify(spaceId)} has an ambiguous custom profile mapping` };
|
|
61
|
+
}
|
|
62
|
+
const profileId = entries[0].directoryBasename;
|
|
63
|
+
if (typeof profileId !== 'string' || profileId.trim() === '') {
|
|
64
|
+
return { error: `Arc Space ${JSON.stringify(spaceId)} has no custom profile id` };
|
|
65
|
+
}
|
|
66
|
+
return profileId;
|
|
67
|
+
}
|
|
68
|
+
export function parseSidebarSpaces(sidebarPath) {
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
error: error instanceof SyntaxError
|
|
76
|
+
? 'Arc StorableSidebar.json is not valid JSON'
|
|
77
|
+
: `Cannot read Arc StorableSidebar.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!isRecord(parsed) || parsed.version !== 1) {
|
|
81
|
+
const version = isRecord(parsed) ? parsed.version : undefined;
|
|
82
|
+
return { error: `Unsupported StorableSidebar version: ${String(version)}` };
|
|
83
|
+
}
|
|
84
|
+
if (!isRecord(parsed.sidebar) || !Array.isArray(parsed.sidebar.containers)) {
|
|
85
|
+
return { error: 'StorableSidebar.json has no sidebar.containers array' };
|
|
86
|
+
}
|
|
87
|
+
const spaces = [];
|
|
88
|
+
const ids = new Set();
|
|
89
|
+
for (const container of parsed.sidebar.containers) {
|
|
90
|
+
if (!isRecord(container) || container.spaces === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
if (!Array.isArray(container.spaces) || container.spaces.length % 2 !== 0) {
|
|
93
|
+
return { error: 'StorableSidebar.json contains a malformed alternating spaces array' };
|
|
94
|
+
}
|
|
95
|
+
for (let i = 0; i < container.spaces.length; i += 2) {
|
|
96
|
+
const encodedId = container.spaces[i];
|
|
97
|
+
const value = container.spaces[i + 1];
|
|
98
|
+
if (typeof encodedId !== 'string' || !isRecord(value)) {
|
|
99
|
+
return { error: `StorableSidebar.json contains a malformed Space at index ${i}` };
|
|
100
|
+
}
|
|
101
|
+
if (typeof value.id !== 'string' || value.id !== encodedId) {
|
|
102
|
+
return { error: `Arc Space key ${JSON.stringify(encodedId)} does not match its stable id` };
|
|
103
|
+
}
|
|
104
|
+
if (typeof value.title !== 'string') {
|
|
105
|
+
return { error: `Arc Space ${JSON.stringify(encodedId)} has no title` };
|
|
106
|
+
}
|
|
107
|
+
if (ids.has(encodedId)) {
|
|
108
|
+
return { error: `Arc Space ${JSON.stringify(encodedId)} appears more than once` };
|
|
109
|
+
}
|
|
110
|
+
const profileId = parseProfileId(value.profile, encodedId);
|
|
111
|
+
if (typeof profileId !== 'string')
|
|
112
|
+
return profileId;
|
|
113
|
+
ids.add(encodedId);
|
|
114
|
+
spaces.push({ id: encodedId, title: value.title, profileId });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return spaces;
|
|
118
|
+
}
|
|
119
|
+
export function discoverArcProfilesAt(arcDir) {
|
|
120
|
+
const userDataDir = path.join(arcDir, 'User Data');
|
|
121
|
+
if (!fs.existsSync(userDataDir)) {
|
|
122
|
+
return { ok: false, kind: 'not-installed', reason: `Arc user data directory not found: ${userDataDir}` };
|
|
123
|
+
}
|
|
124
|
+
const profileNames = parseLocalStateProfiles(path.join(userDataDir, 'Local State'));
|
|
125
|
+
if ('error' in profileNames)
|
|
126
|
+
return { ok: false, kind: 'invalid', reason: profileNames.error };
|
|
127
|
+
if (profileNames.size === 0) {
|
|
128
|
+
return { ok: false, kind: 'invalid', reason: 'No Arc profiles found in Local State' };
|
|
129
|
+
}
|
|
130
|
+
const spaces = parseSidebarSpaces(path.join(arcDir, 'StorableSidebar.json'));
|
|
131
|
+
if ('error' in spaces)
|
|
132
|
+
return { ok: false, kind: 'invalid', reason: spaces.error };
|
|
133
|
+
const profiles = new Map();
|
|
134
|
+
for (const [profileId, displayName] of profileNames) {
|
|
135
|
+
profiles.set(profileId, { profileId, displayName, spaces: [] });
|
|
136
|
+
}
|
|
137
|
+
for (const space of spaces) {
|
|
138
|
+
const profile = profiles.get(space.profileId);
|
|
139
|
+
if (!profile) {
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
kind: 'invalid',
|
|
143
|
+
reason: `Arc Space ${JSON.stringify(space.id)} maps to unknown profile ${JSON.stringify(space.profileId)}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
profile.spaces.push({ id: space.id, title: space.title });
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, profiles: [...profiles.values()], userDataDir };
|
|
149
|
+
}
|
|
150
|
+
export function discoverArcProfiles() {
|
|
151
|
+
const arcDir = arcSupportDir();
|
|
152
|
+
if (!arcDir) {
|
|
153
|
+
return { ok: false, kind: 'unsupported', reason: 'Arc is not supported on this platform' };
|
|
154
|
+
}
|
|
155
|
+
return discoverArcProfilesAt(arcDir);
|
|
156
|
+
}
|
|
157
|
+
function slugify(value) {
|
|
158
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Flatten discovery into one agents-cli profile per Space (the concept agents
|
|
162
|
+
* already know), named `arc-<space-title>`. Two Spaces with the same title get
|
|
163
|
+
* the first six characters of their stable Space id appended, so the names
|
|
164
|
+
* stay distinct without inventing a second selector.
|
|
165
|
+
*/
|
|
166
|
+
export function arcSpaceProfiles(result) {
|
|
167
|
+
if (!result.ok)
|
|
168
|
+
return [];
|
|
169
|
+
const flat = [];
|
|
170
|
+
for (const profile of result.profiles) {
|
|
171
|
+
for (const space of profile.spaces) {
|
|
172
|
+
flat.push({
|
|
173
|
+
name: `arc-${slugify(space.title) || 'space'}`,
|
|
174
|
+
profileId: profile.profileId,
|
|
175
|
+
profileName: profile.displayName,
|
|
176
|
+
spaceId: space.id,
|
|
177
|
+
spaceTitle: space.title,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const counts = new Map();
|
|
182
|
+
for (const entry of flat)
|
|
183
|
+
counts.set(entry.name, (counts.get(entry.name) ?? 0) + 1);
|
|
184
|
+
return flat.map((entry) => (counts.get(entry.name) ?? 0) > 1
|
|
185
|
+
? { ...entry, name: `${entry.name}-${slugify(entry.spaceId).slice(0, 6)}` }
|
|
186
|
+
: entry);
|
|
187
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RefNode, RefOpts } from './refs.js';
|
|
2
|
+
/** Build one synchronous expression that reads DOM-backed refs from native Arc. */
|
|
3
|
+
export declare function arcRefsExpression(opts?: RefOpts): string;
|
|
4
|
+
export declare function parseArcRefsResult(value: unknown, opts?: RefOpts): {
|
|
5
|
+
refs: string;
|
|
6
|
+
nodeMap: Map<number, RefNode>;
|
|
7
|
+
opts: {
|
|
8
|
+
interactive: boolean;
|
|
9
|
+
limit: number;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export declare function arcClickExpression(selector: string): string;
|
|
13
|
+
export declare function arcFillExpression(selector: string, text: string, clear?: boolean): string;
|
|
14
|
+
export declare function arcScrollExpression(deltaX: number, deltaY: number): string;
|