@phnx-labs/agents-cli 1.18.4 → 1.18.6
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 +18 -7
- package/README.md +1 -1
- package/dist/commands/browser.js +275 -141
- package/dist/commands/factory.js +13 -1
- package/dist/commands/rules.js +14 -0
- package/dist/commands/secrets.js +66 -11
- package/dist/lib/browser/cdp.js +7 -1
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +52 -26
- package/dist/lib/browser/drivers/local.js +29 -2
- package/dist/lib/browser/drivers/ssh.js +82 -7
- package/dist/lib/browser/ipc.js +55 -0
- package/dist/lib/browser/profiles.d.ts +46 -2
- package/dist/lib/browser/profiles.js +123 -19
- package/dist/lib/browser/runtime-state.d.ts +117 -0
- package/dist/lib/browser/runtime-state.js +259 -0
- package/dist/lib/browser/service.d.ts +16 -0
- package/dist/lib/browser/service.js +163 -16
- package/dist/lib/browser/types.d.ts +13 -1
- package/dist/lib/daemon.js +36 -3
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/secrets/bundles.d.ts +20 -0
- package/dist/lib/secrets/bundles.js +56 -0
- package/dist/lib/secrets/index.js +8 -8
- package/dist/lib/teams/agents.d.ts +1 -1
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/version.js +25 -0
- package/package.json +1 -1
package/dist/commands/browser.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
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
|
-
}
|
|
11
1
|
import * as fs from 'fs';
|
|
12
2
|
import * as path from 'path';
|
|
13
3
|
import { listProfiles, getProfile, createProfile, deleteProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, } from '../lib/browser/profiles.js';
|
|
14
4
|
import { findBrowserPath, getPortOccupant } from '../lib/browser/chrome.js';
|
|
5
|
+
import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
|
|
15
6
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
16
7
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
17
8
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
@@ -22,27 +13,14 @@ import { registerCommandGroups } from '../lib/help.js';
|
|
|
22
13
|
/**
|
|
23
14
|
* Resolve which browser task a command targets. Order:
|
|
24
15
|
* 1. `--task <name>` flag (explicit per-command override)
|
|
25
|
-
* 2.
|
|
26
|
-
* 3. `$AGENTS_BROWSER_TASK` (set once at the start of an agent run)
|
|
16
|
+
* 2. `$AGENTS_BROWSER_TASK` (set once at the start of an agent run)
|
|
27
17
|
*
|
|
28
18
|
* Each agent process has its own environment, so the env-var path is safe for
|
|
29
19
|
* parallel agents — they can't see each other's value.
|
|
30
20
|
*/
|
|
31
|
-
|
|
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) {
|
|
21
|
+
function resolveTaskName(opts) {
|
|
40
22
|
if (opts.task)
|
|
41
23
|
return opts.task;
|
|
42
|
-
if (legacyTaskArg !== undefined && legacyTaskArg !== '') {
|
|
43
|
-
warnLegacyTaskPositional();
|
|
44
|
-
return legacyTaskArg;
|
|
45
|
-
}
|
|
46
24
|
const fromEnv = process.env.AGENTS_BROWSER_TASK;
|
|
47
25
|
if (fromEnv)
|
|
48
26
|
return fromEnv;
|
|
@@ -50,21 +28,6 @@ function resolveTaskName(opts, legacyTaskArg) {
|
|
|
50
28
|
console.error('Tip: T=$(agents browser start --profile <p>) && export AGENTS_BROWSER_TASK=$T');
|
|
51
29
|
process.exit(1);
|
|
52
30
|
}
|
|
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
31
|
// `-t` is taken by `--tab` on most commands, so `--task` is long-form only.
|
|
69
32
|
// Agents normally set $AGENTS_BROWSER_TASK once and never type this flag.
|
|
70
33
|
const TASK_OPTION_FLAG = '--task <name>';
|
|
@@ -81,7 +44,7 @@ const BROWSER_HELP_GROUPS = [
|
|
|
81
44
|
},
|
|
82
45
|
{
|
|
83
46
|
title: 'Capture evidence',
|
|
84
|
-
names: ['console', 'errors', 'requests', 'responsebody', 'record'],
|
|
47
|
+
names: ['console', 'errors', 'requests', 'responsebody', 'record', 'logs'],
|
|
85
48
|
},
|
|
86
49
|
];
|
|
87
50
|
export function registerBrowserCommand(program) {
|
|
@@ -279,10 +242,32 @@ function registerProfilesCommands(browser) {
|
|
|
279
242
|
});
|
|
280
243
|
profiles
|
|
281
244
|
.command('delete <name>')
|
|
282
|
-
.description('Delete a browser profile')
|
|
283
|
-
.
|
|
245
|
+
.description('Delete a browser profile (drops YAML config + all cached runtime dirs)')
|
|
246
|
+
.option('--keep-cache', "Leave ~/.agents/.cache/browser/<name>* dirs in place (don't wipe chrome-data)")
|
|
247
|
+
.action(async (name, opts) => {
|
|
284
248
|
await deleteProfile(name);
|
|
285
|
-
|
|
249
|
+
// The composite naming change introduced multiple cache dirs per
|
|
250
|
+
// profile (`<name>`, `<name>@endpoint-0`, …). Sweep them all unless
|
|
251
|
+
// the user explicitly wants the chrome-data preserved (e.g. for
|
|
252
|
+
// re-import into a freshly-created profile of the same name).
|
|
253
|
+
let removed = 0;
|
|
254
|
+
if (!opts.keepCache) {
|
|
255
|
+
const cacheDirs = listProfileCacheDirs(name);
|
|
256
|
+
removed = cacheDirs.length;
|
|
257
|
+
for (const dir of cacheDirs) {
|
|
258
|
+
// `removeProfileCache` operates by profile-name; for the
|
|
259
|
+
// composite dirs we already have the absolute path. Use rmSync
|
|
260
|
+
// directly so we don't depend on naming round-trips.
|
|
261
|
+
try {
|
|
262
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
catch { /* ignore */ }
|
|
265
|
+
}
|
|
266
|
+
// The canonical wipe also covers the legacy dir if present.
|
|
267
|
+
removeProfileCache(name);
|
|
268
|
+
}
|
|
269
|
+
console.log(`Deleted profile: ${name}` +
|
|
270
|
+
(removed > 0 ? ` (and ${removed} cache dir${removed === 1 ? '' : 's'})` : ''));
|
|
286
271
|
});
|
|
287
272
|
profiles
|
|
288
273
|
.command('doctor <name>')
|
|
@@ -306,12 +291,31 @@ function registerProfilesCommands(browser) {
|
|
|
306
291
|
detail: err instanceof Error ? err.message : String(err),
|
|
307
292
|
});
|
|
308
293
|
}
|
|
309
|
-
// 2. Configured port
|
|
294
|
+
// 2. Configured port. For local cdp:// we check the local port. For
|
|
295
|
+
// ssh:// the port lives on a remote host — doctor's previous
|
|
296
|
+
// behavior was to lsof the LOCAL port number, which was both
|
|
297
|
+
// misleading and arbitrary (after the SSH-binds-locally change
|
|
298
|
+
// the local port now matches the remote, so a positive answer
|
|
299
|
+
// is plausible; but doctor still shouldn't report on remote
|
|
300
|
+
// state without an --remote-probe explicitly).
|
|
310
301
|
const port = extractConfiguredPort(profile);
|
|
311
302
|
let attachingToExistingBrowser = false;
|
|
303
|
+
const firstEndpointTarget = (() => {
|
|
304
|
+
const presets = getEndpointPresets(profile);
|
|
305
|
+
const first = Object.keys(presets)[0];
|
|
306
|
+
return first ? presets[first].target : undefined;
|
|
307
|
+
})();
|
|
308
|
+
const isSshEndpoint = firstEndpointTarget?.startsWith('ssh://') ?? false;
|
|
312
309
|
if (port === undefined) {
|
|
313
310
|
checks.push({ label: 'port', ok: true, detail: 'no port in endpoint' });
|
|
314
311
|
}
|
|
312
|
+
else if (isSshEndpoint) {
|
|
313
|
+
checks.push({
|
|
314
|
+
label: 'port',
|
|
315
|
+
ok: true,
|
|
316
|
+
detail: `${port} (remote on ${firstEndpointTarget}) — skipping local check`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
315
319
|
else {
|
|
316
320
|
const occupant = getPortOccupant(port);
|
|
317
321
|
if (!occupant) {
|
|
@@ -464,11 +468,10 @@ function registerTaskCommands(browser) {
|
|
|
464
468
|
});
|
|
465
469
|
browser
|
|
466
470
|
.command('done')
|
|
467
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
468
471
|
.description('Complete a task and close its tabs')
|
|
469
472
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
470
|
-
.action(async (
|
|
471
|
-
const
|
|
473
|
+
.action(async (opts) => {
|
|
474
|
+
const task = resolveTaskName(opts);
|
|
472
475
|
const response = await sendIPCRequest({
|
|
473
476
|
action: 'done',
|
|
474
477
|
task,
|
|
@@ -481,11 +484,23 @@ function registerTaskCommands(browser) {
|
|
|
481
484
|
});
|
|
482
485
|
browser
|
|
483
486
|
.command('stop')
|
|
484
|
-
.
|
|
485
|
-
.description('Stop a browser task and close its tabs')
|
|
487
|
+
.description('Stop a browser task and close its tabs; with --profile, detach the whole profile (close browser + drop cached connection)')
|
|
486
488
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
487
|
-
.
|
|
488
|
-
|
|
489
|
+
.option('-p, --profile <name>', 'Detach the whole profile (incl. composite "name@endpoint") instead of stopping a single task')
|
|
490
|
+
.action(async (opts) => {
|
|
491
|
+
if (opts.profile) {
|
|
492
|
+
const response = await sendIPCRequest({
|
|
493
|
+
action: 'stop',
|
|
494
|
+
profile: opts.profile,
|
|
495
|
+
});
|
|
496
|
+
if (!response.ok) {
|
|
497
|
+
console.error(response.error);
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
console.log(`Stopped profile: ${opts.profile}`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const task = resolveTaskName(opts);
|
|
489
504
|
const response = await sendIPCRequest({
|
|
490
505
|
action: 'stop',
|
|
491
506
|
task,
|
|
@@ -497,57 +512,53 @@ function registerTaskCommands(browser) {
|
|
|
497
512
|
console.log(`Stopped task: ${task}`);
|
|
498
513
|
});
|
|
499
514
|
browser
|
|
500
|
-
.command('navigate
|
|
501
|
-
.addArgument(hiddenLegacyArg('legacyUrl'))
|
|
515
|
+
.command('navigate')
|
|
502
516
|
.description('Navigate current tab to URL (creates tab if none exist)')
|
|
503
517
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
518
|
+
.requiredOption('--url <url>', 'URL to navigate to')
|
|
504
519
|
.option('-p, --profile <name>', 'Browser profile (optional if task is unique)')
|
|
505
|
-
.action(async (
|
|
506
|
-
const
|
|
507
|
-
const [url] = positionals;
|
|
520
|
+
.action(async (opts) => {
|
|
521
|
+
const task = resolveTaskName(opts);
|
|
508
522
|
const response = await sendIPCRequest({
|
|
509
523
|
action: 'navigate',
|
|
510
524
|
task,
|
|
511
|
-
url,
|
|
525
|
+
url: opts.url,
|
|
512
526
|
profile: opts.profile,
|
|
513
527
|
});
|
|
514
528
|
if (!response.ok) {
|
|
515
529
|
console.error(response.error);
|
|
516
530
|
process.exit(1);
|
|
517
531
|
}
|
|
518
|
-
console.log(`Navigated ${response.tabId} to ${url}`);
|
|
532
|
+
console.log(`Navigated ${response.tabId} to ${opts.url}`);
|
|
519
533
|
});
|
|
520
534
|
// Tab subcommand group
|
|
521
535
|
const tab = browser.command('tab').description('Manage tabs');
|
|
522
536
|
tab
|
|
523
|
-
.command('add
|
|
524
|
-
.addArgument(hiddenLegacyArg('legacyUrl'))
|
|
537
|
+
.command('add')
|
|
525
538
|
.description('Open URL in new tab (becomes current)')
|
|
526
539
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
540
|
+
.requiredOption('--url <url>', 'URL to open in the new tab')
|
|
527
541
|
.option('-p, --profile <name>', 'Browser profile')
|
|
528
|
-
.action(async (
|
|
529
|
-
const
|
|
530
|
-
const [url] = positionals;
|
|
542
|
+
.action(async (opts) => {
|
|
543
|
+
const task = resolveTaskName(opts);
|
|
531
544
|
const response = await sendIPCRequest({
|
|
532
545
|
action: 'tab-add',
|
|
533
546
|
task,
|
|
534
|
-
url,
|
|
547
|
+
url: opts.url,
|
|
535
548
|
profile: opts.profile,
|
|
536
549
|
});
|
|
537
550
|
if (!response.ok) {
|
|
538
551
|
console.error(response.error);
|
|
539
552
|
process.exit(1);
|
|
540
553
|
}
|
|
541
|
-
console.log(`Opened tab ${response.tabId}: ${url}`);
|
|
554
|
+
console.log(`Opened tab ${response.tabId}: ${opts.url}`);
|
|
542
555
|
});
|
|
543
556
|
tab
|
|
544
557
|
.command('focus <tabId>')
|
|
545
|
-
.addArgument(hiddenLegacyArg('legacyTabId'))
|
|
546
558
|
.description('Switch to tab (by ID, prefix, or URL substring)')
|
|
547
559
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
548
|
-
.action(async (
|
|
549
|
-
const
|
|
550
|
-
const [tabId] = positionals;
|
|
560
|
+
.action(async (tabId, opts) => {
|
|
561
|
+
const task = resolveTaskName(opts);
|
|
551
562
|
const response = await sendIPCRequest({
|
|
552
563
|
action: 'tab-focus',
|
|
553
564
|
task,
|
|
@@ -612,12 +623,13 @@ function registerTaskCommands(browser) {
|
|
|
612
623
|
}
|
|
613
624
|
});
|
|
614
625
|
browser
|
|
615
|
-
.command('screenshot
|
|
626
|
+
.command('screenshot')
|
|
616
627
|
.description('Take a screenshot — auto-saved per task; --output only needed when you want a specific path')
|
|
617
628
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
629
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
618
630
|
.option('-o, --output <path>', 'Specific output path (otherwise auto-saved under sessions/<task>/)')
|
|
619
631
|
.option('-q, --quality <mode>', 'compressed (JPEG, capped at ~100 KB — default) or raw (PNG, pixel-faithful)', 'compressed')
|
|
620
|
-
.action(async (
|
|
632
|
+
.action(async (opts) => {
|
|
621
633
|
const task = resolveTaskName(opts);
|
|
622
634
|
if (opts.quality !== 'compressed' && opts.quality !== 'raw') {
|
|
623
635
|
console.error('--quality must be "compressed" or "raw"');
|
|
@@ -626,7 +638,7 @@ function registerTaskCommands(browser) {
|
|
|
626
638
|
const response = await sendIPCRequest({
|
|
627
639
|
action: 'screenshot',
|
|
628
640
|
task,
|
|
629
|
-
tabId,
|
|
641
|
+
tabId: opts.tab,
|
|
630
642
|
path: opts.output,
|
|
631
643
|
quality: opts.quality,
|
|
632
644
|
});
|
|
@@ -641,16 +653,43 @@ function registerTaskCommands(browser) {
|
|
|
641
653
|
const size = humanizeBytes(response.bytes);
|
|
642
654
|
const dims = response.width && response.height ? `${response.width}×${response.height}` : 'unknown size';
|
|
643
655
|
console.error(`Saved screenshot to ${response.path} (${size}, ${dims})`);
|
|
656
|
+
// When auto-saving (no --output), surface the directory once so the
|
|
657
|
+
// agent doesn't have to dirname() the path or guess where files land.
|
|
658
|
+
if (!opts.output && response.path) {
|
|
659
|
+
const dir = path.dirname(response.path);
|
|
660
|
+
console.error(`Tip: auto-saving to ${dir}. Pass --output <path> to choose a path.`);
|
|
661
|
+
}
|
|
644
662
|
});
|
|
645
663
|
browser
|
|
646
|
-
.command('evaluate
|
|
647
|
-
.addArgument(hiddenLegacyArg('legacyExpression'))
|
|
664
|
+
.command('evaluate')
|
|
648
665
|
.description('Evaluate JavaScript in current tab')
|
|
649
666
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
650
667
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
651
|
-
.
|
|
652
|
-
|
|
653
|
-
|
|
668
|
+
.option('-e, --expression <js>', 'JavaScript expression to evaluate')
|
|
669
|
+
.option('-f, --file <path>', 'Path to a .js file whose contents will be evaluated')
|
|
670
|
+
.action(async (opts) => {
|
|
671
|
+
const task = resolveTaskName(opts);
|
|
672
|
+
if (opts.expression && opts.file) {
|
|
673
|
+
console.error('Pass exactly one of --expression or --file');
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
let expression;
|
|
677
|
+
if (opts.file) {
|
|
678
|
+
try {
|
|
679
|
+
expression = fs.readFileSync(opts.file, 'utf8');
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
console.error(`Cannot read --file ${opts.file}: ${err.message}`);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (opts.expression) {
|
|
687
|
+
expression = opts.expression;
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
console.error('Pass --expression <js> or --file <path>');
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
654
693
|
const response = await sendIPCRequest({
|
|
655
694
|
action: 'evaluate',
|
|
656
695
|
task,
|
|
@@ -663,6 +702,73 @@ function registerTaskCommands(browser) {
|
|
|
663
702
|
}
|
|
664
703
|
console.log(JSON.stringify(response.result, null, 2));
|
|
665
704
|
});
|
|
705
|
+
browser
|
|
706
|
+
.command('ps')
|
|
707
|
+
.description('List every browser/electron/tunnel process agents has tracked (alive or stale) — works without the daemon')
|
|
708
|
+
.option('--json', 'Output machine-readable JSON')
|
|
709
|
+
.action((opts) => {
|
|
710
|
+
const snapshots = listAllProfileSnapshots();
|
|
711
|
+
// Cross-check against what's actually listening locally so we can
|
|
712
|
+
// surface "port claimed by us but nothing is listening" (= leaked
|
|
713
|
+
// cache file) and "port listening but not in our records" (= someone
|
|
714
|
+
// else owns it; a new profile pointing here would collide).
|
|
715
|
+
const portOwners = new Map();
|
|
716
|
+
const conflicts = [];
|
|
717
|
+
for (const s of snapshots) {
|
|
718
|
+
const port = s.meta?.port;
|
|
719
|
+
if (!port)
|
|
720
|
+
continue;
|
|
721
|
+
const occupant = getPortOccupant(port);
|
|
722
|
+
if (!occupant) {
|
|
723
|
+
if (s.pidAlive || s.tunnelAlive) {
|
|
724
|
+
conflicts.push(`${s.name}: port ${port} marked active but nothing is listening`);
|
|
725
|
+
}
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const ourPid = s.meta?.tunnelPid && s.meta.kind === 'tunnel'
|
|
729
|
+
? s.meta.tunnelPid
|
|
730
|
+
: s.meta?.pid;
|
|
731
|
+
if (ourPid && occupant.pid !== ourPid) {
|
|
732
|
+
conflicts.push(`${s.name}: port ${port} listened on by ${occupant.command} (pid ${occupant.pid}) but our record says pid ${ourPid}`);
|
|
733
|
+
}
|
|
734
|
+
portOwners.set(port, occupant);
|
|
735
|
+
}
|
|
736
|
+
if (opts.json) {
|
|
737
|
+
console.log(JSON.stringify({ snapshots, conflicts }, null, 2));
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (snapshots.length === 0) {
|
|
741
|
+
console.log('No tracked browser state. Run `agents browser start --profile <name>` to spawn one.');
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
console.log('PROFILE KIND PID TUNNEL PORT ALIVE TASKS OWNER');
|
|
745
|
+
console.log('-----------------------------------------------------------------------------------------------');
|
|
746
|
+
for (const s of snapshots) {
|
|
747
|
+
const kind = s.meta?.kind ?? '-';
|
|
748
|
+
const pid = s.meta?.pid ?? '-';
|
|
749
|
+
const tunnelPid = s.meta?.tunnelPid ?? '-';
|
|
750
|
+
const port = s.meta?.port ?? '-';
|
|
751
|
+
const alive = aliveLabel(s);
|
|
752
|
+
const owner = s.meta?.daemonPid
|
|
753
|
+
? `daemon${s.daemonAlive ? '' : '(dead)'}=${s.meta.daemonPid}`
|
|
754
|
+
: '-';
|
|
755
|
+
console.log(`${s.name.padEnd(40)} ${String(kind).padEnd(9)} ${String(pid).padEnd(6)} ${String(tunnelPid).padEnd(7)} ${String(port).padEnd(6)} ${alive.padEnd(6)} ${String(s.taskCount).padEnd(6)} ${owner}`);
|
|
756
|
+
}
|
|
757
|
+
if (conflicts.length > 0) {
|
|
758
|
+
console.log('');
|
|
759
|
+
console.log('Conflicts / leaks detected:');
|
|
760
|
+
for (const c of conflicts)
|
|
761
|
+
console.log(` - ${c}`);
|
|
762
|
+
console.log('');
|
|
763
|
+
console.log('Run `agents browser stop --profile <name>` to clean up a specific profile, or restart the daemon to trigger the orphan reaper.');
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
function aliveLabel(s) {
|
|
767
|
+
const k = s.meta?.kind;
|
|
768
|
+
if (k === 'tunnel')
|
|
769
|
+
return s.tunnelAlive ? 'yes' : 'stale';
|
|
770
|
+
return s.pidAlive ? 'yes' : 'stale';
|
|
771
|
+
}
|
|
666
772
|
browser
|
|
667
773
|
.command('status')
|
|
668
774
|
.description('Show running browser tasks')
|
|
@@ -877,15 +983,14 @@ function registerTaskCommands(browser) {
|
|
|
877
983
|
});
|
|
878
984
|
browser
|
|
879
985
|
.command('refs')
|
|
880
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
881
986
|
.description('Get DOM refs for interactive elements')
|
|
882
987
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
883
988
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
884
989
|
.option('--all', 'Include non-interactive elements')
|
|
885
990
|
.option('-l, --limit <n>', 'Max elements (default 500)', '500')
|
|
886
991
|
.option('--json', 'Output machine-readable JSON')
|
|
887
|
-
.action(async (
|
|
888
|
-
const
|
|
992
|
+
.action(async (opts) => {
|
|
993
|
+
const task = resolveTaskName(opts);
|
|
889
994
|
const response = await sendIPCRequest({
|
|
890
995
|
action: 'refs',
|
|
891
996
|
task,
|
|
@@ -910,13 +1015,11 @@ function registerTaskCommands(browser) {
|
|
|
910
1015
|
});
|
|
911
1016
|
browser
|
|
912
1017
|
.command('click <ref>')
|
|
913
|
-
.addArgument(hiddenLegacyArg('legacyRef'))
|
|
914
1018
|
.description('Click an element by ref')
|
|
915
1019
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
916
1020
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
917
|
-
.action(async (
|
|
918
|
-
const
|
|
919
|
-
const [ref] = positionals;
|
|
1021
|
+
.action(async (ref, opts) => {
|
|
1022
|
+
const task = resolveTaskName(opts);
|
|
920
1023
|
const response = await sendIPCRequest({
|
|
921
1024
|
action: 'click',
|
|
922
1025
|
task,
|
|
@@ -930,21 +1033,25 @@ function registerTaskCommands(browser) {
|
|
|
930
1033
|
console.log('Clicked');
|
|
931
1034
|
});
|
|
932
1035
|
browser
|
|
933
|
-
.command('type <ref>
|
|
934
|
-
.addArgument(hiddenLegacyArg('legacyText'))
|
|
1036
|
+
.command('type <ref>')
|
|
935
1037
|
.description('Type text into an element by ref')
|
|
936
1038
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
937
1039
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1040
|
+
.requiredOption('--text <text>', 'Text to type (use quotes for spaces/special chars)')
|
|
938
1041
|
.option('--clear', 'Clear editor content before typing')
|
|
939
|
-
.action(async (
|
|
940
|
-
const
|
|
941
|
-
const
|
|
1042
|
+
.action(async (ref, opts) => {
|
|
1043
|
+
const task = resolveTaskName(opts);
|
|
1044
|
+
const refNum = parseInt(ref, 10);
|
|
1045
|
+
if (!Number.isFinite(refNum)) {
|
|
1046
|
+
console.error(`<ref> must be an integer, got: ${ref}`);
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
942
1049
|
const response = await sendIPCRequest({
|
|
943
1050
|
action: 'type',
|
|
944
1051
|
task,
|
|
945
1052
|
tabId: opts.tab,
|
|
946
|
-
ref:
|
|
947
|
-
text,
|
|
1053
|
+
ref: refNum,
|
|
1054
|
+
text: opts.text,
|
|
948
1055
|
clear: opts.clear,
|
|
949
1056
|
});
|
|
950
1057
|
if (!response.ok) {
|
|
@@ -955,13 +1062,11 @@ function registerTaskCommands(browser) {
|
|
|
955
1062
|
});
|
|
956
1063
|
browser
|
|
957
1064
|
.command('press <key>')
|
|
958
|
-
.addArgument(hiddenLegacyArg('legacyKey'))
|
|
959
1065
|
.description('Press a key (Enter, Tab, Escape, etc)')
|
|
960
1066
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
961
1067
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
962
|
-
.action(async (
|
|
963
|
-
const
|
|
964
|
-
const [key] = positionals;
|
|
1068
|
+
.action(async (key, opts) => {
|
|
1069
|
+
const task = resolveTaskName(opts);
|
|
965
1070
|
const response = await sendIPCRequest({
|
|
966
1071
|
action: 'press',
|
|
967
1072
|
task,
|
|
@@ -976,13 +1081,11 @@ function registerTaskCommands(browser) {
|
|
|
976
1081
|
});
|
|
977
1082
|
browser
|
|
978
1083
|
.command('hover <ref>')
|
|
979
|
-
.addArgument(hiddenLegacyArg('legacyRef'))
|
|
980
1084
|
.description('Hover over an element by ref')
|
|
981
1085
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
982
1086
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
983
|
-
.action(async (
|
|
984
|
-
const
|
|
985
|
-
const [ref] = positionals;
|
|
1087
|
+
.action(async (ref, opts) => {
|
|
1088
|
+
const task = resolveTaskName(opts);
|
|
986
1089
|
const response = await sendIPCRequest({
|
|
987
1090
|
action: 'hover',
|
|
988
1091
|
task,
|
|
@@ -996,22 +1099,30 @@ function registerTaskCommands(browser) {
|
|
|
996
1099
|
console.log('Hovered');
|
|
997
1100
|
});
|
|
998
1101
|
browser
|
|
999
|
-
.command('scroll
|
|
1000
|
-
.
|
|
1001
|
-
.description('Scroll the page by pixel amount')
|
|
1102
|
+
.command('scroll')
|
|
1103
|
+
.description('Scroll the page by pixel amount (negatives scroll up/left)')
|
|
1002
1104
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1003
1105
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1106
|
+
.option('--dx <n>', 'Horizontal pixels (negative = left)', (v) => parseInt(v, 10), 0)
|
|
1107
|
+
.option('--dy <n>', 'Vertical pixels (negative = up)', (v) => parseInt(v, 10), 0)
|
|
1004
1108
|
.option('-x, --at-x <x>', 'X coordinate to dispatch scroll from (default 0)', parseInt)
|
|
1005
1109
|
.option('-y, --at-y <y>', 'Y coordinate to dispatch scroll from (default 0)', parseInt)
|
|
1006
|
-
.action(async (
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1110
|
+
.action(async (opts) => {
|
|
1111
|
+
const task = resolveTaskName(opts);
|
|
1112
|
+
if (!Number.isFinite(opts.dx) || !Number.isFinite(opts.dy)) {
|
|
1113
|
+
console.error('--dx and --dy must be integers');
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
if (opts.dx === 0 && opts.dy === 0) {
|
|
1117
|
+
console.error('Pass --dx and/or --dy (at least one must be non-zero)');
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1009
1120
|
const response = await sendIPCRequest({
|
|
1010
1121
|
action: 'scroll',
|
|
1011
1122
|
task,
|
|
1012
1123
|
tabId: opts.tab,
|
|
1013
|
-
scrollX:
|
|
1014
|
-
scrollY:
|
|
1124
|
+
scrollX: opts.dx,
|
|
1125
|
+
scrollY: opts.dy,
|
|
1015
1126
|
scrollAtX: opts.atX,
|
|
1016
1127
|
scrollAtY: opts.atY,
|
|
1017
1128
|
});
|
|
@@ -1023,7 +1134,6 @@ function registerTaskCommands(browser) {
|
|
|
1023
1134
|
});
|
|
1024
1135
|
browser
|
|
1025
1136
|
.command('upload')
|
|
1026
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1027
1137
|
.description('Upload file(s) — supports hidden file inputs, drag-drop targets, and OS chooser interception')
|
|
1028
1138
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1029
1139
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
@@ -1033,8 +1143,8 @@ function registerTaskCommands(browser) {
|
|
|
1033
1143
|
.option('--drop', 'Force drag-drop pattern even if ref is an <input type=file>')
|
|
1034
1144
|
.option('--input', 'Force file-input pattern (DOM.setFileInputFiles)')
|
|
1035
1145
|
.option('--timeout <ms>', 'Timeout for chooser interception (Pattern C)', (v) => parseInt(v, 10))
|
|
1036
|
-
.action(async (
|
|
1037
|
-
const
|
|
1146
|
+
.action(async (opts) => {
|
|
1147
|
+
const task = resolveTaskName(opts);
|
|
1038
1148
|
const files = opts.file ?? [];
|
|
1039
1149
|
if (files.length === 0) {
|
|
1040
1150
|
console.error('--file <path> is required (repeat for multiple files)');
|
|
@@ -1075,15 +1185,13 @@ function registerTaskCommands(browser) {
|
|
|
1075
1185
|
const setCmd = browser.command('set').description('Set browser emulation options');
|
|
1076
1186
|
setCmd
|
|
1077
1187
|
.command('viewport <width> <height>')
|
|
1078
|
-
.addArgument(hiddenLegacyArg('legacyHeight'))
|
|
1079
1188
|
.description('Set viewport size')
|
|
1080
1189
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1081
1190
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1082
1191
|
.option('-m, --mobile', 'Enable mobile emulation')
|
|
1083
1192
|
.option('-s, --scale <factor>', 'Device scale factor', parseFloat)
|
|
1084
|
-
.action(async (
|
|
1085
|
-
const
|
|
1086
|
-
const [width, height] = positionals;
|
|
1193
|
+
.action(async (width, height, opts) => {
|
|
1194
|
+
const task = resolveTaskName(opts);
|
|
1087
1195
|
const response = await sendIPCRequest({
|
|
1088
1196
|
action: 'set-viewport',
|
|
1089
1197
|
task,
|
|
@@ -1101,13 +1209,11 @@ function registerTaskCommands(browser) {
|
|
|
1101
1209
|
});
|
|
1102
1210
|
setCmd
|
|
1103
1211
|
.command('device <device-name>')
|
|
1104
|
-
.addArgument(hiddenLegacyArg('legacyDeviceName'))
|
|
1105
1212
|
.description('Emulate a device (iPhone 14, iPad, MacBook Pro)')
|
|
1106
1213
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1107
1214
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1108
|
-
.action(async (
|
|
1109
|
-
const
|
|
1110
|
-
const [deviceName] = positionals;
|
|
1215
|
+
.action(async (deviceName, opts) => {
|
|
1216
|
+
const task = resolveTaskName(opts);
|
|
1111
1217
|
const response = await sendIPCRequest({
|
|
1112
1218
|
action: 'set-device',
|
|
1113
1219
|
task,
|
|
@@ -1133,14 +1239,13 @@ function registerTaskCommands(browser) {
|
|
|
1133
1239
|
// ─── Console & Errors ────────────────────────────────────────────────────────
|
|
1134
1240
|
browser
|
|
1135
1241
|
.command('console')
|
|
1136
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1137
1242
|
.description('Read console logs from a tab')
|
|
1138
1243
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1139
1244
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1140
1245
|
.option('-l, --level <level>', 'Filter by level (log, info, warn, error)')
|
|
1141
1246
|
.option('--clear', 'Clear logs after reading')
|
|
1142
|
-
.action(async (
|
|
1143
|
-
const
|
|
1247
|
+
.action(async (opts) => {
|
|
1248
|
+
const task = resolveTaskName(opts);
|
|
1144
1249
|
const response = await sendIPCRequest({
|
|
1145
1250
|
action: 'console',
|
|
1146
1251
|
task,
|
|
@@ -1164,13 +1269,12 @@ function registerTaskCommands(browser) {
|
|
|
1164
1269
|
});
|
|
1165
1270
|
browser
|
|
1166
1271
|
.command('errors')
|
|
1167
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1168
1272
|
.description('Read page errors from a tab')
|
|
1169
1273
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1170
1274
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1171
1275
|
.option('--clear', 'Clear errors after reading')
|
|
1172
|
-
.action(async (
|
|
1173
|
-
const
|
|
1276
|
+
.action(async (opts) => {
|
|
1277
|
+
const task = resolveTaskName(opts);
|
|
1174
1278
|
const response = await sendIPCRequest({
|
|
1175
1279
|
action: 'errors',
|
|
1176
1280
|
task,
|
|
@@ -1197,14 +1301,13 @@ function registerTaskCommands(browser) {
|
|
|
1197
1301
|
// ─── Network ─────────────────────────────────────────────────────────────────
|
|
1198
1302
|
browser
|
|
1199
1303
|
.command('requests')
|
|
1200
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1201
1304
|
.description('Read captured network requests')
|
|
1202
1305
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1203
1306
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1204
1307
|
.option('-f, --filter <text>', 'Filter URLs containing text')
|
|
1205
1308
|
.option('--clear', 'Clear requests after reading')
|
|
1206
|
-
.action(async (
|
|
1207
|
-
const
|
|
1309
|
+
.action(async (opts) => {
|
|
1310
|
+
const task = resolveTaskName(opts);
|
|
1208
1311
|
const response = await sendIPCRequest({
|
|
1209
1312
|
action: 'requests',
|
|
1210
1313
|
task,
|
|
@@ -1227,17 +1330,51 @@ function registerTaskCommands(browser) {
|
|
|
1227
1330
|
console.log(`${req.method.padEnd(8)}${status.padEnd(8)}${req.url.slice(0, 100)}`);
|
|
1228
1331
|
}
|
|
1229
1332
|
});
|
|
1333
|
+
browser
|
|
1334
|
+
.command('logs <task>')
|
|
1335
|
+
.description('Read merged rush-app + rush-cli JSONL logs for a task')
|
|
1336
|
+
.option('--source <name>', 'Source to scope to: rush-app or rush-cli (default both)')
|
|
1337
|
+
.option('--lines <n>', 'Tail N entries (default 200; ignored when --since)', (v) => parseInt(v, 10))
|
|
1338
|
+
.option('--since <when>', 'Absolute timestamp or relative offset (e.g. 5m, 2h, 1d)')
|
|
1339
|
+
.option('--until <when>', 'Absolute timestamp or relative offset (e.g. 5m, 2h, 1d)')
|
|
1340
|
+
.option('--level <level>', 'Filter entries by level field')
|
|
1341
|
+
.option('--message <name>', 'Filter entries by exact message field')
|
|
1342
|
+
.option('--filter <text>', 'Filter entries whose JSON contains this substring')
|
|
1343
|
+
.option('-f, --follow', 'Follow mode (not yet implemented)')
|
|
1344
|
+
.action(async (task, opts) => {
|
|
1345
|
+
if (opts.follow) {
|
|
1346
|
+
process.stderr.write('follow mode not yet implemented; coming next pass\n');
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1349
|
+
const response = await sendIPCRequest({
|
|
1350
|
+
action: 'getAppLogs',
|
|
1351
|
+
task,
|
|
1352
|
+
source: opts.source,
|
|
1353
|
+
lines: opts.lines,
|
|
1354
|
+
since: opts.since,
|
|
1355
|
+
until: opts.until,
|
|
1356
|
+
appLevel: opts.level,
|
|
1357
|
+
message: opts.message,
|
|
1358
|
+
filter: opts.filter,
|
|
1359
|
+
});
|
|
1360
|
+
if (!response.ok) {
|
|
1361
|
+
console.error(response.error);
|
|
1362
|
+
process.exit(1);
|
|
1363
|
+
}
|
|
1364
|
+
const entries = response.appLogs ?? [];
|
|
1365
|
+
for (const entry of entries) {
|
|
1366
|
+
console.log(JSON.stringify(entry));
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1230
1369
|
browser
|
|
1231
1370
|
.command('responsebody <url-pattern>')
|
|
1232
|
-
.addArgument(hiddenLegacyArg('legacyUrlPattern'))
|
|
1233
1371
|
.description('Wait for and read a response body by URL pattern')
|
|
1234
1372
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1235
1373
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1236
1374
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1237
1375
|
.option('--max-chars <n>', 'Max characters to return', parseInt)
|
|
1238
|
-
.action(async (
|
|
1239
|
-
const
|
|
1240
|
-
const [urlPattern] = positionals;
|
|
1376
|
+
.action(async (urlPattern, opts) => {
|
|
1377
|
+
const task = resolveTaskName(opts);
|
|
1241
1378
|
const response = await sendIPCRequest({
|
|
1242
1379
|
action: 'response-body',
|
|
1243
1380
|
task,
|
|
@@ -1255,7 +1392,6 @@ function registerTaskCommands(browser) {
|
|
|
1255
1392
|
// ─── Wait ────────────────────────────────────────────────────────────────────
|
|
1256
1393
|
browser
|
|
1257
1394
|
.command('wait')
|
|
1258
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1259
1395
|
.description('Wait for a condition')
|
|
1260
1396
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1261
1397
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
@@ -1265,8 +1401,8 @@ function registerTaskCommands(browser) {
|
|
|
1265
1401
|
.option('--fn <js>', 'Wait for JS expression to return truthy')
|
|
1266
1402
|
.option('--state <state>', 'Wait for load state (domcontentloaded, load, networkidle)')
|
|
1267
1403
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1268
|
-
.action(async (
|
|
1269
|
-
const
|
|
1404
|
+
.action(async (opts) => {
|
|
1405
|
+
const task = resolveTaskName(opts);
|
|
1270
1406
|
let waitType;
|
|
1271
1407
|
let waitValue;
|
|
1272
1408
|
if (opts.time) {
|
|
@@ -1310,13 +1446,12 @@ function registerTaskCommands(browser) {
|
|
|
1310
1446
|
// ─── Downloads ───────────────────────────────────────────────────────────────
|
|
1311
1447
|
browser
|
|
1312
1448
|
.command('download')
|
|
1313
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1314
1449
|
.description('Set download directory for a task')
|
|
1315
1450
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1316
1451
|
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
1317
1452
|
.requiredOption('-p, --path <dir>', 'Download directory path')
|
|
1318
|
-
.action(async (
|
|
1319
|
-
const
|
|
1453
|
+
.action(async (opts) => {
|
|
1454
|
+
const task = resolveTaskName(opts);
|
|
1320
1455
|
const response = await sendIPCRequest({
|
|
1321
1456
|
action: 'set-download-path',
|
|
1322
1457
|
task,
|
|
@@ -1376,12 +1511,11 @@ function registerTaskCommands(browser) {
|
|
|
1376
1511
|
});
|
|
1377
1512
|
browser
|
|
1378
1513
|
.command('waitdownload')
|
|
1379
|
-
.addArgument(hiddenLegacyArg('legacyTask'))
|
|
1380
1514
|
.description('Wait for a download to complete')
|
|
1381
1515
|
.option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
|
|
1382
1516
|
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
1383
|
-
.action(async (
|
|
1384
|
-
const
|
|
1517
|
+
.action(async (opts) => {
|
|
1518
|
+
const task = resolveTaskName(opts);
|
|
1385
1519
|
const response = await sendIPCRequest({
|
|
1386
1520
|
action: 'wait-download',
|
|
1387
1521
|
task,
|