explorbot 0.1.16 → 0.1.18

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 (47) hide show
  1. package/bin/explorbot-cli.ts +14 -1
  2. package/boat/doc-collector/bin/doc-collector-cli.ts +5 -0
  3. package/boat/doc-collector/package.json +24 -0
  4. package/boat/doc-collector/src/ai/documentarian.ts +184 -0
  5. package/boat/doc-collector/src/cli.ts +119 -0
  6. package/boat/doc-collector/src/config.ts +162 -0
  7. package/boat/doc-collector/src/docbot.ts +391 -0
  8. package/boat/doc-collector/src/docs-renderer.ts +187 -0
  9. package/boat/doc-collector/src/path-filter.ts +46 -0
  10. package/boat/doc-collector/src/research-navigation.ts +90 -0
  11. package/dist/bin/explorbot-cli.js +15 -1
  12. package/dist/boat/doc-collector/bin/doc-collector-cli.js +4 -0
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +157 -0
  14. package/dist/boat/doc-collector/src/cli.js +104 -0
  15. package/dist/boat/doc-collector/src/config.js +129 -0
  16. package/dist/boat/doc-collector/src/docbot.js +326 -0
  17. package/dist/boat/doc-collector/src/docs-renderer.js +141 -0
  18. package/dist/boat/doc-collector/src/path-filter.js +35 -0
  19. package/dist/boat/doc-collector/src/research-navigation.js +71 -0
  20. package/dist/package.json +4 -1
  21. package/dist/src/ai/pilot.js +3 -8
  22. package/dist/src/ai/researcher/coordinates.js +1 -1
  23. package/dist/src/ai/researcher/parser.js +3 -0
  24. package/dist/src/ai/researcher.js +2 -1
  25. package/dist/src/ai/tester.js +1 -0
  26. package/dist/src/commands/explore-command.js +359 -43
  27. package/dist/src/config.js +10 -3
  28. package/dist/src/explorbot.js +19 -5
  29. package/dist/src/explorer.js +14 -1
  30. package/dist/src/state-manager.js +3 -0
  31. package/dist/src/utils/test-plan-markdown.js +8 -1
  32. package/dist/src/utils/url-matcher.js +5 -3
  33. package/dist/src/utils/web-element.js +3 -2
  34. package/package.json +4 -1
  35. package/src/ai/pilot.ts +3 -8
  36. package/src/ai/researcher/coordinates.ts +1 -1
  37. package/src/ai/researcher/parser.ts +3 -0
  38. package/src/ai/researcher.ts +2 -1
  39. package/src/ai/tester.ts +1 -0
  40. package/src/commands/explore-command.ts +362 -42
  41. package/src/config.ts +13 -3
  42. package/src/explorbot.ts +22 -7
  43. package/src/explorer.ts +12 -1
  44. package/src/state-manager.ts +4 -0
  45. package/src/utils/test-plan-markdown.ts +8 -1
  46. package/src/utils/url-matcher.ts +5 -2
  47. package/src/utils/web-element.ts +3 -2
