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.
- package/README.md +27 -1
- package/bin/explorbot-cli.ts +86 -15
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +85 -14
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +2 -2
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +46 -38
- package/dist/src/ai/bosun.js +16 -2
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +320 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +19 -398
- package/dist/src/ai/navigator.js +133 -80
- package/dist/src/ai/pilot.js +254 -13
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +33 -13
- package/dist/src/ai/provider.js +55 -18
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +43 -41
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +18 -10
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/browser-server.js +16 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +12 -9
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +3 -2
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +54 -19
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +11 -10
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +43 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +20 -8
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +174 -83
- package/dist/src/explorbot.js +31 -22
- package/dist/src/explorer.js +12 -5
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +30 -7
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/next-steps.js +37 -0
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +2 -2
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +44 -37
- package/src/ai/bosun.ts +16 -2
- package/src/ai/conversation.ts +37 -0
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +383 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +35 -468
- package/src/ai/navigator.ts +140 -85
- package/src/ai/pilot.ts +259 -14
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +34 -14
- package/src/ai/provider.ts +52 -18
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +47 -42
- package/src/ai/rules.ts +27 -14
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +53 -29
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/browser-server.ts +17 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +13 -9
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +4 -3
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +58 -21
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +11 -10
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +47 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +20 -9
- package/src/commands/rerun-command.ts +5 -0
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/components/App.tsx +16 -5
- package/src/config.ts +6 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +198 -100
- package/src/explorbot.ts +33 -23
- package/src/explorer.ts +14 -5
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +32 -7
- package/src/utils/logger.ts +1 -1
- package/src/utils/next-steps.ts +51 -0
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
package/src/utils/error-page.ts
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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
|
|
14
|
+
for (const field of headingFields) {
|
|
12
15
|
for (const error of HTTP_ERRORS) {
|
|
13
|
-
if (field.toLowerCase().includes(error.toLowerCase())) return
|
|
16
|
+
if (field.toLowerCase().includes(error.toLowerCase())) return 'error';
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
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
|
|
31
|
+
if (bodyMatch && bodyMatch[1].trim().length < SMALL_PAGE_THRESHOLD) return 'loading';
|
|
21
32
|
|
|
22
|
-
return
|
|
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
|
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -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,
|
|
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';
|
package/src/utils/test-files.ts
CHANGED
|
@@ -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
|
|
package/src/utils/url-matcher.ts
CHANGED
|
@@ -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;
|