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/dist/src/config.js
CHANGED
|
@@ -9,17 +9,21 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import path, { basename, dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
12
13
|
import { parseEnv } from 'node:util';
|
|
14
|
+
import dedent from 'dedent';
|
|
13
15
|
import matter from 'gray-matter';
|
|
14
|
-
import {
|
|
16
|
+
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
|
|
17
|
+
import { getCliName } from './utils/cli-name.js';
|
|
18
|
+
import { log, tag } from './utils/logger.js';
|
|
15
19
|
export const PROVIDERS = {
|
|
16
|
-
openai: async () => (await import('@ai-sdk/openai')).createOpenAI(),
|
|
17
|
-
anthropic: async () => (await import('@ai-sdk/anthropic')).createAnthropic(),
|
|
18
|
-
google: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI(),
|
|
19
|
-
groq: async () => (await import('@ai-sdk/groq')).createGroq(),
|
|
20
|
-
mistral: async () => (await import('@ai-sdk/mistral')).createMistral(),
|
|
21
|
-
openrouter: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter(),
|
|
22
|
-
sambanova: async () => (await import('sambanova-ai-provider')).createSambaNova(),
|
|
20
|
+
openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
|
|
21
|
+
anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
|
|
22
|
+
google: { envKey: 'GOOGLE_GENERATIVE_AI_API_KEY', load: async () => (await import('@ai-sdk/google')).createGoogleGenerativeAI() },
|
|
23
|
+
groq: { envKey: 'GROQ_API_KEY', load: async () => (await import('@ai-sdk/groq')).createGroq() },
|
|
24
|
+
mistral: { envKey: 'MISTRAL_API_KEY', load: async () => (await import('@ai-sdk/mistral')).createMistral() },
|
|
25
|
+
openrouter: { envKey: 'OPENROUTER_API_KEY', load: async () => (await import('@openrouter/ai-sdk-provider')).createOpenRouter() },
|
|
26
|
+
sambanova: { envKey: 'SAMBANOVA_API_KEY', load: async () => (await import('sambanova-ai-provider')).createSambaNova() },
|
|
23
27
|
};
|
|
24
28
|
let cachedOutputRoot = null;
|
|
25
29
|
const config = {
|
|
@@ -38,7 +42,8 @@ export const EXPLORBOT_ENV_VARS = [
|
|
|
38
42
|
{ name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
|
|
39
43
|
{ name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
|
|
40
44
|
{ name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
|
|
41
|
-
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to
|
|
45
|
+
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
|
|
46
|
+
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
42
47
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
43
48
|
{ name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
|
|
44
49
|
{ name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
|
|
@@ -49,13 +54,24 @@ export class ConfigParser {
|
|
|
49
54
|
static recommended = null;
|
|
50
55
|
config = null;
|
|
51
56
|
configPath = null;
|
|
52
|
-
|
|
57
|
+
runtimeTarget = null;
|
|
58
|
+
site = null;
|
|
59
|
+
siteStartPath = '/';
|
|
53
60
|
constructor() { }
|
|
54
|
-
static loadEnv(filePath) {
|
|
61
|
+
static loadEnv(filePath, keepExisting = false) {
|
|
55
62
|
const resolved = resolve(filePath);
|
|
56
63
|
if (!existsSync(resolved))
|
|
57
64
|
return;
|
|
58
|
-
|
|
65
|
+
const parsed = parseEnv(readFileSync(resolved, 'utf8'));
|
|
66
|
+
if (!keepExisting) {
|
|
67
|
+
Object.assign(process.env, parsed);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
71
|
+
if (key in process.env)
|
|
72
|
+
continue;
|
|
73
|
+
process.env[key] = value;
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
static recommendedModels() {
|
|
61
77
|
ConfigParser.recommended ||= JSON.parse(readFileSync(new URL('../models.json', import.meta.url), 'utf8'));
|
|
@@ -68,7 +84,8 @@ export class ConfigParser {
|
|
|
68
84
|
return ConfigParser.instance;
|
|
69
85
|
}
|
|
70
86
|
async loadConfig(options) {
|
|
71
|
-
|
|
87
|
+
const target = options?.baseUrl || options?.from || null;
|
|
88
|
+
if (this.config && !options?.config && !options?.path && this.runtimeTarget === target) {
|
|
72
89
|
return this.config;
|
|
73
90
|
}
|
|
74
91
|
// Store the initial working directory for reference
|
|
@@ -81,6 +98,7 @@ export class ConfigParser {
|
|
|
81
98
|
process.chdir(resolvedWorkingPath);
|
|
82
99
|
}
|
|
83
100
|
ConfigParser.loadEnv('.env');
|
|
101
|
+
ConfigParser.loadEnv(globalEnvPath(), true);
|
|
84
102
|
try {
|
|
85
103
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
86
104
|
let loadedConfig = null;
|
|
@@ -94,14 +112,22 @@ export class ConfigParser {
|
|
|
94
112
|
log(`Configuration loaded from: ${resolvedPath}`);
|
|
95
113
|
}
|
|
96
114
|
if (!resolvedPath) {
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
let envUrl = options?.baseUrl;
|
|
116
|
+
if (!envUrl && target?.startsWith('http'))
|
|
117
|
+
envUrl = target;
|
|
118
|
+
const outputRoot = resolveOutputRoot(process.env.EXPLORBOT_URL || envUrl);
|
|
119
|
+
loadedConfig = await this.buildEnvConfig(envUrl, outputRoot);
|
|
99
120
|
sourcePath = join(outputRoot, 'explorbot.config.js');
|
|
100
121
|
log(`Configuration built from EXPLORBOT_* environment variables. Output: ${outputRoot}`);
|
|
101
122
|
}
|
|
102
123
|
this.config = this.resolveConfig(loadedConfig, options);
|
|
103
|
-
this.
|
|
124
|
+
await resolveConfigModels(this.config.ai);
|
|
125
|
+
this.runtimeTarget = target;
|
|
104
126
|
this.configPath = sourcePath;
|
|
127
|
+
this.site = null;
|
|
128
|
+
if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
|
|
129
|
+
this.enterGlobalMode(this.config, target);
|
|
130
|
+
}
|
|
105
131
|
// Restore original directory after successful config load
|
|
106
132
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
107
133
|
process.chdir(originalCwd);
|
|
@@ -113,6 +139,8 @@ export class ConfigParser {
|
|
|
113
139
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
114
140
|
process.chdir(originalCwd);
|
|
115
141
|
}
|
|
142
|
+
if (error instanceof ConfigMissingError)
|
|
143
|
+
throw error;
|
|
116
144
|
throw new Error(`Failed to load configuration: ${error}`);
|
|
117
145
|
}
|
|
118
146
|
}
|
|
@@ -127,22 +155,45 @@ export class ConfigParser {
|
|
|
127
155
|
}
|
|
128
156
|
getOutputDir() {
|
|
129
157
|
const config = this.getConfig();
|
|
130
|
-
|
|
131
|
-
if (!configPath)
|
|
158
|
+
if (!this.configPath)
|
|
132
159
|
throw new Error('Config path not found');
|
|
133
|
-
return path.join(
|
|
160
|
+
return path.join(this.getProjectRoot(), config.dirs?.output || 'output');
|
|
134
161
|
}
|
|
135
162
|
getProjectRoot() {
|
|
163
|
+
if (this.site)
|
|
164
|
+
return this.site.dir;
|
|
136
165
|
const configPath = this.getConfigPath();
|
|
137
166
|
if (configPath)
|
|
138
167
|
return path.dirname(configPath);
|
|
139
168
|
return process.cwd();
|
|
140
169
|
}
|
|
141
170
|
resolveProjectDir(relativeDir) {
|
|
142
|
-
|
|
143
|
-
if (!configPath)
|
|
171
|
+
if (!this.configPath)
|
|
144
172
|
return relativeDir;
|
|
145
|
-
return path.join(
|
|
173
|
+
return path.join(this.getProjectRoot(), relativeDir);
|
|
174
|
+
}
|
|
175
|
+
isGlobalMode() {
|
|
176
|
+
return !!this.site;
|
|
177
|
+
}
|
|
178
|
+
getSite() {
|
|
179
|
+
return this.site;
|
|
180
|
+
}
|
|
181
|
+
resolveTargetPath(target) {
|
|
182
|
+
if (!this.site) {
|
|
183
|
+
const configured = this.config?.playwright?.url || this.config?.web?.url;
|
|
184
|
+
const targetOrigin = target ? URL.parse(target)?.origin : null;
|
|
185
|
+
const baseOrigin = configured ? URL.parse(configured)?.origin : null;
|
|
186
|
+
if (targetOrigin && baseOrigin && targetOrigin !== baseOrigin) {
|
|
187
|
+
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.`);
|
|
188
|
+
}
|
|
189
|
+
return target || '/';
|
|
190
|
+
}
|
|
191
|
+
if (!target)
|
|
192
|
+
return this.siteStartPath;
|
|
193
|
+
const resolved = resolveSiteTarget(target, this.site.url);
|
|
194
|
+
if (resolved.baseUrl !== this.site.url)
|
|
195
|
+
return target;
|
|
196
|
+
return resolved.path;
|
|
146
197
|
}
|
|
147
198
|
getStatesDir() {
|
|
148
199
|
return outputPath('states');
|
|
@@ -159,7 +210,9 @@ export class ConfigParser {
|
|
|
159
210
|
if (ConfigParser.instance) {
|
|
160
211
|
ConfigParser.instance.config = null;
|
|
161
212
|
ConfigParser.instance.configPath = null;
|
|
162
|
-
ConfigParser.instance.
|
|
213
|
+
ConfigParser.instance.runtimeTarget = null;
|
|
214
|
+
ConfigParser.instance.site = null;
|
|
215
|
+
ConfigParser.instance.siteStartPath = '/';
|
|
163
216
|
}
|
|
164
217
|
}
|
|
165
218
|
// For testing purposes only - sets up minimal default config
|
|
@@ -206,11 +259,19 @@ export class ConfigParser {
|
|
|
206
259
|
// Ignore cleanup errors
|
|
207
260
|
}
|
|
208
261
|
}
|
|
262
|
+
enterGlobalMode(config, target) {
|
|
263
|
+
const site = resolveSiteTarget(target || undefined, config.web?.url || config.playwright?.url);
|
|
264
|
+
this.site = registerSite(site.baseUrl);
|
|
265
|
+
this.siteStartPath = site.path;
|
|
266
|
+
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
267
|
+
config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
268
|
+
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
269
|
+
}
|
|
209
270
|
async buildEnvConfig(baseUrl, outputRoot) {
|
|
210
271
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
211
272
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
212
273
|
if (!provider && !modelSpec) {
|
|
213
|
-
throw new
|
|
274
|
+
throw new ConfigMissingError(missingConfigMessage());
|
|
214
275
|
}
|
|
215
276
|
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
216
277
|
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
@@ -244,11 +305,14 @@ export class ConfigParser {
|
|
|
244
305
|
ai.agenticModel = await resolveModel(agenticSpec, 'agenticModel');
|
|
245
306
|
if (!agenticSpec && recommended.agenticModel)
|
|
246
307
|
ai.agenticModel = await resolveModel(provider, 'agenticModel');
|
|
308
|
+
const dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
309
|
+
if (process.env.EXPLORBOT_OUTPUT)
|
|
310
|
+
dirs.output = '.';
|
|
247
311
|
return {
|
|
248
312
|
playwright: { browser: 'chromium', url, show: false },
|
|
249
313
|
ai,
|
|
250
|
-
dirs
|
|
251
|
-
experience: { disabled:
|
|
314
|
+
dirs,
|
|
315
|
+
experience: { disabled: !!process.env.EXPLORBOT_EPHEMERAL },
|
|
252
316
|
};
|
|
253
317
|
}
|
|
254
318
|
findConfigFile() {
|
|
@@ -259,13 +323,16 @@ export class ConfigParser {
|
|
|
259
323
|
return fullPath;
|
|
260
324
|
}
|
|
261
325
|
}
|
|
262
|
-
|
|
326
|
+
if (envConfigRequested())
|
|
327
|
+
return null;
|
|
328
|
+
return findGlobalConfig();
|
|
263
329
|
}
|
|
264
330
|
async loadConfigModule(configPath) {
|
|
265
331
|
const ext = configPath.split('.').pop();
|
|
332
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
266
333
|
if (ext === 'ts') {
|
|
267
334
|
try {
|
|
268
|
-
const module = await import(__rewriteRelativeImportExtension(
|
|
335
|
+
const module = await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
269
336
|
return module;
|
|
270
337
|
}
|
|
271
338
|
catch (error) {
|
|
@@ -274,7 +341,7 @@ export class ConfigParser {
|
|
|
274
341
|
}
|
|
275
342
|
}
|
|
276
343
|
else if (ext === 'js' || ext === 'mjs') {
|
|
277
|
-
const module = await import(__rewriteRelativeImportExtension(
|
|
344
|
+
const module = await import(__rewriteRelativeImportExtension(moduleUrl));
|
|
278
345
|
return module;
|
|
279
346
|
}
|
|
280
347
|
else {
|
|
@@ -365,27 +432,77 @@ export async function resolveModel(spec, role = 'model') {
|
|
|
365
432
|
}
|
|
366
433
|
return createModel(spec, modelId);
|
|
367
434
|
}
|
|
368
|
-
export
|
|
435
|
+
export class ConfigMissingError extends Error {
|
|
436
|
+
}
|
|
437
|
+
export function envConfigRequested() {
|
|
438
|
+
return !!(process.env.EXPLORBOT_AI_PROVIDER || process.env.EXPLORBOT_AI_MODEL);
|
|
439
|
+
}
|
|
440
|
+
export function missingConfigMessage(configFile = 'explorbot.config.js') {
|
|
441
|
+
const cli = getCliName();
|
|
442
|
+
return dedent `
|
|
443
|
+
No AI configuration found. Set up explorbot in one of these ways:
|
|
444
|
+
|
|
445
|
+
Global - configure this machine once, then run from any directory:
|
|
446
|
+
${cli} init --global
|
|
447
|
+
|
|
448
|
+
Local - create ${configFile} for this project:
|
|
449
|
+
${cli} init
|
|
450
|
+
|
|
451
|
+
Environment - one-off run, no files written:
|
|
452
|
+
EXPLORBOT_AI_PROVIDER=openrouter EXPLORBOT_URL=https://your-app.example.com ${cli} ...
|
|
453
|
+
|
|
454
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
export async function resolveConfigModels(ai) {
|
|
458
|
+
if (!ai)
|
|
459
|
+
return;
|
|
460
|
+
const roles = ['model', 'visionModel', 'agenticModel'];
|
|
461
|
+
for (const role of roles) {
|
|
462
|
+
if (typeof ai[role] === 'string')
|
|
463
|
+
ai[role] = await resolveModel(ai[role], role);
|
|
464
|
+
}
|
|
465
|
+
for (const agent of Object.values(ai.agents || {})) {
|
|
466
|
+
if (typeof agent?.model === 'string')
|
|
467
|
+
agent.model = await resolveModel(agent.model);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
export function resolveOutputRoot(baseUrl) {
|
|
369
471
|
if (cachedOutputRoot)
|
|
370
472
|
return cachedOutputRoot;
|
|
371
473
|
const configured = process.env.EXPLORBOT_OUTPUT;
|
|
372
|
-
if (
|
|
373
|
-
cachedOutputRoot =
|
|
474
|
+
if (configured) {
|
|
475
|
+
cachedOutputRoot = resolve(configured);
|
|
476
|
+
mkdirSync(cachedOutputRoot, { recursive: true });
|
|
477
|
+
return cachedOutputRoot;
|
|
478
|
+
}
|
|
479
|
+
if (baseUrl) {
|
|
480
|
+
cachedOutputRoot = resolveStateRoot(baseUrl, !!process.env.EXPLORBOT_EPHEMERAL);
|
|
374
481
|
return cachedOutputRoot;
|
|
375
482
|
}
|
|
376
|
-
cachedOutputRoot =
|
|
377
|
-
mkdirSync(cachedOutputRoot, { recursive: true });
|
|
483
|
+
cachedOutputRoot = mkdtempSync(join(tmpdir(), 'explorbot-'));
|
|
378
484
|
return cachedOutputRoot;
|
|
379
485
|
}
|
|
486
|
+
export function resolveStateRoot(baseUrl, ephemeral) {
|
|
487
|
+
const url = URL.parse(baseUrl);
|
|
488
|
+
if (ephemeral || !url?.host)
|
|
489
|
+
return mkdtempSync(join(tmpdir(), 'explorbot-'));
|
|
490
|
+
return registerSite(url.origin).dir;
|
|
491
|
+
}
|
|
380
492
|
export function materializeKnowledge(outputRoot) {
|
|
381
493
|
const inline = process.env.EXPLORBOT_KNOWLEDGE;
|
|
382
494
|
const knowledgeFile = process.env.EXPLORBOT_KNOWLEDGE_FILE;
|
|
495
|
+
const knowledgeDir = join(outputRoot, 'knowledge');
|
|
496
|
+
const globalFile = join(knowledgeDir, 'global.md');
|
|
497
|
+
const envDir = join(knowledgeDir, 'env');
|
|
498
|
+
if (!inline)
|
|
499
|
+
rmSync(globalFile, { force: true });
|
|
500
|
+
rmSync(envDir, { recursive: true, force: true });
|
|
383
501
|
if (!inline && !knowledgeFile)
|
|
384
502
|
return;
|
|
385
|
-
const knowledgeDir = join(outputRoot, 'knowledge');
|
|
386
503
|
mkdirSync(knowledgeDir, { recursive: true });
|
|
387
504
|
if (inline) {
|
|
388
|
-
writeFileSync(
|
|
505
|
+
writeFileSync(globalFile, matter.stringify(inline, { url: '*', endpoint: '*' }));
|
|
389
506
|
}
|
|
390
507
|
if (!knowledgeFile)
|
|
391
508
|
return;
|
|
@@ -393,12 +510,13 @@ export function materializeKnowledge(outputRoot) {
|
|
|
393
510
|
if (!existsSync(source)) {
|
|
394
511
|
throw new Error(`Knowledge file from EXPLORBOT_KNOWLEDGE_FILE not found: ${source}`);
|
|
395
512
|
}
|
|
396
|
-
|
|
513
|
+
mkdirSync(envDir, { recursive: true });
|
|
514
|
+
copyFileSync(source, join(envDir, basename(source)));
|
|
397
515
|
}
|
|
398
516
|
export async function createModel(provider, modelId) {
|
|
399
|
-
const
|
|
400
|
-
if (!
|
|
517
|
+
const info = PROVIDERS[provider];
|
|
518
|
+
if (!info) {
|
|
401
519
|
throw new Error(`Unknown AI provider "${provider}". Supported providers: ${Object.keys(PROVIDERS).join(', ')}`);
|
|
402
520
|
}
|
|
403
|
-
return (await
|
|
521
|
+
return (await info.load())(modelId);
|
|
404
522
|
}
|
|
@@ -10,6 +10,8 @@ export declare class ExecutionController extends EventEmitter {
|
|
|
10
10
|
constructor();
|
|
11
11
|
static getInstance(): ExecutionController;
|
|
12
12
|
setInputCallback(callback: InputCallback): void;
|
|
13
|
+
hasInputCallback(): boolean;
|
|
14
|
+
clearInputCallback(): void;
|
|
13
15
|
startExecution(): void;
|
|
14
16
|
getAbortSignal(): AbortSignal | undefined;
|
|
15
17
|
interrupt(): void;
|
|
@@ -20,6 +20,12 @@ export class ExecutionController extends EventEmitter {
|
|
|
20
20
|
setInputCallback(callback) {
|
|
21
21
|
this.inputCallback = callback;
|
|
22
22
|
}
|
|
23
|
+
hasInputCallback() {
|
|
24
|
+
return !!this.inputCallback;
|
|
25
|
+
}
|
|
26
|
+
clearInputCallback() {
|
|
27
|
+
this.inputCallback = null;
|
|
28
|
+
}
|
|
23
29
|
startExecution() {
|
|
24
30
|
this.interrupted = false;
|
|
25
31
|
this.abortController = new AbortController();
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Browser } from 'playwright';
|
|
1
2
|
import type { AgentDeps } from './ai/agent.js';
|
|
2
3
|
import { Captain } from './ai/captain.js';
|
|
3
4
|
import { Driller } from './ai/driller.js';
|
|
@@ -14,7 +15,7 @@ import { Researcher } from './ai/researcher.js';
|
|
|
14
15
|
import { SessionAnalyst } from './ai/session-analyst.js';
|
|
15
16
|
import { Tester } from './ai/tester.js';
|
|
16
17
|
import { RequestStore } from './api/request-store.js';
|
|
17
|
-
import type { ExplorbotConfig } from './config.js';
|
|
18
|
+
import type { ExplorbotConfig, ReporterConfig } from './config.js';
|
|
18
19
|
import { ConfigParser } from './config.js';
|
|
19
20
|
import { ExperienceTracker } from './experience-tracker.js';
|
|
20
21
|
import Explorer from './explorer.js';
|
|
@@ -34,6 +35,11 @@ export interface ExplorBotOptions {
|
|
|
34
35
|
headless?: boolean;
|
|
35
36
|
incognito?: boolean;
|
|
36
37
|
session?: string | boolean;
|
|
38
|
+
instance?: string;
|
|
39
|
+
optionalAi?: boolean;
|
|
40
|
+
reporter?: ReporterConfig;
|
|
41
|
+
attachedBrowser?: Browser;
|
|
42
|
+
applicationSpec?: string;
|
|
37
43
|
}
|
|
38
44
|
export type UserResolveFunction = (error?: Error, showWelcome?: boolean) => Promise<string | null>;
|
|
39
45
|
export declare class ExplorBot {
|
|
@@ -47,6 +53,7 @@ export declare class ExplorBot {
|
|
|
47
53
|
planFeature?: string;
|
|
48
54
|
lastPlanError: Error | null;
|
|
49
55
|
lastSavedPlanPath: string | null;
|
|
56
|
+
aiFailure: string | null;
|
|
50
57
|
agents: Record<string, any>;
|
|
51
58
|
sessionPlans: Plan[];
|
|
52
59
|
lastReportedTestCount: number;
|
|
@@ -59,8 +66,10 @@ export declare class ExplorBot {
|
|
|
59
66
|
constructor(options?: ExplorBotOptions);
|
|
60
67
|
get isExploring(): boolean;
|
|
61
68
|
setUserResolve(fn: UserResolveFunction): void;
|
|
69
|
+
attachBrowser(browser: Browser): void;
|
|
62
70
|
start(): Promise<void>;
|
|
63
71
|
startProviderOnly(): Promise<void>;
|
|
72
|
+
aiFailureReason(): string | null;
|
|
64
73
|
stop(): Promise<void>;
|
|
65
74
|
visitInitialState(): Promise<void>;
|
|
66
75
|
visit(url: string): Promise<void>;
|
|
@@ -112,4 +121,5 @@ export declare class ExplorBot {
|
|
|
112
121
|
getSessionTests(): Test[];
|
|
113
122
|
printSessionAnalysis(): Promise<void>;
|
|
114
123
|
isHistorianEnabled(): boolean;
|
|
124
|
+
bootstrapOptionalProvider(): Promise<void>;
|
|
115
125
|
}
|
package/dist/src/explorbot.js
CHANGED
|
@@ -43,6 +43,7 @@ export class ExplorBot {
|
|
|
43
43
|
planFeature;
|
|
44
44
|
lastPlanError = null;
|
|
45
45
|
lastSavedPlanPath = null;
|
|
46
|
+
aiFailure = null;
|
|
46
47
|
agents = {};
|
|
47
48
|
sessionPlans = [];
|
|
48
49
|
lastReportedTestCount = 0;
|
|
@@ -66,6 +67,9 @@ export class ExplorBot {
|
|
|
66
67
|
setUserResolve(fn) {
|
|
67
68
|
this.userResolveFn = fn;
|
|
68
69
|
}
|
|
70
|
+
attachBrowser(browser) {
|
|
71
|
+
this.options.attachedBrowser = browser;
|
|
72
|
+
}
|
|
69
73
|
async start() {
|
|
70
74
|
if (this.explorer) {
|
|
71
75
|
return;
|
|
@@ -81,7 +85,7 @@ export class ExplorBot {
|
|
|
81
85
|
playwrightRecorder: this.playwrightRecorder(),
|
|
82
86
|
});
|
|
83
87
|
await this.explorer.start();
|
|
84
|
-
if (!this.options.incognito) {
|
|
88
|
+
if (!this.options.incognito && this.provider) {
|
|
85
89
|
await this.agentExperienceCompactor().autocompact();
|
|
86
90
|
}
|
|
87
91
|
}
|
|
@@ -97,19 +101,25 @@ export class ExplorBot {
|
|
|
97
101
|
this.config = await this.configParser.loadConfig(this.options);
|
|
98
102
|
if (this.options.session === true)
|
|
99
103
|
this.options.session = path.join(this.configParser.getOutputDir(), 'session.json');
|
|
104
|
+
if (typeof this.options.session === 'string')
|
|
105
|
+
this.options.session = path.resolve(this.options.path || '.', this.options.session);
|
|
106
|
+
if (this.options.optionalAi)
|
|
107
|
+
return this.bootstrapOptionalProvider();
|
|
100
108
|
this.provider = new AIProvider(this.config.ai);
|
|
101
109
|
await this.provider.validateConnection();
|
|
102
110
|
}
|
|
111
|
+
aiFailureReason() {
|
|
112
|
+
return this.aiFailure;
|
|
113
|
+
}
|
|
103
114
|
async stop() {
|
|
104
115
|
this.agents.quartermaster?.stop();
|
|
105
116
|
await this.explorer?.stop();
|
|
106
117
|
}
|
|
107
118
|
async visitInitialState() {
|
|
108
|
-
|
|
109
|
-
await this.visit(url);
|
|
119
|
+
await this.visit(this.options.from || '/');
|
|
110
120
|
}
|
|
111
121
|
async visit(url) {
|
|
112
|
-
return this.agentNavigator().visit(url);
|
|
122
|
+
return this.agentNavigator().visit(this.configParser.resolveTargetPath(url));
|
|
113
123
|
}
|
|
114
124
|
async openTab() {
|
|
115
125
|
await this.explorer.openTab();
|
|
@@ -121,7 +131,7 @@ export class ExplorBot {
|
|
|
121
131
|
return this.explorer;
|
|
122
132
|
}
|
|
123
133
|
knowledgeTracker() {
|
|
124
|
-
return (this._knowledgeTracker ||= new KnowledgeTracker());
|
|
134
|
+
return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
|
|
125
135
|
}
|
|
126
136
|
experienceTracker() {
|
|
127
137
|
return (this._experienceTracker ||= new ExperienceTracker(this.knowledgeTracker()));
|
|
@@ -130,7 +140,7 @@ export class ExplorBot {
|
|
|
130
140
|
return (this._stateManager ||= new StateManager(this.experienceTracker(), this.knowledgeTracker()));
|
|
131
141
|
}
|
|
132
142
|
reporter() {
|
|
133
|
-
return (this._reporter ||= new Reporter(this.config.reporter, this.stateManager()));
|
|
143
|
+
return (this._reporter ||= new Reporter(this.options.reporter || this.config.reporter, this.stateManager()));
|
|
134
144
|
}
|
|
135
145
|
requestStore() {
|
|
136
146
|
return (this._requestStore ||= new RequestStore(this.configParser.getOutputDir()));
|
|
@@ -451,4 +461,15 @@ export class ExplorBot {
|
|
|
451
461
|
isHistorianEnabled() {
|
|
452
462
|
return this.config.ai?.agents?.historian?.enabled !== false;
|
|
453
463
|
}
|
|
464
|
+
async bootstrapOptionalProvider() {
|
|
465
|
+
try {
|
|
466
|
+
const provider = new AIProvider(this.config.ai);
|
|
467
|
+
await provider.validateConnection();
|
|
468
|
+
this.provider = provider;
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
this.aiFailure = browserErrorMessage(error);
|
|
472
|
+
tag('debug').log(`AI provider unavailable: ${this.aiFailure}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
454
475
|
}
|
package/dist/src/explorer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserContextOptions, Page } from 'playwright';
|
|
1
|
+
import type { Browser, BrowserContextOptions, Page } from 'playwright';
|
|
2
2
|
import { ActionResult } from './action-result.js';
|
|
3
3
|
import Action from './action.js';
|
|
4
4
|
import type { RequestStore } from './api/request-store.js';
|
|
@@ -56,6 +56,7 @@ declare class Explorer {
|
|
|
56
56
|
initializeContainer(): void;
|
|
57
57
|
convertToCodeceptConfig(config: ExplorbotConfig): any;
|
|
58
58
|
connectOrLaunchBrowser(): Promise<void>;
|
|
59
|
+
openContextPage(): Promise<void>;
|
|
59
60
|
createBrowserContextOptions(): BrowserContextOptions;
|
|
60
61
|
attachXhrCapture(): void;
|
|
61
62
|
runWithRecovery<T>(label: string, operation: () => Promise<T>): Promise<T>;
|
|
@@ -81,6 +82,8 @@ export interface ExplorerOptions {
|
|
|
81
82
|
headless?: boolean;
|
|
82
83
|
incognito?: boolean;
|
|
83
84
|
session?: string;
|
|
85
|
+
instance?: string;
|
|
86
|
+
attachedBrowser?: Browser;
|
|
84
87
|
}
|
|
85
88
|
export interface ExplorerDeps {
|
|
86
89
|
stateManager: StateManager;
|
package/dist/src/explorer.js
CHANGED
|
@@ -10,7 +10,6 @@ import { ActionResult } from "./action-result.js";
|
|
|
10
10
|
import Action from './action.js';
|
|
11
11
|
import { XhrCapture } from "./api/xhr-capture.js";
|
|
12
12
|
import { ConfigParser } from './config.js';
|
|
13
|
-
import { TestResult } from "./test-plan.js";
|
|
14
13
|
import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
15
14
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
16
15
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
@@ -74,8 +73,8 @@ class Explorer {
|
|
|
74
73
|
throw new Error('Playwright helper not available');
|
|
75
74
|
}
|
|
76
75
|
await this.connectOrLaunchBrowser();
|
|
77
|
-
const hasSession = this.options?.session && existsSync(this.options.session);
|
|
78
|
-
await this.
|
|
76
|
+
const hasSession = !this.options?.attachedBrowser && this.options?.session && existsSync(this.options.session);
|
|
77
|
+
await this.openContextPage();
|
|
79
78
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
80
79
|
this.attachXhrCapture();
|
|
81
80
|
if (hasSession) {
|
|
@@ -91,8 +90,9 @@ class Explorer {
|
|
|
91
90
|
if (!this.started)
|
|
92
91
|
return;
|
|
93
92
|
this.started = false;
|
|
93
|
+
const attached = !!this.options?.attachedBrowser;
|
|
94
94
|
await this.stopCaptures();
|
|
95
|
-
if (this.options?.session && this.playwrightHelper?.browserContext) {
|
|
95
|
+
if (!attached && this.options?.session && this.playwrightHelper?.browserContext) {
|
|
96
96
|
const dir = path.dirname(this.options.session);
|
|
97
97
|
if (!existsSync(dir))
|
|
98
98
|
mkdirSync(dir, { recursive: true });
|
|
@@ -105,7 +105,14 @@ class Explorer {
|
|
|
105
105
|
await Promise.all([this.reporter.finishRun(), this.playwrightHelper._stopBrowser(), codeceptjs.recorder.stop()]);
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
|
-
|
|
108
|
+
if (attached) {
|
|
109
|
+
tag('info').log('Disconnecting from attached browser (its pages stay open)');
|
|
110
|
+
await this.playwrightHelper.browser?.close().catch((err) => {
|
|
111
|
+
debugLog('Failed to disconnect from attached browser:', err);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!attached)
|
|
115
|
+
tag('info').log('Closing browser context (persistent browser stays running)');
|
|
109
116
|
await this.closeBrowserContext();
|
|
110
117
|
this.playwrightHelper.browser = null;
|
|
111
118
|
this.playwrightHelper.isRunning = false;
|
|
@@ -247,8 +254,15 @@ class Explorer {
|
|
|
247
254
|
return codeceptConfig;
|
|
248
255
|
}
|
|
249
256
|
async connectOrLaunchBrowser() {
|
|
257
|
+
if (this.options?.attachedBrowser) {
|
|
258
|
+
this.playwrightHelper.browser = this.options.attachedBrowser;
|
|
259
|
+
this.playwrightHelper.isRunning = true;
|
|
260
|
+
this.isSharedBrowser = true;
|
|
261
|
+
tag('success').log('Attached to a browser opened by another tool');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
250
264
|
const { getAliveEndpoint } = await import('./browser-server.js');
|
|
251
|
-
const endpoint = await getAliveEndpoint();
|
|
265
|
+
const endpoint = await getAliveEndpoint(this.options?.instance);
|
|
252
266
|
if (endpoint) {
|
|
253
267
|
const browserName = this.config.playwright.browser || 'chromium';
|
|
254
268
|
this.playwrightHelper.options[browserName] ||= {};
|
|
@@ -261,6 +275,19 @@ class Explorer {
|
|
|
261
275
|
}
|
|
262
276
|
await this.playwrightHelper._startBrowser();
|
|
263
277
|
}
|
|
278
|
+
async openContextPage() {
|
|
279
|
+
const attached = this.options?.attachedBrowser;
|
|
280
|
+
if (!attached) {
|
|
281
|
+
await this.playwrightHelper._createContextPage(this.createBrowserContextOptions());
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const context = attached.contexts()[0] || (await attached.newContext());
|
|
285
|
+
const pages = context.pages();
|
|
286
|
+
const page = pages[pages.length - 1] || (await context.newPage());
|
|
287
|
+
this.playwrightHelper.browserContext = context;
|
|
288
|
+
await this.playwrightHelper._setPage(page);
|
|
289
|
+
debugLog(`Adopted attached browser page: ${page.url()}`);
|
|
290
|
+
}
|
|
264
291
|
createBrowserContextOptions() {
|
|
265
292
|
const helperOptions = this.playwrightHelper.options || {};
|
|
266
293
|
const contextOptions = {
|
|
@@ -427,6 +454,10 @@ class Explorer {
|
|
|
427
454
|
await this.playwrightRecorder.stop();
|
|
428
455
|
}
|
|
429
456
|
async closeBrowserContext() {
|
|
457
|
+
if (this.options?.attachedBrowser) {
|
|
458
|
+
this.playwrightHelper.browserContext = null;
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
430
461
|
if (!this.playwrightHelper.browserContext)
|
|
431
462
|
return;
|
|
432
463
|
await this.playwrightHelper.browserContext.close().catch((err) => {
|
|
@@ -447,7 +478,7 @@ class Explorer {
|
|
|
447
478
|
});
|
|
448
479
|
}
|
|
449
480
|
await this.connectOrLaunchBrowser();
|
|
450
|
-
await this.
|
|
481
|
+
await this.openContextPage();
|
|
451
482
|
await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
|
|
452
483
|
this.attachXhrCapture();
|
|
453
484
|
this.listenToStateChanged();
|
|
@@ -542,6 +573,8 @@ class Explorer {
|
|
|
542
573
|
async closeOtherTabs() {
|
|
543
574
|
if (!this.playwrightHelper)
|
|
544
575
|
return;
|
|
576
|
+
if (this.options?.attachedBrowser)
|
|
577
|
+
return;
|
|
545
578
|
const context = this.playwrightHelper.page.context();
|
|
546
579
|
const pages = context.pages();
|
|
547
580
|
if (pages.length <= 1)
|
|
@@ -590,12 +623,12 @@ class Explorer {
|
|
|
590
623
|
if (this.observedTestPages.has(page))
|
|
591
624
|
return;
|
|
592
625
|
this.testPageErrorHandler ||= (err) => {
|
|
593
|
-
this._activeTest?.
|
|
626
|
+
this._activeTest?.addObservation(`Console error: ${err.message}`);
|
|
594
627
|
};
|
|
595
628
|
this.testConsoleHandler ||= (msg) => {
|
|
596
629
|
if (msg.type() !== 'error')
|
|
597
630
|
return;
|
|
598
|
-
this._activeTest?.
|
|
631
|
+
this._activeTest?.addObservation(`Console error: ${msg.text()}`);
|
|
599
632
|
};
|
|
600
633
|
this.testDialogHandler ||= (dialog) => {
|
|
601
634
|
const dialogType = dialog.type();
|