explorbot 0.2.3 → 0.2.5

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.
Files changed (174) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +26 -8
  3. package/boat/api-tester/src/cli.ts +17 -0
  4. package/boat/api-tester/src/config.ts +4 -2
  5. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  6. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  7. package/boat/doc-collector/src/cli.ts +14 -1
  8. package/boat/doc-collector/src/config.ts +4 -2
  9. package/boat/prima/bin/prima-cli.ts +0 -0
  10. package/boat/prima/src/activity-line.ts +33 -0
  11. package/boat/prima/src/cli.ts +127 -86
  12. package/boat/prima/src/envelope.ts +102 -52
  13. package/boat/prima/src/prima.ts +567 -128
  14. package/boat/prima/src/pw-parser.ts +11 -1
  15. package/boat/prima/src/pw-registry.ts +4 -5
  16. package/boat/prima/src/session-log.ts +126 -0
  17. package/dist/bin/explorbot-cli.js +26 -8
  18. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  19. package/dist/boat/api-tester/src/cli.js +17 -0
  20. package/dist/boat/api-tester/src/config.js +4 -2
  21. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  22. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  23. package/dist/boat/doc-collector/src/cli.js +14 -1
  24. package/dist/boat/doc-collector/src/config.js +4 -2
  25. package/dist/boat/prima/src/activity-line.js +30 -0
  26. package/dist/boat/prima/src/cli.js +109 -77
  27. package/dist/boat/prima/src/envelope.js +94 -44
  28. package/dist/boat/prima/src/prima.js +533 -119
  29. package/dist/boat/prima/src/pw-parser.js +13 -1
  30. package/dist/boat/prima/src/pw-registry.js +4 -5
  31. package/dist/boat/prima/src/session-log.js +108 -0
  32. package/dist/package.json +3 -2
  33. package/dist/rules/navigator/verification-actions.md +20 -0
  34. package/dist/src/action-result.d.ts +7 -0
  35. package/dist/src/action-result.js +4 -0
  36. package/dist/src/action.d.ts +2 -0
  37. package/dist/src/action.js +41 -2
  38. package/dist/src/ai/captain/web-mode.js +6 -3
  39. package/dist/src/ai/captain.js +2 -0
  40. package/dist/src/ai/navigator.d.ts +34 -0
  41. package/dist/src/ai/navigator.js +237 -181
  42. package/dist/src/ai/pilot.d.ts +7 -0
  43. package/dist/src/ai/pilot.js +90 -2
  44. package/dist/src/ai/provider.d.ts +2 -2
  45. package/dist/src/ai/provider.js +14 -23
  46. package/dist/src/ai/rerunner.js +2 -1
  47. package/dist/src/ai/researcher/cache.d.ts +2 -0
  48. package/dist/src/ai/researcher/cache.js +10 -2
  49. package/dist/src/ai/researcher.js +3 -2
  50. package/dist/src/ai/rules.js +17 -10
  51. package/dist/src/ai/session-analyst.js +2 -0
  52. package/dist/src/ai/task-agent.js +4 -1
  53. package/dist/src/ai/tester.d.ts +6 -3
  54. package/dist/src/ai/tester.js +50 -46
  55. package/dist/src/ai/tools.d.ts +14 -0
  56. package/dist/src/ai/tools.js +117 -37
  57. package/dist/src/commands/config-command.d.ts +51 -0
  58. package/dist/src/commands/config-command.js +117 -0
  59. package/dist/src/commands/index.js +2 -0
  60. package/dist/src/config.d.ts +9 -1
  61. package/dist/src/config.js +53 -4
  62. package/dist/src/execution-controller.d.ts +2 -0
  63. package/dist/src/execution-controller.js +6 -0
  64. package/dist/src/explorbot.d.ts +2 -1
  65. package/dist/src/explorbot.js +7 -2
  66. package/dist/src/explorer.js +2 -3
  67. package/dist/src/playwright-recorder.js +30 -0
  68. package/dist/src/remote.d.ts +55 -0
  69. package/dist/src/remote.js +235 -0
  70. package/dist/src/reporter.d.ts +1 -0
  71. package/dist/src/reporter.js +7 -1
  72. package/dist/src/state-manager.d.ts +2 -1
  73. package/dist/src/state-manager.js +3 -1
  74. package/dist/src/stats.d.ts +1 -0
  75. package/dist/src/stats.js +1 -0
  76. package/dist/src/test-plan.d.ts +3 -0
  77. package/dist/src/test-plan.js +26 -0
  78. package/dist/src/utils/aria.d.ts +2 -8
  79. package/dist/src/utils/aria.js +69 -40
  80. package/dist/src/utils/html.js +1 -0
  81. package/dist/src/utils/logger.d.ts +7 -1
  82. package/dist/src/utils/logger.js +32 -0
  83. package/dist/src/utils/page-readiness.js +18 -1
  84. package/dist/src/utils/url-matcher.js +3 -0
  85. package/dist/src/utils/web-element.d.ts +2 -0
  86. package/dist/src/utils/web-element.js +8 -0
  87. package/dist/src/utils/web-sandbox.d.ts +1 -1
  88. package/dist/src/utils/web-sandbox.js +2 -3
  89. package/docs/api-testing/basics.md +90 -0
  90. package/docs/api-testing/planning.md +57 -0
  91. package/docs/api-testing/running-tests.md +55 -0
  92. package/docs/assets/cloud-report.png +0 -0
  93. package/docs/assets/html-report.png +0 -0
  94. package/docs/assets/langfuse-trace.png +0 -0
  95. package/docs/assets/successful-explore-run.png +0 -0
  96. package/docs/basics/getting-started.md +140 -0
  97. package/docs/basics/prerequisites.md +63 -0
  98. package/docs/basics/providers.md +362 -0
  99. package/docs/basics/running.md +78 -0
  100. package/docs/contributing/ai-integration-tests.md +57 -0
  101. package/docs/contributing/contributing.md +90 -0
  102. package/docs/contributing/demo-videos.md +36 -0
  103. package/docs/contributing/npm-package.md +138 -0
  104. package/docs/contributing/observability.md +227 -0
  105. package/docs/contributing/regression-tests.md +103 -0
  106. package/docs/contributing/testing.md +95 -0
  107. package/docs/doc-collection/basics.md +128 -0
  108. package/docs/doc-collection/crawling.md +67 -0
  109. package/docs/doc-collection/interactive-mode.md +99 -0
  110. package/docs/index.json +87 -0
  111. package/docs/reference/commands.md +997 -0
  112. package/docs/reference/configuration.md +569 -0
  113. package/docs/reference/scripting.md +303 -0
  114. package/docs/reference/websocket.md +50 -0
  115. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  116. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  117. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  118. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  119. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  120. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  121. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  122. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  123. package/docs/web-testing/agents.md +158 -0
  124. package/docs/web-testing/automated-tests.md +134 -0
  125. package/docs/web-testing/basics.md +91 -0
  126. package/docs/web-testing/customization.md +131 -0
  127. package/docs/web-testing/hooks.md +238 -0
  128. package/docs/web-testing/page-interaction.md +84 -0
  129. package/docs/web-testing/planner.md +122 -0
  130. package/docs/web-testing/rerun.md +164 -0
  131. package/docs/web-testing/researcher.md +380 -0
  132. package/docs/workflow/agentic-usage.md +233 -0
  133. package/docs/workflow/application-spec.md +73 -0
  134. package/docs/workflow/ci.md +202 -0
  135. package/docs/workflow/knowledge.md +310 -0
  136. package/docs/workflow/planning-styles.md +67 -0
  137. package/docs/workflow/reporting.md +133 -0
  138. package/docs/workflow/test-plans.md +90 -0
  139. package/package.json +3 -2
  140. package/rules/navigator/verification-actions.md +20 -0
  141. package/src/action-result.ts +11 -0
  142. package/src/action.ts +43 -3
  143. package/src/ai/captain/web-mode.ts +6 -3
  144. package/src/ai/captain.ts +3 -0
  145. package/src/ai/navigator.ts +255 -186
  146. package/src/ai/pilot.ts +104 -2
  147. package/src/ai/provider.ts +14 -24
  148. package/src/ai/rerunner.ts +2 -1
  149. package/src/ai/researcher/cache.ts +12 -2
  150. package/src/ai/researcher.ts +3 -2
  151. package/src/ai/rules.ts +17 -10
  152. package/src/ai/session-analyst.ts +2 -0
  153. package/src/ai/task-agent.ts +3 -1
  154. package/src/ai/tester.ts +52 -45
  155. package/src/ai/tools.ts +136 -37
  156. package/src/commands/config-command.ts +146 -0
  157. package/src/commands/index.ts +2 -0
  158. package/src/config.ts +60 -5
  159. package/src/execution-controller.ts +8 -0
  160. package/src/explorbot.ts +7 -3
  161. package/src/explorer.ts +2 -2
  162. package/src/playwright-recorder.ts +23 -0
  163. package/src/remote.ts +244 -0
  164. package/src/reporter.ts +7 -1
  165. package/src/state-manager.ts +6 -2
  166. package/src/stats.ts +1 -0
  167. package/src/test-plan.ts +29 -0
  168. package/src/utils/aria.ts +65 -45
  169. package/src/utils/html.ts +1 -0
  170. package/src/utils/logger.ts +33 -2
  171. package/src/utils/page-readiness.ts +24 -1
  172. package/src/utils/url-matcher.ts +3 -0
  173. package/src/utils/web-element.ts +9 -0
  174. package/src/utils/web-sandbox.ts +3 -4
