explorbot 0.1.8 → 0.1.10

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 (113) hide show
  1. package/bin/explorbot-cli.ts +70 -8
  2. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  3. package/boat/api-tester/src/ai/curler.ts +1 -1
  4. package/boat/api-tester/src/apibot.ts +2 -2
  5. package/boat/api-tester/src/config.ts +1 -1
  6. package/dist/bin/explorbot-cli.js +70 -7
  7. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  8. package/dist/boat/api-tester/src/apibot.js +2 -2
  9. package/dist/package.json +1 -1
  10. package/dist/src/ai/bosun.js +5 -1
  11. package/dist/src/ai/experience-compactor.js +235 -50
  12. package/dist/src/ai/historian.js +13 -6
  13. package/dist/src/ai/navigator.js +62 -62
  14. package/dist/src/ai/pilot.js +22 -0
  15. package/dist/src/ai/planner/subpages.js +1 -30
  16. package/dist/src/ai/planner.js +4 -4
  17. package/dist/src/ai/provider.js +1 -1
  18. package/dist/src/ai/rerunner.js +3 -3
  19. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  20. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  21. package/dist/src/ai/researcher/locators.js +1 -1
  22. package/dist/src/ai/researcher/sections.js +8 -1
  23. package/dist/src/ai/researcher.js +4 -11
  24. package/dist/src/ai/tools.js +5 -3
  25. package/dist/src/api/request-store.js +20 -0
  26. package/dist/src/api/xhr-capture.js +19 -3
  27. package/dist/src/command-handler.js +1 -1
  28. package/dist/src/commands/add-rule-command.js +1 -1
  29. package/dist/src/commands/base-command.js +20 -0
  30. package/dist/src/commands/clean-command.js +1 -1
  31. package/dist/src/commands/compact-command.js +138 -0
  32. package/dist/src/commands/context-command.js +7 -1
  33. package/dist/src/commands/drill-command.js +4 -1
  34. package/dist/src/commands/experience-command.js +104 -0
  35. package/dist/src/commands/explore-command.js +33 -7
  36. package/dist/src/commands/freesail-command.js +2 -0
  37. package/dist/src/commands/index.js +7 -3
  38. package/dist/src/commands/init-command.js +2 -2
  39. package/dist/src/commands/learn-command.js +1 -1
  40. package/dist/src/commands/navigate-command.js +4 -1
  41. package/dist/src/commands/plan-clear-command.js +4 -1
  42. package/dist/src/commands/plan-command.js +11 -4
  43. package/dist/src/commands/plan-edit-command.js +1 -1
  44. package/dist/src/commands/plan-load-command.js +4 -1
  45. package/dist/src/commands/plan-reload-command.js +4 -1
  46. package/dist/src/commands/plan-save-command.js +1 -1
  47. package/dist/src/commands/research-command.js +5 -2
  48. package/dist/src/commands/start-command.js +5 -1
  49. package/dist/src/commands/test-command.js +7 -1
  50. package/dist/src/experience-tracker.js +191 -56
  51. package/dist/src/explorbot.js +26 -14
  52. package/dist/src/explorer.js +3 -3
  53. package/dist/src/reporter.js +17 -2
  54. package/dist/src/stats.js +2 -0
  55. package/dist/src/suite.js +1 -1
  56. package/dist/src/utils/error-page.js +10 -0
  57. package/dist/src/utils/logger.js +1 -1
  58. package/dist/src/utils/rules-loader.js +1 -1
  59. package/dist/src/utils/test-files.js +1 -1
  60. package/dist/src/utils/url-matcher.js +50 -0
  61. package/package.json +1 -1
  62. package/src/ai/bosun.ts +5 -1
  63. package/src/ai/experience-compactor.ts +270 -63
  64. package/src/ai/historian.ts +12 -7
  65. package/src/ai/navigator.ts +68 -66
  66. package/src/ai/pilot.ts +22 -0
  67. package/src/ai/planner/subpages.ts +1 -24
  68. package/src/ai/planner.ts +5 -5
  69. package/src/ai/provider.ts +1 -1
  70. package/src/ai/rerunner.ts +3 -3
  71. package/src/ai/researcher/deep-analysis.ts +1 -1
  72. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  73. package/src/ai/researcher/locators.ts +2 -2
  74. package/src/ai/researcher/sections.ts +7 -1
  75. package/src/ai/researcher.ts +4 -11
  76. package/src/ai/task-agent.ts +1 -1
  77. package/src/ai/tools.ts +6 -4
  78. package/src/api/request-store.ts +22 -0
  79. package/src/api/xhr-capture.ts +21 -3
  80. package/src/command-handler.ts +1 -1
  81. package/src/commands/add-rule-command.ts +2 -2
  82. package/src/commands/base-command.ts +26 -1
  83. package/src/commands/clean-command.ts +2 -2
  84. package/src/commands/compact-command.ts +156 -0
  85. package/src/commands/context-command.ts +8 -2
  86. package/src/commands/drill-command.ts +5 -2
  87. package/src/commands/experience-command.ts +125 -0
  88. package/src/commands/explore-command.ts +35 -9
  89. package/src/commands/freesail-command.ts +2 -0
  90. package/src/commands/index.ts +7 -3
  91. package/src/commands/init-command.ts +2 -2
  92. package/src/commands/learn-command.ts +2 -2
  93. package/src/commands/navigate-command.ts +5 -2
  94. package/src/commands/plan-clear-command.ts +5 -2
  95. package/src/commands/plan-command.ts +12 -5
  96. package/src/commands/plan-edit-command.ts +2 -2
  97. package/src/commands/plan-load-command.ts +5 -2
  98. package/src/commands/plan-reload-command.ts +5 -2
  99. package/src/commands/plan-save-command.ts +2 -2
  100. package/src/commands/research-command.ts +6 -3
  101. package/src/commands/start-command.ts +6 -2
  102. package/src/commands/test-command.ts +8 -2
  103. package/src/experience-tracker.ts +220 -71
  104. package/src/explorbot.ts +28 -15
  105. package/src/explorer.ts +3 -3
  106. package/src/reporter.ts +17 -3
  107. package/src/stats.ts +4 -0
  108. package/src/suite.ts +1 -1
  109. package/src/utils/error-page.ts +10 -0
  110. package/src/utils/logger.ts +1 -1
  111. package/src/utils/rules-loader.ts +1 -1
  112. package/src/utils/test-files.ts +1 -1
  113. package/src/utils/url-matcher.ts +43 -0
