@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.
- package/CHANGELOG.md +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- 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
|
|
159
|
-
throw new Error('Browser "comet" is
|
|
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
|
+
}
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -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
|
|
309
|
-
return { ok: false, error: 'Task
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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;
|
package/dist/lib/browser/refs.js
CHANGED
|
@@ -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
|
-
|
|
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>;
|