@phnx-labs/agents-cli 1.18.3 → 1.18.4
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 +78 -0
- package/README.md +14 -7
- package/dist/commands/browser.js +355 -118
- package/dist/lib/browser/devices.d.ts +11 -0
- package/dist/lib/browser/devices.js +14 -3
- package/dist/lib/browser/ipc.js +29 -9
- package/dist/lib/browser/profiles.d.ts +23 -1
- package/dist/lib/browser/profiles.js +63 -3
- package/dist/lib/browser/service.d.ts +41 -10
- package/dist/lib/browser/service.js +321 -64
- package/dist/lib/browser/types.d.ts +55 -2
- package/dist/lib/browser/types.js +20 -0
- package/dist/lib/help.js +30 -3
- package/dist/lib/types.d.ts +12 -1
- package/package.json +1 -1
package/dist/commands/browser.js
CHANGED
|
@@ -1,22 +1,101 @@
|
|
|
1
|
+
import { Argument } from 'commander';
|
|
2
|
+
// Hidden trailing positional we use to capture the deprecated `<task>` slot
|
|
3
|
+
// without polluting `--help`. Required for backward compat during migration.
|
|
4
|
+
// commander v12 Argument lacks `hideHelp()`; we set the field our custom
|
|
5
|
+
// help formatter (src/lib/help.ts) already filters on.
|
|
6
|
+
function hiddenLegacyArg(name = 'legacyArg') {
|
|
7
|
+
const arg = new Argument(`[${name}]`, 'deprecated — legacy task positional');
|
|
8
|
+
arg.hidden = true;
|
|
9
|
+
return arg;
|
|
10
|
+
}
|
|
1
11
|
import * as fs from 'fs';
|
|
2
12
|
import * as path from 'path';
|
|
3
|
-
import { listProfiles, getProfile, createProfile, deleteProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, } from '../lib/browser/profiles.js';
|
|
13
|
+
import { listProfiles, getProfile, createProfile, deleteProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, } from '../lib/browser/profiles.js';
|
|
4
14
|
import { findBrowserPath, getPortOccupant } from '../lib/browser/chrome.js';
|
|
15
|
+
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
5
16
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
6
17
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
7
18
|
import { sendIPCRequest } from '../lib/browser/ipc.js';
|
|
8
19
|
import { browserTaskPicker } from './browser-picker.js';
|
|
9
20
|
import { isInteractiveTerminal } from './utils.js';
|
|
21
|
+
import { registerCommandGroups } from '../lib/help.js';
|
|
22
|
+
/**
|
|
23
|
+
* Resolve which browser task a command targets. Order:
|
|
24
|
+
* 1. `--task <name>` flag (explicit per-command override)
|
|
25
|
+
* 2. Legacy positional `<task>` arg (deprecated; emits a one-time warning)
|
|
26
|
+
* 3. `$AGENTS_BROWSER_TASK` (set once at the start of an agent run)
|
|
27
|
+
*
|
|
28
|
+
* Each agent process has its own environment, so the env-var path is safe for
|
|
29
|
+
* parallel agents — they can't see each other's value.
|
|
30
|
+
*/
|
|
31
|
+
let legacyTaskWarned = false;
|
|
32
|
+
function warnLegacyTaskPositional() {
|
|
33
|
+
if (legacyTaskWarned)
|
|
34
|
+
return;
|
|
35
|
+
legacyTaskWarned = true;
|
|
36
|
+
console.error('warning: passing <task> as a positional argument is deprecated. ' +
|
|
37
|
+
'Set AGENTS_BROWSER_TASK once per shell, or use --task <name> for a one-off override.');
|
|
38
|
+
}
|
|
39
|
+
function resolveTaskName(opts, legacyTaskArg) {
|
|
40
|
+
if (opts.task)
|
|
41
|
+
return opts.task;
|
|
42
|
+
if (legacyTaskArg !== undefined && legacyTaskArg !== '') {
|
|
43
|
+
warnLegacyTaskPositional();
|
|
44
|
+
return legacyTaskArg;
|
|
45
|
+
}
|
|
46
|
+
const fromEnv = process.env.AGENTS_BROWSER_TASK;
|
|
47
|
+
if (fromEnv)
|
|
48
|
+
return fromEnv;
|
|
49
|
+
console.error('No task specified. Pass --task <name> or set AGENTS_BROWSER_TASK in your shell.');
|
|
50
|
+
console.error('Tip: T=$(agents browser start --profile <p>) && export AGENTS_BROWSER_TASK=$T');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Split positional args into (deprecated leading task, new-shape positionals).
|
|
55
|
+
*
|
|
56
|
+
* If exactly one more positional than the new shape expects is supplied, treat
|
|
57
|
+
* the first one as the deprecated `<task>` positional and shift the rest. Used
|
|
58
|
+
* to keep old `agents browser navigate <task> <url>` style invocations working
|
|
59
|
+
* during the migration to `--task <name>` / `$AGENTS_BROWSER_TASK`.
|
|
60
|
+
*/
|
|
61
|
+
function unpackPositionals(rawArgs, newPositionalCount, opts) {
|
|
62
|
+
const defined = rawArgs.filter((a) => a !== undefined);
|
|
63
|
+
if (defined.length === newPositionalCount + 1) {
|
|
64
|
+
return { task: resolveTaskName(opts, defined[0]), positionals: defined.slice(1) };
|
|
65
|
+
}
|
|
66
|
+
return { task: resolveTaskName(opts), positionals: defined };
|
|
67
|
+
}
|
|
68
|
+
// `-t` is taken by `--tab` on most commands, so `--task` is long-form only.
|
|
69
|
+
// Agents normally set $AGENTS_BROWSER_TASK once and never type this flag.
|
|
70
|
+
const TASK_OPTION_FLAG = '--task <name>';
|
|
71
|
+
const TASK_OPTION_DESC = 'Task name (defaults to $AGENTS_BROWSER_TASK)';
|
|
72
|
+
// Help groups — surfaces the actual mental model an agent follows
|
|
73
|
+
// ("open a session / drive the page / capture evidence / rare extras")
|
|
74
|
+
// instead of an alphabetical dump. Everything not listed falls into a
|
|
75
|
+
// trailing "Other commands" section automatically.
|
|
76
|
+
const BROWSER_HELP_GROUPS = [
|
|
77
|
+
{ title: 'Session lifecycle', names: ['start', 'done', 'status'] },
|
|
78
|
+
{
|
|
79
|
+
title: 'Drive the page',
|
|
80
|
+
names: ['navigate', 'tabs', 'screenshot', 'evaluate', 'click', 'type', 'press', 'wait'],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: 'Capture evidence',
|
|
84
|
+
names: ['console', 'errors', 'requests', 'responsebody', 'record'],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
10
87
|
export function registerBrowserCommand(program) {
|
|
11
88
|
const browser = program
|
|
12
89
|
.command('browser')
|
|
13
90
|
.description('Browser automation via CDP');
|
|
14
91
|
registerProfilesCommands(browser);
|
|
15
92
|
registerTaskCommands(browser);
|
|
93
|
+
registerCommandGroups(browser, BROWSER_HELP_GROUPS);
|
|
16
94
|
}
|
|
17
95
|
export function registerBrowserSubcommands(program) {
|
|
18
96
|
registerProfilesCommands(program);
|
|
19
97
|
registerTaskCommands(program);
|
|
98
|
+
registerCommandGroups(program, BROWSER_HELP_GROUPS);
|
|
20
99
|
}
|
|
21
100
|
function registerProfilesCommands(browser) {
|
|
22
101
|
const profiles = browser
|
|
@@ -38,7 +117,10 @@ function registerProfilesCommands(browser) {
|
|
|
38
117
|
console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'DESCRIPTION'.padEnd(38) + 'ENDPOINTS');
|
|
39
118
|
console.log('-'.repeat(92));
|
|
40
119
|
for (const p of allProfiles) {
|
|
41
|
-
const
|
|
120
|
+
const presets = getEndpointPresets(p);
|
|
121
|
+
const endpoints = Object.entries(presets)
|
|
122
|
+
.map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
|
|
123
|
+
.join(', ');
|
|
42
124
|
const desc = (p.description ?? '').slice(0, 36).padEnd(38);
|
|
43
125
|
console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + desc + endpoints);
|
|
44
126
|
}
|
|
@@ -47,7 +129,10 @@ function registerProfilesCommands(browser) {
|
|
|
47
129
|
console.log('NAME'.padEnd(20) + 'BROWSER'.padEnd(12) + 'ENDPOINTS');
|
|
48
130
|
console.log('-'.repeat(72));
|
|
49
131
|
for (const p of allProfiles) {
|
|
50
|
-
const
|
|
132
|
+
const presets = getEndpointPresets(p);
|
|
133
|
+
const endpoints = Object.entries(presets)
|
|
134
|
+
.map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
|
|
135
|
+
.join(', ');
|
|
51
136
|
console.log(p.name.padEnd(20) + (p.browser || '-').padEnd(12) + endpoints);
|
|
52
137
|
}
|
|
53
138
|
}
|
|
@@ -61,7 +146,7 @@ function registerProfilesCommands(browser) {
|
|
|
61
146
|
.option('-s, --secrets <bundle>', 'Secrets bundle to inject')
|
|
62
147
|
.option('-d, --description <text>', 'Profile description')
|
|
63
148
|
.option('--headless', 'Run in headless mode')
|
|
64
|
-
.option('--window <WxH>',
|
|
149
|
+
.option('--window <WxH>', `Window size in CSS pixels (default: ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}, MacBook Pro 14")`)
|
|
65
150
|
.option('--position <X,Y>', 'Window position on screen, e.g. 80,80')
|
|
66
151
|
.option('--binary <path>', 'Absolute path to the browser/app binary (required with --browser custom)')
|
|
67
152
|
.option('--electron', 'Treat this profile as an Electron desktop app: never call Target.createTarget; bind to the visible window using --target-filter or the skip-invisible heuristic')
|
|
@@ -100,15 +185,16 @@ function registerProfilesCommands(browser) {
|
|
|
100
185
|
const freePort = await findFreeProfilePort();
|
|
101
186
|
endpoints = [`cdp://127.0.0.1:${freePort}`];
|
|
102
187
|
}
|
|
103
|
-
// Viewport is mandatory — default to
|
|
188
|
+
// Viewport is mandatory — default to MacBook Pro 14" (1512x982) if
|
|
189
|
+
// --window is not provided. See lib/browser/devices.ts DEFAULT_VIEWPORT.
|
|
104
190
|
let viewport = {
|
|
105
|
-
width:
|
|
106
|
-
height:
|
|
191
|
+
width: DEFAULT_VIEWPORT.width,
|
|
192
|
+
height: DEFAULT_VIEWPORT.height,
|
|
107
193
|
};
|
|
108
194
|
if (opts.window) {
|
|
109
195
|
const m = String(opts.window).match(/^(\d+)x(\d+)$/);
|
|
110
196
|
if (!m) {
|
|
111
|
-
console.error(
|
|
197
|
+
console.error(`--window must be WxH, e.g. ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}`);
|
|
112
198
|
process.exit(1);
|
|
113
199
|
}
|
|
114
200
|
viewport.width = parseInt(m[1], 10);
|
|
@@ -167,9 +253,24 @@ function registerProfilesCommands(browser) {
|
|
|
167
253
|
console.log(`Target filter: ${profile.targetFilter}`);
|
|
168
254
|
if (profile.description)
|
|
169
255
|
console.log(`Description: ${profile.description}`);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
256
|
+
const presets = getEndpointPresets(profile);
|
|
257
|
+
const defaultName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
|
|
258
|
+
? profile.defaultEndpoint
|
|
259
|
+
: Object.keys(presets)[0];
|
|
260
|
+
console.log('Endpoints:');
|
|
261
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
262
|
+
const marker = presetName === defaultName ? ' (default)' : '';
|
|
263
|
+
const isLegacy = presetName.startsWith('endpoint-');
|
|
264
|
+
console.log(` - ${isLegacy ? preset.target : `${presetName}: ${preset.target}`}${marker}`);
|
|
265
|
+
if (preset.binary)
|
|
266
|
+
console.log(` binary: ${preset.binary}`);
|
|
267
|
+
if (preset.targetFilter)
|
|
268
|
+
console.log(` targetFilter: ${preset.targetFilter}`);
|
|
269
|
+
}
|
|
270
|
+
if (profile.viewport) {
|
|
271
|
+
const v = profile.viewport;
|
|
272
|
+
const pos = v.x !== undefined && v.y !== undefined ? ` @ ${v.x},${v.y}` : '';
|
|
273
|
+
console.log(`Viewport: ${v.width}×${v.height}${pos}`);
|
|
173
274
|
}
|
|
174
275
|
if (profile.secrets)
|
|
175
276
|
console.log(`Secrets: ${profile.secrets}`);
|
|
@@ -183,27 +284,6 @@ function registerProfilesCommands(browser) {
|
|
|
183
284
|
await deleteProfile(name);
|
|
184
285
|
console.log(`Deleted profile: ${name}`);
|
|
185
286
|
});
|
|
186
|
-
profiles
|
|
187
|
-
.command('launch <name>')
|
|
188
|
-
.description('Start (or attach to) the profile\'s browser without creating a task')
|
|
189
|
-
.action(async (name) => {
|
|
190
|
-
const profile = await getProfile(name);
|
|
191
|
-
if (!profile) {
|
|
192
|
-
console.error(`Profile "${name}" not found`);
|
|
193
|
-
process.exit(1);
|
|
194
|
-
}
|
|
195
|
-
const response = await sendIPCRequest({
|
|
196
|
-
action: 'launch-profile',
|
|
197
|
-
profile: name,
|
|
198
|
-
});
|
|
199
|
-
if (!response.ok) {
|
|
200
|
-
console.error(response.error);
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
const pidLabel = response.pid ? `pid ${response.pid}` : 'attached';
|
|
204
|
-
console.log(`Launched "${name}" on port ${response.port} (${pidLabel})`);
|
|
205
|
-
console.log(`Next: agents browser start --profile ${name} --url <url>`);
|
|
206
|
-
});
|
|
207
287
|
profiles
|
|
208
288
|
.command('doctor <name>')
|
|
209
289
|
.description('Diagnose a browser profile: binary, port, user-data-dir, onboarding state')
|
|
@@ -302,7 +382,9 @@ function registerProfilesCommands(browser) {
|
|
|
302
382
|
checks.push({
|
|
303
383
|
label: 'onboarding',
|
|
304
384
|
ok: false,
|
|
305
|
-
detail: 'Local State is empty — run `agents browser
|
|
385
|
+
detail: 'Local State is empty — run `agents browser start --profile ' +
|
|
386
|
+
name +
|
|
387
|
+
'` and finish any first-run screens before automating',
|
|
306
388
|
});
|
|
307
389
|
}
|
|
308
390
|
}
|
|
@@ -310,7 +392,9 @@ function registerProfilesCommands(browser) {
|
|
|
310
392
|
checks.push({
|
|
311
393
|
label: 'onboarding',
|
|
312
394
|
ok: false,
|
|
313
|
-
detail: 'Not
|
|
395
|
+
detail: 'Not initialized yet — run `agents browser start --profile ' +
|
|
396
|
+
name +
|
|
397
|
+
'` and finish any first-run screens before automating',
|
|
314
398
|
});
|
|
315
399
|
}
|
|
316
400
|
}
|
|
@@ -322,63 +406,69 @@ function registerProfilesCommands(browser) {
|
|
|
322
406
|
if (!allOk)
|
|
323
407
|
process.exit(1);
|
|
324
408
|
});
|
|
325
|
-
profiles
|
|
326
|
-
.command('prime <name>')
|
|
327
|
-
.description('Launch the profile so you can complete first-run onboarding interactively')
|
|
328
|
-
.action(async (name) => {
|
|
329
|
-
const profile = await getProfile(name);
|
|
330
|
-
if (!profile) {
|
|
331
|
-
console.error(`Profile "${name}" not found`);
|
|
332
|
-
process.exit(1);
|
|
333
|
-
}
|
|
334
|
-
const response = await sendIPCRequest({
|
|
335
|
-
action: 'launch-profile',
|
|
336
|
-
profile: name,
|
|
337
|
-
});
|
|
338
|
-
if (!response.ok) {
|
|
339
|
-
console.error(response.error);
|
|
340
|
-
process.exit(1);
|
|
341
|
-
}
|
|
342
|
-
const pidLabel = response.pid ? `pid ${response.pid}` : 'attached';
|
|
343
|
-
console.log(`Launched "${name}" on port ${response.port} (${pidLabel}).`);
|
|
344
|
-
console.log('');
|
|
345
|
-
console.log('Finish any first-run / onboarding screens in the browser window');
|
|
346
|
-
console.log('(welcome, profile setup, default-browser prompt, sign-in, etc.).');
|
|
347
|
-
console.log('Once you reach a normal browsing surface, this profile is primed');
|
|
348
|
-
console.log('— its user-data-dir persists across runs, so you only do this once.');
|
|
349
|
-
console.log('');
|
|
350
|
-
console.log(`Next: agents browser start --profile ${name} --url <url>`);
|
|
351
|
-
});
|
|
352
409
|
}
|
|
353
410
|
function registerTaskCommands(browser) {
|
|
354
411
|
browser
|
|
355
412
|
.command('start')
|
|
356
413
|
.description('Start a browser task with a profile')
|
|
357
414
|
.requiredOption('-p, --profile <name>', 'Browser profile to use')
|
|
358
|
-
.option(
|
|
415
|
+
.option(TASK_OPTION_FLAG, 'Task name (auto-generated if omitted)')
|
|
416
|
+
.option('-e, --endpoint <name>', 'Endpoint preset (defaults to the profile\'s default)')
|
|
359
417
|
.option('-u, --url <url>', 'Open URL in first tab')
|
|
360
418
|
.action(async (opts) => {
|
|
419
|
+
// Pre-check the profile locally so we fail fast with a helpful error
|
|
420
|
+
// instead of round-tripping a generic "Profile not found" through the daemon.
|
|
421
|
+
const profile = await getProfile(opts.profile);
|
|
422
|
+
if (!profile) {
|
|
423
|
+
console.error(`Profile "${opts.profile}" not found.`);
|
|
424
|
+
const all = await listProfiles();
|
|
425
|
+
if (all.length > 0) {
|
|
426
|
+
console.error(`Available profiles: ${all.map((p) => p.name).join(', ')}`);
|
|
427
|
+
}
|
|
428
|
+
console.error(`Create one with: agents browser profiles create ${opts.profile} --browser <chrome|comet|chromium|brave|edge|custom>`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
// Pre-check the endpoint name too — same fail-fast rationale.
|
|
432
|
+
if (opts.endpoint) {
|
|
433
|
+
const presets = getEndpointPresets(profile);
|
|
434
|
+
if (!presets[opts.endpoint]) {
|
|
435
|
+
console.error(`Endpoint "${opts.endpoint}" not found on profile "${opts.profile}". ` +
|
|
436
|
+
`Available: ${Object.keys(presets).join(', ')}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
361
440
|
const response = await sendIPCRequest({
|
|
362
441
|
action: 'start',
|
|
363
442
|
profile: opts.profile,
|
|
364
443
|
taskName: opts.task,
|
|
365
444
|
url: opts.url,
|
|
445
|
+
endpoint: opts.endpoint,
|
|
366
446
|
});
|
|
367
447
|
if (!response.ok) {
|
|
368
448
|
console.error(response.error);
|
|
369
449
|
process.exit(1);
|
|
370
450
|
}
|
|
451
|
+
// stdout: just the resolved name, one line, no decoration. Lets callers do:
|
|
452
|
+
// export AGENTS_BROWSER_TASK=$(agents browser start --profile work)
|
|
453
|
+
console.log(response.task);
|
|
454
|
+
// stderr: human-friendly commentary so a TTY user still sees what happened.
|
|
455
|
+
// Shell substitution captures stdout only, so $(...) stays clean.
|
|
371
456
|
if (opts.url && response.tabId) {
|
|
372
|
-
console.
|
|
457
|
+
console.error(`Started task "${response.task}" with tab ${response.tabId}`);
|
|
373
458
|
}
|
|
374
459
|
else {
|
|
375
|
-
console.
|
|
460
|
+
console.error(`Started task "${response.task}"`);
|
|
376
461
|
}
|
|
462
|
+
console.error(`Tip: export AGENTS_BROWSER_TASK=${response.task}`);
|
|
463
|
+
console.error('Try: agents browser screenshot | agents browser console --level error');
|
|
377
464
|
});
|
|
378
465
|
browser
|
|
379
|
-
.command('done
|
|
466
|
+
.command('done')
|
|
467
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
380
468
|
.description('Complete a task and close its tabs')
|
|
381
|
-
.
|
|
469
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
470
|
+
.action(async (legacyTask, opts) => {
|
|
471
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
382
472
|
const response = await sendIPCRequest({
|
|
383
473
|
action: 'done',
|
|
384
474
|
task,
|
|
@@ -390,9 +480,12 @@ function registerTaskCommands(browser) {
|
|
|
390
480
|
console.log(`Completed task: ${task}`);
|
|
391
481
|
});
|
|
392
482
|
browser
|
|
393
|
-
.command('stop
|
|
483
|
+
.command('stop')
|
|
484
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
394
485
|
.description('Stop a browser task and close its tabs')
|
|
395
|
-
.
|
|
486
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
487
|
+
.action(async (legacyTask, opts) => {
|
|
488
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
396
489
|
const response = await sendIPCRequest({
|
|
397
490
|
action: 'stop',
|
|
398
491
|
task,
|
|
@@ -404,10 +497,14 @@ function registerTaskCommands(browser) {
|
|
|
404
497
|
console.log(`Stopped task: ${task}`);
|
|
405
498
|
});
|
|
406
499
|
browser
|
|
407
|
-
.command('navigate <
|
|
500
|
+
.command('navigate <url>')
|
|
501
|
+
.addArgument(hiddenLegacyArg('legacyUrl'))
|
|
408
502
|
.description('Navigate current tab to URL (creates tab if none exist)')
|
|
503
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
409
504
|
.option('-p, --profile <name>', 'Browser profile (optional if task is unique)')
|
|
410
|
-
.action(async (
|
|
505
|
+
.action(async (arg1, arg2, opts) => {
|
|
506
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
507
|
+
const [url] = positionals;
|
|
411
508
|
const response = await sendIPCRequest({
|
|
412
509
|
action: 'navigate',
|
|
413
510
|
task,
|
|
@@ -423,10 +520,14 @@ function registerTaskCommands(browser) {
|
|
|
423
520
|
// Tab subcommand group
|
|
424
521
|
const tab = browser.command('tab').description('Manage tabs');
|
|
425
522
|
tab
|
|
426
|
-
.command('add <
|
|
523
|
+
.command('add <url>')
|
|
524
|
+
.addArgument(hiddenLegacyArg('legacyUrl'))
|
|
427
525
|
.description('Open URL in new tab (becomes current)')
|
|
526
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
428
527
|
.option('-p, --profile <name>', 'Browser profile')
|
|
429
|
-
.action(async (
|
|
528
|
+
.action(async (arg1, arg2, opts) => {
|
|
529
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
530
|
+
const [url] = positionals;
|
|
430
531
|
const response = await sendIPCRequest({
|
|
431
532
|
action: 'tab-add',
|
|
432
533
|
task,
|
|
@@ -440,9 +541,13 @@ function registerTaskCommands(browser) {
|
|
|
440
541
|
console.log(`Opened tab ${response.tabId}: ${url}`);
|
|
441
542
|
});
|
|
442
543
|
tab
|
|
443
|
-
.command('focus <
|
|
544
|
+
.command('focus <tabId>')
|
|
545
|
+
.addArgument(hiddenLegacyArg('legacyTabId'))
|
|
444
546
|
.description('Switch to tab (by ID, prefix, or URL substring)')
|
|
445
|
-
.
|
|
547
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
548
|
+
.action(async (arg1, arg2, opts) => {
|
|
549
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
550
|
+
const [tabId] = positionals;
|
|
446
551
|
const response = await sendIPCRequest({
|
|
447
552
|
action: 'tab-focus',
|
|
448
553
|
task,
|
|
@@ -455,9 +560,11 @@ function registerTaskCommands(browser) {
|
|
|
455
560
|
console.log(`Focused tab ${response.tabId}`);
|
|
456
561
|
});
|
|
457
562
|
tab
|
|
458
|
-
.command('close
|
|
563
|
+
.command('close [tabId]')
|
|
459
564
|
.description('Close tab(s) — omit tabId to close all')
|
|
460
|
-
.
|
|
565
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
566
|
+
.action(async (tabId, opts) => {
|
|
567
|
+
const task = resolveTaskName(opts);
|
|
461
568
|
const response = await sendIPCRequest({
|
|
462
569
|
action: 'tab-close',
|
|
463
570
|
task,
|
|
@@ -469,11 +576,13 @@ function registerTaskCommands(browser) {
|
|
|
469
576
|
}
|
|
470
577
|
console.log(tabId ? `Closed tab ${tabId}` : `Closed all tabs for ${task}`);
|
|
471
578
|
});
|
|
472
|
-
|
|
473
|
-
.command('
|
|
474
|
-
.description('List tabs for
|
|
579
|
+
browser
|
|
580
|
+
.command('tabs')
|
|
581
|
+
.description('List tabs open for the current task')
|
|
582
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
475
583
|
.option('--json', 'Output machine-readable JSON')
|
|
476
|
-
.action(async (
|
|
584
|
+
.action(async (opts) => {
|
|
585
|
+
const task = resolveTaskName(opts);
|
|
477
586
|
const response = await sendIPCRequest({
|
|
478
587
|
action: 'tab-list',
|
|
479
588
|
task,
|
|
@@ -503,27 +612,45 @@ function registerTaskCommands(browser) {
|
|
|
503
612
|
}
|
|
504
613
|
});
|
|
505
614
|
browser
|
|
506
|
-
.command('screenshot
|
|
507
|
-
.description('Take a screenshot')
|
|
508
|
-
.option(
|
|
509
|
-
.
|
|
615
|
+
.command('screenshot [tabId]')
|
|
616
|
+
.description('Take a screenshot — auto-saved per task; --output only needed when you want a specific path')
|
|
617
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
618
|
+
.option('-o, --output <path>', 'Specific output path (otherwise auto-saved under sessions/<task>/)')
|
|
619
|
+
.option('-q, --quality <mode>', 'compressed (JPEG, capped at ~100 KB — default) or raw (PNG, pixel-faithful)', 'compressed')
|
|
620
|
+
.action(async (tabId, opts) => {
|
|
621
|
+
const task = resolveTaskName(opts);
|
|
622
|
+
if (opts.quality !== 'compressed' && opts.quality !== 'raw') {
|
|
623
|
+
console.error('--quality must be "compressed" or "raw"');
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
510
626
|
const response = await sendIPCRequest({
|
|
511
627
|
action: 'screenshot',
|
|
512
628
|
task,
|
|
513
629
|
tabId,
|
|
514
630
|
path: opts.output,
|
|
631
|
+
quality: opts.quality,
|
|
515
632
|
});
|
|
516
633
|
if (!response.ok) {
|
|
517
634
|
console.error(response.error);
|
|
518
635
|
process.exit(1);
|
|
519
636
|
}
|
|
637
|
+
// stdout: just the path, so `P=$(agents browser screenshot)` works.
|
|
520
638
|
console.log(response.path);
|
|
639
|
+
// stderr: human commentary with size + dimensions, so an agent can
|
|
640
|
+
// see at a glance what was captured without `ls -l && file` round-trips.
|
|
641
|
+
const size = humanizeBytes(response.bytes);
|
|
642
|
+
const dims = response.width && response.height ? `${response.width}×${response.height}` : 'unknown size';
|
|
643
|
+
console.error(`Saved screenshot to ${response.path} (${size}, ${dims})`);
|
|
521
644
|
});
|
|
522
645
|
browser
|
|
523
|
-
.command('evaluate <
|
|
646
|
+
.command('evaluate <expression>')
|
|
647
|
+
.addArgument(hiddenLegacyArg('legacyExpression'))
|
|
524
648
|
.description('Evaluate JavaScript in current tab')
|
|
649
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
525
650
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
526
|
-
.action(async (
|
|
651
|
+
.action(async (arg1, arg2, opts) => {
|
|
652
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
653
|
+
const [expression] = positionals;
|
|
527
654
|
const response = await sendIPCRequest({
|
|
528
655
|
action: 'evaluate',
|
|
529
656
|
task,
|
|
@@ -749,13 +876,16 @@ function registerTaskCommands(browser) {
|
|
|
749
876
|
}
|
|
750
877
|
});
|
|
751
878
|
browser
|
|
752
|
-
.command('refs
|
|
879
|
+
.command('refs')
|
|
880
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
753
881
|
.description('Get DOM refs for interactive elements')
|
|
882
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
754
883
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
755
884
|
.option('--all', 'Include non-interactive elements')
|
|
756
885
|
.option('-l, --limit <n>', 'Max elements (default 500)', '500')
|
|
757
886
|
.option('--json', 'Output machine-readable JSON')
|
|
758
|
-
.action(async (
|
|
887
|
+
.action(async (legacyTask, opts) => {
|
|
888
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
759
889
|
const response = await sendIPCRequest({
|
|
760
890
|
action: 'refs',
|
|
761
891
|
task,
|
|
@@ -779,10 +909,14 @@ function registerTaskCommands(browser) {
|
|
|
779
909
|
console.log(response.refs);
|
|
780
910
|
});
|
|
781
911
|
browser
|
|
782
|
-
.command('click <
|
|
912
|
+
.command('click <ref>')
|
|
913
|
+
.addArgument(hiddenLegacyArg('legacyRef'))
|
|
783
914
|
.description('Click an element by ref')
|
|
915
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
784
916
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
785
|
-
.action(async (
|
|
917
|
+
.action(async (arg1, arg2, opts) => {
|
|
918
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
919
|
+
const [ref] = positionals;
|
|
786
920
|
const response = await sendIPCRequest({
|
|
787
921
|
action: 'click',
|
|
788
922
|
task,
|
|
@@ -796,11 +930,15 @@ function registerTaskCommands(browser) {
|
|
|
796
930
|
console.log('Clicked');
|
|
797
931
|
});
|
|
798
932
|
browser
|
|
799
|
-
.command('type <
|
|
933
|
+
.command('type <ref> <text>')
|
|
934
|
+
.addArgument(hiddenLegacyArg('legacyText'))
|
|
800
935
|
.description('Type text into an element by ref')
|
|
936
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
801
937
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
802
938
|
.option('--clear', 'Clear editor content before typing')
|
|
803
|
-
.action(async (
|
|
939
|
+
.action(async (arg1, arg2, arg3, opts) => {
|
|
940
|
+
const { task, positionals } = unpackPositionals([arg1, arg2, arg3], 2, opts);
|
|
941
|
+
const [ref, text] = positionals;
|
|
804
942
|
const response = await sendIPCRequest({
|
|
805
943
|
action: 'type',
|
|
806
944
|
task,
|
|
@@ -816,10 +954,14 @@ function registerTaskCommands(browser) {
|
|
|
816
954
|
console.log('Typed');
|
|
817
955
|
});
|
|
818
956
|
browser
|
|
819
|
-
.command('press <
|
|
957
|
+
.command('press <key>')
|
|
958
|
+
.addArgument(hiddenLegacyArg('legacyKey'))
|
|
820
959
|
.description('Press a key (Enter, Tab, Escape, etc)')
|
|
960
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
821
961
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
822
|
-
.action(async (
|
|
962
|
+
.action(async (arg1, arg2, opts) => {
|
|
963
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
964
|
+
const [key] = positionals;
|
|
823
965
|
const response = await sendIPCRequest({
|
|
824
966
|
action: 'press',
|
|
825
967
|
task,
|
|
@@ -833,10 +975,14 @@ function registerTaskCommands(browser) {
|
|
|
833
975
|
console.log('Pressed');
|
|
834
976
|
});
|
|
835
977
|
browser
|
|
836
|
-
.command('hover <
|
|
978
|
+
.command('hover <ref>')
|
|
979
|
+
.addArgument(hiddenLegacyArg('legacyRef'))
|
|
837
980
|
.description('Hover over an element by ref')
|
|
981
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
838
982
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
839
|
-
.action(async (
|
|
983
|
+
.action(async (arg1, arg2, opts) => {
|
|
984
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
985
|
+
const [ref] = positionals;
|
|
840
986
|
const response = await sendIPCRequest({
|
|
841
987
|
action: 'hover',
|
|
842
988
|
task,
|
|
@@ -850,12 +996,16 @@ function registerTaskCommands(browser) {
|
|
|
850
996
|
console.log('Hovered');
|
|
851
997
|
});
|
|
852
998
|
browser
|
|
853
|
-
.command('scroll <
|
|
999
|
+
.command('scroll <deltaX> <deltaY>')
|
|
1000
|
+
.addArgument(hiddenLegacyArg('legacyDeltaY'))
|
|
854
1001
|
.description('Scroll the page by pixel amount')
|
|
1002
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
855
1003
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
856
1004
|
.option('-x, --at-x <x>', 'X coordinate to dispatch scroll from (default 0)', parseInt)
|
|
857
1005
|
.option('-y, --at-y <y>', 'Y coordinate to dispatch scroll from (default 0)', parseInt)
|
|
858
|
-
.action(async (
|
|
1006
|
+
.action(async (arg1, arg2, arg3, opts) => {
|
|
1007
|
+
const { task, positionals } = unpackPositionals([arg1, arg2, arg3], 2, opts);
|
|
1008
|
+
const [deltaX, deltaY] = positionals;
|
|
859
1009
|
const response = await sendIPCRequest({
|
|
860
1010
|
action: 'scroll',
|
|
861
1011
|
task,
|
|
@@ -872,8 +1022,10 @@ function registerTaskCommands(browser) {
|
|
|
872
1022
|
console.log('Scrolled');
|
|
873
1023
|
});
|
|
874
1024
|
browser
|
|
875
|
-
.command('upload
|
|
1025
|
+
.command('upload')
|
|
1026
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
876
1027
|
.description('Upload file(s) — supports hidden file inputs, drag-drop targets, and OS chooser interception')
|
|
1028
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
877
1029
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
878
1030
|
.option('-r, --ref <n>', 'Ref of the upload target element (file input or drop zone)', (v) => parseInt(v, 10))
|
|
879
1031
|
.option('--trigger <n>', 'Ref of a button that opens the OS file chooser (Pattern C)', (v) => parseInt(v, 10))
|
|
@@ -881,7 +1033,8 @@ function registerTaskCommands(browser) {
|
|
|
881
1033
|
.option('--drop', 'Force drag-drop pattern even if ref is an <input type=file>')
|
|
882
1034
|
.option('--input', 'Force file-input pattern (DOM.setFileInputFiles)')
|
|
883
1035
|
.option('--timeout <ms>', 'Timeout for chooser interception (Pattern C)', (v) => parseInt(v, 10))
|
|
884
|
-
.action(async (
|
|
1036
|
+
.action(async (legacyTask, opts) => {
|
|
1037
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
885
1038
|
const files = opts.file ?? [];
|
|
886
1039
|
if (files.length === 0) {
|
|
887
1040
|
console.error('--file <path> is required (repeat for multiple files)');
|
|
@@ -921,12 +1074,16 @@ function registerTaskCommands(browser) {
|
|
|
921
1074
|
// ─── Viewport & Device ───────────────────────────────────────────────────────
|
|
922
1075
|
const setCmd = browser.command('set').description('Set browser emulation options');
|
|
923
1076
|
setCmd
|
|
924
|
-
.command('viewport <
|
|
1077
|
+
.command('viewport <width> <height>')
|
|
1078
|
+
.addArgument(hiddenLegacyArg('legacyHeight'))
|
|
925
1079
|
.description('Set viewport size')
|
|
1080
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
926
1081
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
927
1082
|
.option('-m, --mobile', 'Enable mobile emulation')
|
|
928
1083
|
.option('-s, --scale <factor>', 'Device scale factor', parseFloat)
|
|
929
|
-
.action(async (
|
|
1084
|
+
.action(async (arg1, arg2, arg3, opts) => {
|
|
1085
|
+
const { task, positionals } = unpackPositionals([arg1, arg2, arg3], 2, opts);
|
|
1086
|
+
const [width, height] = positionals;
|
|
930
1087
|
const response = await sendIPCRequest({
|
|
931
1088
|
action: 'set-viewport',
|
|
932
1089
|
task,
|
|
@@ -943,10 +1100,14 @@ function registerTaskCommands(browser) {
|
|
|
943
1100
|
console.log(`Viewport set to ${width}x${height}${opts.mobile ? ' (mobile)' : ''}`);
|
|
944
1101
|
});
|
|
945
1102
|
setCmd
|
|
946
|
-
.command('device <
|
|
1103
|
+
.command('device <device-name>')
|
|
1104
|
+
.addArgument(hiddenLegacyArg('legacyDeviceName'))
|
|
947
1105
|
.description('Emulate a device (iPhone 14, iPad, MacBook Pro)')
|
|
1106
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
948
1107
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
949
|
-
.action(async (
|
|
1108
|
+
.action(async (arg1, arg2, opts) => {
|
|
1109
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
1110
|
+
const [deviceName] = positionals;
|
|
950
1111
|
const response = await sendIPCRequest({
|
|
951
1112
|
action: 'set-device',
|
|
952
1113
|
task,
|
|
@@ -971,12 +1132,15 @@ function registerTaskCommands(browser) {
|
|
|
971
1132
|
});
|
|
972
1133
|
// ─── Console & Errors ────────────────────────────────────────────────────────
|
|
973
1134
|
browser
|
|
974
|
-
.command('console
|
|
1135
|
+
.command('console')
|
|
1136
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
975
1137
|
.description('Read console logs from a tab')
|
|
1138
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
976
1139
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
977
1140
|
.option('-l, --level <level>', 'Filter by level (log, info, warn, error)')
|
|
978
1141
|
.option('--clear', 'Clear logs after reading')
|
|
979
|
-
.action(async (
|
|
1142
|
+
.action(async (legacyTask, opts) => {
|
|
1143
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
980
1144
|
const response = await sendIPCRequest({
|
|
981
1145
|
action: 'console',
|
|
982
1146
|
task,
|
|
@@ -999,11 +1163,14 @@ function registerTaskCommands(browser) {
|
|
|
999
1163
|
}
|
|
1000
1164
|
});
|
|
1001
1165
|
browser
|
|
1002
|
-
.command('errors
|
|
1166
|
+
.command('errors')
|
|
1167
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1003
1168
|
.description('Read page errors from a tab')
|
|
1169
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1004
1170
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1005
1171
|
.option('--clear', 'Clear errors after reading')
|
|
1006
|
-
.action(async (
|
|
1172
|
+
.action(async (legacyTask, opts) => {
|
|
1173
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
1007
1174
|
const response = await sendIPCRequest({
|
|
1008
1175
|
action: 'errors',
|
|
1009
1176
|
task,
|
|
@@ -1029,12 +1196,15 @@ function registerTaskCommands(browser) {
|
|
|
1029
1196
|
});
|
|
1030
1197
|
// ─── Network ─────────────────────────────────────────────────────────────────
|
|
1031
1198
|
browser
|
|
1032
|
-
.command('requests
|
|
1199
|
+
.command('requests')
|
|
1200
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1033
1201
|
.description('Read captured network requests')
|
|
1202
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1034
1203
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1035
1204
|
.option('-f, --filter <text>', 'Filter URLs containing text')
|
|
1036
1205
|
.option('--clear', 'Clear requests after reading')
|
|
1037
|
-
.action(async (
|
|
1206
|
+
.action(async (legacyTask, opts) => {
|
|
1207
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
1038
1208
|
const response = await sendIPCRequest({
|
|
1039
1209
|
action: 'requests',
|
|
1040
1210
|
task,
|
|
@@ -1058,12 +1228,16 @@ function registerTaskCommands(browser) {
|
|
|
1058
1228
|
}
|
|
1059
1229
|
});
|
|
1060
1230
|
browser
|
|
1061
|
-
.command('responsebody <
|
|
1231
|
+
.command('responsebody <url-pattern>')
|
|
1232
|
+
.addArgument(hiddenLegacyArg('legacyUrlPattern'))
|
|
1062
1233
|
.description('Wait for and read a response body by URL pattern')
|
|
1234
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1063
1235
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1064
1236
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1065
1237
|
.option('--max-chars <n>', 'Max characters to return', parseInt)
|
|
1066
|
-
.action(async (
|
|
1238
|
+
.action(async (arg1, arg2, opts) => {
|
|
1239
|
+
const { task, positionals } = unpackPositionals([arg1, arg2], 1, opts);
|
|
1240
|
+
const [urlPattern] = positionals;
|
|
1067
1241
|
const response = await sendIPCRequest({
|
|
1068
1242
|
action: 'response-body',
|
|
1069
1243
|
task,
|
|
@@ -1080,8 +1254,10 @@ function registerTaskCommands(browser) {
|
|
|
1080
1254
|
});
|
|
1081
1255
|
// ─── Wait ────────────────────────────────────────────────────────────────────
|
|
1082
1256
|
browser
|
|
1083
|
-
.command('wait
|
|
1257
|
+
.command('wait')
|
|
1258
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1084
1259
|
.description('Wait for a condition')
|
|
1260
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1085
1261
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1086
1262
|
.option('--time <ms>', 'Wait for milliseconds')
|
|
1087
1263
|
.option('--selector <css>', 'Wait for CSS selector to appear')
|
|
@@ -1089,7 +1265,8 @@ function registerTaskCommands(browser) {
|
|
|
1089
1265
|
.option('--fn <js>', 'Wait for JS expression to return truthy')
|
|
1090
1266
|
.option('--state <state>', 'Wait for load state (domcontentloaded, load, networkidle)')
|
|
1091
1267
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1092
|
-
.action(async (
|
|
1268
|
+
.action(async (legacyTask, opts) => {
|
|
1269
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
1093
1270
|
let waitType;
|
|
1094
1271
|
let waitValue;
|
|
1095
1272
|
if (opts.time) {
|
|
@@ -1132,11 +1309,14 @@ function registerTaskCommands(browser) {
|
|
|
1132
1309
|
});
|
|
1133
1310
|
// ─── Downloads ───────────────────────────────────────────────────────────────
|
|
1134
1311
|
browser
|
|
1135
|
-
.command('download
|
|
1312
|
+
.command('download')
|
|
1313
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1136
1314
|
.description('Set download directory for a task')
|
|
1315
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1137
1316
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1138
1317
|
.requiredOption('-p, --path <dir>', 'Download directory path')
|
|
1139
|
-
.action(async (
|
|
1318
|
+
.action(async (legacyTask, opts) => {
|
|
1319
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
1140
1320
|
const response = await sendIPCRequest({
|
|
1141
1321
|
action: 'set-download-path',
|
|
1142
1322
|
task,
|
|
@@ -1149,11 +1329,59 @@ function registerTaskCommands(browser) {
|
|
|
1149
1329
|
}
|
|
1150
1330
|
console.log(`Download path set to ${opts.path}`);
|
|
1151
1331
|
});
|
|
1332
|
+
// ─── Recording ─────────────────────────────────────────────────────────────
|
|
1333
|
+
const record = browser.command('record').description('Record a video of the page');
|
|
1334
|
+
record
|
|
1335
|
+
.command('start')
|
|
1336
|
+
.description('Start recording — auto-saved under sessions/<task>/recordings/. Bounded by --fps, --duration, --max-mb.')
|
|
1337
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1338
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1339
|
+
.option('--fps <n>', 'Frames per second (1–30, default 5)', (v) => parseInt(v, 10))
|
|
1340
|
+
.option('--duration <sec>', 'Hard duration cap in seconds (default 60)', (v) => parseInt(v, 10))
|
|
1341
|
+
.option('--max-mb <mb>', 'Stop when output exceeds this many MB (default 25)', (v) => parseInt(v, 10))
|
|
1342
|
+
.action(async (opts) => {
|
|
1343
|
+
const task = resolveTaskName(opts);
|
|
1344
|
+
const response = await sendIPCRequest({
|
|
1345
|
+
action: 'record-start',
|
|
1346
|
+
task,
|
|
1347
|
+
tabId: opts.tab,
|
|
1348
|
+
fps: opts.fps,
|
|
1349
|
+
duration: opts.duration,
|
|
1350
|
+
maxMb: opts.maxMb,
|
|
1351
|
+
});
|
|
1352
|
+
if (!response.ok) {
|
|
1353
|
+
console.error(response.error);
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
// stdout: path (for capture into a variable). stderr: human commentary.
|
|
1357
|
+
console.log(response.path);
|
|
1358
|
+
console.error(`Recording task "${task}" at ${response.fps} fps (cap ${response.durationCapSec}s / ${response.maxMb} MB) → ${response.path}`);
|
|
1359
|
+
console.error('Stop with: agents browser record stop');
|
|
1360
|
+
});
|
|
1361
|
+
record
|
|
1362
|
+
.command('stop')
|
|
1363
|
+
.description('Stop an in-progress recording')
|
|
1364
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1365
|
+
.action(async (opts) => {
|
|
1366
|
+
const task = resolveTaskName(opts);
|
|
1367
|
+
const response = await sendIPCRequest({ action: 'record-stop', task });
|
|
1368
|
+
if (!response.ok) {
|
|
1369
|
+
console.error(response.error);
|
|
1370
|
+
process.exit(1);
|
|
1371
|
+
}
|
|
1372
|
+
console.log(response.path);
|
|
1373
|
+
const size = humanizeBytes(response.bytes);
|
|
1374
|
+
const seconds = ((response.durationMs ?? 0) / 1000).toFixed(1);
|
|
1375
|
+
console.error(`Saved recording to ${response.path} (${size}, ${seconds}s, stopped: ${response.stopReason})`);
|
|
1376
|
+
});
|
|
1152
1377
|
browser
|
|
1153
|
-
.command('waitdownload
|
|
1378
|
+
.command('waitdownload')
|
|
1379
|
+
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1154
1380
|
.description('Wait for a download to complete')
|
|
1381
|
+
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1155
1382
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1156
|
-
.action(async (
|
|
1383
|
+
.action(async (legacyTask, opts) => {
|
|
1384
|
+
const { task } = unpackPositionals([legacyTask], 0, opts);
|
|
1157
1385
|
const response = await sendIPCRequest({
|
|
1158
1386
|
action: 'wait-download',
|
|
1159
1387
|
task,
|
|
@@ -1180,6 +1408,15 @@ function formatAge(timestamp) {
|
|
|
1180
1408
|
const hours = Math.floor(minutes / 60);
|
|
1181
1409
|
return `${hours}h ago`;
|
|
1182
1410
|
}
|
|
1411
|
+
function humanizeBytes(n) {
|
|
1412
|
+
if (n === undefined)
|
|
1413
|
+
return 'unknown size';
|
|
1414
|
+
if (n < 1024)
|
|
1415
|
+
return `${n} B`;
|
|
1416
|
+
if (n < 1024 * 1024)
|
|
1417
|
+
return `${(n / 1024).toFixed(0)} KB`;
|
|
1418
|
+
return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
1419
|
+
}
|
|
1183
1420
|
function formatDuration(ms) {
|
|
1184
1421
|
const seconds = Math.floor(ms / 1000);
|
|
1185
1422
|
if (seconds < 60)
|