@phnx-labs/agents-cli 1.22.116 → 1.22.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.d.ts +18 -0
  9. package/dist/commands/exec.js +104 -12
  10. package/dist/commands/repo.js +13 -49
  11. package/dist/commands/route.js +5 -5
  12. package/dist/commands/routines.js +39 -12
  13. package/dist/commands/sessions-render.d.ts +1 -1
  14. package/dist/commands/sessions-render.js +0 -1
  15. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  16. package/dist/commands/sessions.test-fixture.js +0 -37
  17. package/dist/commands/setup-browser.d.ts +17 -12
  18. package/dist/commands/setup-browser.js +52 -87
  19. package/dist/commands/setup-preferences.d.ts +0 -17
  20. package/dist/commands/setup-preferences.js +18 -49
  21. package/dist/commands/setup-term.d.ts +4 -3
  22. package/dist/commands/setup-term.js +5 -4
  23. package/dist/commands/setup.js +11 -11
  24. package/dist/commands/ssh.js +10 -1
  25. package/dist/commands/sync.js +6 -44
  26. package/dist/commands/teams.js +0 -1
  27. package/dist/commands/update.d.ts +19 -0
  28. package/dist/commands/update.js +34 -6
  29. package/dist/commands/view.d.ts +12 -1
  30. package/dist/commands/view.js +47 -2
  31. package/dist/index.js +107 -26
  32. package/dist/lib/account-capabilities.js +0 -2
  33. package/dist/lib/account-catalog.d.ts +38 -0
  34. package/dist/lib/account-catalog.js +110 -26
  35. package/dist/lib/account-provider-registry.js +1 -1
  36. package/dist/lib/account-registry.d.ts +1 -1
  37. package/dist/lib/account-registry.js +36 -2
  38. package/dist/lib/accounting/rotate.js +22 -8
  39. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  40. package/dist/lib/accounting/usage-ingest.js +115 -45
  41. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  42. package/dist/lib/accounting/usage-sync.js +227 -66
  43. package/dist/lib/accounting/usage.d.ts +16 -3
  44. package/dist/lib/accounting/usage.js +26 -8
  45. package/dist/lib/accounts/slots.d.ts +8 -0
  46. package/dist/lib/accounts/slots.js +17 -0
  47. package/dist/lib/add-dir.js +0 -1
  48. package/dist/lib/agent-cli-commands.js +0 -1
  49. package/dist/lib/agent-spec/agents.d.ts +6 -7
  50. package/dist/lib/agent-spec/agents.js +6 -60
  51. package/dist/lib/auth-health.d.ts +69 -1
  52. package/dist/lib/auth-health.js +133 -4
  53. package/dist/lib/browser/context.d.ts +82 -0
  54. package/dist/lib/browser/context.js +88 -0
  55. package/dist/lib/browser/paths.d.ts +25 -0
  56. package/dist/lib/browser/paths.js +51 -0
  57. package/dist/lib/browser/record.d.ts +38 -0
  58. package/dist/lib/browser/record.js +57 -0
  59. package/dist/lib/browser/sessions-list.js +1 -2
  60. package/dist/lib/browser-client.d.ts +158 -0
  61. package/dist/lib/browser-client.js +234 -0
  62. package/dist/lib/channels/owner-sink.d.ts +1 -18
  63. package/dist/lib/channels/owner-sink.js +29 -82
  64. package/dist/lib/channels/providers/rush.d.ts +7 -7
  65. package/dist/lib/channels/providers/rush.js +127 -57
  66. package/dist/lib/claude-statusline.d.ts +1 -1
  67. package/dist/lib/claude-statusline.js +15 -3
  68. package/dist/lib/commands.js +1 -1
  69. package/dist/lib/crabbox/lease.js +1 -1
  70. package/dist/lib/crabbox/runtimes.js +1 -3
  71. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  72. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  73. package/dist/lib/daemon/auth-sync-service.js +37 -43
  74. package/dist/lib/daemon/daemon.d.ts +25 -14
  75. package/dist/lib/daemon/daemon.js +92 -154
  76. package/dist/lib/daemon/runner.d.ts +2 -2
  77. package/dist/lib/daemon/runner.js +32 -9
  78. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  79. package/dist/lib/daemon/self-update-service.js +40 -36
  80. package/dist/lib/daemon/service.d.ts +12 -8
  81. package/dist/lib/daemon/supervisor.d.ts +65 -54
  82. package/dist/lib/daemon/supervisor.js +136 -171
  83. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  84. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  85. package/dist/lib/daemon/usage-sync-service.js +46 -78
  86. package/dist/lib/daemon-health.d.ts +22 -5
  87. package/dist/lib/daemon-health.js +47 -3
  88. package/dist/lib/daemon-services.d.ts +1 -1
  89. package/dist/lib/daemon-services.js +0 -10
  90. package/dist/lib/daemon-ticks.js +4 -2
  91. package/dist/lib/devices/doctor-findings.js +8 -11
  92. package/dist/lib/devices/harness-inventory.js +14 -5
  93. package/dist/lib/exec-account-home.js +5 -0
  94. package/dist/lib/exec.js +0 -11
  95. package/dist/lib/feed/events.js +0 -1
  96. package/dist/lib/feed/tool-activity.js +5 -2
  97. package/dist/lib/fleet-shared-state.d.ts +45 -0
  98. package/dist/lib/fleet-shared-state.js +78 -9
  99. package/dist/lib/fs-atomic.js +25 -0
  100. package/dist/lib/harness-auth-capabilities.js +0 -1
  101. package/dist/lib/helper-versions.js +1 -1
  102. package/dist/lib/hooks/cache.d.ts +7 -0
  103. package/dist/lib/hooks/cache.js +27 -2
  104. package/dist/lib/hooks/install.js +7 -1
  105. package/dist/lib/hosts/ready.d.ts +38 -8
  106. package/dist/lib/hosts/ready.js +62 -14
  107. package/dist/lib/hosts/remote-cmd.js +1 -0
  108. package/dist/lib/installations/active-check.d.ts +27 -0
  109. package/dist/lib/installations/active-check.js +61 -0
  110. package/dist/lib/installations/migrate.js +6 -3
  111. package/dist/lib/installations/shims.js +9 -7
  112. package/dist/lib/installations/update.js +21 -15
  113. package/dist/lib/installations/versions.js +12 -1
  114. package/dist/lib/manifest.js +1 -1
  115. package/dist/lib/models.js +0 -2
  116. package/dist/lib/open-url.js +28 -54
  117. package/dist/lib/placement.d.ts +2 -0
  118. package/dist/lib/placement.js +5 -0
  119. package/dist/lib/profiles.js +0 -2
  120. package/dist/lib/refresh.js +1 -1
  121. package/dist/lib/routine-readiness.d.ts +35 -0
  122. package/dist/lib/routine-readiness.js +74 -16
  123. package/dist/lib/rules/compile.js +6 -0
  124. package/dist/lib/sandbox.js +0 -2
  125. package/dist/lib/secrets-policy.d.ts +89 -11
  126. package/dist/lib/secrets-policy.js +175 -28
  127. package/dist/lib/session/db.d.ts +1 -1
  128. package/dist/lib/session/db.js +65 -18
  129. package/dist/lib/session/discover.js +1 -283
  130. package/dist/lib/session/mirror.d.ts +4 -3
  131. package/dist/lib/session/mirror.js +4 -3
  132. package/dist/lib/session/parse.d.ts +0 -2
  133. package/dist/lib/session/parse.js +1 -138
  134. package/dist/lib/session/throughput.d.ts +1 -3
  135. package/dist/lib/session/throughput.js +0 -20
  136. package/dist/lib/session/types.d.ts +2 -2
  137. package/dist/lib/session/types.js +2 -2
  138. package/dist/lib/sessions-client.d.ts +60 -1
  139. package/dist/lib/sessions-client.js +227 -4
  140. package/dist/lib/setup-tool-status.js +8 -12
  141. package/dist/lib/signin-badge.d.ts +3 -2
  142. package/dist/lib/signin-badge.js +6 -3
  143. package/dist/lib/smart-launch.js +4 -1
  144. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  147. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  148. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  149. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  150. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  151. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  152. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  153. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  154. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  155. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  156. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  157. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  158. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  159. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  160. package/dist/lib/staleness/writers/sources.js +5 -0
  161. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  162. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  163. package/dist/lib/state.d.ts +6 -4
  164. package/dist/lib/state.js +7 -17
  165. package/dist/lib/teams/parsers.d.ts +1 -1
  166. package/dist/lib/teams/parsers.js +1 -136
  167. package/dist/lib/teams/placement-probe.js +50 -15
  168. package/dist/lib/teams/scheduler.d.ts +7 -0
  169. package/dist/lib/types.d.ts +10 -1
  170. package/dist/lib/types.js +1 -1
  171. package/dist/lib/view-types.d.ts +32 -0
  172. package/dist/lib/watchdog/read.js +0 -1
  173. package/dist/lib/watchdog/rotate.js +1 -2
  174. package/dist/lib/watchdog/runner.js +2 -2
  175. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  176. package/dist/lib/watchdog/watchdogTail.js +0 -31
  177. package/package.json +1 -1
  178. package/scripts/postinstall.js +8 -8
  179. package/dist/browser.d.ts +0 -2
  180. package/dist/browser.js +0 -17
  181. package/dist/commands/browser-picker.d.ts +0 -18
  182. package/dist/commands/browser-picker.js +0 -97
  183. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  184. package/dist/lib/browser/arc-discovery.js +0 -197
  185. package/dist/lib/browser/arc-dom.d.ts +0 -14
  186. package/dist/lib/browser/arc-dom.js +0 -121
  187. package/dist/lib/browser/caller-identity.d.ts +0 -40
  188. package/dist/lib/browser/caller-identity.js +0 -175
  189. package/dist/lib/browser/cdp.d.ts +0 -51
  190. package/dist/lib/browser/cdp.js +0 -254
  191. package/dist/lib/browser/chrome.d.ts +0 -141
  192. package/dist/lib/browser/chrome.js +0 -704
  193. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  194. package/dist/lib/browser/chromium-discovery.js +0 -96
  195. package/dist/lib/browser/devices.d.ts +0 -34
  196. package/dist/lib/browser/devices.js +0 -61
  197. package/dist/lib/browser/domain-skills.d.ts +0 -71
  198. package/dist/lib/browser/domain-skills.js +0 -195
  199. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  200. package/dist/lib/browser/drivers/arc.js +0 -276
  201. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  202. package/dist/lib/browser/drivers/firefox.js +0 -377
  203. package/dist/lib/browser/drivers/local.d.ts +0 -55
  204. package/dist/lib/browser/drivers/local.js +0 -266
  205. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  206. package/dist/lib/browser/drivers/ssh.js +0 -467
  207. package/dist/lib/browser/editor.d.ts +0 -3
  208. package/dist/lib/browser/editor.js +0 -50
  209. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  210. package/dist/lib/browser/ffmpeg.js +0 -184
  211. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  212. package/dist/lib/browser/firefox-discovery.js +0 -162
  213. package/dist/lib/browser/har.d.ts +0 -85
  214. package/dist/lib/browser/har.js +0 -77
  215. package/dist/lib/browser/hygiene.d.ts +0 -97
  216. package/dist/lib/browser/hygiene.js +0 -153
  217. package/dist/lib/browser/index.d.ts +0 -5
  218. package/dist/lib/browser/index.js +0 -5
  219. package/dist/lib/browser/input.d.ts +0 -7
  220. package/dist/lib/browser/input.js +0 -92
  221. package/dist/lib/browser/ipc.d.ts +0 -204
  222. package/dist/lib/browser/ipc.js +0 -1453
  223. package/dist/lib/browser/login-detection.d.ts +0 -87
  224. package/dist/lib/browser/login-detection.js +0 -274
  225. package/dist/lib/browser/profiles.d.ts +0 -422
  226. package/dist/lib/browser/profiles.js +0 -1157
  227. package/dist/lib/browser/refs.d.ts +0 -89
  228. package/dist/lib/browser/refs.js +0 -191
  229. package/dist/lib/browser/registry.d.ts +0 -70
  230. package/dist/lib/browser/registry.js +0 -252
  231. package/dist/lib/browser/remote-control.d.ts +0 -59
  232. package/dist/lib/browser/remote-control.js +0 -85
  233. package/dist/lib/browser/resolve-target.d.ts +0 -66
  234. package/dist/lib/browser/resolve-target.js +0 -264
  235. package/dist/lib/browser/runtime-state.d.ts +0 -291
  236. package/dist/lib/browser/runtime-state.js +0 -584
  237. package/dist/lib/browser/secret-ref.d.ts +0 -10
  238. package/dist/lib/browser/secret-ref.js +0 -14
  239. package/dist/lib/browser/service.d.ts +0 -812
  240. package/dist/lib/browser/service.js +0 -4693
  241. package/dist/lib/browser/stream.d.ts +0 -17
  242. package/dist/lib/browser/stream.js +0 -72
  243. package/dist/lib/browser/task-index.d.ts +0 -78
  244. package/dist/lib/browser/task-index.js +0 -199
  245. package/dist/lib/browser/types.d.ts +0 -647
  246. package/dist/lib/browser/types.js +0 -61
  247. package/dist/lib/browser/upload.d.ts +0 -29
  248. package/dist/lib/browser/upload.js +0 -298
  249. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  250. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  251. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  252. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  253. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  254. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -0,0 +1,51 @@