@@ -0,0 +1,117 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { ConfigParser, EXPLORBOT_ENV_VARS, configuredModels } from '../config.js';
5
+ import { listSites } from '../global-config.js';
6
+ import { Reporter } from '../reporter.js';
7
+ import { getCliName } from '../utils/cli-name.js';
8
+ import { tag } from '../utils/logger.js';
9
+ import { BaseCommand } from './base-command.js';
10
+ export class ConfigCommand extends BaseCommand {
11
+ name = 'config';
12
+ description = 'Show models, config file and paths used by this run';
13
+ async execute() {
14
+ const parser = ConfigParser.getInstance();
15
+ tag('info').log(ConfigCommand.render(this.explorBot.getConfig(), { configPath: parser.getConfigPath(), root: parser.getProjectRoot() }));
16
+ }
17
+ static async summary(options = {}) {
18
+ const parser = ConfigParser.getInstance();
19
+ const [site] = listSites();
20
+ const load = (baseUrl) => parser.loadConfig({ config: options.config, path: options.path, baseUrl });
21
+ const config = await load(options.url).catch((error) => {
22
+ if (!site)
23
+ throw error;
24
+ return load(site.url);
25
+ });
26
+ return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json });
27
+ }
28
+ static data(config, options = {}) {
29
+ let configPath = '';
30
+ if (options.configPath && existsSync(options.configPath))
31
+ configPath = options.configPath;
32
+ const dirs = {};
33
+ if (options.root) {
34
+ for (const [name, dir] of Object.entries({ output: 'output', ...config.dirs })) {
35
+ dirs[name] = path.join(options.root, dir);
36
+ }
37
+ }
38
+ const env = {};
39
+ for (const variable of EXPLORBOT_ENV_VARS) {
40
+ const value = process.env[variable.name];
41
+ if (value)
42
+ env[variable.name] = value;
43
+ }
44
+ const models = {};
45
+ const providers = {};
46
+ for (const [role, model] of Object.entries(configuredModels(config.ai))) {
47
+ models[role] = model.name;
48
+ if (model.provider)
49
+ providers[role] = model.provider;
50
+ }
51
+ return {
52
+ config: configPath,
53
+ url: config.playwright?.url || config.web?.url || config.api?.baseEndpoint || '',
54
+ browser: config.playwright?.browser || '',
55
+ headless: !config.playwright?.show,
56
+ dirs,
57
+ models,
58
+ providers,
59
+ integrations: {
60
+ langfuse: !!config.ai?.langfuse?.enabled,
61
+ testomatio: Reporter.resolveEnabled(config.reporter),
62
+ },
63
+ env,
64
+ };
65
+ }
66
+ static render(config, options = {}) {
67
+ const data = ConfigCommand.data(config, options);
68
+ if (options.json)
69
+ return JSON.stringify(data, null, 2);
70
+ const lines = [];
71
+ const section = (title, entries) => {
72
+ if (!entries.length)
73
+ return;
74
+ const width = Math.max(...entries.map(([label]) => label.length));
75
+ lines.push(chalk.bold(title));
76
+ for (const [label, value] of entries)
77
+ lines.push(` ${chalk.dim(label.padEnd(width))} ${value}`);
78
+ lines.push('');
79
+ };
80
+ const general = [['config', data.config || 'EXPLORBOT_* environment variables']];
81
+ if (data.url)
82
+ general.push(['url', data.url]);
83
+ if (data.browser) {
84
+ let window = 'visible';
85
+ if (data.headless)
86
+ window = 'headless';
87
+ general.push(['browser', `${data.browser}, ${window}`]);
88
+ }
89
+ for (const [name, dir] of Object.entries(data.dirs))
90
+ general.push([name, dir]);
91
+ section('Config', general);
92
+ const providerWidth = Math.max(0, ...Object.values(data.providers).map((provider) => provider.length));
93
+ const models = Object.entries(data.models).map(([role, model]) => {
94
+ if (!providerWidth)
95
+ return [role, model];
96
+ return [role, `${chalk.dim((data.providers[role] || '').padEnd(providerWidth))} ${model}`];
97
+ });
98
+ if (!models.length)
99
+ models.push(['model', chalk.red(`not configured — run ${getCliName()} init`)]);
100
+ section('Models', models);
101
+ const integrations = [];
102
+ if (data.integrations.langfuse)
103
+ integrations.push(['langfuse', 'traces sent']);
104
+ if (data.integrations.testomatio)
105
+ integrations.push(['testomatio', 'runs reported']);
106
+ section('Integrations', integrations);
107
+ const env = Object.entries(data.env).map(([name, value]) => {
108
+ let shown = value;
109
+ if (shown.length > 60)
110
+ shown = `${shown.slice(0, 57)}...`;
111
+ return [name, shown];
112
+ });
113
+ section('Environment', env);
114
+ lines.push(chalk.dim(`Every EXPLORBOT_* variable: ${getCliName()} --help`));
115
+ return lines.join('\n');
116
+ }
117
+ }
@@ -1,6 +1,7 @@
1
1
  import { AddRuleCommand } from './add-rule-command.js';
