@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands/browser.d.ts +1 -0
  3. package/dist/commands/browser.js +156 -11
  4. package/dist/lib/browser/arc-discovery.d.ts +60 -0
  5. package/dist/lib/browser/arc-discovery.js +187 -0
  6. package/dist/lib/browser/arc-dom.d.ts +14 -0
  7. package/dist/lib/browser/arc-dom.js +121 -0
  8. package/dist/lib/browser/chrome.d.ts +17 -0
  9. package/dist/lib/browser/chrome.js +121 -16
  10. package/dist/lib/browser/chromium-discovery.d.ts +27 -0
  11. package/dist/lib/browser/chromium-discovery.js +96 -0
  12. package/dist/lib/browser/drivers/arc.d.ts +57 -0
  13. package/dist/lib/browser/drivers/arc.js +281 -0
  14. package/dist/lib/browser/drivers/firefox.d.ts +103 -0
  15. package/dist/lib/browser/drivers/firefox.js +377 -0
  16. package/dist/lib/browser/drivers/local.d.ts +8 -0
  17. package/dist/lib/browser/drivers/local.js +38 -3
  18. package/dist/lib/browser/firefox-discovery.d.ts +68 -0
  19. package/dist/lib/browser/firefox-discovery.js +162 -0
  20. package/dist/lib/browser/profiles.d.ts +39 -1
  21. package/dist/lib/browser/profiles.js +239 -9
  22. package/dist/lib/browser/refs.d.ts +2 -0
  23. package/dist/lib/browser/refs.js +2 -1
  24. package/dist/lib/browser/resolve-target.d.ts +2 -0
  25. package/dist/lib/browser/resolve-target.js +14 -0
  26. package/dist/lib/browser/runtime-state.js +9 -1
  27. package/dist/lib/browser/service.d.ts +86 -3
  28. package/dist/lib/browser/service.js +1045 -42
  29. package/dist/lib/browser/types.d.ts +85 -2
  30. package/dist/lib/daemon/usage-sync-service.js +12 -0
  31. package/dist/lib/open-url.js +6 -0
  32. package/dist/lib/types.d.ts +20 -1
  33. 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
@@ -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 a browser profile (drops YAML config + all cached runtime dirs)')
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
- Arc is your PERSONAL browser and Comet is what agents drive (PHNX-3967).
1098
- Arc is single-instance with no debug port, so agents can only attach to a
1099
- running Arc, never launch one — it stays yours. Point agents at Comet as
1100
- one canonical signed-in profile that never spawns a second window:
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
- An \`--attach-only\` profile (and Arc, always) attaches to a browser you
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 = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
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 = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
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;