explorbot 0.1.9 → 0.1.11

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 (157) hide show
  1. package/README.md +27 -1
  2. package/bin/explorbot-cli.ts +86 -15
  3. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  4. package/boat/api-tester/src/ai/curler.ts +1 -1
  5. package/boat/api-tester/src/apibot.ts +2 -2
  6. package/boat/api-tester/src/config.ts +1 -1
  7. package/dist/bin/explorbot-cli.js +85 -14
  8. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  9. package/dist/boat/api-tester/src/apibot.js +2 -2
  10. package/dist/package.json +2 -2
  11. package/dist/rules/navigator/output.md +9 -0
  12. package/dist/rules/navigator/verification-actions.md +2 -0
  13. package/dist/src/action-result.js +23 -1
  14. package/dist/src/action.js +46 -38
  15. package/dist/src/ai/bosun.js +16 -2
  16. package/dist/src/ai/conversation.js +39 -0
  17. package/dist/src/ai/experience-compactor.js +235 -50
  18. package/dist/src/ai/historian/codeceptjs.js +109 -0
  19. package/dist/src/ai/historian/experience.js +320 -0
  20. package/dist/src/ai/historian/mixin.js +2 -0
  21. package/dist/src/ai/historian/playwright.js +145 -0
  22. package/dist/src/ai/historian/utils.js +18 -0
  23. package/dist/src/ai/historian.js +19 -398
  24. package/dist/src/ai/navigator.js +133 -80
  25. package/dist/src/ai/pilot.js +254 -13
  26. package/dist/src/ai/planner/subpages.js +1 -30
  27. package/dist/src/ai/planner.js +33 -13
  28. package/dist/src/ai/provider.js +55 -18
  29. package/dist/src/ai/rerunner.js +3 -3
  30. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  31. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  32. package/dist/src/ai/researcher/locators.js +1 -1
  33. package/dist/src/ai/researcher/sections.js +8 -1
  34. package/dist/src/ai/researcher.js +43 -41
  35. package/dist/src/ai/rules.js +26 -14
  36. package/dist/src/ai/tester.js +90 -26
  37. package/dist/src/ai/tools.js +18 -10
  38. package/dist/src/api/request-store.js +20 -0
  39. package/dist/src/api/xhr-capture.js +19 -3
  40. package/dist/src/browser-server.js +16 -3
  41. package/dist/src/command-handler.js +1 -1
  42. package/dist/src/commands/add-rule-command.js +12 -9
  43. package/dist/src/commands/base-command.js +20 -0
  44. package/dist/src/commands/clean-command.js +3 -2
  45. package/dist/src/commands/compact-command.js +138 -0
  46. package/dist/src/commands/context-command.js +7 -1
  47. package/dist/src/commands/drill-command.js +4 -1
  48. package/dist/src/commands/experience-command.js +104 -0
  49. package/dist/src/commands/explore-command.js +54 -19
  50. package/dist/src/commands/freesail-command.js +2 -0
  51. package/dist/src/commands/index.js +7 -3
  52. package/dist/src/commands/init-command.js +11 -10
  53. package/dist/src/commands/learn-command.js +1 -1
  54. package/dist/src/commands/navigate-command.js +4 -1
  55. package/dist/src/commands/plan-clear-command.js +4 -1
  56. package/dist/src/commands/plan-command.js +43 -4
  57. package/dist/src/commands/plan-edit-command.js +1 -1
  58. package/dist/src/commands/plan-load-command.js +4 -1
  59. package/dist/src/commands/plan-reload-command.js +4 -1
  60. package/dist/src/commands/plan-save-command.js +20 -8
  61. package/dist/src/commands/rerun-command.js +4 -0
  62. package/dist/src/commands/research-command.js +5 -2
  63. package/dist/src/commands/start-command.js +5 -1
  64. package/dist/src/commands/test-command.js +7 -1
  65. package/dist/src/components/App.js +15 -5
  66. package/dist/src/execution-controller.js +13 -2
  67. package/dist/src/experience-tracker.js +174 -83
  68. package/dist/src/explorbot.js +31 -22
  69. package/dist/src/explorer.js +12 -5
  70. package/dist/src/observability.js +50 -99
  71. package/dist/src/playwright-recorder.js +309 -0
  72. package/dist/src/reporter.js +17 -2
  73. package/dist/src/stats.js +2 -0
  74. package/dist/src/suite.js +1 -1
  75. package/dist/src/test-plan.js +12 -0
  76. package/dist/src/utils/aria.js +37 -1
  77. package/dist/src/utils/error-page.js +30 -7
  78. package/dist/src/utils/logger.js +1 -1
  79. package/dist/src/utils/next-steps.js +37 -0
  80. package/dist/src/utils/rules-loader.js +1 -1
  81. package/dist/src/utils/test-files.js +1 -1
  82. package/dist/src/utils/url-matcher.js +50 -0
  83. package/package.json +2 -2
  84. package/rules/navigator/output.md +9 -0
  85. package/rules/navigator/verification-actions.md +2 -0
  86. package/src/action-result.ts +26 -1
  87. package/src/action.ts +44 -37
  88. package/src/ai/bosun.ts +16 -2
  89. package/src/ai/conversation.ts +37 -0
  90. package/src/ai/experience-compactor.ts +270 -63
  91. package/src/ai/historian/codeceptjs.ts +130 -0
  92. package/src/ai/historian/experience.ts +383 -0
  93. package/src/ai/historian/mixin.ts +4 -0
  94. package/src/ai/historian/playwright.ts +169 -0
  95. package/src/ai/historian/utils.ts +23 -0
  96. package/src/ai/historian.ts +35 -468
  97. package/src/ai/navigator.ts +140 -85
  98. package/src/ai/pilot.ts +259 -14
  99. package/src/ai/planner/subpages.ts +1 -24
  100. package/src/ai/planner.ts +34 -14
  101. package/src/ai/provider.ts +52 -18
  102. package/src/ai/rerunner.ts +3 -3
  103. package/src/ai/researcher/deep-analysis.ts +1 -1
  104. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/sections.ts +7 -1
  107. package/src/ai/researcher.ts +47 -42
  108. package/src/ai/rules.ts +27 -14
  109. package/src/ai/task-agent.ts +1 -1
  110. package/src/ai/tester.ts +94 -26
  111. package/src/ai/tools.ts +53 -29
  112. package/src/api/request-store.ts +22 -0
  113. package/src/api/xhr-capture.ts +21 -3
  114. package/src/browser-server.ts +17 -3
  115. package/src/command-handler.ts +1 -1
  116. package/src/commands/add-rule-command.ts +13 -9
  117. package/src/commands/base-command.ts +26 -1
  118. package/src/commands/clean-command.ts +4 -3
  119. package/src/commands/compact-command.ts +156 -0
  120. package/src/commands/context-command.ts +8 -2
  121. package/src/commands/drill-command.ts +5 -2
  122. package/src/commands/experience-command.ts +125 -0
  123. package/src/commands/explore-command.ts +58 -21
  124. package/src/commands/freesail-command.ts +2 -0
  125. package/src/commands/index.ts +7 -3
  126. package/src/commands/init-command.ts +11 -10
  127. package/src/commands/learn-command.ts +2 -2
  128. package/src/commands/navigate-command.ts +5 -2
  129. package/src/commands/plan-clear-command.ts +5 -2
  130. package/src/commands/plan-command.ts +47 -5
  131. package/src/commands/plan-edit-command.ts +2 -2
  132. package/src/commands/plan-load-command.ts +5 -2
  133. package/src/commands/plan-reload-command.ts +5 -2
  134. package/src/commands/plan-save-command.ts +20 -9
  135. package/src/commands/rerun-command.ts +5 -0
  136. package/src/commands/research-command.ts +6 -3
  137. package/src/commands/start-command.ts +6 -2
  138. package/src/commands/test-command.ts +8 -2
  139. package/src/components/App.tsx +16 -5
  140. package/src/config.ts +6 -1
  141. package/src/execution-controller.ts +14 -3
  142. package/src/experience-tracker.ts +198 -100
  143. package/src/explorbot.ts +33 -23
  144. package/src/explorer.ts +14 -5
  145. package/src/observability.ts +50 -109
  146. package/src/playwright-recorder.ts +305 -0
  147. package/src/reporter.ts +17 -3
  148. package/src/stats.ts +4 -0
  149. package/src/suite.ts +1 -1
  150. package/src/test-plan.ts +12 -0
  151. package/src/utils/aria.ts +38 -1
  152. package/src/utils/error-page.ts +32 -7
  153. package/src/utils/logger.ts +1 -1
  154. package/src/utils/next-steps.ts +51 -0
  155. package/src/utils/rules-loader.ts +1 -1
  156. package/src/utils/test-files.ts +1 -1
  157. package/src/utils/url-matcher.ts +43 -0
