@phnx-labs/agents-cli 1.20.34 → 1.20.36

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 (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -32,8 +32,12 @@ export function getSocketPath() {
32
32
  function getIpcEndpoint() {
33
33
  return ipcEndpoint(getSocketPath());
34
34
  }
35
- /** Can we open a connection to the daemon right now? Used on Windows where a
36
- * named pipe can't be probed with fs.existsSync. Resolves false on any error. */
35
+ /** Can we open a connection to the daemon right now? Resolves false on any
36
+ * error. This is the authoritative liveness check on every platform: a live
37
+ * daemon accepts the connection, while a stale POSIX socket file left behind by
38
+ * a crashed daemon (or one that "appears and is immediately destroyed", #556)
39
+ * rejects with ECONNREFUSED and is correctly reported as not reachable —
40
+ * something fs.existsSync can't distinguish. */
37
41
  function probeDaemon(endpoint, timeoutMs = 500) {
38
42
  return new Promise((resolve) => {
39
43
  const sock = net.createConnection(endpoint);
@@ -45,16 +49,16 @@ function probeDaemon(endpoint, timeoutMs = 500) {
45
49
  sock.on('error', () => { clearTimeout(timer); finish(false); });
46
50
  });
47
51
  }
48
- /** Is the daemon reachable? existsSync probe on POSIX, connect probe on Windows. */
49
- async function isDaemonReachable() {
50
- if (IS_WINDOWS)
51
- return probeDaemon(getIpcEndpoint());
52
- return fs.existsSync(getSocketPath());
52
+ /** Is the daemon reachable? A real connect probe on every platform a socket
53
+ * file existing on disk is not proof a daemon is listening on it. */
54
+ export async function isDaemonReachable() {
55
+ return probeDaemon(getIpcEndpoint());
53
56
  }
54
- async function waitForSocket(socketPath, timeoutMs) {
57
+ async function waitForSocket(_socketPath, timeoutMs) {
58
+ const endpoint = getIpcEndpoint();
55
59
  const deadline = Date.now() + timeoutMs;
56
60
  while (Date.now() < deadline) {
57
- if (IS_WINDOWS ? await probeDaemon(getIpcEndpoint()) : fs.existsSync(socketPath))
61
+ if (await probeDaemon(endpoint))
58
62
  return;
59
63
  await new Promise((resolve) => setTimeout(resolve, 100));
60
64
  }
@@ -284,6 +288,13 @@ export class BrowserIPCServer {
284
288
  const shot = await this.service.screenshot(request.task, request.tabId, request.path, request.quality);
285
289
  return { ok: true, path: shot.path, bytes: shot.bytes, width: shot.width, height: shot.height };
286
290
  }
291
+ case 'pdf': {
292
+ if (!request.task) {
293
+ return { ok: false, error: 'Task required' };
294
+ }
295
+ const doc = await this.service.printToPdf(request.task, request.tabId, request.path);
296
+ return { ok: true, path: doc.path, bytes: doc.bytes };
297
+ }
287
298
  case 'refs': {
288
299
  if (!request.task) {
289
300
  return { ok: false, error: 'Task required' };
@@ -301,10 +312,24 @@ export class BrowserIPCServer {
301
312
  return { ok: true, refs, nodes };
302
313
  }
303
314
  case 'click': {
304
- if (!request.task || request.ref === undefined) {
305
- return { ok: false, error: 'Task and ref required' };
315
+ if (!request.task) {
316
+ return { ok: false, error: 'Task required' };
317
+ }
318
+ // Coordinate click (`--at X,Y`) bypasses ref resolution entirely.
319
+ if (request.atX !== undefined && request.atY !== undefined) {
320
+ await this.service.clickAt(request.task, request.atX, request.atY, request.tabId);
321
+ return { ok: true };
322
+ }
323
+ if (request.ref === undefined) {
324
+ return { ok: false, error: 'Task and ref (or --at X,Y) required' };
325
+ }
326
+ const { healed } = await this.service.click(request.task, request.ref, request.tabId);
327
+ if (healed) {
328
+ return {
329
+ ok: true,
330
+ message: `self-healed ref ${healed.from} -> ${healed.to} (${healed.role} "${healed.name}")`,
331
+ };
306
332
  }
307
- await this.service.click(request.task, request.ref, request.tabId);
308
333
  return { ok: true };
309
334
  }
310
335
  case 'type': {
@@ -12,7 +12,7 @@ export declare function getProfile(name: string): Promise<BrowserProfile | null>
12
12
  * Re-uses an existing `default` profile as-is (we don't second-guess the user
13
13
  * if they've already customized it). On first run we walk the priority list
14
14
  * (macOS: chrome > brave > edge > chromium > comet; Linux: chrome > chromium >
15
- * brave > edge; Windows: edge > chrome > brave) and pin the profile to the
15
+ * brave > edge; Windows: edge > chrome > brave > comet) and pin the profile to the
16
16
  * first match. Throws an actionable error if none of those binaries are
17
17
  * installed so the user knows exactly which browsers we'd accept.
18
18
  */
@@ -1,7 +1,6 @@
1
1
  import * as path from 'path';
2
- import { execFileSync } from 'child_process';
3
2
  import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, writeMeta, } from '../state.js';
4
- import { findBrowserPath, findFirstInstalledBrowser } from './chrome.js';
3
+ import { findBrowserPath, findFirstInstalledBrowser, isPortInUse } from './chrome.js';
5
4
  import { DEFAULT_VIEWPORT } from './devices.js';
6
5
  export const DEFAULT_BROWSER_PROFILE_NAME = 'default';
7
6
  export function getBrowserRuntimeDir() {
@@ -76,7 +75,7 @@ export async function getProfile(name) {
76
75
  * Re-uses an existing `default` profile as-is (we don't second-guess the user
77
76
  * if they've already customized it). On first run we walk the priority list
78
77
  * (macOS: chrome > brave > edge > chromium > comet; Linux: chrome > chromium >
79
- * brave > edge; Windows: edge > chrome > brave) and pin the profile to the
78
+ * brave > edge; Windows: edge > chrome > brave > comet) and pin the profile to the
80
79
  * first match. Throws an actionable error if none of those binaries are
81
80
  * installed so the user knows exactly which browsers we'd accept.
82
81
  */
@@ -153,14 +152,13 @@ export async function findFreeProfilePort() {
153
152
  for (let port = 9222; port <= 9399; port++) {
154
153
  if (usedByProfile.has(port))
155
154
  continue;
156
- try {
157
- execFileSync('lsof', ['-i', `:${port}`], { stdio: 'ignore' });
158
- // lsof succeeded something is listening port is in use
159
- }
160
- catch {
161
- // lsof threw → nothing on this port → it's free
155
+ // Platform-aware bound-port probe (lsof on POSIX, netstat on Windows).
156
+ // The old inline lsof call threw ENOENT on Windows — where lsof doesn't
157
+ // exist so EVERY port scanned as "free", including one an already-
158
+ // running browser was listening on (typically 9222), and the new profile
159
+ // would silently attach to that browser instead of launching its own.
160
+ if (!isPortInUse(port))
162
161
  return port;
163
- }
164
162
  }
165
163
  throw new Error('No available ports in range 9222-9399');
166
164
  }
@@ -12,9 +12,74 @@ export interface RefNode {
12
12
  backendNodeId?: number;
13
13
  editor?: string;
14
14
  }
15
+ /**
16
+ * Stable, navigation-surviving identity for a ref. Unlike {@link RefNode}, this
17
+ * carries no `backendNodeId` (which goes stale the moment the DOM re-renders) —
18
+ * only the accessible descriptor a fresh `getRefs` can be re-matched against.
19
+ * Cached in per-task state so a later `click <ref>` can self-heal when the
20
+ * integer ref has drifted (see {@link healRef}).
21
+ */
22
+ export interface RefDescriptor {
23
+ ref: number;
24
+ role: string;
25
+ name: string;
26
+ attrs: string[];
27
+ /**
28
+ * Best-effort CSS selector captured via DOM. Reserved for future
29
+ * selector-based healing; matching today is role+name+attrs+position.
30
+ */
31
+ selector?: string;
32
+ }
33
+ /**
34
+ * A cached ref listing for one tab: the stable descriptors plus the resolved
35
+ * {@link RefOpts} the listing was built with. A later `click`/`type` MUST
36
+ * rebuild its node map with these same opts, or the integer refs it looks up
37
+ * are numbered against a *different* accessibility filter than the one the
38
+ * user saw in `browser refs` — the drift that makes self-heal silently click
39
+ * the wrong element on the second click.
40
+ */
41
+ export interface RefSnapshot {
42
+ descriptors: RefDescriptor[];
43
+ opts: {
44
+ interactive: boolean;
45
+ limit: number;
46
+ };
47
+ }
48
+ /** Snapshot the stable descriptor for every ref in a freshly-built node map. */
49
+ export declare function describeRefs(nodeMap: Map<number, RefNode>): RefDescriptor[];
50
+ /**
51
+ * Re-resolve a stale ref against a freshly-built node map by matching the
52
+ * cached descriptor. Pure — no CDP, no DOM.
53
+ *
54
+ * The integer ref assigned by {@link getRefs} is positional: it shifts whenever
55
+ * the accessibility tree changes (navigation, re-render, or even a different
56
+ * `interactive` filter). The descriptor's (role, name) pair is the coarse
57
+ * identity, but a page can carry duplicate (role, name) elements — two "Submit"
58
+ * buttons, one action button per repeated list row. Matching on (role, name)
59
+ * alone would heal to whichever duplicate the fresh scan happens to hit first,
60
+ * which can be the wrong one. So among (role, name) matches we tie-break, in
61
+ * priority order:
62
+ *
63
+ * 1. **DOM-backed** — a candidate with a `backendNodeId` is resolvable to
64
+ * coordinates; one without is unclickable, so healing to it is useless.
65
+ * 2. **Attribute similarity** — the cached `attrs` (disabled / checked /
66
+ * selected / …) disambiguate otherwise-identical twins (the *disabled*
67
+ * Submit vs the enabled one).
68
+ * 3. **Positional proximity** — the fresh ref closest to the original cached
69
+ * ref index wins, so a repeated list-row action heals to its nearest
70
+ * positional twin rather than jumping across the page.
71
+ *
72
+ * Returns the new integer ref, or `null` when no node with the same role+name
73
+ * exists in the fresh map (unhealable — the element is genuinely gone).
74
+ */
75
+ export declare function healRef(descriptor: Pick<RefDescriptor, 'ref' | 'role' | 'name' | 'attrs'>, freshNodeMap: Map<number, RefNode>): number | null;
15
76
  export declare function getRefs(cdp: CDPClient, sessionId: string, opts?: RefOpts): Promise<{
16
77
  refs: string;
17
78
  nodeMap: Map<number, RefNode>;
79
+ opts: {
80
+ interactive: boolean;
81
+ limit: number;
82
+ };
18
83
  }>;
19
84
  export declare function resolveRefToCoords(cdp: CDPClient, sessionId: string, nodeMap: Map<number, RefNode>, ref: number): Promise<{
20
85
  x: number;
@@ -1,3 +1,75 @@
1
+ /** Snapshot the stable descriptor for every ref in a freshly-built node map. */
2
+ export function describeRefs(nodeMap) {
3
+ return Array.from(nodeMap.values()).map((n) => ({
4
+ ref: n.ref,
5
+ role: n.role,
6
+ name: n.name,
7
+ attrs: n.attrs.slice(),
8
+ }));
9
+ }
10
+ /**
11
+ * Re-resolve a stale ref against a freshly-built node map by matching the
12
+ * cached descriptor. Pure — no CDP, no DOM.
13
+ *
14
+ * The integer ref assigned by {@link getRefs} is positional: it shifts whenever
15
+ * the accessibility tree changes (navigation, re-render, or even a different
16
+ * `interactive` filter). The descriptor's (role, name) pair is the coarse
17
+ * identity, but a page can carry duplicate (role, name) elements — two "Submit"
18
+ * buttons, one action button per repeated list row. Matching on (role, name)
19
+ * alone would heal to whichever duplicate the fresh scan happens to hit first,
20
+ * which can be the wrong one. So among (role, name) matches we tie-break, in
21
+ * priority order:
22
+ *
23
+ * 1. **DOM-backed** — a candidate with a `backendNodeId` is resolvable to
24
+ * coordinates; one without is unclickable, so healing to it is useless.
25
+ * 2. **Attribute similarity** — the cached `attrs` (disabled / checked /
26
+ * selected / …) disambiguate otherwise-identical twins (the *disabled*
27
+ * Submit vs the enabled one).
28
+ * 3. **Positional proximity** — the fresh ref closest to the original cached
29
+ * ref index wins, so a repeated list-row action heals to its nearest
30
+ * positional twin rather than jumping across the page.
31
+ *
32
+ * Returns the new integer ref, or `null` when no node with the same role+name
33
+ * exists in the fresh map (unhealable — the element is genuinely gone).
34
+ */
35
+ export function healRef(descriptor, freshNodeMap) {
36
+ const { ref, role, name, attrs } = descriptor;
37
+ const cachedAttrs = new Set(attrs);
38
+ // How well a candidate's attribute set matches the cached one: reward shared
39
+ // attrs, penalize both extras the candidate carries and cached attrs it is
40
+ // missing. Exact set equality scores highest; an all-attrs-empty comparison
41
+ // scores 0 for every candidate (so the tie-break falls through to proximity).
42
+ const attrScore = (node) => {
43
+ let shared = 0;
44
+ for (const a of node.attrs)
45
+ if (cachedAttrs.has(a))
46
+ shared += 1;
47
+ const extra = node.attrs.length - shared;
48
+ const missing = cachedAttrs.size - shared;
49
+ return shared * 2 - extra - missing;
50
+ };
51
+ let best = null;
52
+ let bestKey = null;
53
+ for (const node of freshNodeMap.values()) {
54
+ if (node.role !== role || node.name !== name)
55
+ continue;
56
+ // Lexicographic key, all "higher is better":
57
+ // [ DOM-backed, attribute similarity, -distance-to-original-ref ].
58
+ const key = [
59
+ node.backendNodeId !== undefined ? 1 : 0,
60
+ attrScore(node),
61
+ -Math.abs(node.ref - ref),
62
+ ];
63
+ if (bestKey === null ||
64
+ key[0] > bestKey[0] ||
65
+ (key[0] === bestKey[0] && key[1] > bestKey[1]) ||
66
+ (key[0] === bestKey[0] && key[1] === bestKey[1] && key[2] > bestKey[2])) {
67
+ best = node;
68
+ bestKey = key;
69
+ }
70
+ }
71
+ return best ? best.ref : null;
72
+ }
1
73
  const EDITOR_DETECT_FN = `(function() {
2
74
  let el = this;
3
75
  for (let i = 0; i < 5; i++) {
@@ -101,7 +173,7 @@ export async function getRefs(cdp, sessionId, opts = {}) {
101
173
  : `- ${role}${nameStr} [ref=${ref}]${attrStr}${editorStr}`;
102
174
  lines.push(line);
103
175
  }
104
- return { refs: lines.join('\n'), nodeMap };
176
+ return { refs: lines.join('\n'), nodeMap, opts: { interactive, limit } };
105
177
  }
106
178
  function truncate(s, max) {
107
179
  if (s.length <= max)
@@ -269,6 +269,7 @@ function liveProcessCommand(pid) {
269
269
  const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], {
270
270
  encoding: 'utf-8',
271
271
  stdio: ['ignore', 'pipe', 'ignore'],
272
+ windowsHide: true,
272
273
  }).trim();
273
274
  // Rows look like: "node.exe","1234","Console","1","12,345 K"
274
275
  // A no-match prints an "INFO: No tasks..." line that won't match the regex.
@@ -43,6 +43,13 @@ export declare function pickWindowTarget<T extends {
43
43
  export declare function parseSinceUntil(s: string): Date;
44
44
  export declare function readNewestMatchingRemoteFileCommand(dir: string, prefix: string, tailLines: number): string;
45
45
  export declare function readNewestMatchingFile(dir: string, prefix: string, tailLines: number): string;
46
+ /** Describes a ref that was re-resolved from a drifted integer via its cached descriptor. */
47
+ export interface HealInfo {
48
+ from: number;
49
+ to: number;
50
+ role: string;
51
+ name: string;
52
+ }
46
53
  export declare class BrowserService {
47
54
  private static readonly SOURCE_PREFIX;
48
55
  private connections;
@@ -104,6 +111,17 @@ export declare class BrowserService {
104
111
  width: number;
105
112
  height: number;
106
113
  }>;
114
+ /**
115
+ * Export the current tab as a PDF via CDP `Page.printToPDF`. Reuses the
116
+ * screenshot session + tab resolution so `--tab`, path sandboxing, and the
117
+ * auto-path (`sessions/<task>/<ts>.pdf`) all behave identically to
118
+ * `screenshot`. `printBackground: true` matches Chrome's default print flow
119
+ * — without it, dark-mode pages render on a blank sheet.
120
+ */
121
+ printToPdf(taskId: string, tabHint?: string, outputPath?: string): Promise<{
122
+ path: string;
123
+ bytes: number;
124
+ }>;
107
125
  private recordings;
108
126
  recordStart(taskId: string, tabHint?: string, opts?: {
109
127
  fps?: number;
@@ -126,12 +144,30 @@ export declare class BrowserService {
126
144
  path?: string;
127
145
  elapsedMs?: number;
128
146
  }>;
129
- private refsCache;
130
147
  refs(taskId: string, tabHint?: string, opts?: RefOpts): Promise<{
131
148
  refs: string;
132
149
  nodeMap: Map<number, RefNode>;
133
150
  }>;
134
- click(taskId: string, ref: number, tabHint?: string): Promise<void>;
151
+ /** Record the last ref listing (descriptors + opts) for a tab into state. */
152
+ private cacheRefDescriptors;
153
+ /**
154
+ * Re-resolve a caller-supplied ref against a freshly-built node map, healing
155
+ * it back to the right element when the integer ref has drifted since the
156
+ * cached `refs` listing. Shared by `click` and `type` so both self-heal
157
+ * identically. Returns the ref to act on plus, when a heal occurred, the
158
+ * {@link HealInfo} to surface. Throws when the cached element is gone.
159
+ */
160
+ private resolveHealedRef;
161
+ click(taskId: string, ref: number, tabHint?: string): Promise<{
162
+ healed?: HealInfo;
163
+ }>;
164
+ /**
165
+ * Click raw viewport coordinates, bypassing ref resolution entirely. Backs
166
+ * `browser click --at X,Y` — the escape hatch when the accessibility tree
167
+ * exposes no usable ref (canvas apps, custom-drawn UI) and the caller has
168
+ * located the target from a screenshot.
169
+ */
170
+ clickAt(taskId: string, x: number, y: number, tabHint?: string): Promise<void>;
135
171
  type(taskId: string, ref: number, text: string, tabHint?: string, clear?: boolean): Promise<void>;
136
172
  press(taskId: string, key: string, tabHint?: string): Promise<void>;
137
173
  hover(taskId: string, ref: number, tabHint?: string): Promise<void>;