package/src/reporter.ts CHANGED
@@ -3,6 +3,8 @@ import { Client } from '@testomatio/reporter';
3
3
  import type { Step } from '@testomatio/reporter/types/types.js';
4
4
  import { ConfigParser, outputPath } from './config.js';
5
5
  import type { ReporterConfig } from './config.js';
6
+ import type { StateManager } from './state-manager.js';
7
+ import { Stats } from './stats.js';
6
8
  import { Test } from './test-plan.js';
7
9
  import { createDebug } from './utils/logger.js';
8
10
 
@@ -18,22 +20,33 @@ export interface ReporterStep {
18
20
  }
19
21
 
20
22
  export class Reporter {
21
- private client: Client;
23
+ private client!: Client;
22
24
  private isRunStarted = false;
23
25
  private reporterEnabled: boolean;
26
+ private stateManager?: StateManager;
24
27
 
25
- constructor(config?: ReporterConfig) {
28
+ constructor(config?: ReporterConfig, stateManager?: StateManager) {
26
29
  this.reporterEnabled = Reporter.resolveEnabled(config);
30
+ this.stateManager = stateManager;
27
31
 
28
32
  if (this.reporterEnabled && (!process.env.TESTOMATIO || config?.html)) {
29
33
  this.configureHtmlPipe();
30
34
  }
31
35
 
32
- this.client = new Client({ apiKey: process.env.TESTOMATIO || '' });
33
36
  const pipe = process.env.TESTOMATIO && config?.html ? 'both' : process.env.TESTOMATIO ? 'testomatio' : 'html';
34
37
  debugLog('Reporter initialized', { enabled: this.reporterEnabled, pipe });
35
38
  }
36
39
 
40
+ private buildTitle(): string {
41
+ if (process.env.TESTOMATIO_TITLE) return process.env.TESTOMATIO_TITLE;
42
+ const url = this.stateManager?.getCurrentState()?.url;
43
+ const parts = ['Explorbot session'];
44
+ if (url) parts.push(url);
45
+ if (Stats.focus) parts.push(`focus: "${Stats.focus}"`);
46
+ parts.push(`at ${new Date().toISOString().slice(0, 16)}`);
47
+ return parts.join(' ');
48
+ }
49
+
37
50
  static resolveEnabled(config?: ReporterConfig): boolean {
38
51
  if (config?.enabled === true) return true;
39
52
  if (config?.enabled === false) return false;
@@ -56,6 +69,7 @@ export class Reporter {
56
69
  }
57
70
 
58
71
  try {
72
+ this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
59
73
  const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
60
74
  const timeoutPromise = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
61
75
 
package/src/stats.ts CHANGED
@@ -4,11 +4,15 @@ interface TokenUsage {
4
4
  total: number;
5
5
  }
6
6
 
7
+ export type ExplorbotMode = 'explore' | 'test' | 'freesail' | 'tui';
8
+
7
9
  export class Stats {
8
10
  static startTime = Date.now();
9
11
  static researches = 0;
10
12
  static tests = 0;
11
13
  static plans = 0;
14
+ static mode?: ExplorbotMode;
15
+ static focus?: string;
12
16
  static models: Record<string, TokenUsage> = {};
13
17
 
14
18
  static recordTokens(_agent: string, model: string, usage: TokenUsage): void {
package/src/suite.ts CHANGED
@@ -3,8 +3,8 @@ import path from 'node:path';
3
3
  import { Reflection } from '@codeceptjs/reflection';
4
4
  import { ConfigParser } from './config.ts';
5
5
  import { normalizeUrl } from './state-manager.ts';
6
- import { parsePlanFromMarkdown } from './utils/test-plan-markdown.ts';
7
6
  import { createDebug } from './utils/logger.ts';
7
+ import { parsePlanFromMarkdown } from './utils/test-plan-markdown.ts';
8
8
 
9
9
  const debugLog = createDebug('explorbot:suite');
10
10
 
@@ -21,3 +21,13 @@ export function isErrorPage(actionResult: ActionResult): boolean {
21
21
 
22
22
  return false;
23
23
  }
24
+
25
+ export class ErrorPageError extends Error {
26
+ constructor(
27
+ public readonly url: string,
28
+ public readonly title?: string
29
+ ) {
30
+ super(`Error page detected at ${url}${title ? ` (${title})` : ''}`);
31
+ this.name = 'ErrorPageError';
32
+ }
33
+ }
@@ -5,9 +5,9 @@ import chalk from 'chalk';
5
5
  import debug from 'debug';
6
6
  import dedent from 'dedent';
7
7
  import { marked } from 'marked';
8
+ import stripAnsi from 'strip-ansi';
8
9
  import { ConfigParser } from '../config.js';
9
10
  import { Observability } from '../observability.ts';
10
- import stripAnsi from 'strip-ansi';
11
11
  import { parseMarkdownToTerminal } from './markdown-terminal.ts';
12
12
 
13
13
  export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'step' | 'multiline' | 'html' | 'input';
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { tag } from './logger.ts';
@@ -3,9 +3,9 @@ import path from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { highlight } from 'cli-highlight';
5
5
  import * as codeceptjs from 'codeceptjs';
6
- import store from 'codeceptjs/lib/store';
7
6
  import stepsListener from 'codeceptjs/lib/listener/steps';
8
7
  import storeListener from 'codeceptjs/lib/listener/store';
8
+ import store from 'codeceptjs/lib/store';
9
9
  import figureSet from 'figures';
10
10
  import { ConfigParser } from '../config.ts';
11
11
 
@@ -1,4 +1,47 @@
1
1
  import micromatch from 'micromatch';
2
+ import { ConfigParser } from '../config.js';
3
+
4
+ export function isDynamicSegment(segment: string): boolean {
5
+ try {
6
+ const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
7
+ if (configRegex) return new RegExp(configRegex, 'i').test(segment);
8
+ } catch {
9
+ /* config not loaded yet */
10
+ }
11
+
12
+ // numeric: /users/123
13
+ if (/^\d+$/.test(segment)) return true;
14
+ // UUID: /items/550e8400-e29b-41d4-a716-446655440000
15
+ if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment)) return true;
16
+ // ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
17
+ if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment)) return true;
18
+ // hex ID (4+ chars): /suite/70dae98a
19
+ if (/^[a-f0-9]{4,}$/i.test(segment)) return true;
20
+ // hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
21
+ if (/^[a-f0-9]{8,}-/i.test(segment)) return true;
22
+ // short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
23
+ if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment)) return true;
24
+ return false;
25
+ }
26
+
27
+ export function hasDynamicUrlSegment(url: string): boolean {
28
+ return url.split('/').some((seg) => seg.length > 0 && isDynamicSegment(seg));
29
+ }
30
+
31
+ export function generalizeSegment(segment: string): string {
32
+ if (/^\d+$/.test(segment)) return '\\d+';
33
+ if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment)) return '[a-f0-9-]+';
34
+ if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment)) return '[0-9A-HJKMNP-TV-Z]+';
35
+ if (/^[a-f0-9]+$/i.test(segment)) return '[a-f0-9]+';
36
+ return '[^/]+';
37
+ }
38
+
39
+ export function generalizeUrl(url: string): string {
40
+ return url
41
+ .split('/')
42
+ .map((seg) => (seg.length > 0 && isDynamicSegment(seg) ? generalizeSegment(seg) : seg))
43
+ .join('/');
44
+ }
2
45
 
3
46
  export function matchesUrl(pattern: string, path: string): boolean {
4
47
  if (pattern === '*') return true;