@@ -4,20 +4,45 @@ import { isBodyEmpty } from './html.js';
4
4
  const HTTP_ERRORS = ['400 Bad Request', '401 Unauthorized', '403 Forbidden', '404 Not Found', '405 Method Not Allowed', '408 Request Timeout', '500 Internal Server Error', '502 Bad Gateway', '503 Service Unavailable', '504 Gateway Timeout'];
5
5
 
6
6
  const SMALL_PAGE_THRESHOLD = 500;
7
+ const LOADING_WORD = /\bloading\b/i;
7
8
 
8
- export function isErrorPage(actionResult: ActionResult): boolean {
9
- const checkFields = [actionResult.title, actionResult.h1, actionResult.h2].filter(Boolean) as string[];
9
+ export type PageCondition = 'ok' | 'loading' | 'error';
10
+
11
+ export function detectPageCondition(actionResult: ActionResult): PageCondition {
12
+ const headingFields = [actionResult.title, actionResult.h1, actionResult.h2].filter(Boolean) as string[];
10
13
 
11
- for (const field of checkFields) {
14
+ for (const field of headingFields) {
12
15
  for (const error of HTTP_ERRORS) {
13
- if (field.toLowerCase().includes(error.toLowerCase())) return true;
16
+ if (field.toLowerCase().includes(error.toLowerCase())) return 'error';
14
17
  }
15
18
  }
16
19
 
17
- if (!actionResult.html || isBodyEmpty(actionResult.html)) return true;
20
+ const aria = actionResult.ariaSnapshot || '';
21
+ if (/\bprogressbar\b/i.test(aria)) return 'loading';
22
+ if (/\[busy\]/.test(aria)) return 'loading';
23
+
24
+ for (const field of headingFields) {
25
+ if (LOADING_WORD.test(field)) return 'loading';
26
+ }
27
+
28
+ if (!actionResult.html || isBodyEmpty(actionResult.html)) return 'loading';
18
29
 
19
30
  const bodyMatch = actionResult.html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
20
- if (bodyMatch && bodyMatch[1].trim().length < SMALL_PAGE_THRESHOLD) return true;
31
+ if (bodyMatch && bodyMatch[1].trim().length < SMALL_PAGE_THRESHOLD) return 'loading';
21
32
 
22
- return false;
33
+ return 'ok';
34
+ }
35
+
36
+ export function isErrorPage(actionResult: ActionResult): boolean {
37
+ return detectPageCondition(actionResult) === 'error';
38
+ }
39
+
40
+ export class ErrorPageError extends Error {
41
+ constructor(
42
+ public readonly url: string,
43
+ public readonly title?: string
44
+ ) {
45
+ super(`Error page detected at ${url}${title ? ` (${title})` : ''}`);
46
+ this.name = 'ErrorPageError';
47
+ }
23
48
  }
@@ -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';
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { tag } from './logger.js';
3
+
4
+ export interface NextStepCommand {
5
+ label: string;
6
+ command: string;
7
+ }
8
+
9
+ export interface NextStepSection {
10
+ label: string;
11
+ path?: string;
12
+ commands?: NextStepCommand[];
13
+ }
14
+
15
+ export function relativeToCwd(absPath: string): string {
16
+ const rel = path.relative(process.cwd(), absPath);
17
+ return rel || '.';
18
+ }
19
+
20
+ export function printNextSteps(sections: NextStepSection[]): void {
21
+ if (sections.length === 0) return;
22
+
23
+ const blocks: string[] = [];
24
+ for (const section of sections) {
25
+ const lines: string[] = [];
26
+ const headerPath = section.path ? relativeToCwd(section.path) : '';
27
+ lines.push(headerPath ? `${section.label}: ${headerPath}` : section.label);
28
+
29
+ const commands = section.commands || [];
30
+ if (commands.length > 0) {
31
+ const labeled = commands.filter((c) => c.label);
32
+ const maxLabel = labeled.length > 0 ? Math.max(...labeled.map((c) => c.label.length)) : 0;
33
+ for (const cmd of commands) {
34
+ if (!cmd.label) {
35
+ lines.push(` ${cmd.command}`);
36
+ continue;
37
+ }
38
+ const padded = `${cmd.label}:`.padEnd(maxLabel + 2);
39
+ lines.push(` ${padded} ${cmd.command}`);
40
+ }
41
+ }
42
+ blocks.push(lines.join('\n'));
43
+ }
44
+
45
+ for (let i = 0; i < blocks.length; i++) {
46
+ if (i > 0) tag('info').log('');
47
+ for (const line of blocks[i].split('\n')) {
48
+ tag('info').log(line);
49
+ }
50
+ }
51
+ }
@@ -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;