@phnx-labs/agents-cli 1.22.92 → 1.22.94

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 (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -0
  3. package/dist/commands/browser.d.ts +1 -0
  4. package/dist/commands/browser.js +145 -11
  5. package/dist/lib/browser/arc-discovery.d.ts +60 -0
  6. package/dist/lib/browser/arc-discovery.js +187 -0
  7. package/dist/lib/browser/arc-dom.d.ts +14 -0
  8. package/dist/lib/browser/arc-dom.js +121 -0
  9. package/dist/lib/browser/drivers/arc.d.ts +57 -0
  10. package/dist/lib/browser/drivers/arc.js +281 -0
  11. package/dist/lib/browser/profiles.d.ts +5 -1
  12. package/dist/lib/browser/profiles.js +94 -9
  13. package/dist/lib/browser/refs.d.ts +2 -0
  14. package/dist/lib/browser/refs.js +2 -1
  15. package/dist/lib/browser/resolve-target.d.ts +2 -0
  16. package/dist/lib/browser/resolve-target.js +8 -0
  17. package/dist/lib/browser/runtime-state.js +9 -1
  18. package/dist/lib/browser/service.d.ts +37 -3
  19. package/dist/lib/browser/service.js +571 -41
  20. package/dist/lib/browser/types.d.ts +55 -1
  21. package/dist/lib/claude-account-token.d.ts +13 -0
  22. package/dist/lib/claude-account-token.js +29 -0
  23. package/dist/lib/daemon/attention-notify-service.d.ts +5 -2
  24. package/dist/lib/daemon/attention-notify-service.js +9 -2
  25. package/dist/lib/exec.js +11 -0
  26. package/dist/lib/feed/answer.js +9 -0
  27. package/dist/lib/feed/attention.d.ts +18 -2
  28. package/dist/lib/feed/attention.js +0 -0
  29. package/dist/lib/feed/feed.d.ts +6 -2
  30. package/dist/lib/feed/feed.js +10 -4
  31. package/dist/lib/harness/adapter.d.ts +23 -0
  32. package/dist/lib/harness/adapter.js +29 -0
  33. package/dist/lib/harness/adapters/claude.js +5 -3
  34. package/dist/lib/harness/adapters/copilot.js +4 -2
  35. package/dist/lib/harness/adapters/grok.js +4 -2
  36. package/dist/lib/harness/adapters/kimi.js +4 -2
  37. package/dist/lib/harness/adapters/muse.js +4 -3
  38. package/dist/lib/harness/adapters/opencode.js +4 -2
  39. package/dist/lib/helper-download.js +1 -1
  40. package/dist/lib/helper-versions.js +1 -1
  41. package/dist/lib/installations/shims.d.ts +2 -2
  42. package/dist/lib/installations/shims.js +16 -8
  43. package/dist/lib/menubar/download-menubar.js +4 -1
  44. package/dist/lib/menubar/install-menubar.d.ts +10 -9
  45. package/dist/lib/menubar/install-menubar.js +17 -15
  46. package/dist/lib/secrets-policy.d.ts +5 -2
  47. package/dist/lib/secrets-policy.js +18 -4
  48. package/dist/lib/session/active.d.ts +18 -11
  49. package/dist/lib/session/active.js +52 -44
  50. package/dist/lib/session/session-cache.d.ts +1 -1
  51. package/dist/lib/session/session-cache.js +1 -0
  52. package/dist/lib/session/state.d.ts +26 -0
  53. package/dist/lib/session/state.js +43 -27
  54. package/dist/lib/types.d.ts +7 -0
  55. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.94
4
+
5
+ - **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
+
7
+ - **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
+
9
+ - **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
+
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
+ - **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
+
3
15
  ## 1.22.92
4
16
 
5
17
  - **AGI Menu 1.1.0 is the helper the CLI installs (PHNX-4007).** The menu-bar
package/README.md CHANGED
@@ -1577,6 +1577,11 @@ duplicate icon. `agents menubar setup` is the recovery command when a machine is
1577
1577
  already wrong -- it ends any duplicate, installs the bundle, wires the login
1578
1578
  item, and verifies exactly one helper came back up.
1579
1579
 
1580
+ The helper itself is AGI Menu, developed in
1581
+ [phnx-labs/agi-menu](https://github.com/phnx-labs/agi-menu) and published as a
1582
+ signed, notarized build on this repo's `menubar/v<x.y.z>` releases; the CLI
1583
+ downloads and verifies it on demand, so nothing is built on your machine.
1584
+
1580
1585
  The dropdown surfaces a **NEEDS YOU** queue (agents waiting on a question, a plan review, or a permission prompt), the running roster, and a routines summary -- the same live state as `agents sessions --active`, one click away.
1581
1586
 
1582
1587
  ### Quick-issue bar (⌘⇧O)
@@ -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, persistDiscoveredArcProfile, } 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 persistDiscoveredArcProfile(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 ? 'identity' : profileKind(profile.name),
488
507
  isConfiguredDefault: profile.name === configuredDefault,
489
508
  })), null, 2));
