explorbot 0.1.32-beta.1 → 0.2.1
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 +35 -19
- package/bin/explorbot-cli.ts +33 -10
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +26 -28
- package/boat/doc-collector/src/ai/tools.ts +74 -16
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +30 -9
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +34 -2
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +29 -9
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
- package/dist/boat/doc-collector/src/ai/tools.js +53 -16
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +27 -8
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +30 -1
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/models.json +30 -0
- package/dist/package.json +23 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +13 -12
- package/dist/src/action.js +37 -113
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -29
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +17 -45
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +2 -17
- package/dist/src/ai/driller.js +26 -50
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +4 -3
- package/dist/src/ai/historian/playwright.d.ts +4 -1
- package/dist/src/ai/historian/playwright.js +4 -3
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +10 -8
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +8 -6
- package/dist/src/ai/navigator.js +34 -65
- package/dist/src/ai/pilot.d.ts +13 -8
- package/dist/src/ai/pilot.js +53 -59
- package/dist/src/ai/planner.d.ts +5 -4
- package/dist/src/ai/planner.js +66 -52
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -137
- package/dist/src/ai/quartermaster.d.ts +22 -22
- package/dist/src/ai/quartermaster.js +8 -14
- package/dist/src/ai/rerunner.d.ts +3 -13
- package/dist/src/ai/rerunner.js +23 -44
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +8 -11
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +7 -3
- package/dist/src/ai/researcher.js +40 -106
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +19 -6
- package/dist/src/ai/task-agent.js +37 -33
- package/dist/src/ai/tester.d.ts +6 -14
- package/dist/src/ai/tester.js +46 -111
- package/dist/src/ai/tools.d.ts +26 -14
- package/dist/src/ai/tools.js +138 -132
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +8 -6
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +2 -2
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +5 -3
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +3 -3
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +156 -10
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +82 -155
- package/dist/src/explorbot.d.ts +21 -13
- package/dist/src/explorbot.js +84 -80
- package/dist/src/explorer.d.ts +66 -102
- package/dist/src/explorer.js +332 -623
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +10 -30
- package/dist/src/state-manager.js +11 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +3 -1
- package/dist/src/utils/web-element.js +32 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/models.json +30 -0
- package/package.json +23 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +38 -132
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -30
- package/src/ai/captain.ts +17 -42
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +53 -83
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +5 -3
- package/src/ai/historian/playwright.ts +7 -4
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +6 -8
- package/src/ai/navigator.ts +39 -70
- package/src/ai/pilot.ts +59 -61
- package/src/ai/planner.ts +76 -62
- package/src/ai/provider.ts +85 -146
- package/src/ai/quartermaster.ts +39 -45
- package/src/ai/rerunner.ts +27 -50
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +10 -11
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +9 -3
- package/src/ai/researcher.ts +42 -119
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +45 -38
- package/src/ai/tester.ts +51 -129
- package/src/ai/tools.ts +145 -153
- package/src/command-handler.ts +1 -1
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +8 -6
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +2 -2
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +5 -3
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +3 -3
- package/src/config.ts +185 -11
- package/src/experience-tracker.ts +89 -152
- package/src/explorbot.ts +88 -83
- package/src/explorer.ts +402 -693
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +20 -155
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- package/src/utils/web-element.ts +34 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
|
|
5
|
+
export function loadMarkdownFiles(dir: string, options: { recursive?: boolean; onError?: (filePath: string, error: unknown) => void } = {}): MarkdownFile[] {
|
|
6
|
+
if (!existsSync(dir)) return [];
|
|
7
|
+
|
|
8
|
+
const results: MarkdownFile[] = [];
|
|
9
|
+
const files = readdirSync(dir, { recursive: options.recursive === true })
|
|
10
|
+
.filter((file): file is string => typeof file === 'string' && file.endsWith('.md'))
|
|
11
|
+
.map((file) => join(dir, file));
|
|
12
|
+
|
|
13
|
+
for (const filePath of files) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = matter(readFileSync(filePath, 'utf8'));
|
|
16
|
+
results.push({ filePath, content: parsed.content, data: parsed.data, mtime: statSync(filePath).mtime });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
options.onError?.(filePath, error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MarkdownFile {
|
|
26
|
+
filePath: string;
|
|
27
|
+
content: string;
|
|
28
|
+
data: Record<string, any>;
|
|
29
|
+
mtime: Date;
|
|
30
|
+
}
|
|
@@ -408,6 +408,10 @@ export class MarkdownQuery {
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
replace(content: string): string {
|
|
411
|
+
return this.replaceEach(() => content);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
replaceEach(replacer: (match: MarkdownQuery, index: number) => string): string {
|
|
411
415
|
const sorted = [...this.matches].sort((a, b) => a.start - b.start);
|
|
412
416
|
|
|
413
417
|
const kept: MatchedRange[] = [];
|
|
@@ -418,10 +422,11 @@ export class MarkdownQuery {
|
|
|
418
422
|
lastEnd = range.start + range.length;
|
|
419
423
|
}
|
|
420
424
|
|
|
425
|
+
const replacements = kept.map((range, index) => replacer(new MarkdownQuery(this.source, [range]), index));
|
|
421
426
|
let result = this.source;
|
|
422
427
|
for (let i = kept.length - 1; i >= 0; i--) {
|
|
423
428
|
const range = kept[i];
|
|
424
|
-
result = result.slice(0, range.start) +
|
|
429
|
+
result = result.slice(0, range.start) + replacements[i] + result.slice(range.start + range.length);
|
|
425
430
|
}
|
|
426
431
|
|
|
427
432
|
return result;
|
|
@@ -459,6 +464,15 @@ export class MarkdownQuery {
|
|
|
459
464
|
each(): MarkdownQuery[] {
|
|
460
465
|
return this.matches.map((m) => new MarkdownQuery(this.source, [m]));
|
|
461
466
|
}
|
|
467
|
+
|
|
468
|
+
meta(): Array<{ type: string; depth: number | null; text: string }> {
|
|
469
|
+
return this.matches.map((range) => {
|
|
470
|
+
const token = range.token as any;
|
|
471
|
+
let depth: number | null = null;
|
|
472
|
+
if (token.type === 'heading') depth = token.depth;
|
|
473
|
+
return { type: token.type, depth, text: getTokenText(range.token) };
|
|
474
|
+
});
|
|
475
|
+
}
|
|
462
476
|
}
|
|
463
477
|
|
|
464
478
|
export function mdq(source: string): MarkdownQuery {
|
|
@@ -49,7 +49,7 @@ function waitForPageBodyContent(page: any, timeout: number): Promise<void> {
|
|
|
49
49
|
.catch(() => {});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function sleep(ms: number): Promise<void> {
|
|
52
|
+
export function sleep(ms: number): Promise<void> {
|
|
53
53
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
54
|
}
|
|
55
55
|
|
package/src/utils/secrets.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const MIN_SECRET_LENGTH = 4;
|
|
2
|
+
const SECRET_NAME_TOKENS = ['password', 'passwd', 'secret', 'token', 'apikey', 'api_key', 'credential', 'private_key', 'privatekey', 'access_key', 'jwt', 'auth', 'bearer', 'authorization', 'cert', 'certificate'];
|
|
2
3
|
const secretValues = new Set<string>();
|
|
3
4
|
|
|
4
5
|
export function registerSecret(value: string): void {
|
|
@@ -10,7 +11,7 @@ export function registerSecret(value: string): void {
|
|
|
10
11
|
export function redactSecrets(text: string): string {
|
|
11
12
|
if (!text) return text;
|
|
12
13
|
let result = text;
|
|
13
|
-
for (const value of secretValues) {
|
|
14
|
+
for (const value of [...secretValues].sort((a, b) => b.length - a.length)) {
|
|
14
15
|
if (!result.includes(value)) continue;
|
|
15
16
|
result = result.split(value).join('***REDACTED***');
|
|
16
17
|
}
|
|
@@ -21,8 +22,6 @@ export function clearRegisteredSecrets(): void {
|
|
|
21
22
|
secretValues.clear();
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const SECRET_NAME_TOKENS = ['password', 'passwd', 'secret', 'token', 'apikey', 'api_key', 'credential', 'private_key', 'privatekey', 'access_key'];
|
|
25
|
-
|
|
26
25
|
export function isSecretName(name: string): boolean {
|
|
27
26
|
const lower = name.toLowerCase();
|
|
28
27
|
return SECRET_NAME_TOKENS.some((token) => lower.includes(token));
|
package/src/utils/strings.ts
CHANGED
|
@@ -6,6 +6,18 @@ export function truncateJson(input: any): string {
|
|
|
6
6
|
return str.length <= 80 ? str : `${str.slice(0, 77)}...`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export function slugify(text: string): string {
|
|
10
|
+
return text
|
|
11
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
12
|
+
.replace(/_+/g, '_')
|
|
13
|
+
.replace(/^_|_$/g, '')
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeInlineText(text: string): string {
|
|
18
|
+
return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
export function sanitizeFilename(name: string): string {
|
|
10
22
|
return name
|
|
11
23
|
.toLowerCase()
|
package/src/utils/test-files.ts
CHANGED
|
@@ -76,7 +76,7 @@ export async function dryRunTestFile(filePath: string): Promise<void> {
|
|
|
76
76
|
},
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
(global as any).output_dir =
|
|
79
|
+
(global as any).output_dir = ConfigParser.getInstance().getStatesDir();
|
|
80
80
|
(global as any).codecept_dir = projectRoot;
|
|
81
81
|
|
|
82
82
|
codeceptjs.container.create(codeceptConfig, {});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from './html.ts';
|
|
2
|
+
import { createDebug } from './logger.js';
|
|
3
|
+
import { WebElement } from './web-element.ts';
|
|
4
|
+
|
|
5
|
+
const debugLog = createDebug('explorbot:web-annotate');
|
|
6
|
+
|
|
7
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
8
|
+
|
|
9
|
+
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
10
|
+
|
|
11
|
+
function parseAriaRefs(ariaSnapshot: string): Array<{ role: string; name: string; ref: string }> {
|
|
12
|
+
const entries: Array<{ role: string; name: string; ref: string }> = [];
|
|
13
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
14
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
15
|
+
if (!match) continue;
|
|
16
|
+
if (!ANNOTATABLE_ROLES.has(match[2])) continue;
|
|
17
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
18
|
+
}
|
|
19
|
+
return entries;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
23
|
+
const ariaSnapshot: string = await page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
24
|
+
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
25
|
+
|
|
26
|
+
const byRole = new Map<string, Array<{ name: string; ref: string }>>();
|
|
27
|
+
for (const { role, name, ref } of refEntries) {
|
|
28
|
+
let list = byRole.get(role);
|
|
29
|
+
if (!list) {
|
|
30
|
+
list = [];
|
|
31
|
+
byRole.set(role, list);
|
|
32
|
+
}
|
|
33
|
+
list.push({ name, ref });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const elements: WebElement[] = [];
|
|
37
|
+
for (const [role, entries] of byRole) {
|
|
38
|
+
try {
|
|
39
|
+
const rawList = await page.getByRole(role).evaluateAll(
|
|
40
|
+
(domElements: Element[], [data, extractFnStr, config]: [Array<{ name: string; ref: string }>, string, typeof ELEMENT_EXTRACTION_CONFIG]) => {
|
|
41
|
+
const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
|
|
42
|
+
const results: any[] = [];
|
|
43
|
+
let ariaIdx = 0;
|
|
44
|
+
for (const el of domElements) {
|
|
45
|
+
if (ariaIdx >= data.length) break;
|
|
46
|
+
el.setAttribute(config.attrs.eidx, data[ariaIdx].ref);
|
|
47
|
+
const elData = extract(el, config);
|
|
48
|
+
if (elData) results.push(elData);
|
|
49
|
+
ariaIdx++;
|
|
50
|
+
}
|
|
51
|
+
return results;
|
|
52
|
+
},
|
|
53
|
+
[entries, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]
|
|
54
|
+
);
|
|
55
|
+
for (const raw of rawList) {
|
|
56
|
+
elements.push(WebElement.fromRawData(raw, role));
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
debugLog(`Failed to annotate role=${role}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ariaSnapshot, elements };
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ELEMENT_EXTRACTION_CONFIG } from './html.ts';
|
|
2
|
+
|
|
3
|
+
export async function eidxInContainer(page: any, containerCss: string | null): Promise<string[]> {
|
|
4
|
+
const selector = containerCss ? `${containerCss} [${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]` : `[${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]`;
|
|
5
|
+
const elements = await page.locator(selector).all();
|
|
6
|
+
const result: string[] = [];
|
|
7
|
+
for (const el of elements) {
|
|
8
|
+
const attr = await el.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx).catch(() => null);
|
|
9
|
+
if (attr) result.push(attr);
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function eidxByLocator(page: any, locator: string, container?: string | null): Promise<string | null> {
|
|
15
|
+
const base = container ? page.locator(container) : page;
|
|
16
|
+
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
17
|
+
return el
|
|
18
|
+
.first()
|
|
19
|
+
.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx)
|
|
20
|
+
.catch(() => null);
|
|
21
|
+
}
|
package/src/utils/web-element.ts
CHANGED
|
@@ -47,6 +47,10 @@ export class WebElement {
|
|
|
47
47
|
return this.attrs[EXPLORBOT_ATTRS.eidx] || this.attrs.eidx || null;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
ourAttr(name: keyof typeof EXPLORBOT_ATTRS): string | undefined {
|
|
51
|
+
return this.attrs[EXPLORBOT_ATTRS[name]];
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
get isNavigationLink(): boolean {
|
|
51
55
|
if (this.tag !== 'a') return false;
|
|
52
56
|
const href = this.attrs.href || '';
|
|
@@ -143,6 +147,36 @@ export class WebElement {
|
|
|
143
147
|
return rawList.map((d) => WebElement.fromRawData(d));
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
static async commonAncestor(page: any, eidxList: string[]): Promise<WebElement | null> {
|
|
151
|
+
const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
|
|
152
|
+
if (validEidxList.length < 2) return null;
|
|
153
|
+
|
|
154
|
+
const raw: RawElementData | null = await page.evaluate(
|
|
155
|
+
([list, extractFnStr, config]: [string[], string, ElementExtractionConfig]) => {
|
|
156
|
+
const extract = new Function(`return ${extractFnStr}`)() as (el: Element, cfg: ElementExtractionConfig) => any;
|
|
157
|
+
const els: Element[] = [];
|
|
158
|
+
for (const eidx of list) {
|
|
159
|
+
const el = document.querySelector(`[${config.attrs.eidx}="${eidx}"]`);
|
|
160
|
+
if (el && !els.includes(el)) els.push(el);
|
|
161
|
+
}
|
|
162
|
+
if (els.length < 2) return null;
|
|
163
|
+
|
|
164
|
+
const containsAll = (node: Element) => els.every((el) => node.contains(el));
|
|
165
|
+
let ancestor: Element | null = els[0].parentElement;
|
|
166
|
+
while (ancestor && !containsAll(ancestor)) ancestor = ancestor.parentElement;
|
|
167
|
+
if (!ancestor) return null;
|
|
168
|
+
|
|
169
|
+
const tag = ancestor.tagName.toLowerCase();
|
|
170
|
+
if (tag === 'body' || tag === 'html') return null;
|
|
171
|
+
return extract(ancestor, config);
|
|
172
|
+
},
|
|
173
|
+
[validEidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG] as [string[], string, ElementExtractionConfig]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!raw) return null;
|
|
177
|
+
return WebElement.fromRawData(raw);
|
|
178
|
+
}
|
|
179
|
+
|
|
146
180
|
static async findByXPath(html: string, xpath: string): Promise<{ totalFound: number; elements: WebElement[]; error?: string }> {
|
|
147
181
|
const result = await evaluateXPath(html, xpath);
|
|
148
182
|
if (result.error) return { totalFound: 0, elements: [], error: result.error };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
|
|
3
|
+
import step from 'codeceptjs/steps';
|
|
4
|
+
|
|
5
|
+
const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
|
|
6
|
+
const ALLOWED_COMMAND_HEADS = ['I.', 'page.', 'await page.', 'await I.'];
|
|
7
|
+
const PLAYWRIGHT_COMMAND_HEADS = ['page.', 'await page.'];
|
|
8
|
+
const PLAYWRIGHT_ARG_NAMES = ['page'];
|
|
9
|
+
const CODECEPT_ARG_NAMES = ['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'];
|
|
10
|
+
|
|
11
|
+
export function sanitizeCodeBlock(code: string): string {
|
|
12
|
+
return code
|
|
13
|
+
.split('\n')
|
|
14
|
+
.map((line) => line.trim())
|
|
15
|
+
.filter((line) => ALLOWED_COMMAND_HEADS.some((head) => line.startsWith(head)))
|
|
16
|
+
.join('\n');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasPlaywrightCommands(code: string): boolean {
|
|
20
|
+
return code.split('\n').some((line) => {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
return PLAYWRIGHT_COMMAND_HEADS.some((head) => trimmed.startsWith(head));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function playwrightSandbox(page: any, code: string): Promise<any> {
|
|
27
|
+
const run = createSandbox(PLAYWRIGHT_ARG_NAMES, `return (async () => { ${code} })()`);
|
|
28
|
+
return run(page);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function codeceptJSSandbox(actor: any, codeOrFn: string | ((...args: any[]) => void)): void {
|
|
32
|
+
if (typeof codeOrFn === 'function') {
|
|
33
|
+
codeOrFn(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const run = createSandbox(CODECEPT_ARG_NAMES, codeOrFn);
|
|
37
|
+
run(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createSandbox(argNames: string[], body: string): (...args: any[]) => any {
|
|
41
|
+
const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
|
|
42
|
+
return (...args: any[]) => fn(...args);
|
|
43
|
+
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
2
|
-
const openrouter = createOpenRouter({
|
|
3
|
-
apiKey: process.env.OPENROUTER_API_KEY,
|
|
4
|
-
});
|
|
5
|
-
export default {
|
|
6
|
-
ai: {
|
|
7
|
-
model: openrouter('mistralai/devstral-small:nitro') // openrouter('x-ai/grok-4-fast') // openrouter('moonshotai/kimi-k2.5:nitro'), // openrouter('x-ai/grok-4-fast') // minimax/minimax-m2.5:nitro
|
|
8
|
-
},
|
|
9
|
-
api: {
|
|
10
|
-
baseEndpoint: 'http://localhost:3000/api/v2/amr_testing',
|
|
11
|
-
headers: {
|
|
12
|
-
'Authorization': `Bearer ${process.env.TESTOMATIO_API_KEY}`,
|
|
13
|
-
'Content-Type': 'application/json',
|
|
14
|
-
},
|
|
15
|
-
spec: [
|
|
16
|
-
'http://127.0.0.1:3000/api/v2/openapi'
|
|
17
|
-
],
|
|
18
|
-
// bootstrap: async ({ headers, baseEndpoint }) => {
|
|
19
|
-
// // Run before tests — e.g. obtain auth token
|
|
20
|
-
// // Return headers to merge: { Authorization: 'Bearer ...' }
|
|
21
|
-
// },
|
|
22
|
-
// teardown: async ({ headers, baseEndpoint }) => {
|
|
23
|
-
// // Run after tests — e.g. cleanup test data
|
|
24
|
-
// },
|
|
25
|
-
},
|
|
26
|
-
dirs: {
|
|
27
|
-
output: 'output',
|
|
28
|
-
knowledge: 'knowledge',
|
|
29
|
-
},
|
|
30
|
-
};
|