1
+ /**
2
+ * paths.ts — the on-disk browser layout the CONSUMER still reads.
3
+ *
4
+ * browser-cli owns the browser process, drivers and IPC, but it keeps every
5
+ * on-disk path the in-repo subsystem used (integration contract §4). So
6
+ * `agents browser sessions` (`sessions-list.ts`) and the feed's browser tool
7
+ * rows (`feed/tool-activity.ts`) read a profile's captures straight off disk —
8
+ * no daemon, no engine — and need just these path helpers. They were the pure
9
+ * path corner of the deleted `profiles.ts` / `runtime-state.ts` engine files.
10
+ *
11
+ * A profile's live tasks and captures may live under a COMPOSITE runtime dir
12
+ * (`<profile>@<endpoint>`, `<profile>@<endpoint>.<fork>`), not the bare
13
+ * `<profile>` dir, so `listProfileCacheDirs` matches every dir that belongs to a
14
+ * profile — the same last-`@` rule browser-cli names them with.
15
+ */
16
+ import * as path from 'node:path';
17
+ import * as fs from 'node:fs';
18
+ import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot } from '../state.js';
19
+ export { getBrowserDurableDir } from '../state.js';
20
+ /** Root of the browser runtime cache — `~/.agents/.cache/browser/`. */
21
+ export function getBrowserRuntimeDir() {
22
+ return getBrowserRuntimeDirRoot();
23
+ }
24
+ /** The runtime cache dir for one profile's bare name. */
25
+ export function getProfileRuntimeDir(name) {
26
+ return path.join(getBrowserRuntimeDir(), name);
27
+ }
28
+ /**
29
+ * The profile a runtime-cache dir key belongs to. browser-cli keys a dir as
30
+ * `<profile>`, `<profile>@<endpoint>`, or `<profile>@<endpoint>.<fork>`. The
31
+ * profile name may itself contain `@` (`me@work`), so split on the LAST one.
32
+ */
33
+ function profileOfCacheKey(key) {
34
+ const at = key.lastIndexOf('@');
35
+ return at === -1 ? key : key.slice(0, at);
36
+ }
37
+ /**
38
+ * Every runtime-cache dir that belongs to a profile, composite forms included.
39
+ * Empty when the root does not exist yet.
40
+ */
41
+ export function listProfileCacheDirs(profileName) {
42
+ const root = getBrowserRuntimeDir();
43
+ if (!fs.existsSync(root))
44
+ return [];
45
+ const matches = [];
46
+ for (const entry of fs.readdirSync(root)) {
47
+ if (profileOfCacheKey(entry) === profileName)
48
+ matches.push(path.join(root, entry));
49
+ }
50
+ return matches;
51
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * record.ts — turn the engine's NDJSON action events (fd 4) into the durable
3
+ * `browser_sessions` row that `agents browser sessions` and `agents sessions
4
+ * --browser` read to link a browser task back to the agent session that drove it.
5
+ *
6
+ * WHY THIS STAYS HERE. `sessions.db` is agents-cli state. Handing the standalone
7
+ * engine a writer for it would have made it a second author of the session index
8
+ * — precisely the "one engine, one executor" rule the repo holds elsewhere.
9
+ * Instead the engine reports what it did on fd 4 and agents-cli, which owns the
10
+ * store, records it.
11
+ *
12
+ * Before PHNX-4101 this upsert ran INSIDE the browser daemon at task start
13
+ * (`BrowserService.start` → `recordBrowserSession`), with the identity resolved
14
+ * in the calling CLI and forwarded over IPC. The engine now streams that identity
15
+ * back on fd 4 instead, and this CLI process — the one the user actually ran, so
16
+ * the actor is right — performs the upsert. The behavior is unchanged; only the
17
+ * trigger moved from an IPC call to a line on a pipe.
18
+ *
19
+ * The feed's browser tool rows are projected from the on-disk captures and the
20
+ * live `tasks.json` (`lib/feed/tool-activity.ts`), not from a per-action event
21
+ * ledger, so — unlike `lib/computer/record.ts` — this emits no feed event: the
22
+ * durable row is the whole job, and every consumer already reads it.
23
+ */
24
+ import type { BrowserActionEvent } from '../browser-client.js';
25
+ /**
26
+ * Record one action the engine performed. Never throws: the action already
27
+ * happened and already reported its own success or failure on the engine's
28
+ * stderr, so a bookkeeping failure must not turn a successful navigate into a
29
+ * failed command.
30
+ *
31
+ * Only an event that names a `task` AND a `profile` can be recorded — the
32
+ * `browser_sessions` row is keyed on `(profile, task)`. A task-less verb
33
+ * (`profiles list`, `status`) carries neither and is skipped, exactly as the old
34
+ * inline recorder only ran for task-scoped verbs.
35
+ */
36
+ export declare function recordBrowserAction(event: BrowserActionEvent, opts?: {
37
+ device?: string;
38
+ }): void;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * record.ts — turn the engine's NDJSON action events (fd 4) into the durable
3
+ * `browser_sessions` row that `agents browser sessions` and `agents sessions
4
+ * --browser` read to link a browser task back to the agent session that drove it.
5
+ *
6
+ * WHY THIS STAYS HERE. `sessions.db` is agents-cli state. Handing the standalone
7
+ * engine a writer for it would have made it a second author of the session index
8
+ * — precisely the "one engine, one executor" rule the repo holds elsewhere.
9
+ * Instead the engine reports what it did on fd 4 and agents-cli, which owns the
10
+ * store, records it.
11
+ *
12
+ * Before PHNX-4101 this upsert ran INSIDE the browser daemon at task start
13
+ * (`BrowserService.start` → `recordBrowserSession`), with the identity resolved
14
+ * in the calling CLI and forwarded over IPC. The engine now streams that identity
15
+ * back on fd 4 instead, and this CLI process — the one the user actually ran, so
16
+ * the actor is right — performs the upsert. The behavior is unchanged; only the
17
+ * trigger moved from an IPC call to a line on a pipe.
18
+ *
19
+ * The feed's browser tool rows are projected from the on-disk captures and the
20
+ * live `tasks.json` (`lib/feed/tool-activity.ts`), not from a per-action event
21
+ * ledger, so — unlike `lib/computer/record.ts` — this emits no feed event: the
22
+ * durable row is the whole job, and every consumer already reads it.
23
+ */
24
+ import { recordBrowserSession } from '../session/db.js';
25
+ import { resolveActor } from '../actor.js';
26
+ /**
27
+ * Record one action the engine performed. Never throws: the action already
28
+ * happened and already reported its own success or failure on the engine's
29
+ * stderr, so a bookkeeping failure must not turn a successful navigate into a
30
+ * failed command.
31
+ *
32
+ * Only an event that names a `task` AND a `profile` can be recorded — the
33
+ * `browser_sessions` row is keyed on `(profile, task)`. A task-less verb
34
+ * (`profiles list`, `status`) carries neither and is skipped, exactly as the old
35
+ * inline recorder only ran for task-scoped verbs.
36
+ */
37
+ export function recordBrowserAction(event, opts = {}) {
38
+ if (typeof event.task !== 'string' || typeof event.profile !== 'string')
39
+ return;
40
+ // The driven machine. `host` is what a `--device` invocation stamps; `opts.device`
41
+ // is the fallback for an engine that drove the device this CLI resolved but did
42
+ // not echo it back. Both absent → a local run, recorded under this machine.
43
+ const machine = event.host ?? opts.device;
44
+ try {
45
+ recordBrowserSession({
46
+ task: event.task,
47
+ profile: event.profile,
48
+ sessionId: event.sessionId ?? process.env.AGENT_SESSION_ID ?? process.env.AGENTS_SESSION_ID,
49
+ launchId: event.launchId ?? process.env.AGENT_LAUNCH_ID,
50
+ actor: event.actor ?? resolveActor().id,
51
+ ...(machine ? { machine } : {}),
52
+ });
53
+ }
54
+ catch {
55
+ // Recording is best-effort; the action is already done.
56
+ }
57
+ }
@@ -20,8 +20,7 @@ import { formatBytes } from '../format.js';
20
20
  // Re-exported for the picker, which lists artifacts from this module's rows.
21
21
  export { formatBytes };
22
22
  import * as path from 'path';
23
- import { getBrowserRuntimeDir, getProfileRuntimeDir } from './profiles.js';
24
- import { listProfileCacheDirs } from './runtime-state.js';
23
+ import { getBrowserRuntimeDir, getProfileRuntimeDir, listProfileCacheDirs } from './paths.js';
25
24
  import { formatRelativeTime } from '../session/relative-time.js';
26
25
  import { getSessionById, listBrowserSessionRecords, pruneToolSessions } from '../session/db.js';
27
26
  import { listPidSessionEntries } from '../session/pid-registry.js';
@@ -0,0 +1,158 @@
1
+ /**
2
+ * browser-client.ts — the ONE process client through which agents-cli talks to
3
+ * the standalone `browser` CLI (@phnx-labs/browser-cli, PHNX-4101).
4
+ *
5
+ * This is the agents-owned half of the browser extraction, and it is
6
+ * deliberately small. agents-cli no longer carries the CDP/BiDi/Arc drivers, a
7
+ * browser IPC service, a chrome-data/profile store, a network-capture pipeline,
8
+ * or the remote SSH driving loop — the standalone engine owns all of it, exactly
9
+ * as `secrets` took the keychain engine (PHNX-3989), `computer` took the
10
+ * accessibility engine (PHNX-4075) and `sessions` took the transcript engine
11
+ * (PHNX-4012). What stays here is what only the fleet CLI can know: which device
12
+ * a `--device` name resolves to, whether this machine consents to being driven,
13
+ * who the acting session is, and where an action must be recorded.
14
+ *
15
+ * THERE IS NO FALLBACK. A missing executable throws `BROWSER_BIN_MISSING` with
16
+ * install guidance (DIST-1) rather than silently driving a bundled engine —
17
+ * agents-cli has none to drive, and a fallback would re-couple the two release
18
+ * trains this extraction exists to separate.
19
+ *
20
+ * Transport — inherited-fd passthrough, not request/response (matches
21
+ * browser-cli's `bin/browser.cjs` launcher, which forwards fds 3-16):
22
+ *
23
+ * - stdio 0/1/2 are INHERITED. The engine owns the user's terminal: its
24
+ * stdout is the command's stdout, its `--json` is the command's `--json`,
25
+ * its prompts reach a real tty. agents-cli never re-formats engine output,
26
+ * which is what keeps the surface honest as the engine evolves.
27
+ * - fd 3 (`BROWSER_CONTEXT_FD`) carries ONE JSON object — the consumer context
28
+ * built by `lib/browser/context.ts` — written and closed immediately, so the
29
+ * engine reads to EOF and proceeds. The document is read once, before command
30
+ * parsing; a malformed one is a hard error, an absent one the standalone case.
31
+ * - fd 4 (`BROWSER_EVENTS_FD`) carries NDJSON action events back: one JSON
32
+ * object per line, each an action the engine actually performed. agents-cli
33
+ * turns those into feed events and `sessions --browser` history
34
+ * (`lib/browser/record.ts`). The engine may emit none; it must never block
35
+ * on this pipe.
36
+ *
37
+ * Both fds are anonymous pipes on the child's side, the same shape
38
+ * `computer-client.ts` and `secrets-client.ts` settled on after a named FIFO
39
+ * wedged macOS reads. The context is pushed rather than pulled so the engine
40
+ * needs no callback into agents-cli — one direction each way, no reentrancy.
41
+ */
42
+ /** fd the engine reads its one-shot JSON context from. */
43
+ export declare const BROWSER_CONTEXT_FD = 3;
44
+ /** fd the engine writes NDJSON action events to. */
45
+ export declare const BROWSER_EVENTS_FD = 4;
46
+ export declare class BrowserClientError extends Error {
47
+ code: string;
48
+ constructor(code: string, message: string);
49
+ }
50
+ export declare function isBrowserClientError(err: unknown): err is BrowserClientError;
51
+ /**
52
+ * Accept only the real standalone `browser` executable, never agents-cli's own
53
+ * `browser` command shim (`exec "$AGENTS_BIN" browser`, `installations/shims.ts`)
54
+ * — resolving that would recurse into this process. `findInPath` already skips
55
+ * `~/.agents/.cache/shims`; this guard is the second line, mirroring
56
+ * `isStandaloneComputer` (agents-cli never shipped a `dist/browser.js` bin, but
57
+ * the check stays symmetric and future-proof against one).
58
+ */
59
+ export declare function isStandaloneBrowser(bin: string): boolean;
60
+ /**
61
+ * Resolve the standalone executable. `BROWSER_BIN` wins so a dev build can be
62
+ * driven without touching PATH.
63
+ *
64
+ * Resolution uses `findInPath`, which skips `~/.agents/.cache/shims` — see
65
+ * `isStandaloneBrowser` for why that skip is load-bearing.
66
+ */
67
+ export declare function resolveBrowserBin(): string;
68
+ /** True when the standalone `browser` CLI is resolvable — the readiness signal
69
+ * for `agents setup` without a spawn. Never throws. */
70
+ export declare function browserInstalled(): boolean;
71
+ /** A `.js`/`.cjs`/`.mjs` bin is run through this runtime; a real executable is exec'd directly. */
72
+ export declare function invocation(bin: string): {
73
+ command: string;
74
+ prefix: string[];
75
+ };
76
+ /**
77
+ * One action the engine performed, as it appears on the NDJSON events fd.
78
+ *
79
+ * This is the engine's wire shape (browser-cli integration contract §2), not a
80
+ * translation of it: the engine emits `{event: "browser.action", ts, command,
81
+ * invocationId, pid, task, profile, url, host, sessionId, launchId, actor}`.
82
+ * `command` — not `verb` — is the field that names the action, and it is what
83
+ * marks a line as an action event.
84
+ */
85
+ export interface BrowserActionEvent {
86
+ /** Always `browser.action` on this stream. */
87
+ event?: string;
88
+ /** ISO timestamp the engine stamped. */
89
+ ts?: string;
90
+ /** The verb the engine ran (`navigate`, `screenshot`, `click`, …). */
91
+ command: string;
92
+ /** The engine's own id for this run — the grouping key for a session row. */
93
+ invocationId?: string;
94
+ /** The engine process's pid. */
95
+ pid?: number;
96
+ /** The browser task the action targeted. */
97
+ task?: string;
98
+ /** The browser profile the task runs under. */
99
+ profile?: string;
100
+ /** The page URL at the time of the action, when the engine reported one. */
101
+ url?: string;
102
+ /** The driven device for a `--device` invocation; absent when local. */
103
+ host?: string;
104
+ /** Identity, echoed back from the context this CLI handed the engine. */
105
+ sessionId?: string;
106
+ launchId?: string;
107
+ actor?: string;
108
+ /** Free-form detail the engine attaches. */
109
+ [key: string]: unknown;
110
+ }
111
+ /**
112
+ * Split a growing buffer into complete NDJSON lines. Pure so the framing rules —
113
+ * blank lines skipped, a non-JSON line dropped rather than crashing the CLI, a
114
+ * trailing partial line carried forward — are unit-testable without a spawn.
115
+ *
116
+ * A malformed line is dropped, not thrown: these events are telemetry riding
117
+ * alongside a user-visible action that already happened. Failing the command
118
+ * because its receipt was unreadable would be strictly worse than losing the
119
+ * receipt. The action itself already failed loud on its own channel if it failed.
120
+ */
121
+ export declare function parseEventLines(buffer: string): {
122
+ events: BrowserActionEvent[];
123
+ rest: string;
124
+ };
125
+ interface RunBrowserOptions {
126
+ /** argv handed to the standalone, after the program name. */
127
+ argv: string[];
128
+ /** The consumer context serialized onto fd 3. */
129
+ context: unknown;
130
+ /** Called once per action event the engine reports on fd 4. */
131
+ onEvent?: (event: BrowserActionEvent) => void;
132
+ /**
133
+ * Capture the engine's stdout instead of inheriting the terminal.
134
+ *
135
+ * Used only where agents-cli must READ an answer rather than show it (the
136
+ * setup readiness probe polling `status --json`). Verbs never capture:
137
+ * re-printing engine output would make agents-cli a formatter for a surface
138
+ * it no longer owns.
139
+ */
140
+ capture?: boolean;
141
+ }
142
+ interface RunBrowserResult {
143
+ exitCode: number;
144
+ /** Engine stdout, only when `capture` was set. */
145
+ stdout: string;
146
+ }
147
+ /**
148
+ * Run the standalone engine with the consumer context on fd 3 and the action
149
+ * event stream on fd 4. Resolves with the engine's exit code; the caller
150
+ * propagates it so `agents browser` exits exactly as the engine did.
151
+ *
152
+ * Throws `BROWSER_BIN_MISSING` when the standalone is not installed. Every other
153
+ * failure is the engine's own, reported on the inherited stderr.
154
+ */
155
+ export declare function runBrowser(opts: RunBrowserOptions): Promise<RunBrowserResult>;
156
+ /** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
157
+ export declare function _resetBrowserClientForTest(): void;
158
+ export {};
@@ -0,0 +1,234 @@
1
+ /**
2
+ * browser-client.ts — the ONE process client through which agents-cli talks to
3
+ * the standalone `browser` CLI (@phnx-labs/browser-cli, PHNX-4101).
4
+ *
5
+ * This is the agents-owned half of the browser extraction, and it is
6
+ * deliberately small. agents-cli no longer carries the CDP/BiDi/Arc drivers, a
7
+ * browser IPC service, a chrome-data/profile store, a network-capture pipeline,
8
+ * or the remote SSH driving loop — the standalone engine owns all of it, exactly
9
+ * as `secrets` took the keychain engine (PHNX-3989), `computer` took the
10
+ * accessibility engine (PHNX-4075) and `sessions` took the transcript engine
11
+ * (PHNX-4012). What stays here is what only the fleet CLI can know: which device
12
+ * a `--device` name resolves to, whether this machine consents to being driven,
13
+ * who the acting session is, and where an action must be recorded.
14
+ *
15
+ * THERE IS NO FALLBACK. A missing executable throws `BROWSER_BIN_MISSING` with
16
+ * install guidance (DIST-1) rather than silently driving a bundled engine —
17
+ * agents-cli has none to drive, and a fallback would re-couple the two release
18
+ * trains this extraction exists to separate.
19
+ *
20
+ * Transport — inherited-fd passthrough, not request/response (matches
21
+ * browser-cli's `bin/browser.cjs` launcher, which forwards fds 3-16):
22
+ *
23
+ * - stdio 0/1/2 are INHERITED. The engine owns the user's terminal: its
24
+ * stdout is the command's stdout, its `--json` is the command's `--json`,
25
+ * its prompts reach a real tty. agents-cli never re-formats engine output,
26
+ * which is what keeps the surface honest as the engine evolves.
27
+ * - fd 3 (`BROWSER_CONTEXT_FD`) carries ONE JSON object — the consumer context
28
+ * built by `lib/browser/context.ts` — written and closed immediately, so the
29
+ * engine reads to EOF and proceeds. The document is read once, before command
30
+ * parsing; a malformed one is a hard error, an absent one the standalone case.
31
+ * - fd 4 (`BROWSER_EVENTS_FD`) carries NDJSON action events back: one JSON
32
+ * object per line, each an action the engine actually performed. agents-cli
33
+ * turns those into feed events and `sessions --browser` history
34
+ * (`lib/browser/record.ts`). The engine may emit none; it must never block
35
+ * on this pipe.
36
+ *
37
+ * Both fds are anonymous pipes on the child's side, the same shape
38
+ * `computer-client.ts` and `secrets-client.ts` settled on after a named FIFO
39
+ * wedged macOS reads. The context is pushed rather than pulled so the engine
40
+ * needs no callback into agents-cli — one direction each way, no reentrancy.
41
+ */
42
+ import { spawn } from 'node:child_process';
43
+ import { realpathSync, existsSync } from 'node:fs';
44
+ import * as path from 'node:path';
45
+ import { findInPath } from './agent-spec/agents.js';
46
+ const INSTALL_HINT = 'npm i -g @phnx-labs/browser-cli';
47
+ /** fd the engine reads its one-shot JSON context from. */
48
+ export const BROWSER_CONTEXT_FD = 3;
49
+ /** fd the engine writes NDJSON action events to. */
50
+ export const BROWSER_EVENTS_FD = 4;
51
+ export class BrowserClientError extends Error {
52
+ code;
53
+ constructor(code, message) {
54
+ super(message);
55
+ this.code = code;
56
+ this.name = 'BrowserClientError';
57
+ }
58
+ }
59
+ export function isBrowserClientError(err) {
60
+ return err instanceof BrowserClientError;
61
+ }
62
+ let cachedBin;
63
+ function browserEntrypoint(bin) {
64
+ if (!/\.(cmd|ps1)$/i.test(bin))
65
+ return bin;
66
+ const launcher = path.join(path.dirname(bin), 'node_modules', '@phnx-labs', 'browser-cli', 'bin', 'browser.cjs');
67
+ return existsSync(launcher) ? launcher : bin;
68
+ }
69
+ /**
70
+ * Accept only the real standalone `browser` executable, never agents-cli's own
71
+ * `browser` command shim (`exec "$AGENTS_BIN" browser`, `installations/shims.ts`)
72
+ * — resolving that would recurse into this process. `findInPath` already skips
73
+ * `~/.agents/.cache/shims`; this guard is the second line, mirroring
74
+ * `isStandaloneComputer` (agents-cli never shipped a `dist/browser.js` bin, but
75
+ * the check stays symmetric and future-proof against one).
76
+ */
77
+ export function isStandaloneBrowser(bin) {
78
+ let real = browserEntrypoint(bin);
79
+ try {
80
+ real = realpathSync(real);
81
+ }
82
+ catch { /* spawn reports missing explicit paths */ }
83
+ return !/\.(cmd|ps1)$/i.test(real) && !real.endsWith(path.join('dist', 'browser.js'));
84
+ }
85
+ /**
86
+ * Resolve the standalone executable. `BROWSER_BIN` wins so a dev build can be
87
+ * driven without touching PATH.
88
+ *
89
+ * Resolution uses `findInPath`, which skips `~/.agents/.cache/shims` — see
90
+ * `isStandaloneBrowser` for why that skip is load-bearing.
91
+ */
92
+ export function resolveBrowserBin() {
93
+ if (cachedBin)
94
+ return cachedBin;
95
+ const explicit = process.env.BROWSER_BIN?.trim();
96
+ const resolved = explicit && explicit.length > 0 ? (isStandaloneBrowser(explicit) ? explicit : null) : findInPath('browser', { accept: isStandaloneBrowser });
97
+ if (!resolved) {
98
+ throw new BrowserClientError('BROWSER_BIN_MISSING', 'The standalone `browser` CLI was not found. Install it with:\n' +
99
+ ` ${INSTALL_HINT}\n` +
100
+ 'or run `agents setup browser` / `agents clis install browser`, or point $BROWSER_BIN at its executable.');
101
+ }
102
+ cachedBin = browserEntrypoint(resolved);
103
+ return cachedBin;
104
+ }
105
+ /** True when the standalone `browser` CLI is resolvable — the readiness signal
106
+ * for `agents setup` without a spawn. Never throws. */
107
+ export function browserInstalled() {
108
+ try {
109
+ resolveBrowserBin();
110
+ return true;
111
+ }
112
+ catch {
113
+ return false;
114
+ }
115
+ }
116
+ /** A `.js`/`.cjs`/`.mjs` bin is run through this runtime; a real executable is exec'd directly. */
117
+ export function invocation(bin) {
118
+ if (/\.[mc]?js$/.test(bin))
119
+ return { command: process.execPath, prefix: [bin] };
120
+ return { command: bin, prefix: [] };
121
+ }
122
+ /**
123
+ * Split a growing buffer into complete NDJSON lines. Pure so the framing rules —
124
+ * blank lines skipped, a non-JSON line dropped rather than crashing the CLI, a
125
+ * trailing partial line carried forward — are unit-testable without a spawn.
126
+ *
127
+ * A malformed line is dropped, not thrown: these events are telemetry riding
128
+ * alongside a user-visible action that already happened. Failing the command
129
+ * because its receipt was unreadable would be strictly worse than losing the
130
+ * receipt. The action itself already failed loud on its own channel if it failed.
131
+ */
132
+ export function parseEventLines(buffer) {
133
+ const events = [];
134
+ const parts = buffer.split('\n');
135
+ const rest = parts.pop() ?? '';
136
+ for (const line of parts) {
137
+ const trimmed = line.trim();
138
+ if (!trimmed)
139
+ continue;
140
+ try {
141
+ const parsed = JSON.parse(trimmed);
142
+ if (parsed && typeof parsed === 'object' && typeof parsed.command === 'string') {
143
+ events.push(parsed);
144
+ }
145
+ }
146
+ catch {
147
+ // Unreadable receipt — see above.
148
+ }
149
+ }
150
+ return { events, rest };
151
+ }
152
+ /**
153
+ * Run the standalone engine with the consumer context on fd 3 and the action
154
+ * event stream on fd 4. Resolves with the engine's exit code; the caller
155
+ * propagates it so `agents browser` exits exactly as the engine did.
156
+ *
157
+ * Throws `BROWSER_BIN_MISSING` when the standalone is not installed. Every other
158
+ * failure is the engine's own, reported on the inherited stderr.
159
+ */
160
+ export async function runBrowser(opts) {
161
+ const bin = resolveBrowserBin();
162
+ const { command, prefix } = invocation(bin);
163
+ const child = spawn(command, [...prefix, ...opts.argv], {
164
+ stdio: ['inherit', opts.capture ? 'pipe' : 'inherit', 'inherit', 'pipe', 'pipe'],
165
+ env: {
166
+ ...process.env,
167
+ BROWSER_CONTEXT_FD: String(BROWSER_CONTEXT_FD),
168
+ BROWSER_EVENTS_FD: String(BROWSER_EVENTS_FD),
169
+ },
170
+ });
171
+ const contextPipe = child.stdio[BROWSER_CONTEXT_FD];
172
+ const eventsPipe = child.stdio[BROWSER_EVENTS_FD];
173
+ if (contextPipe) {
174
+ // An engine that exits before reading the context (bad argv, `--help`)
175
+ // closes fd 3 and we get EPIPE. That is a normal race, not a failure.
176
+ contextPipe.on('error', () => { });
177
+ contextPipe.end(JSON.stringify(opts.context) + '\n');
178
+ }
179
+ let stdout = '';
180
+ if (opts.capture && child.stdout) {
181
+ child.stdout.setEncoding('utf-8');
182
+ child.stdout.on('data', (chunk) => {
183
+ stdout += chunk;
184
+ });
185
+ }
186
+ let pending = '';
187
+ const drained = new Promise((resolve) => {
188
+ if (!eventsPipe)
189
+ return resolve();
190
+ eventsPipe.setEncoding('utf-8');
191
+ eventsPipe.on('data', (chunk) => {
192
+ const { events, rest } = parseEventLines(pending + chunk);
193
+ pending = rest;
194
+ for (const event of events)
195
+ opts.onEvent?.(event);
196
+ });
197
+ eventsPipe.on('error', () => resolve());
198
+ eventsPipe.on('end', () => {
199
+ // A final line with no trailing newline still counts.
200
+ const { events } = parseEventLines(pending.endsWith('\n') ? pending : pending + '\n');
201
+ pending = '';
202
+ for (const event of events)
203
+ opts.onEvent?.(event);
204
+ resolve();
205
+ });
206
+ });
207
+ const exitCode = await new Promise((resolve, reject) => {
208
+ child.on('error', (err) => {
209
+ reject(new BrowserClientError('BROWSER_SPAWN_FAILED', `Could not run \`${bin}\`: ${err.message}`));
210
+ });
211
+ child.on('close', (code, signal) => {
212
+ // A signalled child has no exit code; report the conventional 128+n so
213
+ // callers and shells see a non-zero status instead of a false success.
214
+ if (code == null)
215
+ return resolve(signal ? 128 + (osSignalNumber(signal) ?? 0) : 1);
216
+ resolve(code);
217
+ });
218
+ });
219
+ await drained;
220
+ return { exitCode, stdout };
221
+ }
222
+ /** Signal name → number for the 128+n exit convention. Only the ones a tunnelled
223
+ * or interrupted engine realistically dies on; anything else contributes 0 and
224
+ * still yields a non-zero status. */
225
+ function osSignalNumber(signal) {
226
+ const table = {
227
+ SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGPIPE: 13, SIGTERM: 15,
228
+ };
229
+ return table[signal];
230
+ }
231
+ /** Test seam: drop the memoized bin so PATH fixtures can re-resolve. */
232
+ export function _resetBrowserClientForTest() {
233
+ cachedBin = undefined;
234
+ }
@@ -1,27 +1,10 @@
1
1
  import type { Meta } from '../types.js';
2
- /** Why the owner sink cannot deliver from this box. */
3
- export type OwnerSinkReason = 'rush-not-on-path' | 'rush-signed-out';
2
+ export type OwnerSinkReason = 'imessage-not-macos' | 'slack-no-token' | 'channel-unsupported';
4
3
  export interface OwnerSinkStatus {
5
- /** Owner delivery is configured for this fleet (humans.yaml / notify.owner).
6
- * When false, no finding is emitted — an un-opted-in box is not "broken". */
7
4
  configured: boolean;
8
- /** This box can actually deliver an owner notification right now. */
9
5
  reachable: boolean;
10
- /** Resolved owner channel (e.g. `imessage`). */
11
6
  channel?: string;
12
- /** Resolved transport after `notify.transports` mapping (usually === channel). */
13
7
  transport?: string;
14
- /** Set only when `reachable` is false. */
15
8
  reason?: OwnerSinkReason;
16
9
  }
17
- /**
18
- * Probe whether THIS box can deliver an owner notification right now. Returns
19
- * `configured:false` (and the caller emits no finding) when the fleet has no
20
- * owner channel configured — an un-opted-in box is not broken. When configured,
21
- * reports whether the resolved transport can actually deliver from here.
22
- *
23
- * Only the concrete rush-backed failure the lane hits is reported as unreachable;
24
- * non-rush transports (desktop / mailbox / …) deliver locally and are treated as
25
- * reachable rather than probed, so this never invents a critical it cannot back.
26
- */
27
10
  export declare function probeOwnerSink(meta: Meta): Promise<OwnerSinkStatus>;