package/src/config.ts CHANGED
@@ -266,6 +266,7 @@ export class ConfigParser {
266
266
  private static instance: ConfigParser;
267
267
  private config: ExplorbotConfig | null = null;
268
268
  private configPath: string | null = null;
269
+ private runtimeBaseUrlOverride: string | null = null;
269
270
 
270
271
  private constructor() {}
271
272
 
@@ -285,8 +286,9 @@ export class ConfigParser {
285
286
  public async loadConfig(options?: {
286
287
  config?: string;
287
288
  path?: string;
289
+ baseUrl?: string;
288
290
  }): Promise<ExplorbotConfig> {
289
- if (this.config && !options?.config && !options?.path) {
291
+ if (this.config && !options?.config && !options?.path && this.runtimeBaseUrlOverride === (options?.baseUrl || null)) {
290
292
  return this.config;
291
293
  }
292
294
 
@@ -317,7 +319,8 @@ export class ConfigParser {
317
319
  throw new Error('Configuration file is empty or invalid');
318
320
  }
319
321
 
320
- this.config = this.resolveConfig(loadedConfig as ExplorbotConfig);
322
+ this.config = this.resolveConfig(loadedConfig as ExplorbotConfig, options);
323
+ this.runtimeBaseUrlOverride = options?.baseUrl || null;
321
324
  this.configPath = resolvedPath;
322
325
 
323
326
  log(`Configuration loaded from: ${resolvedPath}`);
@@ -372,6 +375,7 @@ export class ConfigParser {
372
375
  if (ConfigParser.instance) {
373
376
  ConfigParser.instance.config = null;
374
377
  ConfigParser.instance.configPath = null;
378
+ ConfigParser.instance.runtimeBaseUrlOverride = null;
375
379
  }
376
380
  }
377
381
 
@@ -455,11 +459,17 @@ export class ConfigParser {
455
459
  }
456
460
  }
457
461
 
458
- private resolveConfig(config: ExplorbotConfig): ExplorbotConfig {
462
+ private resolveConfig(config: ExplorbotConfig, options?: { baseUrl?: string }): ExplorbotConfig {
459
463
  if (config.web?.url && !config.playwright?.url) {
460
464
  config.playwright = config.playwright || { browser: 'chromium', url: '' };
461
465
  config.playwright.url = config.web.url;
462
466
  }
467
+
468
+ if (options?.baseUrl) {
469
+ config.playwright = config.playwright || { browser: 'chromium', url: '' };
470
+ config.playwright.url = options.baseUrl;
471
+ }
472
+
463
473
  return config;
464
474
  }
465
475
 
package/src/explorbot.ts CHANGED
@@ -27,12 +27,14 @@ import { KnowledgeTracker } from './knowledge-tracker.ts';
27
27
  import { WebPageState } from './state-manager.ts';
28
28
  import type { Suite } from './suite.ts';
29
29
  import { Plan, type Test } from './test-plan.ts';
30
+ import { parsePlansFromMarkdown } from './utils/test-plan-markdown.ts';
30
31
  import { setVerboseMode, tag } from './utils/logger.ts';
31
32
  import { relativeToCwd } from './utils/next-steps.ts';
32
33
  import { sanitizeFilename } from './utils/strings.ts';
33
34
 
34
35
  export interface ExplorBotOptions {
35
36
  from?: string;
37
+ baseUrl?: string;
36
38
  verbose?: boolean;
37
39
  config?: string;
38
40
  path?: string;
@@ -349,7 +351,7 @@ export class ExplorBot {
349
351
  this.agents.planner = undefined;
350
352
  }
351
353
 
352
- async plan(feature?: string, opts: { fresh?: boolean; style?: string; extend?: Plan; completedPlans?: Plan[] } = {}) {
354
+ async plan(feature?: string, opts: { fresh?: boolean; style?: string; extend?: Plan; completedPlans?: Plan[]; noSave?: boolean } = {}) {
353
355
  this.planFeature = feature;
354
356
 
355
357
  if (opts.fresh) {
@@ -379,7 +381,7 @@ export class ExplorBot {
379
381
  return this.currentPlan;
380
382
  }
381
383
 
382
- this.savePlan();
384
+ if (!opts.noSave) this.savePlan();
383
385
 
384
386
  return this.currentPlan;
385
387
  }
@@ -409,19 +411,20 @@ export class ExplorBot {
409
411
  return planPath;
410
412
  }
411
413
 
412
- generatePlanFilename(): string {
414
+ generatePlanFilename(feature?: string): string {
413
415
  const state = this.explorer?.getStateManager().getCurrentState();
414
416
  const urlPath = state?.url || '/';
415
417
  const urlPart = sanitizeFilename(urlPath) || 'root';
416
418
  const suffix = '.md';
417
- if (!this.planFeature) return urlPart.slice(0, 256 - suffix.length) + suffix;
418
- const featurePart = `_${sanitizeFilename(this.planFeature)}`;
419
+ const f = feature ?? this.planFeature;
420
+ if (!f) return urlPart.slice(0, 256 - suffix.length) + suffix;
421
+ const featurePart = `_${sanitizeFilename(f)}`;
419
422
  const maxFeatureLen = 256 - suffix.length - urlPart.length;
420
423
  if (maxFeatureLen <= 1) return urlPart.slice(0, 256 - suffix.length) + suffix;
421
424
  return urlPart + featurePart.slice(0, maxFeatureLen) + suffix;
422
425
  }
423
426
 
424
- loadPlan(filename: string): Plan {
427
+ resolvePlanPath(filename: string): string {
425
428
  let planPath = filename;
426
429
 
427
430
  if (path.isAbsolute(filename)) {
@@ -438,14 +441,26 @@ export class ExplorBot {
438
441
  }
439
442
  }
440
443
 
444
+ return planPath;
445
+ }
446
+
447
+ loadPlan(filename: string): Plan {
448
+ const planPath = this.resolvePlanPath(filename);
441
449
  if (!existsSync(planPath)) {
442
450
  throw new Error(`Plan file not found: ${planPath}`);
443
451
  }
444
-
445
452
  this.setCurrentPlan(Plan.fromMarkdown(planPath));
446
453
  return this.currentPlan!;
447
454
  }
448
455
 
456
+ loadPlans(filename: string): Plan[] {
457
+ const planPath = this.resolvePlanPath(filename);
458
+ if (!existsSync(planPath)) {
459
+ throw new Error(`Plan file not found: ${planPath}`);
460
+ }
461
+ return parsePlansFromMarkdown(planPath);
462
+ }
463
+
449
464
  setCurrentPlan(plan?: Plan): void {
450
465
  this.currentPlan = plan;
451
466
  if (plan && !this.sessionPlans.includes(plan)) {
package/src/explorer.ts CHANGED
@@ -8,6 +8,7 @@ import { createTest } from 'codeceptjs/lib/mocha/test';
8
8
  import { ActionResult } from './action-result.ts';
9
9
  import Action from './action.js';
10
10
  import { AIProvider } from './ai/provider.js';
11
+ import type { BrowserContextOptions } from 'playwright';
11
12
  import { visuallyAnnotateContainers } from './ai/researcher/coordinates.ts';
12
13
  import { RequestStore } from './api/request-store.ts';
13
14
  import { XhrCapture } from './api/xhr-capture.ts';
@@ -238,7 +239,17 @@ class Explorer {
238
239
  }
239
240
  await this.connectOrLaunchBrowser();
240
241
  const hasSession = this.options?.session && existsSync(this.options.session);
241
- const contextOptions = hasSession ? { storageState: this.options!.session } : undefined;
242
+ const helperOptions = this.playwrightHelper.options || {};
243
+ // CodeceptJS skips _createContextPage when sessions/storageState are involved, so we
244
+ // build contextOptions ourselves. Most keys share a name with Playwright's
245
+ // BrowserContextOptions and are copied as-is; `emulate` must be flattened, `basicAuth`
246
+ // renamed to `httpCredentials`, and `storageState` comes from the --session flag.
247
+ const contextOptions: BrowserContextOptions = {
248
+ ...helperOptions,
249
+ };
250
+ if (helperOptions.emulate) Object.assign(contextOptions, helperOptions.emulate);
251
+ if (helperOptions.basicAuth) contextOptions.httpCredentials = helperOptions.basicAuth;
252
+ if (hasSession) contextOptions.storageState = this.options!.session;
242
253
  await this.playwrightHelper._createContextPage(contextOptions);
243
254
  await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
244
255
  this.setupXhrCapture();
@@ -547,6 +547,10 @@ export class StateManager {
547
547
  }
548
548
 
549
549
  export function normalizeUrl(url: string): string {
550
+ if (url.startsWith('/')) {
551
+ return url.replace(/^\/+/, '').replace(/\/+$/g, '');
552
+ }
553
+
550
554
  try {
551
555
  const parsed = new URL(url, 'http://localhost');
552
556
  const path = parsed.pathname.replace(/^\/+|\/+$/g, '');
@@ -149,8 +149,15 @@ export function parsePlansFromMarkdown(filePath: string): Plan[] {
149
149
 
150
150
  if (line.startsWith('<!-- test')) {
151
151
  currentTest = null;
152
- const priorityMatch = line.match(/priority:\s*(\w+)/);
152
+ let block = line;
153
+ let j = i;
154
+ while (!block.includes('-->') && j + 1 < lines.length) {
155
+ j++;
156
+ block += `\n${lines[j].trim()}`;
157
+ }
158
+ const priorityMatch = block.match(/priority:\s*(\w+)/);
153
159
  priority = (priorityMatch?.[1] as 'critical' | 'important' | 'high' | 'normal' | 'low') || 'normal';
160
+ i = j;
154
161
  continue;
155
162
  }
156
163
 
@@ -82,10 +82,13 @@ export function matchesUrl(pattern: string, path: string): boolean {
82
82
  }
83
83
 
84
84
  export function extractStatePath(url: string): string {
85
- if (url.startsWith('/')) return url;
85
+ if (url.startsWith('/')) {
86
+ return `/${url.replace(/^\/+/, '')}`;
87
+ }
86
88
  try {
87
89
  const urlObj = new URL(url);
88
- return `${urlObj.pathname}${urlObj.search}${urlObj.hash}`;
90
+ const normalizedPathname = `/${urlObj.pathname.replace(/^\/+/, '')}`;
91
+ return `${normalizedPathname}${urlObj.search}${urlObj.hash}`;
89
92
  } catch {
90
93
  return url;
91
94
  }
@@ -122,7 +122,8 @@ export class WebElement {
122
122
  }
123
123
 
124
124
  static async fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]> {
125
- if (eidxList.length === 0) return [];
125
+ const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
126
+ if (validEidxList.length === 0) return [];
126
127
 
127
128
  const rawList: RawElementData[] = await page.evaluate(
128
129
  ([list, extractFnStr, config]: [string[], string, ElementExtractionConfig]) => {
@@ -136,7 +137,7 @@ export class WebElement {
136
137
  }
137
138
  return results;
138
139
  },
139
- [eidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] as [string[], string, ElementExtractionConfig]
140
+ [validEidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] as [string[], string, ElementExtractionConfig]
140
141
  );
141
142
 
142
143
  return rawList.map((d) => WebElement.fromRawData(d));