@phnx-labs/agents-cli 1.20.35 → 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 (225) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  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.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -39,7 +39,11 @@ const BROWSER_PATHS = {
39
39
  `${WIN_PROGRAMFILES_X86}\\Google\\Chrome\\Application\\chrome.exe`,
40
40
  `${WIN_LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
41
41
  ],
42
- comet: [],
42
+ comet: [
43
+ `${WIN_PROGRAMFILES}\\Perplexity\\Comet\\Application\\comet.exe`,
44
+ `${WIN_PROGRAMFILES_X86}\\Perplexity\\Comet\\Application\\comet.exe`,
45
+ `${WIN_LOCALAPPDATA}\\Perplexity\\Comet\\Application\\comet.exe`,
46
+ ],
43
47
  chromium: [
44
48
  `${WIN_LOCALAPPDATA}\\Chromium\\Application\\chrome.exe`,
45
49
  ],
@@ -155,8 +159,8 @@ export function findBrowserPath(browserType, customBinary) {
155
159
  return resolveBrowserBinary(p);
156
160
  }
157
161
  }
158
- if (browserType === 'comet' && platform !== 'darwin') {
159
- throw new Error('Browser "comet" is macOS-only (Comet is a macOS Chromium fork). Use chrome, chromium, brave, or edge on this platform.');
162
+ if (browserType === 'comet' && platform === 'linux') {
163
+ throw new Error('Browser "comet" is not available on Linux (Comet ships macOS and Windows builds only). Use chrome, chromium, brave, or edge on this platform.');
160
164
  }
161
165
  throw new Error(`Browser "${browserType}" not found. Install it first.`);
162
166
  }
@@ -171,7 +175,7 @@ const DEFAULT_BROWSER_PRIORITY = {
171
175
  linux: ['chrome', 'chromium', 'brave', 'edge'],
172
176
  // Windows: Edge is preinstalled on every supported build, so it's the
173
177
  // reliable always-there default.
174
- win32: ['edge', 'chrome', 'brave'],
178
+ win32: ['edge', 'chrome', 'brave', 'comet'],
175
179
  };
176
180
  /**
177
181
  * Walk the per-platform priority list and return the first browser that's
@@ -290,11 +294,11 @@ export function killChrome(pid) {
290
294
  // stop first (no /F → posts WM_CLOSE), and only force-kill the tree if that
291
295
  // fails or the process is still around.
292
296
  try {
293
- execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore' });
297
+ execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore', windowsHide: true });
294
298
  }
295
299
  catch {
296
300
  try {
297
- execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
301
+ execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore', windowsHide: true });
298
302
  }
299
303
  catch {
300
304
  // Process already dead
@@ -345,13 +349,18 @@ function sleep(ms) {
345
349
  * Is a TCP port currently bound? `lsof` on POSIX, `netstat -ano` on Windows
346
350
  * (lsof doesn't exist there). Returns false on any tooling error so port
347
351
  * allocation degrades to "assume free" rather than throwing.
352
+ *
353
+ * Exported as the canonical cross-platform probe — `findFreeProfilePort`
354
+ * (profiles.ts) must use this rather than shelling out to lsof directly,
355
+ * or every port scans as free on Windows.
348
356
  */
349
- function isPortInUse(port) {
357
+ export function isPortInUse(port) {
350
358
  if (process.platform === 'win32') {
351
359
  try {
352
360
  const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], {
353
361
  encoding: 'utf8',
354
362
  stdio: ['ignore', 'pipe', 'ignore'],
363
+ windowsHide: true,
355
364
  });
356
365
  // Lines look like: " TCP 0.0.0.0:9200 0.0.0.0:0 LISTENING 1234"
357
366
  return out.split('\n').some((line) => {
@@ -393,6 +402,7 @@ export function getPortOccupant(port) {
393
402
  const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], {
394
403
  encoding: 'utf8',
395
404
  stdio: ['ignore', 'pipe', 'ignore'],
405
+ windowsHide: true,
396
406
  });
397
407
  let pid = 0;
398
408
  for (const line of out.split('\n')) {
@@ -410,6 +420,7 @@ export function getPortOccupant(port) {
410
420
  const tl = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
411
421
  encoding: 'utf8',
412
422
  stdio: ['ignore', 'pipe', 'ignore'],
423
+ windowsHide: true,
413
424
  });
414
425
  const m = tl.match(/^"([^"]+)"/);
415
426
  if (m)
@@ -271,7 +271,7 @@ export async function ensureRemoteBrowser(user, host, browserType, port, remoteO
271
271
  '-o',
272
272
  'BatchMode=yes',
273
273
  remoteCmd,
274
- ], { stdio: ['ignore', 'ignore', 'pipe'] });
274
+ ], { stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
275
275
  let stderr = '';
276
276
  child.stderr?.on('data', (d) => {
277
277
  stderr += d.toString();
@@ -330,6 +330,7 @@ function runSSHCommand(user, host, cmd) {
330
330
  return new Promise((resolve) => {
331
331
  const child = spawn('ssh', [`${user}@${host}`, '-o', 'BatchMode=yes', cmd], {
332
332
  stdio: 'ignore',
333
+ windowsHide: true,
333
334
  });
334
335
  child.on('close', () => resolve());
335
336
  child.on('error', () => resolve());
@@ -0,0 +1,84 @@
1
+ import type { NetworkRequest } from './types.js';
2
+ /**
3
+ * Minimal HAR 1.2 shapes. We only produce what our request buffer can back —
4
+ * fields the buffer doesn't populate (headers, cookies, bodies, timings) are
5
+ * emitted with the spec-mandated empty defaults or `-1` sentinels so validators
6
+ * still accept the document.
7
+ *
8
+ * Spec: http://www.softwareishard.com/blog/har-12-spec/
9
+ */
10
+ export interface HarLog {
11
+ version: '1.2';
12
+ creator: {
13
+ name: string;
14
+ version: string;
15
+ };
16
+ pages: HarPage[];
17
+ entries: HarEntry[];
18
+ }
19
+ export interface HarPage {
20
+ startedDateTime: string;
21
+ id: string;
22
+ title: string;
23
+ pageTimings: {
24
+ onContentLoad: number;
25
+ onLoad: number;
26
+ };
27
+ }
28
+ export interface HarEntry {
29
+ startedDateTime: string;
30
+ time: number;
31
+ request: HarRequest;
32
+ response: HarResponse;
33
+ cache: Record<string, never>;
34
+ timings: HarTimings;
35
+ }
36
+ export interface HarRequest {
37
+ method: string;
38
+ url: string;
39
+ httpVersion: string;
40
+ cookies: never[];
41
+ headers: never[];
42
+ queryString: Array<{
43
+ name: string;
44
+ value: string;
45
+ }>;
46
+ headersSize: number;
47
+ bodySize: number;
48
+ }
49
+ export interface HarResponse {
50
+ status: number;
51
+ statusText: string;
52
+ httpVersion: string;
53
+ cookies: never[];
54
+ headers: never[];
55
+ content: {
56
+ size: number;
57
+ mimeType: string;
58
+ text?: string;
59
+ };
60
+ redirectURL: string;
61
+ headersSize: number;
62
+ bodySize: number;
63
+ }
64
+ export interface HarTimings {
65
+ send: number;
66
+ wait: number;
67
+ receive: number;
68
+ }
69
+ export interface BuildHarOptions {
70
+ creatorName?: string;
71
+ creatorVersion?: string;
72
+ }
73
+ /**
74
+ * Serialize a buffered `NetworkRequest[]` into a HAR 1.2 document.
75
+ *
76
+ * The buffer only records `url`, `method`, `status`, `mimeType`, `timestamp`.
77
+ * Everything downstream — cookies, headers, bodies, per-phase timings — is
78
+ * unknown and must be emitted as the spec's "unknown" sentinels: `-1` for
79
+ * sizes / timings, empty arrays for cookies / headers. HAR validators accept
80
+ * this shape (browsers do the same for opaque cross-origin responses).
81
+ */
82
+ export declare function buildHar(requests: NetworkRequest[], opts?: BuildHarOptions): {
83
+ log: HarLog;
84
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Serialize a buffered `NetworkRequest[]` into a HAR 1.2 document.
3
+ *
4
+ * The buffer only records `url`, `method`, `status`, `mimeType`, `timestamp`.
5
+ * Everything downstream — cookies, headers, bodies, per-phase timings — is
6
+ * unknown and must be emitted as the spec's "unknown" sentinels: `-1` for
7
+ * sizes / timings, empty arrays for cookies / headers. HAR validators accept
8
+ * this shape (browsers do the same for opaque cross-origin responses).
9
+ */
10
+ export function buildHar(requests, opts = {}) {
11
+ const creator = {
12
+ name: opts.creatorName ?? 'agents-cli',
13
+ version: opts.creatorVersion ?? '',
14
+ };
15
+ const startedAt = requests.length > 0 ? Math.min(...requests.map((r) => r.timestamp)) : Date.now();
16
+ const page = {
17
+ startedDateTime: new Date(startedAt).toISOString(),
18
+ id: 'page_1',
19
+ title: 'agents browser session',
20
+ pageTimings: { onContentLoad: -1, onLoad: -1 },
21
+ };
22
+ const entries = requests.map((r) => {
23
+ const queryString = extractQueryString(r.url);
24
+ const mimeType = r.mimeType ?? '';
25
+ return {
26
+ startedDateTime: new Date(r.timestamp).toISOString(),
27
+ time: -1,
28
+ request: {
29
+ method: r.method,
30
+ url: r.url,
31
+ httpVersion: 'HTTP/1.1',
32
+ cookies: [],
33
+ headers: [],
34
+ queryString,
35
+ headersSize: -1,
36
+ bodySize: -1,
37
+ },
38
+ response: {
39
+ status: r.status ?? 0,
40
+ statusText: '',
41
+ httpVersion: 'HTTP/1.1',
42
+ cookies: [],
43
+ headers: [],
44
+ content: { size: -1, mimeType },
45
+ redirectURL: '',
46
+ headersSize: -1,
47
+ bodySize: -1,
48
+ },
49
+ cache: {},
50
+ timings: { send: -1, wait: -1, receive: -1 },
51
+ };
52
+ });
53
+ return {
54
+ log: {
55
+ version: '1.2',
56
+ creator,
57
+ pages: [page],
58
+ entries,
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Pull `?a=1&b=2` off a URL into HAR's `queryString` array. Returns `[]` when
64
+ * the URL has no query or fails to parse (opaque URLs like `data:` shouldn't
65
+ * abort the whole HAR).
66
+ */
67
+ function extractQueryString(url) {
68
+ try {
69
+ const u = new URL(url);
70
+ const out = [];
71
+ u.searchParams.forEach((value, name) => out.push({ name, value }));
72
+ return out;
73
+ }
74
+ catch {
75
+ return [];
76
+ }
77
+ }
@@ -288,6 +288,13 @@ export class BrowserIPCServer {
288
288
  const shot = await this.service.screenshot(request.task, request.tabId, request.path, request.quality);
289
289
  return { ok: true, path: shot.path, bytes: shot.bytes, width: shot.width, height: shot.height };
290
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
+ }
291
298
  case 'refs': {
292
299
  if (!request.task) {
293
300
  return { ok: false, error: 'Task required' };
@@ -305,10 +312,24 @@ export class BrowserIPCServer {
305
312
  return { ok: true, refs, nodes };
306
313
  }
307
314
  case 'click': {
308
- if (!request.task || request.ref === undefined) {
309
- 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
+ };
310
332
  }
311
- await this.service.click(request.task, request.ref, request.tabId);
312
333
  return { ok: true };
313
334
  }
314
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>;