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
package/dist/src/utils/html.js
CHANGED
|
@@ -67,8 +67,10 @@ const INTERACTIVE_EVENT_ATTRIBUTES = new Set(['onclick', 'onchange', 'onblur', '
|
|
|
67
67
|
const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', 'u-hidden', 'is-hidden', 'visually-hidden', 'sr-only', 'screen-reader-only', 'visuallyhidden', 'opacity-0']);
|
|
68
68
|
export const EXPLORBOT_ATTRS = {
|
|
69
69
|
area: 'data-explorbot-area',
|
|
70
|
+
coveredBy: 'data-explorbot-covered-by',
|
|
70
71
|
context: 'data-explorbot-context',
|
|
71
72
|
eidx: 'data-explorbot-eidx',
|
|
73
|
+
hit: 'data-explorbot-hit',
|
|
72
74
|
variant: 'data-explorbot-variant',
|
|
73
75
|
};
|
|
74
76
|
export const HTML_SELECTORS = {
|
|
@@ -139,13 +141,41 @@ export const ELEMENT_EXTRACTION_CONFIG = {
|
|
|
139
141
|
semanticContextContainer: HTML_SELECTORS.semanticContextContainer,
|
|
140
142
|
},
|
|
141
143
|
};
|
|
144
|
+
export function captureHtmlForSnapshot() {
|
|
145
|
+
const clone = document.documentElement.cloneNode(true);
|
|
146
|
+
const liveControls = Array.from(document.querySelectorAll('input, textarea, select'));
|
|
147
|
+
const clonedControls = Array.from(clone.querySelectorAll('input, textarea, select'));
|
|
148
|
+
for (let i = 0; i < liveControls.length; i++) {
|
|
149
|
+
const source = liveControls[i];
|
|
150
|
+
const target = clonedControls[i];
|
|
151
|
+
if (!target)
|
|
152
|
+
continue;
|
|
153
|
+
if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {
|
|
154
|
+
target.setAttribute('data-explorbot-value', source.value);
|
|
155
|
+
}
|
|
156
|
+
if (source instanceof HTMLInputElement && (source.type === 'checkbox' || source.type === 'radio')) {
|
|
157
|
+
let checked = 'false';
|
|
158
|
+
if (source.checked)
|
|
159
|
+
checked = 'true';
|
|
160
|
+
target.setAttribute('data-explorbot-checked', checked);
|
|
161
|
+
}
|
|
162
|
+
if (source instanceof HTMLSelectElement) {
|
|
163
|
+
target.setAttribute('data-explorbot-value', Array.from(source.selectedOptions)
|
|
164
|
+
.map((option) => option.value)
|
|
165
|
+
.join(','));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return clone.outerHTML;
|
|
169
|
+
}
|
|
142
170
|
export function extractElementData(el, config) {
|
|
143
171
|
const cfg = config ||
|
|
144
172
|
{
|
|
145
173
|
attrs: {
|
|
146
174
|
area: 'data-explorbot-area',
|
|
175
|
+
coveredBy: 'data-explorbot-covered-by',
|
|
147
176
|
context: 'data-explorbot-context',
|
|
148
177
|
eidx: 'data-explorbot-eidx',
|
|
178
|
+
hit: 'data-explorbot-hit',
|
|
149
179
|
variant: 'data-explorbot-variant',
|
|
150
180
|
},
|
|
151
181
|
codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'],
|
|
@@ -223,6 +253,43 @@ export function extractElementData(el, config) {
|
|
|
223
253
|
tokens.add('navigates');
|
|
224
254
|
return Array.from(tokens).slice(0, 8);
|
|
225
255
|
}
|
|
256
|
+
function describeOverlappingElement(target) {
|
|
257
|
+
const parts = [target.tagName.toLowerCase()];
|
|
258
|
+
const role = target.getAttribute('role');
|
|
259
|
+
const ariaLabel = target.getAttribute('aria-label');
|
|
260
|
+
const id = target.getAttribute('id');
|
|
261
|
+
const className = (target.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 3).join('.');
|
|
262
|
+
if (id)
|
|
263
|
+
parts.push(`#${id.slice(0, 40)}`);
|
|
264
|
+
if (className)
|
|
265
|
+
parts.push(`.${className.slice(0, 80)}`);
|
|
266
|
+
if (role)
|
|
267
|
+
parts.push(`[role="${role.slice(0, 40)}"]`);
|
|
268
|
+
if (ariaLabel)
|
|
269
|
+
parts.push(`[aria-label="${ariaLabel.slice(0, 80)}"]`);
|
|
270
|
+
return parts.join('');
|
|
271
|
+
}
|
|
272
|
+
function inspectHitTarget(target, rect) {
|
|
273
|
+
const viewportPoints = [
|
|
274
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
275
|
+
{ x: rect.left + Math.min(rect.width - 1, Math.max(1, rect.width * 0.25)), y: rect.top + Math.min(rect.height - 1, Math.max(1, rect.height * 0.25)) },
|
|
276
|
+
{ x: rect.left + Math.min(rect.width - 1, Math.max(1, rect.width * 0.75)), y: rect.top + Math.min(rect.height - 1, Math.max(1, rect.height * 0.75)) },
|
|
277
|
+
].filter((point) => point.x >= 0 && point.y >= 0 && point.x <= window.innerWidth && point.y <= window.innerHeight);
|
|
278
|
+
if (viewportPoints.length === 0) {
|
|
279
|
+
return { hit: 'offscreen' };
|
|
280
|
+
}
|
|
281
|
+
for (const point of viewportPoints) {
|
|
282
|
+
const top = document.elementFromPoint(point.x, point.y);
|
|
283
|
+
if (!top)
|
|
284
|
+
continue;
|
|
285
|
+
if (top === target || target.contains(top)) {
|
|
286
|
+
return { hit: 'target' };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const centerPoint = viewportPoints[0];
|
|
290
|
+
const top = document.elementFromPoint(centerPoint.x, centerPoint.y);
|
|
291
|
+
return { hit: 'covered', coveredBy: top ? describeOverlappingElement(top) : undefined };
|
|
292
|
+
}
|
|
226
293
|
function isEmbeddedCodeEditorFrame(target) {
|
|
227
294
|
const src = (target.getAttribute('src') || '').toLowerCase();
|
|
228
295
|
const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', ');
|
|
@@ -329,6 +396,10 @@ export function extractElementData(el, config) {
|
|
|
329
396
|
allAttrs[cfg.attrs.area] = areaHints.join('|');
|
|
330
397
|
allAttrs[cfg.attrs.context] = findContextLabel(el);
|
|
331
398
|
allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|');
|
|
399
|
+
const hitTarget = inspectHitTarget(el, rect);
|
|
400
|
+
allAttrs[cfg.attrs.hit] = hitTarget.hit;
|
|
401
|
+
if (hitTarget.coveredBy)
|
|
402
|
+
allAttrs[cfg.attrs.coveredBy] = hitTarget.coveredBy;
|
|
332
403
|
return {
|
|
333
404
|
tag: el.tagName.toLowerCase(),
|
|
334
405
|
text: normalizeText(el.textContent || '').slice(0, cfg.maxTextLength),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function loadMarkdownFiles(dir: string, options?: {
|
|
2
|
+
recursive?: boolean;
|
|
3
|
+
onError?: (filePath: string, error: unknown) => void;
|
|
4
|
+
}): MarkdownFile[];
|
|
5
|
+
export interface MarkdownFile {
|
|
6
|
+
filePath: string;
|
|
7
|
+
content: string;
|
|
8
|
+
data: Record<string, any>;
|
|
9
|
+
mtime: Date;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
export function loadMarkdownFiles(dir, options = {}) {
|
|
5
|
+
if (!existsSync(dir))
|
|
6
|
+
return [];
|
|
7
|
+
const results = [];
|
|
8
|
+
const files = readdirSync(dir, { recursive: options.recursive === true })
|
|
9
|
+
.filter((file) => typeof file === 'string' && file.endsWith('.md'))
|
|
10
|
+
.map((file) => join(dir, file));
|
|
11
|
+
for (const filePath of files) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = matter(readFileSync(filePath, 'utf8'));
|
|
14
|
+
results.push({ filePath, content: parsed.content, data: parsed.data, mtime: statSync(filePath).mtime });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
options.onError?.(filePath, error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
@@ -30,11 +30,17 @@ export declare class MarkdownQuery {
|
|
|
30
30
|
get(): string;
|
|
31
31
|
toJson(): Record<string, string>[];
|
|
32
32
|
replace(content: string): string;
|
|
33
|
+
replaceEach(replacer: (match: MarkdownQuery, index: number) => string): string;
|
|
33
34
|
count(): number;
|
|
34
35
|
first(): MarkdownQuery;
|
|
35
36
|
last(): MarkdownQuery;
|
|
36
37
|
before(): MarkdownQuery;
|
|
37
38
|
after(): MarkdownQuery;
|
|
38
39
|
each(): MarkdownQuery[];
|
|
40
|
+
meta(): Array<{
|
|
41
|
+
type: string;
|
|
42
|
+
depth: number | null;
|
|
43
|
+
text: string;
|
|
44
|
+
}>;
|
|
39
45
|
}
|
|
40
46
|
export declare function mdq(source: string): MarkdownQuery;
|
|
@@ -341,6 +341,9 @@ export class MarkdownQuery {
|
|
|
341
341
|
return results;
|
|
342
342
|
}
|
|
343
343
|
replace(content) {
|
|
344
|
+
return this.replaceEach(() => content);
|
|
345
|
+
}
|
|
346
|
+
replaceEach(replacer) {
|
|
344
347
|
const sorted = [...this.matches].sort((a, b) => a.start - b.start);
|
|
345
348
|
const kept = [];
|
|
346
349
|
let lastEnd = -1;
|
|
@@ -350,10 +353,11 @@ export class MarkdownQuery {
|
|
|
350
353
|
kept.push(range);
|
|
351
354
|
lastEnd = range.start + range.length;
|
|
352
355
|
}
|
|
356
|
+
const replacements = kept.map((range, index) => replacer(new MarkdownQuery(this.source, [range]), index));
|
|
353
357
|
let result = this.source;
|
|
354
358
|
for (let i = kept.length - 1; i >= 0; i--) {
|
|
355
359
|
const range = kept[i];
|
|
356
|
-
result = result.slice(0, range.start) +
|
|
360
|
+
result = result.slice(0, range.start) + replacements[i] + result.slice(range.start + range.length);
|
|
357
361
|
}
|
|
358
362
|
return result;
|
|
359
363
|
}
|
|
@@ -386,6 +390,15 @@ export class MarkdownQuery {
|
|
|
386
390
|
each() {
|
|
387
391
|
return this.matches.map((m) => new MarkdownQuery(this.source, [m]));
|
|
388
392
|
}
|
|
393
|
+
meta() {
|
|
394
|
+
return this.matches.map((range) => {
|
|
395
|
+
const token = range.token;
|
|
396
|
+
let depth = null;
|
|
397
|
+
if (token.type === 'heading')
|
|
398
|
+
depth = token.depth;
|
|
399
|
+
return { type: token.type, depth, text: getTokenText(range.token) };
|
|
400
|
+
});
|
|
401
|
+
}
|
|
389
402
|
}
|
|
390
403
|
export function mdq(source) {
|
|
391
404
|
return new MarkdownQuery(source);
|
|
@@ -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();
|
|
3
4
|
export function registerSecret(value) {
|
|
4
5
|
if (!value)
|
|
@@ -11,7 +12,7 @@ export function redactSecrets(text) {
|
|
|
11
12
|
if (!text)
|
|
12
13
|
return text;
|
|
13
14
|
let result = text;
|
|
14
|
-
for (const value of secretValues) {
|
|
15
|
+
for (const value of [...secretValues].sort((a, b) => b.length - a.length)) {
|
|
15
16
|
if (!result.includes(value))
|
|
16
17
|
continue;
|
|
17
18
|
result = result.split(value).join('***REDACTED***');
|
|
@@ -21,7 +22,6 @@ export function redactSecrets(text) {
|
|
|
21
22
|
export function clearRegisteredSecrets() {
|
|
22
23
|
secretValues.clear();
|
|
23
24
|
}
|
|
24
|
-
const SECRET_NAME_TOKENS = ['password', 'passwd', 'secret', 'token', 'apikey', 'api_key', 'credential', 'private_key', 'privatekey', 'access_key'];
|
|
25
25
|
export function isSecretName(name) {
|
|
26
26
|
const lower = name.toLowerCase();
|
|
27
27
|
return SECRET_NAME_TOKENS.some((token) => lower.includes(token));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare function truncateJson(input: any): string;
|
|
2
|
+
export declare function slugify(text: string): string;
|
|
3
|
+
export declare function normalizeInlineText(text: string): string;
|
|
2
4
|
export declare function sanitizeFilename(name: string): string;
|
|
3
5
|
export declare function safeFilename(name: string, ext?: string, maxBytes?: number): string;
|
|
@@ -5,6 +5,16 @@ export function truncateJson(input) {
|
|
|
5
5
|
const str = JSON.stringify(input);
|
|
6
6
|
return str.length <= 80 ? str : `${str.slice(0, 77)}...`;
|
|
7
7
|
}
|
|
8
|
+
export function slugify(text) {
|
|
9
|
+
return text
|
|
10
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
11
|
+
.replace(/_+/g, '_')
|
|
12
|
+
.replace(/^_|_$/g, '')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
export function normalizeInlineText(text) {
|
|
16
|
+
return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
17
|
+
}
|
|
8
18
|
export function sanitizeFilename(name) {
|
|
9
19
|
return name
|
|
10
20
|
.toLowerCase()
|
|
@@ -64,7 +64,7 @@ export async function dryRunTestFile(filePath) {
|
|
|
64
64
|
Playwright: { browser: config.playwright.browser, url: config.playwright.url },
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
|
-
global.output_dir =
|
|
67
|
+
global.output_dir = ConfigParser.getInstance().getStatesDir();
|
|
68
68
|
global.codecept_dir = projectRoot;
|
|
69
69
|
codeceptjs.container.create(codeceptConfig, {});
|
|
70
70
|
await codeceptjs.recorder.start();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from "./html.js";
|
|
2
|
+
import { createDebug } from './logger.js';
|
|
3
|
+
import { WebElement } from "./web-element.js";
|
|
4
|
+
const debugLog = createDebug('explorbot:web-annotate');
|
|
5
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
6
|
+
const ANNOTATABLE_ROLES = new Set(['button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio', 'switch', 'combobox', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'treeitem']);
|
|
7
|
+
function parseAriaRefs(ariaSnapshot) {
|
|
8
|
+
const entries = [];
|
|
9
|
+
for (const line of ariaSnapshot.split('\n')) {
|
|
10
|
+
const match = line.match(REF_LINE_PATTERN);
|
|
11
|
+
if (!match)
|
|
12
|
+
continue;
|
|
13
|
+
if (!ANNOTATABLE_ROLES.has(match[2]))
|
|
14
|
+
continue;
|
|
15
|
+
entries.push({ role: match[2], name: match[3] || '', ref: match[4] });
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
export async function annotatePageElements(page) {
|
|
20
|
+
const ariaSnapshot = await page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
21
|
+
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
22
|
+
const byRole = new Map();
|
|
23
|
+
for (const { role, name, ref } of refEntries) {
|
|
24
|
+
let list = byRole.get(role);
|
|
25
|
+
if (!list) {
|
|
26
|
+
list = [];
|
|
27
|
+
byRole.set(role, list);
|
|
28
|
+
}
|
|
29
|
+
list.push({ name, ref });
|
|
30
|
+
}
|
|
31
|
+
const elements = [];
|
|
32
|
+
for (const [role, entries] of byRole) {
|
|
33
|
+
try {
|
|
34
|
+
const rawList = await page.getByRole(role).evaluateAll((domElements, [data, extractFnStr, config]) => {
|
|
35
|
+
const extract = new Function(`return ${extractFnStr}`)();
|
|
36
|
+
const results = [];
|
|
37
|
+
let ariaIdx = 0;
|
|
38
|
+
for (const el of domElements) {
|
|
39
|
+
if (ariaIdx >= data.length)
|
|
40
|
+
break;
|
|
41
|
+
el.setAttribute(config.attrs.eidx, data[ariaIdx].ref);
|
|
42
|
+
const elData = extract(el, config);
|
|
43
|
+
if (elData)
|
|
44
|
+
results.push(elData);
|
|
45
|
+
ariaIdx++;
|
|
46
|
+
}
|
|
47
|
+
return results;
|
|
48
|
+
}, [entries, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
|
|
49
|
+
for (const raw of rawList) {
|
|
50
|
+
elements.push(WebElement.fromRawData(raw, role));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
debugLog(`Failed to annotate role=${role}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { ariaSnapshot, elements };
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ELEMENT_EXTRACTION_CONFIG } from "./html.js";
|
|
2
|
+
export async function eidxInContainer(page, containerCss) {
|
|
3
|
+
const selector = containerCss ? `${containerCss} [${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]` : `[${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]`;
|
|
4
|
+
const elements = await page.locator(selector).all();
|
|
5
|
+
const result = [];
|
|
6
|
+
for (const el of elements) {
|
|
7
|
+
const attr = await el.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx).catch(() => null);
|
|
8
|
+
if (attr)
|
|
9
|
+
result.push(attr);
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export async function eidxByLocator(page, locator, container) {
|
|
14
|
+
const base = container ? page.locator(container) : page;
|
|
15
|
+
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
16
|
+
return el
|
|
17
|
+
.first()
|
|
18
|
+
.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx)
|
|
19
|
+
.catch(() => null);
|
|
20
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RawElementData } from './html.js';
|
|
1
|
+
import { EXPLORBOT_ATTRS, type RawElementData } from './html.js';
|
|
2
2
|
import { type XPathMatch } from './xpath.js';
|
|
3
3
|
export { extractElementData } from './html.js';
|
|
4
4
|
export declare class WebElement {
|
|
@@ -26,6 +26,7 @@ export declare class WebElement {
|
|
|
26
26
|
get keyAttrs(): string;
|
|
27
27
|
get coordinates(): string;
|
|
28
28
|
get eidx(): string | null;
|
|
29
|
+
ourAttr(name: keyof typeof EXPLORBOT_ATTRS): string | undefined;
|
|
29
30
|
get isNavigationLink(): boolean;
|
|
30
31
|
get filteredClasses(): string[];
|
|
31
32
|
get areaHints(): string[];
|
|
@@ -36,6 +37,7 @@ export declare class WebElement {
|
|
|
36
37
|
static fromPlaywrightLocator(locator: any): Promise<WebElement | null>;
|
|
37
38
|
static fromEidx(page: any, eidx: string): Promise<WebElement | null>;
|
|
38
39
|
static fromEidxList(page: any, eidxList: string[]): Promise<WebElement[]>;
|
|
40
|
+
static commonAncestor(page: any, eidxList: string[]): Promise<WebElement | null>;
|
|
39
41
|
static findByXPath(html: string, xpath: string): Promise<{
|
|
40
42
|
totalFound: number;
|
|
41
43
|
elements: WebElement[];
|
|
@@ -39,6 +39,9 @@ export class WebElement {
|
|
|
39
39
|
get eidx() {
|
|
40
40
|
return this.attrs[EXPLORBOT_ATTRS.eidx] || this.attrs.eidx || null;
|
|
41
41
|
}
|
|
42
|
+
ourAttr(name) {
|
|
43
|
+
return this.attrs[EXPLORBOT_ATTRS[name]];
|
|
44
|
+
}
|
|
42
45
|
get isNavigationLink() {
|
|
43
46
|
if (this.tag !== 'a')
|
|
44
47
|
return false;
|
|
@@ -127,6 +130,35 @@ export class WebElement {
|
|
|
127
130
|
}, [validEidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
|
|
128
131
|
return rawList.map((d) => WebElement.fromRawData(d));
|
|
129
132
|
}
|
|
133
|
+
static async commonAncestor(page, eidxList) {
|
|
134
|
+
const validEidxList = eidxList.filter((eidx) => /^e\d+$/i.test(eidx));
|
|
135
|
+
if (validEidxList.length < 2)
|
|
136
|
+
return null;
|
|
137
|
+
const raw = await page.evaluate(([list, extractFnStr, config]) => {
|
|
138
|
+
const extract = new Function(`return ${extractFnStr}`)();
|
|
139
|
+
const els = [];
|
|
140
|
+
for (const eidx of list) {
|
|
141
|
+
const el = document.querySelector(`[${config.attrs.eidx}="${eidx}"]`);
|
|
142
|
+
if (el && !els.includes(el))
|
|
143
|
+
els.push(el);
|
|
144
|
+
}
|
|
145
|
+
if (els.length < 2)
|
|
146
|
+
return null;
|
|
147
|
+
const containsAll = (node) => els.every((el) => node.contains(el));
|
|
148
|
+
let ancestor = els[0].parentElement;
|
|
149
|
+
while (ancestor && !containsAll(ancestor))
|
|
150
|
+
ancestor = ancestor.parentElement;
|
|
151
|
+
if (!ancestor)
|
|
152
|
+
return null;
|
|
153
|
+
const tag = ancestor.tagName.toLowerCase();
|
|
154
|
+
if (tag === 'body' || tag === 'html')
|
|
155
|
+
return null;
|
|
156
|
+
return extract(ancestor, config);
|
|
157
|
+
}, [validEidxList, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]);
|
|
158
|
+
if (!raw)
|
|
159
|
+
return null;
|
|
160
|
+
return WebElement.fromRawData(raw);
|
|
161
|
+
}
|
|
130
162
|
static async findByXPath(html, xpath) {
|
|
131
163
|
const result = await evaluateXPath(html, xpath);
|
|
132
164
|
if (result.error)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function sanitizeCodeBlock(code: string): string;
|
|
2
|
+
export declare function hasPlaywrightCommands(code: string): boolean;
|
|
3
|
+
export declare function playwrightSandbox(page: any, code: string): Promise<any>;
|
|
4
|
+
export declare function codeceptJSSandbox(actor: any, codeOrFn: string | ((...args: any[]) => void)): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
|
|
3
|
+
import step from 'codeceptjs/steps';
|
|
4
|
+
const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
|
|
5
|
+
const ALLOWED_COMMAND_HEADS = ['I.', 'page.', 'await page.', 'await I.'];
|
|
6
|
+
const PLAYWRIGHT_COMMAND_HEADS = ['page.', 'await page.'];
|
|
7
|
+
const PLAYWRIGHT_ARG_NAMES = ['page'];
|
|
8
|
+
const CODECEPT_ARG_NAMES = ['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker'];
|
|
9
|
+
export function sanitizeCodeBlock(code) {
|
|
10
|
+
return code
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((line) => line.trim())
|
|
13
|
+
.filter((line) => ALLOWED_COMMAND_HEADS.some((head) => line.startsWith(head)))
|
|
14
|
+
.join('\n');
|
|
15
|
+
}
|
|
16
|
+
export function hasPlaywrightCommands(code) {
|
|
17
|
+
return code.split('\n').some((line) => {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
return PLAYWRIGHT_COMMAND_HEADS.some((head) => trimmed.startsWith(head));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function playwrightSandbox(page, code) {
|
|
23
|
+
const run = createSandbox(PLAYWRIGHT_ARG_NAMES, `return (async () => { ${code} })()`);
|
|
24
|
+
return run(page);
|
|
25
|
+
}
|
|
26
|
+
export function codeceptJSSandbox(actor, codeOrFn) {
|
|
27
|
+
if (typeof codeOrFn === 'function') {
|
|
28
|
+
codeOrFn(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const run = createSandbox(CODECEPT_ARG_NAMES, codeOrFn);
|
|
32
|
+
run(actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
33
|
+
}
|
|
34
|
+
function createSandbox(argNames, body) {
|
|
35
|
+
const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
|
|
36
|
+
return (...args) => fn(...args);
|
|
37
|
+
}
|
package/models.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openrouter": {
|
|
3
|
+
"model": "openai/gpt-oss-20b:nitro",
|
|
4
|
+
"visionModel": "google/gemma-4-31b-it:nitro",
|
|
5
|
+
"agenticModel": "google/gemma-4-31b-it:nitro"
|
|
6
|
+
},
|
|
7
|
+
"groq": {
|
|
8
|
+
"model": "openai/gpt-oss-20b",
|
|
9
|
+
"visionModel": "qwen/qwen3.6-27b",
|
|
10
|
+
"agenticModel": "qwen/qwen3.6-27b"
|
|
11
|
+
},
|
|
12
|
+
"openai": {
|
|
13
|
+
"model": "gpt-5.4-nano",
|
|
14
|
+
"visionModel": "gpt-5.4-nano",
|
|
15
|
+
"agenticModel": "gpt-5.6-luna"
|
|
16
|
+
},
|
|
17
|
+
"anthropic": {
|
|
18
|
+
"agenticModel": "claude-haiku-4-5-20251001"
|
|
19
|
+
},
|
|
20
|
+
"mistral": {
|
|
21
|
+
"model": "mistral-small-latest",
|
|
22
|
+
"visionModel": "mistral-small-latest",
|
|
23
|
+
"agenticModel": "mistral-large-latest"
|
|
24
|
+
},
|
|
25
|
+
"google": {
|
|
26
|
+
"model": "gemini-3.1-flash-lite",
|
|
27
|
+
"visionModel": "gemini-3.1-flash-lite",
|
|
28
|
+
"agenticModel": "gemini-3.5-flash"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,19 @@
|
|
|
16
16
|
"bin": {
|
|
17
17
|
"explorbot": "./dist/bin/explorbot-cli.js"
|
|
18
18
|
},
|
|
19
|
-
"files": [
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/",
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"src/**/*.tsx",
|
|
23
|
+
"bin/**/*.ts",
|
|
24
|
+
"boat/api-tester/src/**/*.ts",
|
|
25
|
+
"boat/doc-collector/src/**/*.ts",
|
|
26
|
+
"boat/doc-collector/bin/**/*.ts",
|
|
27
|
+
"boat/doc-collector/package.json",
|
|
28
|
+
"rules/",
|
|
29
|
+
"assets/sample-files/",
|
|
30
|
+
"models.json"
|
|
31
|
+
],
|
|
20
32
|
"scripts": {
|
|
21
33
|
"build": "bun run build:bin",
|
|
22
34
|
"build:bin": "bun build bin/explorbot-cli.ts --outdir bin --target node --external commander --format esm",
|
|
@@ -43,7 +55,13 @@
|
|
|
43
55
|
"check:fix": "biome check --write .",
|
|
44
56
|
"langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts"
|
|
45
57
|
},
|
|
46
|
-
"keywords": [
|
|
58
|
+
"keywords": [
|
|
59
|
+
"cli",
|
|
60
|
+
"react",
|
|
61
|
+
"ink",
|
|
62
|
+
"codeceptjs",
|
|
63
|
+
"playwright"
|
|
64
|
+
],
|
|
47
65
|
"repository": {
|
|
48
66
|
"type": "git",
|
|
49
67
|
"url": "https://github.com/testomatio/explorbot"
|
|
@@ -51,7 +69,9 @@
|
|
|
51
69
|
"author": "",
|
|
52
70
|
"dependencies": {
|
|
53
71
|
"@ai-sdk/anthropic": "^4.0",
|
|
72
|
+
"@ai-sdk/google": "^4.0.18",
|
|
54
73
|
"@ai-sdk/groq": "^4.0",
|
|
74
|
+
"@ai-sdk/mistral": "^4.0.13",
|
|
55
75
|
"@ai-sdk/openai": "^4.0",
|
|
56
76
|
"@ai-sdk/otel": "^1.0.2",
|
|
57
77
|
"@axe-core/playwright": "^4.11.0",
|