2
2
  import { CleanCommand } from './clean-command.js';
3
3
  import { CompactCommand } from './compact-command.js';
4
+ import { ConfigCommand } from './config-command.js';
4
5
  import { ContextAriaCommand } from './context-aria-command.js';
5
6
  import { ContextCommand } from './context-command.js';
6
7
  import { ContextDataCommand } from './context-data-command.js';
@@ -64,6 +65,7 @@ const commandClasses = [
64
65
  RunsCommand,
65
66
  RerunCommand,
66
67
  StatusCommand,
68
+ ConfigCommand,
67
69
  DebugCommand,
68
70
  ExitCommand,
69
71
  ];
@@ -172,6 +172,7 @@ interface HtmlConfig {
172
172
  interface ActionConfig {
173
173
  delay?: number;
174
174
  retries?: number;
175
+ timeout?: number;
175
176
  }
176
177
  interface ReporterConfig {
177
178
  enabled?: boolean;
@@ -271,12 +272,19 @@ export declare class ConfigMissingError extends Error {
271
272
  }
272
273
  export declare function envConfigRequested(): boolean;
273
274
  export declare function missingConfigMessage(configFile?: string): string;
275
+ export declare function modelName(model: unknown): string;
276
+ export declare function modelProvider(model: unknown): string;
277
+ export declare function configuredModels(ai?: AIConfig): Record<string, ConfiguredModel>;
274
278
  export declare function resolveConfigModels(ai?: AIConfig): Promise<void>;
275
279
  export declare function resolveOutputRoot(baseUrl?: string): string;
276
280
  export declare function resolveStateRoot(baseUrl: string, ephemeral?: boolean): string;
277
281
  export declare function materializeKnowledge(outputRoot: string): void;
278
282
  export declare function createModel(provider: string, modelId: string): Promise<any>;
279
283
  type ModelRole = 'model' | 'visionModel' | 'agenticModel';
284
+ interface ConfiguredModel {
285
+ name: string;
286
+ provider: string;
287
+ }
280
288
  interface ProviderInfo {
281
289
  envKey: string;
282
290
  load: () => Promise<(modelId: string) => any>;
@@ -286,4 +294,4 @@ interface EnvVar {
286
294
  description: string;
287
295
  required?: boolean;
288
296
  }
289
- export type { ModelRole, EnvVar, ProviderInfo };
297
+ export type { ModelRole, EnvVar, ProviderInfo, ConfiguredModel };
@@ -9,12 +9,13 @@ 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';
13
14
  import dedent from 'dedent';
14
15
  import matter from 'gray-matter';
15
16
  import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from './global-config.js';
16
17
  import { getCliName } from './utils/cli-name.js';
17
- import { log } from './utils/logger.js';
18
+ import { log, tag } from './utils/logger.js';
18
19
  export const PROVIDERS = {
19
20
  openai: { envKey: 'OPENAI_API_KEY', load: async () => (await import('@ai-sdk/openai')).createOpenAI() },
20
21
  anthropic: { envKey: 'ANTHROPIC_API_KEY', load: async () => (await import('@ai-sdk/anthropic')).createAnthropic() },
@@ -178,8 +179,15 @@ export class ConfigParser {
178
179
  return this.site;
179
180
  }
180
181
  resolveTargetPath(target) {
181
- if (!this.site)
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
+ }
182
189
  return target || '/';
190
+ }
183
191
  if (!target)
184
192
  return this.siteStartPath;
185
193
  const resolved = resolveSiteTarget(target, this.site.url);
@@ -223,6 +231,7 @@ export class ConfigParser {
223
231
  model: { modelId: 'test-model', provider: 'test' },
224
232
  config: {},
225
233
  vision: false,
234
+ langfuse: { enabled: false },
226
235
  },
227
236
  dirs: {
228
237
  knowledge: join(testBaseDir, 'knowledge'),
@@ -321,9 +330,10 @@ export class ConfigParser {
321
330
  }
322
331
  async loadConfigModule(configPath) {
323
332
  const ext = configPath.split('.').pop();
333
+ const moduleUrl = pathToFileURL(resolve(configPath)).href;
324
334
  if (ext === 'ts') {
325
335
  try {
326
- const module = await import(__rewriteRelativeImportExtension(configPath));
336
+ const module = await import(__rewriteRelativeImportExtension(moduleUrl));
327
337
  return module;
328
338
  }
329
339
  catch (error) {
@@ -332,7 +342,7 @@ export class ConfigParser {
332
342
  }
333
343
  }
334
344
  else if (ext === 'js' || ext === 'mjs') {
335
- const module = await import(__rewriteRelativeImportExtension(configPath));
345
+ const module = await import(__rewriteRelativeImportExtension(moduleUrl));
336
346
  return module;
337
347
  }
338
348
  else {
@@ -349,6 +359,17 @@ export class ConfigParser {
349
359
  config.playwright = config.playwright || { browser: 'chromium', url: '' };
350
360
  config.playwright.url = options.baseUrl;
351
361
  }
362
+ if (config.ai) {
363
+ const langfuse = config.ai.langfuse;
364
+ const publicKey = langfuse?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
365
+ const secretKey = langfuse?.secretKey || process.env.LANGFUSE_SECRET_KEY;
366
+ config.ai.langfuse = {
367
+ enabled: langfuse?.enabled ?? Boolean(publicKey && secretKey),
368
+ publicKey,
369
+ secretKey,
370
+ baseUrl: langfuse?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST,
371
+ };
372
+ }
352
373
  return config;
353
374
  }
354
375
  validateConfig(config) {
@@ -445,6 +466,34 @@ export function missingConfigMessage(configFile = 'explorbot.config.js') {
445
466
  Providers: ${Object.keys(PROVIDERS).join(', ')}
446
467
  `;
447
468
  }
469
+ export function modelName(model) {
470
+ if (typeof model === 'string')
471
+ return model;
472
+ return model?.modelId || model?.model || 'unknown';
473
+ }
474
+ export function modelProvider(model) {
475
+ const provider = model?.provider;
476
+ if (typeof provider === 'string')
477
+ return provider.split('.')[0];
478
+ if (typeof model === 'string')
479
+ return model.split('/')[0];
480
+ return '';
481
+ }
482
+ export function configuredModels(ai) {
483
+ if (!ai?.model)
484
+ return {};
485
+ const describe = (model) => ({ name: modelName(model), provider: modelProvider(model) });
486
+ const models = { model: describe(ai.model) };
487
+ if (ai.agenticModel)
488
+ models.agenticModel = describe(ai.agenticModel);
489
+ if (ai.visionModel)
490
+ models.visionModel = describe(ai.visionModel);
491
+ for (const [agent, agentConfig] of Object.entries(ai.agents || {})) {
492
+ if (agentConfig?.model)
493
+ models[agent] = describe(agentConfig.model);
494
+ }
495
+ return models;
496
+ }
448
497
  export async function resolveConfigModels(ai) {
449
498
  if (!ai)
450
499
  return;
@@ -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();
@@ -15,7 +15,7 @@ import { Researcher } from './ai/researcher.js';
15
15
  import { SessionAnalyst } from './ai/session-analyst.js';
16
16
  import { Tester } from './ai/tester.js';
17
17
  import { RequestStore } from './api/request-store.js';
18
- import type { ExplorbotConfig } from './config.js';
18
+ import type { ExplorbotConfig, ReporterConfig } from './config.js';
19
19
  import { ConfigParser } from './config.js';
20
20
  import { ExperienceTracker } from './experience-tracker.js';
21
21
  import Explorer from './explorer.js';
@@ -37,6 +37,7 @@ export interface ExplorBotOptions {
37
37
  session?: string | boolean;
38
38
  instance?: string;
39
39
  optionalAi?: boolean;
40
+ reporter?: ReporterConfig;
40
41
  attachedBrowser?: Browser;
41
42
  applicationSpec?: string;
42
43
  }
@@ -101,6 +101,8 @@ export class ExplorBot {
101
101
  this.config = await this.configParser.loadConfig(this.options);
102
102
  if (this.options.session === true)
103
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);
104
106
  if (this.options.optionalAi)
105
107
  return this.bootstrapOptionalProvider();
106
108
  this.provider = new AIProvider(this.config.ai);
@@ -138,7 +140,7 @@ export class ExplorBot {
138
140
  return (this._stateManager ||= new StateManager(this.experienceTracker(), this.knowledgeTracker()));
139
141
  }
140
142
  reporter() {
141
- return (this._reporter ||= new Reporter(this.config.reporter, this.stateManager()));
143
+ return (this._reporter ||= new Reporter(this.options.reporter || this.config.reporter, this.stateManager()));
142
144
  }
143
145
  requestStore() {
144
146
  return (this._requestStore ||= new RequestStore(this.configParser.getOutputDir()));
@@ -416,9 +418,12 @@ export class ExplorBot {
416
418
  }
417
419
  setCurrentPlan(plan) {
418
420
  this.currentPlan = plan;
419
- if (plan && !this.sessionPlans.includes(plan)) {
421
+ if (!plan)
422
+ return;
423
+ if (!this.sessionPlans.includes(plan)) {
420
424
  this.sessionPlans.push(plan);
421
425
  }
426
+ plan.notifyChange();
422
427
  }
423
428
  getSessionTests() {
424
429
  return this.sessionPlans.flatMap((p) => p.tests.filter((t) => t.startTime != null));
@@ -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";
@@ -624,12 +623,12 @@ class Explorer {
624
623
  if (this.observedTestPages.has(page))
625
624
  return;
626
625
  this.testPageErrorHandler ||= (err) => {
627
- this._activeTest?.addNote(`Console error: ${err.message}`, TestResult.FAILED);
626
+ this._activeTest?.addObservation(`Console error: ${err.message}`);
628
627
  };
629
628
  this.testConsoleHandler ||= (msg) => {
630
629
  if (msg.type() !== 'error')
631
630
  return;
632
- this._activeTest?.addNote(`Console error: ${msg.text()}`, TestResult.FAILED);
631
+ this._activeTest?.addObservation(`Console error: ${msg.text()}`);
633
632
  };
634
633
  this.testDialogHandler ||= (dialog) => {
635
634
  const dialogType = dialog.type();
@@ -292,8 +292,38 @@ function formatSelectOption(options) {
292
292
  return quote(values[0]);
293
293
  return `[${values.map((v) => quote(v)).join(', ')}]`;
294
294
  }
295
+ function assertionLocator(target) {
296
+ if (typeof target === 'string')
297
+ return `page.locator(${JSON.stringify(target)})`;
298
+ if (!target || typeof target !== 'object')
299
+ return null;
300
+ const role = target.role || target.aria;
301
+ const name = target.text ?? target.name ?? target.label;
302
+ if (role && name)
303
+ return `page.getByRole(${JSON.stringify(String(role))}, { name: ${JSON.stringify(String(name))} })`;
304
+ if (role)
305
+ return `page.getByRole(${JSON.stringify(String(role))})`;
306
+ if (name)
307
+ return `page.getByText(${JSON.stringify(String(name))})`;
308
+ if (target.css)
309
+ return `page.locator(${JSON.stringify(String(target.css))})`;
310
+ if (target.xpath)
311
+ return `page.locator(${JSON.stringify(`xpath=${target.xpath}`)})`;
312
+ return null;
313
+ }
295
314
  export function renderAssertion(assertion) {
296
315
  const args = assertion.args;
316
+ const target = assertionLocator(args[0]);
317
+ if (target) {
318
+ if (assertion.name === 'seeElement')
319
+ return `await expect(${target}).toBeVisible();`;
320
+ if (assertion.name === 'dontSeeElement')
321
+ return `await expect(${target}).toBeHidden();`;
322
+ if (assertion.name === 'seeInField' && args[1] !== undefined)
323
+ return `await expect(${target}).toHaveValue(${JSON.stringify(String(args[1]))});`;
324
+ if (assertion.name === 'dontSeeInField' && args[1] !== undefined)
325
+ return `await expect(${target}).not.toHaveValue(${JSON.stringify(String(args[1]))});`;
326
+ }
297
327
  if (assertion.name === 'see' && typeof args[0] === 'string') {
298
328
  return `await expect(page).toContainText(${JSON.stringify(args[0])});`;
299
329
  }
@@ -0,0 +1,55 @@
1
+ import type { Command } from 'commander';
2
+ import { type ActivityEntry } from './activity.js';
3
+ import { type LogDestination, type TaggedLogEntry } from './utils/logger.js';
4
+ /**
5
+ * Streams a run to a remote UI over one WebSocket, dialling out so a child
6
+ * process and a CI bot are the same case.
7
+ *
8
+ * It **is** a LogDestination — that is the whole integration on the logger's
9
+ * side, messages and `data` alike — and it answers asks by installing itself
10
+ * as the execution controller's input callback. Nothing else in explorbot
11
+ * knows it exists.
12
+ */
13
+ export declare class Remote implements LogDestination {
14
+ url: string | null;
15
+ socket: WebSocket | null;
16
+ queue: Frame[];
17
+ reconnectDelay: number;
18
+ reconnectTimer: ReturnType<typeof setTimeout> | null;
19
+ asks: Map<string, (value: string | null) => void>;
20
+ askCounter: number;
21
+ lastActivity: string | null;
22
+ registerOption(program: Command): void;
23
+ attach(url: string, command: string): void;
24
+ isAttached(): boolean;
25
+ send(type: string, data?: Record<string, unknown>): void;
26
+ ask(prompt: string): Promise<string | null>;
27
+ close(exitCode: number): Promise<void>;
28
+ isEnabled(): boolean;
29
+ /**
30
+ * The entry as the logger already built it, so the `LogType` reaches the UI
31
+ * as the frame's level and each kind is styled there instead of being scraped
32
+ * back out of flattened text.
33
+ */
34
+ write(entry: TaggedLogEntry): void;
35
+ connect(): void;
36
+ reconnect(): void;
37
+ flush(): Promise<void>;
38
+ drain(): void;
39
+ push(frame: Frame): void;
40
+ receive(raw: string): void;
41
+ /** A new listener is primed with the current activity, which at attach time is
42
+ * nothing — so skip repeats, the priming null included. */
43
+ reportActivity(activity: ActivityEntry | null): void;
44
+ errorOf(args: any[] | undefined): string | undefined;
45
+ commandPath(command: Command): string;
46
+ }
47
+ export declare const remote: Remote;
48
+ /** Whatever the run wants to say. Not a schema — the UI renders what it knows
49
+ * and ignores the rest, so either side can start sending more at any time. */
50
+ type Frame = {
51
+ type: string;
52
+ ts: number;
53
+ [key: string]: unknown;
54
+ };
55
+ export {};