@tangle-network/browser-agent-driver 0.24.1 → 0.25.0
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/dist/brain/index.d.ts +5 -0
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +80 -32
- package/dist/brain/index.js.map +1 -1
- package/dist/cli-attach.d.ts +128 -0
- package/dist/cli-attach.d.ts.map +1 -0
- package/dist/cli-attach.js +254 -0
- package/dist/cli-attach.js.map +1 -0
- package/dist/cli-preview.d.ts +59 -0
- package/dist/cli-preview.d.ts.map +1 -0
- package/dist/cli-preview.js +144 -0
- package/dist/cli-preview.js.map +1 -0
- package/dist/cli-share.d.ts +77 -0
- package/dist/cli-share.d.ts.map +1 -0
- package/dist/cli-share.js +141 -0
- package/dist/cli-share.js.map +1 -0
- package/dist/cli-ui.d.ts.map +1 -1
- package/dist/cli-ui.js +15 -1
- package/dist/cli-ui.js.map +1 -1
- package/dist/cli.js +262 -14
- package/dist/cli.js.map +1 -1
- package/dist/drivers/cursor-overlay.d.ts +18 -10
- package/dist/drivers/cursor-overlay.d.ts.map +1 -1
- package/dist/drivers/cursor-overlay.js +221 -30
- package/dist/drivers/cursor-overlay.js.map +1 -1
- package/dist/drivers/overlay-label.d.ts +33 -0
- package/dist/drivers/overlay-label.d.ts.map +1 -0
- package/dist/drivers/overlay-label.js +92 -0
- package/dist/drivers/overlay-label.js.map +1 -0
- package/dist/drivers/playwright.d.ts +30 -0
- package/dist/drivers/playwright.d.ts.map +1 -1
- package/dist/drivers/playwright.js +204 -16
- package/dist/drivers/playwright.js.map +1 -1
- package/dist/drivers/snapshot.d.ts +12 -0
- package/dist/drivers/snapshot.d.ts.map +1 -1
- package/dist/drivers/snapshot.js +17 -0
- package/dist/drivers/snapshot.js.map +1 -1
- package/dist/drivers/types.d.ts +13 -0
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/multi-actor.d.ts.map +1 -1
- package/dist/multi-actor.js +4 -0
- package/dist/multi-actor.js.map +1 -1
- package/dist/run-state.d.ts.map +1 -1
- package/dist/run-state.js +12 -2
- package/dist/run-state.js.map +1 -1
- package/dist/runner/interrupt-controller.d.ts +67 -0
- package/dist/runner/interrupt-controller.d.ts.map +1 -0
- package/dist/runner/interrupt-controller.js +142 -0
- package/dist/runner/interrupt-controller.js.map +1 -0
- package/dist/runner/overlay-narration.d.ts +83 -0
- package/dist/runner/overlay-narration.d.ts.map +1 -0
- package/dist/runner/overlay-narration.js +172 -0
- package/dist/runner/overlay-narration.js.map +1 -0
- package/dist/runner/parallel-runner.d.ts +4 -0
- package/dist/runner/parallel-runner.d.ts.map +1 -1
- package/dist/runner/parallel-runner.js +1 -0
- package/dist/runner/parallel-runner.js.map +1 -1
- package/dist/runner/runner.d.ts +17 -0
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +72 -50
- package/dist/runner/runner.js.map +1 -1
- package/dist/runner/stream-webhook.d.ts +70 -0
- package/dist/runner/stream-webhook.d.ts.map +1 -0
- package/dist/runner/stream-webhook.js +132 -0
- package/dist/runner/stream-webhook.js.map +1 -0
- package/dist/skills/domain-loader.d.ts +78 -0
- package/dist/skills/domain-loader.d.ts.map +1 -0
- package/dist/skills/domain-loader.js +201 -0
- package/dist/skills/domain-loader.js.map +1 -0
- package/dist/skills/macro-loader.d.ts +95 -0
- package/dist/skills/macro-loader.d.ts.map +1 -0
- package/dist/skills/macro-loader.js +237 -0
- package/dist/skills/macro-loader.js.map +1 -0
- package/dist/supervisor/critic.js +1 -1
- package/dist/supervisor/policy.js +7 -0
- package/dist/supervisor/policy.js.map +1 -1
- package/dist/test-runner.d.ts +13 -0
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +6 -0
- package/dist/test-runner.js.map +1 -1
- package/dist/types.d.ts +19 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/viewer/viewer.html +122 -8
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -75,6 +75,7 @@ async function main() {
|
|
|
75
75
|
goal: { type: 'string', short: 'g' },
|
|
76
76
|
url: { type: 'string', short: 'u' },
|
|
77
77
|
cases: { type: 'string', short: 'c' },
|
|
78
|
+
'cases-json': { type: 'string' },
|
|
78
79
|
'allowed-domains': { type: 'string' },
|
|
79
80
|
// LLM configuration
|
|
80
81
|
model: { type: 'string', short: 'm' },
|
|
@@ -146,6 +147,8 @@ async function main() {
|
|
|
146
147
|
'user-data-dir': { type: 'string' },
|
|
147
148
|
'profile-dir': { type: 'string' },
|
|
148
149
|
'cdp-url': { type: 'string' },
|
|
150
|
+
attach: { type: 'boolean' },
|
|
151
|
+
'attach-port': { type: 'string' },
|
|
149
152
|
wallet: { type: 'boolean' },
|
|
150
153
|
'wallet-auto-approve': { type: 'boolean' },
|
|
151
154
|
'wallet-password': { type: 'string' },
|
|
@@ -177,6 +180,16 @@ async function main() {
|
|
|
177
180
|
cookie: { type: 'string', multiple: true },
|
|
178
181
|
'wait-for': { type: 'string' },
|
|
179
182
|
'wait-timeout': { type: 'string' },
|
|
183
|
+
// Gen 32 — `bad share` flags
|
|
184
|
+
visibility: { type: 'string' },
|
|
185
|
+
'bad-app-url': { type: 'string' },
|
|
186
|
+
'no-copy': { type: 'boolean' },
|
|
187
|
+
// Gen 32 — preview / stream / interrupt
|
|
188
|
+
'max-steps': { type: 'string' },
|
|
189
|
+
headed: { type: 'boolean' },
|
|
190
|
+
stream: { type: 'string' },
|
|
191
|
+
'stream-token': { type: 'string' },
|
|
192
|
+
interrupt: { type: 'boolean' },
|
|
180
193
|
help: { type: 'boolean', short: 'h', default: false },
|
|
181
194
|
version: { type: 'boolean', short: 'v', default: false },
|
|
182
195
|
},
|
|
@@ -317,6 +330,16 @@ async function main() {
|
|
|
317
330
|
});
|
|
318
331
|
process.exit(0);
|
|
319
332
|
}
|
|
333
|
+
if (command === 'chrome-debug') {
|
|
334
|
+
const { runChromeDebugCommand } = await import('./cli-attach.js');
|
|
335
|
+
const port = values['attach-port'] ? parseInt(values['attach-port'], 10) : undefined;
|
|
336
|
+
const rc = await runChromeDebugCommand({
|
|
337
|
+
port,
|
|
338
|
+
userDataDir: values['user-data-dir'],
|
|
339
|
+
quiet: values.quiet,
|
|
340
|
+
});
|
|
341
|
+
process.exit(rc);
|
|
342
|
+
}
|
|
320
343
|
if (command === 'auth') {
|
|
321
344
|
const sub = positionals[1];
|
|
322
345
|
if (sub === 'save') {
|
|
@@ -351,13 +374,85 @@ async function main() {
|
|
|
351
374
|
cliError(`Unknown auth subcommand: ${sub || '(none)'}. Use "auth save", "auth login", or "auth check".`);
|
|
352
375
|
process.exit(1);
|
|
353
376
|
}
|
|
354
|
-
|
|
355
|
-
|
|
377
|
+
// `bad attach` is a top-level alias for `bad run --attach`. Every other
|
|
378
|
+
// flag on `run` (--goal, --url, --model, --provider, --base-url,
|
|
379
|
+
// --api-key, --show-cursor, --mode, --max-turns, --timeout, --no-memory,
|
|
380
|
+
// --attach-port) works identically. Attach's mental model is "drive my
|
|
381
|
+
// real Chrome," which is distinct enough from "spawn a fresh browser"
|
|
382
|
+
// to deserve its own command name.
|
|
383
|
+
if (command === 'attach') {
|
|
384
|
+
values.attach = true;
|
|
385
|
+
}
|
|
386
|
+
// `bad preview` — plan-only dry-run. Observe the URL once, ask the
|
|
387
|
+
// planner to emit a structured plan, render it, exit. No execution.
|
|
388
|
+
// terraform plan for browser agents.
|
|
389
|
+
if (command === 'preview') {
|
|
390
|
+
if (!values.goal || !values.url) {
|
|
391
|
+
cliError('usage: bad preview --goal "..." --url <url> [--max-steps 12] [--headed] [--json] [--out plan.json]');
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const { handlePreviewCommand, PreviewError } = await import('./cli-preview.js');
|
|
395
|
+
try {
|
|
396
|
+
const result = await handlePreviewCommand({
|
|
397
|
+
goal: values.goal,
|
|
398
|
+
url: values.url,
|
|
399
|
+
model: values.model,
|
|
400
|
+
provider: values.provider,
|
|
401
|
+
apiKey: values['api-key'],
|
|
402
|
+
baseUrl: values['base-url'],
|
|
403
|
+
output: values.sink,
|
|
404
|
+
json: values.json,
|
|
405
|
+
maxSteps: values['max-steps'] ? parseInt(values['max-steps'], 10) : undefined,
|
|
406
|
+
headed: values.headed,
|
|
407
|
+
});
|
|
408
|
+
process.exit(result.plan ? 0 : 1);
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
if (err instanceof PreviewError) {
|
|
412
|
+
cliError(err.message);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// `bad share <run-id>` — create a bad-app share link, copy to clipboard.
|
|
419
|
+
if (command === 'share') {
|
|
420
|
+
const runId = positionals[1];
|
|
421
|
+
if (!runId) {
|
|
422
|
+
cliError('usage: bad share <run-id> [--visibility metadata|full|artifacts] [--json]');
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
const { handleShareCommand, ShareError } = await import('./cli-share.js');
|
|
426
|
+
const visArg = values.visibility;
|
|
427
|
+
const visibility = visArg === 'full' || visArg === 'artifacts' || visArg === 'metadata'
|
|
428
|
+
? visArg
|
|
429
|
+
: undefined;
|
|
430
|
+
try {
|
|
431
|
+
await handleShareCommand({
|
|
432
|
+
runId,
|
|
433
|
+
visibility,
|
|
434
|
+
baseUrl: values['bad-app-url'],
|
|
435
|
+
apiKey: values['api-key'],
|
|
436
|
+
noCopy: values['no-copy'],
|
|
437
|
+
json: values.json,
|
|
438
|
+
});
|
|
439
|
+
process.exit(0);
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
if (err instanceof ShareError) {
|
|
443
|
+
cliError(err.message);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (command !== 'run' && command !== 'attach') {
|
|
450
|
+
cliError(`Unknown command: ${command}. Use "run", "attach", "preview", "runs", "view", "share", "chrome-debug", "design-audit", "showcase", or "auth".`);
|
|
356
451
|
process.exit(1);
|
|
357
452
|
}
|
|
358
453
|
// Validate inputs
|
|
359
|
-
if (!values.goal && !values.cases && !values['resume-run'] && !values['fork-run']) {
|
|
360
|
-
cliError('provide --goal "..." --url "..." for a single task, --cases ./cases.json for a suite, or --resume-run / --fork-run <runId>.');
|
|
454
|
+
if (!values.goal && !values.cases && !values['cases-json'] && !values['resume-run'] && !values['fork-run']) {
|
|
455
|
+
cliError('provide --goal "..." --url "..." for a single task, --cases ./cases.json (or --cases-json \'[...]\') for a suite, or --resume-run / --fork-run <runId>.');
|
|
361
456
|
process.exit(1);
|
|
362
457
|
}
|
|
363
458
|
// Load config file, then overlay CLI flags
|
|
@@ -531,6 +626,40 @@ async function main() {
|
|
|
531
626
|
cliOverrides.profileDir = values['profile-dir'];
|
|
532
627
|
if (values['cdp-url'])
|
|
533
628
|
cliOverrides.cdpUrl = values['cdp-url'];
|
|
629
|
+
// --attach resolves to cdpUrl by probing a running Chrome's DevTools
|
|
630
|
+
// endpoint. Done here (pre-config-merge) so the existing cdpUrl path at
|
|
631
|
+
// cli.ts:916 takes over unchanged. Conflicts with wallet/extension/profile
|
|
632
|
+
// flags are surfaced up-front rather than silently ignored.
|
|
633
|
+
if (values.attach) {
|
|
634
|
+
if (values['cdp-url']) {
|
|
635
|
+
cliError('--attach and --cdp-url are mutually exclusive (both select a CDP endpoint). Use one.');
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
const { resolveAttachEndpoint, validateAttachConflicts } = await import('./cli-attach.js');
|
|
639
|
+
const conflicts = validateAttachConflicts({
|
|
640
|
+
walletEnabled: Boolean(cliOverrides.wallet?.enabled) || Boolean(values.extension?.length),
|
|
641
|
+
profileDir: values['profile-dir'],
|
|
642
|
+
extensionPaths: values.extension,
|
|
643
|
+
userDataDir: values['user-data-dir'],
|
|
644
|
+
});
|
|
645
|
+
if (!conflicts.ok) {
|
|
646
|
+
for (const err of conflicts.errors)
|
|
647
|
+
cliError(err);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
const port = values['attach-port'] ? parseInt(values['attach-port'], 10) : undefined;
|
|
651
|
+
if (port !== undefined && !Number.isFinite(port)) {
|
|
652
|
+
cliError(`Invalid --attach-port value: ${values['attach-port']}`);
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
const info = await resolveAttachEndpoint({ port }).catch((err) => {
|
|
656
|
+
cliError(err instanceof Error ? err.message : String(err));
|
|
657
|
+
process.exit(1);
|
|
658
|
+
});
|
|
659
|
+
cliOverrides.cdpUrl = info.webSocketDebuggerUrl;
|
|
660
|
+
if (!values.quiet)
|
|
661
|
+
cliLog('attach', `connected to ${info.browser ?? 'Chrome'}`);
|
|
662
|
+
}
|
|
534
663
|
if (values.memory !== undefined || values['memory-dir']) {
|
|
535
664
|
cliOverrides.memory = {
|
|
536
665
|
...(cliOverrides.memory ?? {}),
|
|
@@ -694,9 +823,13 @@ async function main() {
|
|
|
694
823
|
// Optional live event bus + SSE viewer. Constructed only when `--live` is
|
|
695
824
|
// passed; otherwise the runner uses an internal no-op bus and pays nothing.
|
|
696
825
|
// The bus is shared across the entire suite so a single SSE connection
|
|
697
|
-
// observes all turns of all test cases.
|
|
826
|
+
// observes all turns of all test cases. Gen 32: when `--stream <url>` is
|
|
827
|
+
// passed the bus is always created (even without --live) so the webhook
|
|
828
|
+
// streamer has something to subscribe to.
|
|
698
829
|
const liveEnabled = values.live === true;
|
|
699
|
-
const
|
|
830
|
+
const streamUrl = typeof values.stream === 'string' && values.stream.length > 0 ? values.stream : undefined;
|
|
831
|
+
const needsBus = liveEnabled || !!streamUrl;
|
|
832
|
+
const liveBus = needsBus ? new TurnEventBus() : undefined;
|
|
700
833
|
const liveCancelController = liveEnabled ? new AbortController() : undefined;
|
|
701
834
|
let liveViewHandle;
|
|
702
835
|
if (liveEnabled && liveBus) {
|
|
@@ -708,6 +841,39 @@ async function main() {
|
|
|
708
841
|
noOpen: values['no-open'] === true,
|
|
709
842
|
});
|
|
710
843
|
}
|
|
844
|
+
// Gen 32 — webhook streamer. When `--stream <url>` is passed, subscribe
|
|
845
|
+
// to the bus and POST every event to <url> as it fires. Auth via
|
|
846
|
+
// `--stream-token` or $BAD_STREAM_TOKEN. Non-fatal on failure — the
|
|
847
|
+
// canonical record is always events.jsonl on disk.
|
|
848
|
+
let webhookStreamer;
|
|
849
|
+
if (streamUrl && liveBus) {
|
|
850
|
+
const { WebhookStreamer } = await import('./runner/stream-webhook.js');
|
|
851
|
+
const token = values['stream-token'] || process.env.BAD_STREAM_TOKEN;
|
|
852
|
+
const streamId = `stream_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
853
|
+
webhookStreamer = new WebhookStreamer({
|
|
854
|
+
url: streamUrl,
|
|
855
|
+
authToken: token,
|
|
856
|
+
streamId,
|
|
857
|
+
onError: (err, dropped) => {
|
|
858
|
+
if (!values.json)
|
|
859
|
+
cliWarn(`stream: ${err.message} (dropped ${dropped} event${dropped === 1 ? '' : 's'})`);
|
|
860
|
+
},
|
|
861
|
+
}).attach(liveBus);
|
|
862
|
+
if (!values.json)
|
|
863
|
+
cliLog('stream', `POST → ${streamUrl} (id ${streamId})`);
|
|
864
|
+
}
|
|
865
|
+
// Gen 32 — interrupt controller. `--interrupt` enables keyboard pause/
|
|
866
|
+
// resume/abort during a run. No-op in non-TTY (CI, piped output).
|
|
867
|
+
let interruptController;
|
|
868
|
+
let detachInterrupt;
|
|
869
|
+
if (values.interrupt === true && process.stdin.isTTY) {
|
|
870
|
+
const { InterruptController } = await import('./runner/interrupt-controller.js');
|
|
871
|
+
interruptController = new InterruptController({
|
|
872
|
+
onStatus: (msg) => { if (!values.json)
|
|
873
|
+
cliLog('interrupt', msg); },
|
|
874
|
+
});
|
|
875
|
+
detachInterrupt = interruptController.attach();
|
|
876
|
+
}
|
|
711
877
|
// Auto-discover bad.config.{ts,mjs,js,...} from cwd plus any explicit
|
|
712
878
|
// --extension paths. Failures are reported but never fatal: a broken user
|
|
713
879
|
// config should warn, not abort the run.
|
|
@@ -717,6 +883,27 @@ async function main() {
|
|
|
717
883
|
: explicitExtPaths
|
|
718
884
|
? [explicitExtPaths]
|
|
719
885
|
: [];
|
|
886
|
+
// Domain skills: markdown-based per-host rule libraries under
|
|
887
|
+
// skills/domain/<host>/SKILL.md. They plumb into the same BadExtension
|
|
888
|
+
// pipeline as user `bad.config.mjs` files via addRulesForDomain, so the
|
|
889
|
+
// existing setExtensionRules injection at brain/index.ts:899 picks them up
|
|
890
|
+
// with no second injection site. Gated by BAD_DOMAIN_SKILLS_DISABLED=1.
|
|
891
|
+
const domainSkillsDisabled = process.env.BAD_DOMAIN_SKILLS_DISABLED === '1';
|
|
892
|
+
let domainSkillExtension;
|
|
893
|
+
let domainSkillsLoadedCount = 0;
|
|
894
|
+
if (!domainSkillsDisabled) {
|
|
895
|
+
const { loadDomainSkills, buildDomainSkillExtension } = await import('./skills/domain-loader.js');
|
|
896
|
+
const domainLoad = await loadDomainSkills();
|
|
897
|
+
if (domainLoad.skills.length > 0) {
|
|
898
|
+
domainSkillExtension = buildDomainSkillExtension(domainLoad.skills);
|
|
899
|
+
domainSkillsLoadedCount = domainLoad.skills.length;
|
|
900
|
+
}
|
|
901
|
+
if (domainLoad.errors.length > 0 && !values.json) {
|
|
902
|
+
for (const err of domainLoad.errors) {
|
|
903
|
+
cliWarn(`domain-skill load failed: ${err.path} — ${err.error}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
720
907
|
const extensionLoad = await loadExtensions({ explicitPaths: explicitExtArr });
|
|
721
908
|
if (extensionLoad.loadedFrom.length > 0 && !values.json) {
|
|
722
909
|
cliLog('extensions', `loaded ${extensionLoad.loadedFrom.length}: ${extensionLoad.loadedFrom.join(', ')}`);
|
|
@@ -726,6 +913,35 @@ async function main() {
|
|
|
726
913
|
cliWarn(`extension load failed: ${err.path} — ${err.error}`);
|
|
727
914
|
}
|
|
728
915
|
}
|
|
916
|
+
if (domainSkillExtension && domainSkillsLoadedCount > 0) {
|
|
917
|
+
// Re-resolve the bundle with the domain-skill synthetic extension
|
|
918
|
+
// merged in. Resolving twice is cheap (the list is short) and keeps
|
|
919
|
+
// domainSkillExtension from having to plumb through a separate wire.
|
|
920
|
+
const { resolveExtensions } = await import('./extensions/types.js');
|
|
921
|
+
const combined = resolveExtensions([...extensionLoad.resolved.extensions, domainSkillExtension]);
|
|
922
|
+
extensionLoad.resolved = combined;
|
|
923
|
+
if (!values.json)
|
|
924
|
+
cliLog('skills', `domain: ${domainSkillsLoadedCount} loaded`);
|
|
925
|
+
}
|
|
926
|
+
// Gen 29: macros. Loaded alongside domain skills; gated by
|
|
927
|
+
// BAD_MACROS_DISABLED=1. The registry is passed to PlaywrightDriver
|
|
928
|
+
// (dispatch) and its promptBlock to the BrowserAgent (visibility).
|
|
929
|
+
const macrosDisabled = process.env.BAD_MACROS_DISABLED === '1';
|
|
930
|
+
let macroRegistry;
|
|
931
|
+
if (!macrosDisabled) {
|
|
932
|
+
const { loadMacros, buildMacroRegistry } = await import('./skills/macro-loader.js');
|
|
933
|
+
const macroLoad = await loadMacros();
|
|
934
|
+
if (macroLoad.errors.length > 0 && !values.json) {
|
|
935
|
+
for (const err of macroLoad.errors) {
|
|
936
|
+
cliWarn(`macro load failed: ${err.path} — ${err.error}`);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (macroLoad.macros.length > 0) {
|
|
940
|
+
macroRegistry = buildMacroRegistry(macroLoad.macros);
|
|
941
|
+
if (!values.json)
|
|
942
|
+
cliLog('skills', `macros: ${macroLoad.macros.length} loaded`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
729
945
|
const concurrency = launchPlan.concurrency;
|
|
730
946
|
const maxTurns = driverConfig.maxTurns ?? 30;
|
|
731
947
|
const screenshotInterval = driverConfig.screenshotInterval ?? 5;
|
|
@@ -807,8 +1023,8 @@ async function main() {
|
|
|
807
1023
|
parentRunId: scenario.parentRunId,
|
|
808
1024
|
}];
|
|
809
1025
|
}
|
|
810
|
-
else if (values.cases) {
|
|
811
|
-
const raw = fs.readFileSync(path.resolve(values.cases), 'utf-8');
|
|
1026
|
+
else if (values['cases-json'] || values.cases) {
|
|
1027
|
+
const raw = values['cases-json'] || fs.readFileSync(path.resolve(values.cases), 'utf-8');
|
|
812
1028
|
const parsed = JSON.parse(raw);
|
|
813
1029
|
const rawCases = Array.isArray(parsed) ? parsed : [parsed];
|
|
814
1030
|
// Ensure required fields — spread raw case first so explicit fields become defaults
|
|
@@ -943,7 +1159,7 @@ async function main() {
|
|
|
943
1159
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
944
1160
|
const persistentLaunchStartedAt = Date.now();
|
|
945
1161
|
persistentContext = await chromium.launchPersistentContext(userDataDir, {
|
|
946
|
-
channel: 'chrome',
|
|
1162
|
+
channel: isStealthProfile ? 'chrome' : 'chromium',
|
|
947
1163
|
headless: launchPlan.headless,
|
|
948
1164
|
args: launchPlan.browserArgs,
|
|
949
1165
|
viewport,
|
|
@@ -1088,10 +1304,11 @@ async function main() {
|
|
|
1088
1304
|
browser = await browserType.launch({
|
|
1089
1305
|
headless: launchPlan.headless,
|
|
1090
1306
|
...(browserName === 'chromium' ? { args: launchPlan.browserArgs } : {}),
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
//
|
|
1094
|
-
|
|
1307
|
+
// System Chrome for stealth profiles only — real TLS/JA3 fingerprint
|
|
1308
|
+
// fixes anti-bot blocking. But system Chrome renders differently than
|
|
1309
|
+
// bundled Chromium on some sites (Allrecipes click timeouts, Amazon
|
|
1310
|
+
// layout shifts), so only enable when anti-bot evasion is needed.
|
|
1311
|
+
...(isStealthProfile && browserName === 'chromium' ? { channel: 'chrome' } : {}),
|
|
1095
1312
|
// Residential/SOCKS5/HTTP proxy — routes all traffic through the proxy
|
|
1096
1313
|
...(launchPlan.proxyServer ? { proxy: { server: launchPlan.proxyServer } } : {}),
|
|
1097
1314
|
});
|
|
@@ -1277,6 +1494,7 @@ async function main() {
|
|
|
1277
1494
|
visionStrategy: config.visionStrategy,
|
|
1278
1495
|
screenshotInterval,
|
|
1279
1496
|
showCursor: values['show-cursor'],
|
|
1497
|
+
...(macroRegistry ? { macros: macroRegistry } : {}),
|
|
1280
1498
|
});
|
|
1281
1499
|
// Apply resource blocking if configured
|
|
1282
1500
|
const resourceBlockingStartedAt = Date.now();
|
|
@@ -1333,7 +1551,11 @@ async function main() {
|
|
|
1333
1551
|
screenshotInterval,
|
|
1334
1552
|
artifactSink: sink,
|
|
1335
1553
|
extensions: extensionLoad.resolved,
|
|
1554
|
+
...(macroRegistry ? { macroPromptBlock: macroRegistry.promptBlock } : {}),
|
|
1336
1555
|
...(liveBus ? { eventBus: liveBus } : {}),
|
|
1556
|
+
...(interruptController
|
|
1557
|
+
? { beforeTurn: async () => { await interruptController.waitIfPaused(); } }
|
|
1558
|
+
: {}),
|
|
1337
1559
|
onProgress: (event) => {
|
|
1338
1560
|
if (values.json) {
|
|
1339
1561
|
console.log(JSON.stringify(event));
|
|
@@ -1363,7 +1585,23 @@ async function main() {
|
|
|
1363
1585
|
let result;
|
|
1364
1586
|
let runError;
|
|
1365
1587
|
try {
|
|
1366
|
-
|
|
1588
|
+
// Gen 32 — wire interrupt controller. When the user presses `q`,
|
|
1589
|
+
// abort() fires which the runner observes via the suite-level
|
|
1590
|
+
// signal. Runs a check before the runner starts so pressing `q`
|
|
1591
|
+
// BEFORE the first turn still aborts cleanly.
|
|
1592
|
+
const interruptSignal = interruptController
|
|
1593
|
+
? (() => {
|
|
1594
|
+
const ac = new AbortController();
|
|
1595
|
+
interruptController.on('abort', () => ac.abort('interrupted by user'));
|
|
1596
|
+
if (interruptController.isAborted)
|
|
1597
|
+
ac.abort('interrupted by user');
|
|
1598
|
+
return ac.signal;
|
|
1599
|
+
})()
|
|
1600
|
+
: undefined;
|
|
1601
|
+
// If --live already provided a signal, merge both. Prefer the user
|
|
1602
|
+
// interrupt signal so `q` wins even when --live is set.
|
|
1603
|
+
const mergedSignal = interruptSignal ?? liveCancelController?.signal;
|
|
1604
|
+
result = await runner.runSuite(cases, mergedSignal ? { signal: mergedSignal } : undefined);
|
|
1367
1605
|
// Write reports for each configured format
|
|
1368
1606
|
const { generateReport } = await import('./test-report.js');
|
|
1369
1607
|
const reporters = driverConfig.reporters ?? ['json'];
|
|
@@ -1399,6 +1637,16 @@ async function main() {
|
|
|
1399
1637
|
}
|
|
1400
1638
|
finally {
|
|
1401
1639
|
renderer?.destroy();
|
|
1640
|
+
// Gen 32 — detach interrupt controller first so stdin leaves raw mode
|
|
1641
|
+
// BEFORE we try to write any shutdown logs. Otherwise the user's TTY
|
|
1642
|
+
// stays in raw mode after the run ends and Ctrl-C, arrow keys, etc.
|
|
1643
|
+
// come through as garbage bytes.
|
|
1644
|
+
if (detachInterrupt)
|
|
1645
|
+
detachInterrupt();
|
|
1646
|
+
// Flush and close the stream webhook so the final events (run-completed,
|
|
1647
|
+
// suite-complete) reach the endpoint before the CLI exits.
|
|
1648
|
+
if (webhookStreamer)
|
|
1649
|
+
await webhookStreamer.close().catch(() => { });
|
|
1402
1650
|
await singleDriver?.close?.().catch(() => { });
|
|
1403
1651
|
stopWalletAutoApprover?.();
|
|
1404
1652
|
await persistentContext?.close().catch(() => { });
|