490
509
  return;
@@ -927,6 +946,10 @@ 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
+ }
930
953
  const presets = getEndpointPresets(profile);
931
954
  const defaultName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
932
955
  ? profile.defaultEndpoint
@@ -952,6 +975,8 @@ function registerProfilesCommands(browser) {
952
975
  console.log(`Headless: true`);
953
976
  // Login state per known service: live session + account identity + whether
954
977
  // login creds are declared in the profile's secrets bundle.
978
+ if (profile.arc)
979
+ return;
955
980
  const active = await loginsForProfile(profile.name);
956
981
  const accounts = await accountsForProfile(profile.name);
957
982
  const lines = [];
@@ -975,7 +1000,7 @@ function registerProfilesCommands(browser) {
975
1000
  profiles
976
1001
  .command('remove <name>')
977
1002
  .alias('delete')
978
- .description('Remove a browser profile (drops YAML config + all cached runtime dirs)')
1003
+ .description('Remove the agents-cli profile alias and cached runtime dirs (never deletes native Arc data)')
979
1004
  .option('--keep-cache', "Leave ~/.agents/.cache/browser/<name>* dirs in place (don't wipe chrome-data)")
980
1005
  .action(async (name, opts) => {
981
1006
  await deleteProfile(name);
@@ -1094,12 +1119,14 @@ function registerProfilesCommands(browser) {
1094
1119
  profile that is in use, the configured default, or the \`auto-chrome\`
1095
1120
  profile a setup wizard created.
1096
1121
 
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:
1122
+ On a Mac with Arc, every Arc Space is listed as a profile (arc-personal,
1123
+ arc-work, ...) discovered read-only from Arc's own metadata — a Space
1124
+ already carries its Arc profile's logins, so it IS the browser profile.
1125
+ Agents attach to the running Arc through Apple Events and never launch or
1126
+ relaunch it. Point agents at Comet instead when a workflow needs screenshots,
1127
+ downloads, network capture, or trusted input:
1101
1128
  agents browser profiles create agents-comet --browser comet --attach-only
1102
- An \`--attach-only\` profile (and Arc, always) attaches to a browser you
1129
+ A CDP \`--attach-only\` profile attaches to a browser you
1103
1130
  already started with remote debugging and fails loud with the relaunch
1104
1131
  command otherwise, pinned to a durable --user-data-dir so the sign-in
1105
1132
  survives quit+relaunch. A foreign browser squatting the canonical port is
@@ -1115,6 +1142,17 @@ function registerProfilesCommands(browser) {
1115
1142
  console.error(`Profile "${name}" not found`);
1116
1143
  process.exit(1);
1117
1144
  }
1145
+ if (profile.arc && !isFleetRemoteInvocation()) {
1146
+ const routed = resolveBrowserTarget(profile.name);
1147
+ if (!routed.local && routed.commandDispatch) {
1148
+ const result = await dispatchBrowserToDevice(routed.device, ['browser', 'profiles', 'doctor', name], 'capture');
1149
+ process.stdout.write(result.stdout);
1150
+ process.stderr.write(result.stderr);
1151
+ if (result.code !== 0)
1152
+ process.exit(result.code);
1153
+ return;
1154
+ }
1155
+ }
1118
1156
  const checks = [];
1119
1157
  // 0. Declaration topology. An identity-bearing profile declared by a
1120
1158
  // different device cannot be diagnosed through a loopback CDP endpoint
@@ -1139,6 +1177,31 @@ function registerProfilesCommands(browser) {
1139
1177
  }
1140
1178
  process.exit(1);
1141
1179
  }
1180
+ if (profile.arc) {
1181
+ checks.push({
1182
+ label: 'space',
1183
+ ok: true,
1184
+ detail: `${profile.arc.spaceTitle} (${profile.arc.spaceId})`,
1185
+ });
1186
+ checks.push({
1187
+ label: 'arc-profile',
1188
+ ok: true,
1189
+ detail: `${profile.arc.profileName} (${profile.arc.profileId})`,
1190
+ });
1191
+ const running = await isArcRunning();
1192
+ checks.push({
1193
+ label: 'Arc process',
1194
+ ok: running,
1195
+ detail: running ? 'running; native automation attaches without launching or restarting Arc' : 'not running — start Arc normally',
1196
+ });
1197
+ const allOk = checks.every((check) => check.ok);
1198
+ for (const check of checks) {
1199
+ console.log(`${check.ok ? 'OK ' : 'FAIL'} ${check.label.padEnd(15)} ${check.detail}`);
1200
+ }
1201
+ if (!allOk)
1202
+ process.exit(1);
1203
+ return;
1204
+ }
1142
1205
  // 1. Binary exists for declared browser type, and is a real executable we
1143
1206
  // can drive — not a distro launcher script. findBrowserPath already
1144
1207
  // unwraps the known Chromium wrappers to their ELF; if it still hands
@@ -1358,8 +1421,41 @@ function registerTaskCommands(browser) {
1358
1421
  }
1359
1422
  if (top === 'stop' && opts.profile && !taskFlag)
1360
1423
  return;
1424
+ // A native Arc endpoint cannot be represented by an SSH-forwarded socket.
1425
+ // For a cold profile-scoped page verb, create the task on the declaring
1426
+ // owner first and bind that owner locally; the ordinary task router below
1427
+ // then forwards this complete verb and every subsequent verb to that host.
1428
+ if (!taskFlag && opts.profile) {
1429
+ try {
1430
+ const profileName = (await resolveProfileRef(opts.profile)) ?? opts.profile;
1431
+ const target = resolveBrowserTarget(profileName);
1432
+ if (!target.local && target.commandDispatch) {
1433
+ const started = await dispatchBrowserToDevice(target.device, ['browser', 'start', '--profile', profileName, '--json'], 'capture');
1434
+ if (started.code !== 0) {
1435
+ process.stdout.write(started.stdout);
1436
+ process.stderr.write(started.stderr);
1437
+ process.exit(started.code);
1438
+ }
1439
+ const task = remoteStartTaskName(started.stdout);
1440
+ if (!task)
1441
+ throw new Error(`Remote start on ${target.device} produced no task name.`);
1442
+ bindTask(task, {
1443
+ device: target.device,
1444
+ profile: profileName,
1445
+ sessionId: callerSessionId(),
1446
+ launchId: callerLaunchId(),
1447
+ createdAt: Date.now(),
1448
+ });
1449
+ inferredTaskName = task;
1450
+ }
1451
+ }
1452
+ catch (err) {
1453
+ console.error(err instanceof Error ? err.message : String(err));
1454
+ process.exit(1);
1455
+ }
1456
+ }
1361
1457
  const route = resolveTaskRoute({
1362
- task: taskFlag ?? process.env.AGENTS_BROWSER_TASK,
1458
+ task: inferredTaskName ?? taskFlag ?? process.env.AGENTS_BROWSER_TASK,
1363
1459
  sessionId: callerSessionId(),
1364
1460
  launchId: callerLaunchId(),
1365
1461
  hub: defaultBrowserHub(),
@@ -1545,7 +1641,7 @@ function registerTaskCommands(browser) {
1545
1641
  process.stderr.write(result.stderr);
1546
1642
  if (result.code !== 0)
1547
1643
  process.exit(result.code);
1548
- const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
1644
+ const taskName = remoteStartTaskName(result.stdout, opts.task);
1549
1645
  if (!taskName) {
1550
1646
  console.error(`Remote start on ${hub} produced no task name.`);
1551
1647
  process.exit(1);
@@ -1614,7 +1710,7 @@ function registerTaskCommands(browser) {
1614
1710
  process.stderr.write(result.stderr);
1615
1711
  if (result.code !== 0)
1616
1712
  process.exit(result.code);
1617
- const taskName = opts.task || result.stdout.trim().split('\n').find((line) => line.length > 0);
1713
+ const taskName = remoteStartTaskName(result.stdout, opts.task);
1618
1714
  if (!taskName) {
1619
1715
  console.error(`Remote start on ${deviceName} produced no task name.`);
1620
1716
  process.exit(1);
@@ -1668,6 +1764,44 @@ function registerTaskCommands(browser) {
1668
1764
  console.error(`Create one with: agents browser profiles create ${profileName} --browser <chrome|comet|chromium|brave|edge|arc|custom>`);
1669
1765
  process.exit(1);
1670
1766
  }
1767
+ // Discovery is read-only for list/show/doctor. Starting is the explicit
1768
+ // adoption point: persist only the agents-cli alias/native identity, never
1769
+ // mutate Arc's own profile or Space data.
1770
+ await persistDiscoveredArcProfile(profileName);
1771
+ // A native endpoint is not CDP and cannot be tunnelled. Re-exec the
1772
+ // complete command on the declaring owner, then bind its returned task
1773
+ // locally so every later verb follows the same owner from task-index.
1774
+ if (!isFleetRemoteInvocation()) {
1775
+ let routed;
1776
+ try {
1777
+ routed = resolveBrowserTarget(profileName, { endpointName: opts.endpoint });
1778
+ }
1779
+ catch (err) {
1780
+ console.error(err instanceof Error ? err.message : String(err));
1781
+ process.exit(1);
1782
+ }
1783
+ if (!routed.local && routed.commandDispatch) {
1784
+ const result = await dispatchBrowserToDevice(routed.device, browserForwardedArgv(), 'capture');
1785
+ process.stdout.write(result.stdout);
1786
+ process.stderr.write(result.stderr);
1787
+ if (result.code !== 0)
1788
+ process.exit(result.code);
1789
+ const taskName = remoteStartTaskName(result.stdout, opts.task);
1790
+ if (!taskName) {
1791
+ console.error(`Remote start on ${routed.device} produced no task name.`);
1792
+ process.exit(1);
1793
+ }
1794
+ bindTask(taskName, {
1795
+ device: routed.device,
1796
+ profile: profileName,
1797
+ url: opts.url,
1798
+ sessionId: callerSessionId(),
1799
+ launchId: callerLaunchId(),
1800
+ createdAt: Date.now(),
1801
+ });
1802
+ return;
1803
+ }
1804
+ }
1671
1805
  // Pre-check the endpoint name too — same fail-fast rationale.
1672
1806
  if (opts.endpoint) {
1673
1807
  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;