explorbot 0.2.2 → 0.2.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/README.md +1 -1
- package/bin/explorbot-cli.ts +59 -38
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +43 -10
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +40 -10
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +12 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/src/config.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import path, { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
4
5
|
import { parseEnv } from 'node:util';
|
|
6
|
+
import dedent from 'dedent';
|
|
5
7
|
import matter from 'gray-matter';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
|
|
9
|
+
import { getCliName } from './utils/cli-name.js';
|
|
10
|
+
import { log, tag } from './utils/logger.js';
|
|
11
|
+
|
|
12
|
+
export const PROVIDERS: Record<string, ProviderInfo> = {
|
|
13
|
+
openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
|
|
14
|
+
anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
|
|
15
|
+
google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
|
|
16
|
+
groq: { envKey: 'GROQ_API_KEY', load: async () => (await import('@ai-sdk/groq')).createGroq() },
|
|
17
|
+
mistral: { envKey: 'MISTRAL_API_KEY', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
|
|
18
|
+
openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
|
|
19
|
+
sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
let cachedOutputRoot: string | null = null;
|
|
@@ -194,6 +198,7 @@ interface HtmlConfig {
|
|
|
194
198
|
interface ActionConfig {
|
|
195
199
|
delay?: number;
|
|
196
200
|
retries?: number;
|
|
201
|
+
timeout?: number;
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
interface ReporterConfig {
|
|
@@ -227,6 +232,7 @@ interface ExplorbotConfig {
|
|
|
227
232
|
knowledge: string;
|
|
228
233
|
experience: string;
|
|
229
234
|
output: string;
|
|
235
|
+
spec?: string;
|
|
230
236
|
};
|
|
231
237
|
experience?: {
|
|
232
238
|
maxReadLines?: number;
|
|
@@ -260,7 +266,8 @@ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
|
|
|
260
266
|
{ name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
|
|
261
267
|
{ name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
|
|
262
268
|
{ name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
|
|
263
|
-
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to
|
|
269
|
+
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
|
|
270
|
+
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
264
271
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
265
272
|
{ name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
|
|
266
273
|
{ name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
|
|
@@ -299,14 +306,26 @@ export class ConfigParser {
|
|
|
299
306
|
private static recommended: Record<string, Record<string, string>> | null = null;
|
|
300
307
|
private config: ExplorbotConfig | null = null;
|
|
301
308
|
private configPath: string | null = null;
|
|
302
|
-
private
|
|
309
|
+
private runtimeTarget: string | null = null;
|
|
310
|
+
private site: SiteRecord | null = null;
|
|
311
|
+
private siteStartPath = '/';
|
|
303
312
|
|
|
304
313
|
private constructor() {}
|
|
305
314
|
|
|
306
|
-
public static loadEnv(filePath: string): void {
|
|
315
|
+
public static loadEnv(filePath: string, keepExisting = false): void {
|
|
307
316
|
const resolved = resolve(filePath);
|
|
308
317
|
if (!existsSync(resolved)) return;
|
|
309
|
-
|
|
318
|
+
|
|
319
|
+
const parsed = parseEnv(readFileSync(resolved, 'utf8'));
|
|
320
|
+
if (!keepExisting) {
|
|
321
|
+
Object.assign(process.env, parsed);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
326
|
+
if (key in process.env) continue;
|
|
327
|
+
process.env[key] = value as string;
|
|
328
|
+
}
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
public static recommendedModels(): Record<string, Record<string, string>> {
|
|
@@ -325,8 +344,10 @@ export class ConfigParser {
|
|
|
325
344
|
config?: string;
|
|
326
345
|
path?: string;
|
|
327
346
|
baseUrl?: string;
|
|
347
|
+
from?: string;
|
|
328
348
|
}): Promise<ExplorbotConfig> {
|
|
329
|
-
|
|
349
|
+
const target = options?.baseUrl || options?.from || null;
|
|
350
|
+
if (this.config && !options?.config && !options?.path && this.runtimeTarget === target) {
|
|
330
351
|
return this.config;
|
|
331
352
|
}
|
|
332
353
|
|
|
@@ -342,6 +363,7 @@ export class ConfigParser {
|
|
|
342
363
|
}
|
|
343
364
|
|
|
344
365
|
ConfigParser.loadEnv('.env');
|
|
366
|
+
ConfigParser.loadEnv(globalEnvPath(), true);
|
|
345
367
|
|
|
346
368
|
try {
|
|
347
369
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
@@ -361,16 +383,25 @@ export class ConfigParser {
|
|
|
361
383
|
}
|
|
362
384
|
|
|
363
385
|
if (!resolvedPath) {
|
|
364
|
-
|
|
365
|
-
|
|
386
|
+
let envUrl = options?.baseUrl;
|
|
387
|
+
if (!envUrl && target?.startsWith('http')) envUrl = target;
|
|
388
|
+
|
|
389
|
+
const outputRoot = resolveOutputRoot(process.env.EXPLORBOT_URL || envUrl);
|
|
390
|
+
loadedConfig = await this.buildEnvConfig(envUrl, outputRoot);
|
|
366
391
|
sourcePath = join(outputRoot, 'explorbot.config.js');
|
|
367
392
|
|
|
368
393
|
log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
|
|
369
394
|
}
|
|
370
395
|
|
|
371
396
|
this.config = this.resolveConfig(loadedConfig as ExplorbotConfig, options);
|
|
372
|
-
this.
|
|
397
|
+
await resolveConfigModels(this.config.ai);
|
|
398
|
+
this.runtimeTarget = target;
|
|
373
399
|
this.configPath = sourcePath;
|
|
400
|
+
this.site = null;
|
|
401
|
+
|
|
402
|
+
if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
|
|
403
|
+
this.enterGlobalMode(this.config, target);
|
|
404
|
+
}
|
|
374
405
|
|
|
375
406
|
// Restore original directory after successful config load
|
|
376
407
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
@@ -383,6 +414,7 @@ export class ConfigParser {
|
|
|
383
414
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
384
415
|
process.chdir(originalCwd);
|
|
385
416
|
}
|
|
417
|
+
if (error instanceof ConfigMissingError) throw error;
|
|
386
418
|
throw new Error(`Failed to load configuration: ${error}`);
|
|
387
419
|
}
|
|
388
420
|
}
|
|
@@ -400,21 +432,45 @@ export class ConfigParser {
|
|
|
400
432
|
|
|
401
433
|
public getOutputDir(): string {
|
|
402
434
|
const config = this.getConfig();
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return path.join(path.dirname(configPath), config.dirs?.output || 'output');
|
|
435
|
+
if (!this.configPath) throw new Error('Config path not found');
|
|
436
|
+
return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
|
|
406
437
|
}
|
|
407
438
|
|
|
408
439
|
public getProjectRoot(): string {
|
|
440
|
+
if (this.site) return this.site.dir;
|
|
409
441
|
const configPath = this.getConfigPath();
|
|
410
442
|
if (configPath) return path.dirname(configPath);
|
|
411
443
|
return process.cwd();
|
|
412
444
|
}
|
|
413
445
|
|
|
414
446
|
public resolveProjectDir(relativeDir: string): string {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
447
|
+
if (!this.configPath) return relativeDir;
|
|
448
|
+
return path.join(this.getProjectRoot(), relativeDir);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
public isGlobalMode(): boolean {
|
|
452
|
+
return !!this.site;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
public getSite(): SiteRecord | null {
|
|
456
|
+
return this.site;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public resolveTargetPath(target?: string): string {
|
|
460
|
+
if (!this.site) {
|
|
461
|
+
const configured = this.config?.playwright?.url || this.config?.web?.url;
|
|
462
|
+
const targetOrigin = target ? URL.parse(target)?.origin : null;
|
|
463
|
+
const baseOrigin = configured ? URL.parse(configured)?.origin : null;
|
|
464
|
+
if (targetOrigin && baseOrigin && targetOrigin !== baseOrigin) {
|
|
465
|
+
tag('warning').log(`Exploring ${targetOrigin} but base URL is ${baseOrigin}. Relative navigation resolves against the base URL — set web.url to ${targetOrigin} to avoid it.`);
|
|
466
|
+
}
|
|
467
|
+
return target || '/';
|
|
468
|
+
}
|
|
469
|
+
if (!target) return this.siteStartPath;
|
|
470
|
+
|
|
471
|
+
const resolved = resolveSiteTarget(target, this.site.url);
|
|
472
|
+
if (resolved.baseUrl !== this.site.url) return target;
|
|
473
|
+
return resolved.path;
|
|
418
474
|
}
|
|
419
475
|
|
|
420
476
|
public getStatesDir(): string {
|
|
@@ -435,7 +491,9 @@ export class ConfigParser {
|
|
|
435
491
|
if (ConfigParser.instance) {
|
|
436
492
|
ConfigParser.instance.config = null;
|
|
437
493
|
ConfigParser.instance.configPath = null;
|
|
438
|
-
ConfigParser.instance.
|
|
494
|
+
ConfigParser.instance.runtimeTarget = null;
|
|
495
|
+
ConfigParser.instance.site = null;
|
|
496
|
+
ConfigParser.instance.siteStartPath = '/';
|
|
439
497
|
}
|
|
440
498
|
}
|
|
441
499
|
|
|
@@ -486,11 +544,22 @@ export class ConfigParser {
|
|
|
486
544
|
}
|
|
487
545
|
}
|
|
488
546
|
|
|
547
|
+
private enterGlobalMode(config: ExplorbotConfig, target: string | null): void {
|
|
548
|
+
const site = resolveSiteTarget(target || undefined, config.web?.url || config.playwright?.url);
|
|
549
|
+
this.site = registerSite(site.baseUrl);
|
|
550
|
+
this.siteStartPath = site.path;
|
|
551
|
+
|
|
552
|
+
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
553
|
+
config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
554
|
+
|
|
555
|
+
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
489
558
|
private async buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig> {
|
|
490
559
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
491
560
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
492
561
|
if (!provider && !modelSpec) {
|
|
493
|
-
throw new
|
|
562
|
+
throw new ConfigMissingError(missingConfigMessage());
|
|
494
563
|
}
|
|
495
564
|
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
496
565
|
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
@@ -524,11 +593,14 @@ export class ConfigParser {
|
|
|
524
593
|
if (agenticSpec) ai.agenticModel = await resolveModel(agenticSpec, 'agenticModel');
|
|
525
594
|
if (!agenticSpec && recommended.agenticModel) ai.agenticModel = await resolveModel(provider!, 'agenticModel');
|
|
526
595
|
|
|
596
|
+
const dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
597
|
+
if (process.env.EXPLORBOT_OUTPUT) dirs.output = '.';
|
|
598
|
+
|
|
527
599
|
return {
|
|
528
600
|
playwright: { browser: 'chromium', url, show: false },
|
|
529
601
|
ai,
|
|
530
|
-
dirs
|
|
531
|
-
experience: { disabled:
|
|
602
|
+
dirs,
|
|
603
|
+
experience: { disabled: !!process.env.EXPLORBOT_EPHEMERAL },
|
|
532
604
|
};
|
|
533
605
|
}
|
|
534
606
|
|
|
@@ -542,22 +614,24 @@ export class ConfigParser {
|
|
|
542
614
|
}
|
|
543
615
|
}
|
|
544
616
|
|
|
545
|
-
return null;
|
|
617
|
+
if (envConfigRequested()) return null;
|
|
618
|
+
return findGlobalConfig();
|
|
546
619
|
}
|
|
547
620
|
|
|
548
621
|
private async loadConfigModule(configPath: string): Promise<any> {
|
|
549
622
|
const ext = configPath.split('.').pop();
|
|
623
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
550
624
|
|
|
551
625
|
if (ext === 'ts') {
|
|
552
626
|
try {
|
|
553
|
-
const module = await import(
|
|
627
|
+
const module = await import(moduleUrl);
|
|
554
628
|
return module;
|
|
555
629
|
} catch (error) {
|
|
556
630
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
557
631
|
return require(configPath);
|
|
558
632
|
}
|
|
559
633
|
} else if (ext === 'js' || ext === 'mjs') {
|
|
560
|
-
const module = await import(
|
|
634
|
+
const module = await import(moduleUrl);
|
|
561
635
|
return module;
|
|
562
636
|
} else {
|
|
563
637
|
const content = readFileSync(configPath, 'utf8');
|
|
@@ -664,30 +738,84 @@ export async function resolveModel(spec: string, role: ModelRole = 'model'): Pro
|
|
|
664
738
|
return createModel(spec, modelId);
|
|
665
739
|
}
|
|
666
740
|
|
|
667
|
-
export
|
|
741
|
+
export class ConfigMissingError extends Error {}
|
|
742
|
+
|
|
743
|
+
export function envConfigRequested(): boolean {
|
|
744
|
+
return !!(process.env.EXPLORBOT_AI_PROVIDER || process.env.EXPLORBOT_AI_MODEL);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function missingConfigMessage(configFile = 'explorbot.config.js'): string {
|
|
748
|
+
const cli = getCliName();
|
|
749
|
+
return dedent`
|
|
750
|
+
No AI configuration found. Set up explorbot in one of these ways:
|
|
751
|
+
|
|
752
|
+
Global - configure this machine once, then run from any directory:
|
|
753
|
+
${cli} init --global
|
|
754
|
+
|
|
755
|
+
Local - create ${configFile} for this project:
|
|
756
|
+
${cli} init
|
|
757
|
+
|
|
758
|
+
Environment - one-off run, no files written:
|
|
759
|
+
EXPLORBOT_AI_PROVIDER=openrouter EXPLORBOT_URL=https://your-app.example.com ${cli} ...
|
|
760
|
+
|
|
761
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export async function resolveConfigModels(ai?: AIConfig): Promise<void> {
|
|
766
|
+
if (!ai) return;
|
|
767
|
+
|
|
768
|
+
const roles: ModelRole[] = ['model', 'visionModel', 'agenticModel'];
|
|
769
|
+
for (const role of roles) {
|
|
770
|
+
if (typeof ai[role] === 'string') ai[role] = await resolveModel(ai[role], role);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
for (const agent of Object.values(ai.agents || {})) {
|
|
774
|
+
if (typeof agent?.model === 'string') agent.model = await resolveModel(agent.model);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
export function resolveOutputRoot(baseUrl?: string): string {
|
|
668
779
|
if (cachedOutputRoot) return cachedOutputRoot;
|
|
669
780
|
|
|
670
781
|
const configured = process.env.EXPLORBOT_OUTPUT;
|
|
671
|
-
if (
|
|
672
|
-
cachedOutputRoot =
|
|
782
|
+
if (configured) {
|
|
783
|
+
cachedOutputRoot = resolve(configured);
|
|
784
|
+
mkdirSync(cachedOutputRoot, { recursive: true });
|
|
785
|
+
return cachedOutputRoot;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (baseUrl) {
|
|
789
|
+
cachedOutputRoot = resolveStateRoot(baseUrl, !!process.env.EXPLORBOT_EPHEMERAL);
|
|
673
790
|
return cachedOutputRoot;
|
|
674
791
|
}
|
|
675
792
|
|
|
676
|
-
cachedOutputRoot =
|
|
677
|
-
mkdirSync(cachedOutputRoot, { recursive: true });
|
|
793
|
+
cachedOutputRoot = mkdtempSync(join(tmpdir(), 'explorbot-'));
|
|
678
794
|
return cachedOutputRoot;
|
|
679
795
|
}
|
|
680
796
|
|
|
797
|
+
export function resolveStateRoot(baseUrl: string, ephemeral?: boolean): string {
|
|
798
|
+
const url = URL.parse(baseUrl);
|
|
799
|
+
if (ephemeral || !url?.host) return mkdtempSync(join(tmpdir(), 'explorbot-'));
|
|
800
|
+
|
|
801
|
+
return registerSite(url.origin).dir;
|
|
802
|
+
}
|
|
803
|
+
|
|
681
804
|
export function materializeKnowledge(outputRoot: string): void {
|
|
682
805
|
const inline = process.env.EXPLORBOT_KNOWLEDGE;
|
|
683
806
|
const knowledgeFile = process.env.EXPLORBOT_KNOWLEDGE_FILE;
|
|
807
|
+
const knowledgeDir = join(outputRoot, 'knowledge');
|
|
808
|
+
const globalFile = join(knowledgeDir, 'global.md');
|
|
809
|
+
const envDir = join(knowledgeDir, 'env');
|
|
810
|
+
|
|
811
|
+
if (!inline) rmSync(globalFile, { force: true });
|
|
812
|
+
rmSync(envDir, { recursive: true, force: true });
|
|
684
813
|
if (!inline && !knowledgeFile) return;
|
|
685
814
|
|
|
686
|
-
const knowledgeDir = join(outputRoot, 'knowledge');
|
|
687
815
|
mkdirSync(knowledgeDir, { recursive: true });
|
|
688
816
|
|
|
689
817
|
if (inline) {
|
|
690
|
-
writeFileSync(
|
|
818
|
+
writeFileSync(globalFile, matter.stringify(inline, { url: '*', endpoint: '*' }));
|
|
691
819
|
}
|
|
692
820
|
|
|
693
821
|
if (!knowledgeFile) return;
|
|
@@ -696,23 +824,29 @@ export function materializeKnowledge(outputRoot: string): void {
|
|
|
696
824
|
if (!existsSync(source)) {
|
|
697
825
|
throw new Error(`Knowledge file from EXPLORBOT_KNOWLEDGE_FILE not found: ${source}`);
|
|
698
826
|
}
|
|
699
|
-
|
|
827
|
+
mkdirSync(envDir, { recursive: true });
|
|
828
|
+
copyFileSync(source, join(envDir, basename(source)));
|
|
700
829
|
}
|
|
701
830
|
|
|
702
831
|
export async function createModel(provider: string, modelId: string): Promise<any> {
|
|
703
|
-
const
|
|
704
|
-
if (!
|
|
832
|
+
const info = PROVIDERS[provider];
|
|
833
|
+
if (!info) {
|
|
705
834
|
throw new Error(`Unknown AI provider "${provider}". Supported providers: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
706
835
|
}
|
|
707
|
-
return (await
|
|
836
|
+
return (await info.load())(modelId);
|
|
708
837
|
}
|
|
709
838
|
|
|
710
839
|
type ModelRole = 'model' | 'visionModel' | 'agenticModel';
|
|
711
840
|
|
|
841
|
+
interface ProviderInfo {
|
|
842
|
+
envKey: string;
|
|
843
|
+
load: () => Promise<(modelId: string) => any>;
|
|
844
|
+
}
|
|
845
|
+
|
|
712
846
|
interface EnvVar {
|
|
713
847
|
name: string;
|
|
714
848
|
description: string;
|
|
715
849
|
required?: boolean;
|
|
716
850
|
}
|
|
717
851
|
|
|
718
|
-
export type { ModelRole, EnvVar };
|
|
852
|
+
export type { ModelRole, EnvVar, ProviderInfo };
|
|
@@ -27,6 +27,14 @@ export class ExecutionController extends EventEmitter {
|
|
|
27
27
|
this.inputCallback = callback;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
hasInputCallback(): boolean {
|
|
31
|
+
return !!this.inputCallback;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clearInputCallback(): void {
|
|
35
|
+
this.inputCallback = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
30
38
|
startExecution(): void {
|
|
31
39
|
this.interrupted = false;
|
|
32
40
|
this.abortController = new AbortController();
|
package/src/explorbot.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import type { Browser } from 'playwright';
|
|
3
4
|
import type { AgentDeps } from './ai/agent.ts';
|
|
4
5
|
import { Captain } from './ai/captain.ts';
|
|
5
6
|
import { Driller } from './ai/driller.ts';
|
|
@@ -19,7 +20,7 @@ import { createAgentTools } from './ai/tools.ts';
|
|
|
19
20
|
import { ApiClient } from './api/api-client.ts';
|
|
20
21
|
import { RequestStore } from './api/request-store.ts';
|
|
21
22
|
import { loadSpec } from './api/spec-reader.ts';
|
|
22
|
-
import type { ExplorbotConfig } from './config.js';
|
|
23
|
+
import type { ExplorbotConfig, ReporterConfig } from './config.js';
|
|
23
24
|
import { ConfigParser } from './config.ts';
|
|
24
25
|
import { ExperienceTracker } from './experience-tracker.ts';
|
|
25
26
|
import Explorer from './explorer.ts';
|
|
@@ -46,6 +47,11 @@ export interface ExplorBotOptions {
|
|
|
46
47
|
headless?: boolean;
|
|
47
48
|
incognito?: boolean;
|
|
48
49
|
session?: string | boolean;
|
|
50
|
+
instance?: string;
|
|
51
|
+
optionalAi?: boolean;
|
|
52
|
+
reporter?: ReporterConfig;
|
|
53
|
+
attachedBrowser?: Browser;
|
|
54
|
+
applicationSpec?: string;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export type UserResolveFunction = (error?: Error, showWelcome?: boolean) => Promise<string | null>;
|
|
@@ -61,6 +67,7 @@ export class ExplorBot {
|
|
|
61
67
|
private planFeature?: string;
|
|
62
68
|
lastPlanError: Error | null = null;
|
|
63
69
|
lastSavedPlanPath: string | null = null;
|
|
70
|
+
private aiFailure: string | null = null;
|
|
64
71
|
private agents: Record<string, any> = {};
|
|
65
72
|
private sessionPlans: Plan[] = [];
|
|
66
73
|
private lastReportedTestCount = 0;
|
|
@@ -88,6 +95,10 @@ export class ExplorBot {
|
|
|
88
95
|
this.userResolveFn = fn;
|
|
89
96
|
}
|
|
90
97
|
|
|
98
|
+
attachBrowser(browser: Browser): void {
|
|
99
|
+
this.options.attachedBrowser = browser;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
async start(): Promise<void> {
|
|
92
103
|
if (this.explorer) {
|
|
93
104
|
return;
|
|
@@ -104,7 +115,7 @@ export class ExplorBot {
|
|
|
104
115
|
playwrightRecorder: this.playwrightRecorder(),
|
|
105
116
|
});
|
|
106
117
|
await this.explorer.start();
|
|
107
|
-
if (!this.options.incognito) {
|
|
118
|
+
if (!this.options.incognito && this.provider) {
|
|
108
119
|
await this.agentExperienceCompactor().autocompact();
|
|
109
120
|
}
|
|
110
121
|
} catch (error) {
|
|
@@ -118,22 +129,27 @@ export class ExplorBot {
|
|
|
118
129
|
if (this.provider) return;
|
|
119
130
|
this.config = await this.configParser.loadConfig(this.options);
|
|
120
131
|
if (this.options.session === true) this.options.session = path.join(this.configParser.getOutputDir(), 'session.json');
|
|
132
|
+
if (typeof this.options.session === 'string') this.options.session = path.resolve(this.options.path || '.', this.options.session);
|
|
133
|
+
if (this.options.optionalAi) return this.bootstrapOptionalProvider();
|
|
121
134
|
this.provider = new AIProvider(this.config.ai);
|
|
122
135
|
await this.provider.validateConnection();
|
|
123
136
|
}
|
|
124
137
|
|
|
138
|
+
aiFailureReason(): string | null {
|
|
139
|
+
return this.aiFailure;
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
async stop(): Promise<void> {
|
|
126
143
|
this.agents.quartermaster?.stop();
|
|
127
144
|
await this.explorer?.stop();
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
async visitInitialState(): Promise<void> {
|
|
131
|
-
|
|
132
|
-
await this.visit(url);
|
|
148
|
+
await this.visit(this.options.from || '/');
|
|
133
149
|
}
|
|
134
150
|
|
|
135
151
|
async visit(url: string): Promise<void> {
|
|
136
|
-
return this.agentNavigator().visit(url);
|
|
152
|
+
return this.agentNavigator().visit(this.configParser.resolveTargetPath(url));
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
async openTab(): Promise<void> {
|
|
@@ -149,7 +165,7 @@ export class ExplorBot {
|
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
knowledgeTracker(): KnowledgeTracker {
|
|
152
|
-
return (this._knowledgeTracker ||= new KnowledgeTracker());
|
|
168
|
+
return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
experienceTracker(): ExperienceTracker {
|
|
@@ -161,7 +177,7 @@ export class ExplorBot {
|
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
reporter(): Reporter {
|
|
164
|
-
return (this._reporter ||= new Reporter(this.config.reporter, this.stateManager()));
|
|
180
|
+
return (this._reporter ||= new Reporter(this.options.reporter || this.config.reporter, this.stateManager()));
|
|
165
181
|
}
|
|
166
182
|
|
|
167
183
|
requestStore(): RequestStore {
|
|
@@ -518,4 +534,15 @@ export class ExplorBot {
|
|
|
518
534
|
private isHistorianEnabled(): boolean {
|
|
519
535
|
return this.config.ai?.agents?.historian?.enabled !== false;
|
|
520
536
|
}
|
|
537
|
+
|
|
538
|
+
private async bootstrapOptionalProvider(): Promise<void> {
|
|
539
|
+
try {
|
|
540
|
+
const provider = new AIProvider(this.config.ai);
|
|
541
|
+
await provider.validateConnection();
|
|
542
|
+
this.provider = provider;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
this.aiFailure = browserErrorMessage(error);
|
|
545
|
+
tag('debug').log(`AI provider unavailable: ${this.aiFailure}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
521
548
|
}
|
package/src/explorer.ts
CHANGED
|
@@ -6,7 +6,7 @@ import stepsListener from 'codeceptjs/lib/listener/steps';
|
|
|
6
6
|
import storeListener from 'codeceptjs/lib/listener/store';
|
|
7
7
|
import { createTest } from 'codeceptjs/lib/mocha/test';
|
|
8
8
|
import dedent from 'dedent';
|
|
9
|
-
import type { BrowserContextOptions, Page } from 'playwright';
|
|
9
|
+
import type { Browser, BrowserContextOptions, Page } from 'playwright';
|
|
10
10
|
import { ActionResult } from './action-result.ts';
|
|
11
11
|
import Action from './action.js';
|
|
12
12
|
import type { RequestStore } from './api/request-store.ts';
|
|
@@ -103,8 +103,8 @@ class Explorer {
|
|
|
103
103
|
throw new Error('Playwright helper not available');
|
|
104
104
|
}
|
|
105
105
|
await this.connectOrLaunchBrowser();
|
|
106
|
-
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
107
|
-
await this.
|
|
106
|
+
const hasSession = !this.options?.attachedBrowser && this.options?.session && existsSync(this.options.session);
|
|
107
|
+
await this.openContextPage();
|
|
108
108
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
109
109
|
this.attachXhrCapture();
|
|
110
110
|
if (hasSession) {
|
|
@@ -123,10 +123,11 @@ class Explorer {
|
|
|
123
123
|
async stop(): Promise<void> {
|
|
124
124
|
if (!this.started) return;
|
|
125
125
|
this.started = false;
|
|
126
|
+
const attached = !!this.options?.attachedBrowser;
|
|
126
127
|
|
|
127
128
|
await this.stopCaptures();
|
|
128
129
|
|
|
129
|
-
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
130
|
+
if (!attached && this.options?.session && this.playwrightHelper?.browserContext) {
|
|
130
131
|
const dir = path.dirname(this.options.session);
|
|
131
132
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
132
133
|
await this.playwrightHelper.browserContext.storageState({ path: this.options.session });
|
|
@@ -141,7 +142,14 @@ class Explorer {
|
|
|
141
142
|
return;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
if (attached) {
|
|
146
|
+
tag('info').log('Disconnecting from attached browser (its pages stay open)');
|
|
147
|
+
await this.playwrightHelper.browser?.close().catch((err: unknown) => {
|
|
148
|
+
debugLog('Failed to disconnect from attached browser:', err);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!attached) tag('info').log('Closing browser context (persistent browser stays running)');
|
|
145
153
|
await this.closeBrowserContext();
|
|
146
154
|
this.playwrightHelper.browser = null;
|
|
147
155
|
this.playwrightHelper.isRunning = false;
|
|
@@ -309,8 +317,16 @@ class Explorer {
|
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
private async connectOrLaunchBrowser(): Promise<void> {
|
|
320
|
+
if (this.options?.attachedBrowser) {
|
|
321
|
+
this.playwrightHelper.browser = this.options.attachedBrowser;
|
|
322
|
+
this.playwrightHelper.isRunning = true;
|
|
323
|
+
this.isSharedBrowser = true;
|
|
324
|
+
tag('success').log('Attached to a browser opened by another tool');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
312
328
|
const { getAliveEndpoint } = await import('./browser-server.js');
|
|
313
|
-
const endpoint = await getAliveEndpoint();
|
|
329
|
+
const endpoint = await getAliveEndpoint(this.options?.instance);
|
|
314
330
|
|
|
315
331
|
if (endpoint) {
|
|
316
332
|
const browserName = this.config.playwright.browser || 'chromium';
|
|
@@ -326,6 +342,21 @@ class Explorer {
|
|
|
326
342
|
await this.playwrightHelper._startBrowser();
|
|
327
343
|
}
|
|
328
344
|
|
|
345
|
+
private async openContextPage(): Promise<void> {
|
|
346
|
+
const attached = this.options?.attachedBrowser;
|
|
347
|
+
if (!attached) {
|
|
348
|
+
await this.playwrightHelper._createContextPage(this.createBrowserContextOptions());
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const context = attached.contexts()[0] || (await attached.newContext());
|
|
353
|
+
const pages = context.pages();
|
|
354
|
+
const page = pages[pages.length - 1] || (await context.newPage());
|
|
355
|
+
this.playwrightHelper.browserContext = context;
|
|
356
|
+
await this.playwrightHelper._setPage(page);
|
|
357
|
+
debugLog(`Adopted attached browser page: ${page.url()}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
329
360
|
private createBrowserContextOptions(): BrowserContextOptions {
|
|
330
361
|
const helperOptions = this.playwrightHelper.options || {};
|
|
331
362
|
const contextOptions: BrowserContextOptions = {
|
|
@@ -506,6 +537,10 @@ class Explorer {
|
|
|
506
537
|
}
|
|
507
538
|
|
|
508
539
|
private async closeBrowserContext(): Promise<void> {
|
|
540
|
+
if (this.options?.attachedBrowser) {
|
|
541
|
+
this.playwrightHelper.browserContext = null;
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
509
544
|
if (!this.playwrightHelper.browserContext) return;
|
|
510
545
|
await this.playwrightHelper.browserContext.close().catch((err: unknown) => {
|
|
511
546
|
debugLog('Failed to close browser context:', err);
|
|
@@ -529,7 +564,7 @@ class Explorer {
|
|
|
529
564
|
}
|
|
530
565
|
|
|
531
566
|
await this.connectOrLaunchBrowser();
|
|
532
|
-
await this.
|
|
567
|
+
await this.openContextPage();
|
|
533
568
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
534
569
|
this.attachXhrCapture();
|
|
535
570
|
this.listenToStateChanged();
|
|
@@ -631,6 +666,7 @@ class Explorer {
|
|
|
631
666
|
|
|
632
667
|
private async closeOtherTabs(): Promise<void> {
|
|
633
668
|
if (!this.playwrightHelper) return;
|
|
669
|
+
if (this.options?.attachedBrowser) return;
|
|
634
670
|
|
|
635
671
|
const context = this.playwrightHelper.page.context();
|
|
636
672
|
const pages = context.pages();
|
|
@@ -685,11 +721,11 @@ class Explorer {
|
|
|
685
721
|
if (this.observedTestPages.has(page)) return;
|
|
686
722
|
|
|
687
723
|
this.testPageErrorHandler ||= (err: Error) => {
|
|
688
|
-
this._activeTest?.
|
|
724
|
+
this._activeTest?.addObservation(`Console error: ${err.message}`);
|
|
689
725
|
};
|
|
690
726
|
this.testConsoleHandler ||= (msg: any) => {
|
|
691
727
|
if (msg.type() !== 'error') return;
|
|
692
|
-
this._activeTest?.
|
|
728
|
+
this._activeTest?.addObservation(`Console error: ${msg.text()}`);
|
|
693
729
|
};
|
|
694
730
|
this.testDialogHandler ||= (dialog: any) => {
|
|
695
731
|
const dialogType = dialog.type();
|
|
@@ -733,6 +769,8 @@ export interface ExplorerOptions {
|
|
|
733
769
|
headless?: boolean;
|
|
734
770
|
incognito?: boolean;
|
|
735
771
|
session?: string;
|
|
772
|
+
instance?: string;
|
|
773
|
+
attachedBrowser?: Browser;
|
|
736
774
|
}
|
|
737
775
|
|
|
738
776
|
export interface ExplorerDeps {
|