explorbot 0.1.32-beta.1 → 0.2.0
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 +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- 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 +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -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 +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- 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.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- 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 +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -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/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/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- 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 +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- 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/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/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class TTLCache<T> {
|
|
2
|
+
private store = new Map<string, CacheEntry<T>>();
|
|
3
|
+
private ttlMs: number;
|
|
4
|
+
|
|
5
|
+
constructor(ttlMs = Number.POSITIVE_INFINITY) {
|
|
6
|
+
this.ttlMs = ttlMs;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get(key: string): T | undefined {
|
|
10
|
+
const entry = this.store.get(key);
|
|
11
|
+
if (!entry) return undefined;
|
|
12
|
+
if (Date.now() - entry.at > this.ttlMs) {
|
|
13
|
+
this.store.delete(key);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
return entry.value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
set(key: string, value: T): void {
|
|
20
|
+
this.store.set(key, { value, at: Date.now() });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async fetch(key: string, compute: () => Promise<T>): Promise<T> {
|
|
24
|
+
const cached = this.get(key);
|
|
25
|
+
if (cached !== undefined) return cached;
|
|
26
|
+
const value = await compute();
|
|
27
|
+
this.set(key, value);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delete(key: string): void {
|
|
32
|
+
this.store.delete(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clear(): void {
|
|
36
|
+
this.store.clear();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type CacheEntry<T> = { value: T; at: number };
|
|
@@ -29,6 +29,15 @@ function indent(text: string): string {
|
|
|
29
29
|
.join('\n');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export function formatHeadings(headings: { h1?: string; h2?: string; h3?: string; h4?: string }): string[] {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
if (headings.h1) lines.push(`H1: ${headings.h1}`);
|
|
35
|
+
if (headings.h2) lines.push(`H2: ${headings.h2}`);
|
|
36
|
+
if (headings.h3) lines.push(`H3: ${headings.h3}`);
|
|
37
|
+
if (headings.h4) lines.push(`H4: ${headings.h4}`);
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export type ContextMode = 'attached' | 'compact' | 'full';
|
|
33
42
|
|
|
34
43
|
export function formatContextSummary(data: ContextData, mode: ContextMode = 'compact'): string {
|
|
@@ -51,11 +60,7 @@ function formatContextAttached(data: ContextData): string {
|
|
|
51
60
|
}
|
|
52
61
|
lines.push('');
|
|
53
62
|
|
|
54
|
-
const headingLines
|
|
55
|
-
if (data.headings.h1) headingLines.push(`H1: ${data.headings.h1}`);
|
|
56
|
-
if (data.headings.h2) headingLines.push(`H2: ${data.headings.h2}`);
|
|
57
|
-
if (data.headings.h3) headingLines.push(`H3: ${data.headings.h3}`);
|
|
58
|
-
if (data.headings.h4) headingLines.push(`H4: ${data.headings.h4}`);
|
|
63
|
+
const headingLines = formatHeadings(data.headings);
|
|
59
64
|
|
|
60
65
|
if (headingLines.length > 0) {
|
|
61
66
|
lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
|
|
@@ -116,11 +121,7 @@ function formatContextCompact(data: ContextData): string {
|
|
|
116
121
|
}
|
|
117
122
|
lines.push('');
|
|
118
123
|
|
|
119
|
-
const headingLines
|
|
120
|
-
if (data.headings.h1) headingLines.push(`H1: ${data.headings.h1}`);
|
|
121
|
-
if (data.headings.h2) headingLines.push(`H2: ${data.headings.h2}`);
|
|
122
|
-
if (data.headings.h3) headingLines.push(`H3: ${data.headings.h3}`);
|
|
123
|
-
if (data.headings.h4) headingLines.push(`H4: ${data.headings.h4}`);
|
|
124
|
+
const headingLines = formatHeadings(data.headings);
|
|
124
125
|
|
|
125
126
|
if (headingLines.length > 0) {
|
|
126
127
|
lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
|
|
@@ -177,11 +178,7 @@ function formatContextFull(data: ContextData): string {
|
|
|
177
178
|
}
|
|
178
179
|
lines.push('');
|
|
179
180
|
|
|
180
|
-
const headingLines
|
|
181
|
-
if (data.headings.h1) headingLines.push(`H1: ${data.headings.h1}`);
|
|
182
|
-
if (data.headings.h2) headingLines.push(`H2: ${data.headings.h2}`);
|
|
183
|
-
if (data.headings.h3) headingLines.push(`H3: ${data.headings.h3}`);
|
|
184
|
-
if (data.headings.h4) headingLines.push(`H4: ${data.headings.h4}`);
|
|
181
|
+
const headingLines = formatHeadings(data.headings);
|
|
185
182
|
|
|
186
183
|
if (headingLines.length > 0) {
|
|
187
184
|
lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
|
package/src/utils/html.ts
CHANGED
|
@@ -85,8 +85,10 @@ const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', '
|
|
|
85
85
|
|
|
86
86
|
export const EXPLORBOT_ATTRS = {
|
|
87
87
|
area: 'data-explorbot-area',
|
|
88
|
+
coveredBy: 'data-explorbot-covered-by',
|
|
88
89
|
context: 'data-explorbot-context',
|
|
89
90
|
eidx: 'data-explorbot-eidx',
|
|
91
|
+
hit: 'data-explorbot-hit',
|
|
90
92
|
variant: 'data-explorbot-variant',
|
|
91
93
|
} as const;
|
|
92
94
|
|
|
@@ -169,14 +171,49 @@ export type ComponentScopeExtractionConfig = {
|
|
|
169
171
|
limits: typeof HTML_EXTRACTION_LIMITS;
|
|
170
172
|
};
|
|
171
173
|
|
|
174
|
+
export function captureHtmlForSnapshot(): string {
|
|
175
|
+
const clone = document.documentElement.cloneNode(true) as HTMLElement;
|
|
176
|
+
const liveControls = Array.from(document.querySelectorAll('input, textarea, select')) as Array<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>;
|
|
177
|
+
const clonedControls = Array.from(clone.querySelectorAll('input, textarea, select')) as HTMLElement[];
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < liveControls.length; i++) {
|
|
180
|
+
const source = liveControls[i];
|
|
181
|
+
const target = clonedControls[i];
|
|
182
|
+
if (!target) continue;
|
|
183
|
+
|
|
184
|
+
if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {
|
|
185
|
+
target.setAttribute('data-explorbot-value', source.value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (source instanceof HTMLInputElement && (source.type === 'checkbox' || source.type === 'radio')) {
|
|
189
|
+
let checked = 'false';
|
|
190
|
+
if (source.checked) checked = 'true';
|
|
191
|
+
target.setAttribute('data-explorbot-checked', checked);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (source instanceof HTMLSelectElement) {
|
|
195
|
+
target.setAttribute(
|
|
196
|
+
'data-explorbot-value',
|
|
197
|
+
Array.from(source.selectedOptions)
|
|
198
|
+
.map((option) => option.value)
|
|
199
|
+
.join(',')
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return clone.outerHTML;
|
|
205
|
+
}
|
|
206
|
+
|
|
172
207
|
export function extractElementData(el: Element, config?: ElementExtractionConfig) {
|
|
173
208
|
const cfg =
|
|
174
209
|
config ||
|
|
175
210
|
({
|
|
176
211
|
attrs: {
|
|
177
212
|
area: 'data-explorbot-area',
|
|
213
|
+
coveredBy: 'data-explorbot-covered-by',
|
|
178
214
|
context: 'data-explorbot-context',
|
|
179
215
|
eidx: 'data-explorbot-eidx',
|
|
216
|
+
hit: 'data-explorbot-hit',
|
|
180
217
|
variant: 'data-explorbot-variant',
|
|
181
218
|
},
|
|
182
219
|
codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'],
|
|
@@ -246,6 +283,45 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig
|
|
|
246
283
|
return Array.from(tokens).slice(0, 8);
|
|
247
284
|
}
|
|
248
285
|
|
|
286
|
+
function describeOverlappingElement(target: Element): string {
|
|
287
|
+
const parts = [target.tagName.toLowerCase()];
|
|
288
|
+
const role = target.getAttribute('role');
|
|
289
|
+
const ariaLabel = target.getAttribute('aria-label');
|
|
290
|
+
const id = target.getAttribute('id');
|
|
291
|
+
const className = (target.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 3).join('.');
|
|
292
|
+
|
|
293
|
+
if (id) parts.push(`#${id.slice(0, 40)}`);
|
|
294
|
+
if (className) parts.push(`.${className.slice(0, 80)}`);
|
|
295
|
+
if (role) parts.push(`[role="${role.slice(0, 40)}"]`);
|
|
296
|
+
if (ariaLabel) parts.push(`[aria-label="${ariaLabel.slice(0, 80)}"]`);
|
|
297
|
+
|
|
298
|
+
return parts.join('');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function inspectHitTarget(target: Element, rect: DOMRect): { hit: string; coveredBy?: string } {
|
|
302
|
+
const viewportPoints = [
|
|
303
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
304
|
+
{ 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)) },
|
|
305
|
+
{ 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)) },
|
|
306
|
+
].filter((point) => point.x >= 0 && point.y >= 0 && point.x <= window.innerWidth && point.y <= window.innerHeight);
|
|
307
|
+
|
|
308
|
+
if (viewportPoints.length === 0) {
|
|
309
|
+
return { hit: 'offscreen' };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const point of viewportPoints) {
|
|
313
|
+
const top = document.elementFromPoint(point.x, point.y);
|
|
314
|
+
if (!top) continue;
|
|
315
|
+
if (top === target || target.contains(top)) {
|
|
316
|
+
return { hit: 'target' };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const centerPoint = viewportPoints[0];
|
|
321
|
+
const top = document.elementFromPoint(centerPoint.x, centerPoint.y);
|
|
322
|
+
return { hit: 'covered', coveredBy: top ? describeOverlappingElement(top) : undefined };
|
|
323
|
+
}
|
|
324
|
+
|
|
249
325
|
function isEmbeddedCodeEditorFrame(target: Element): boolean {
|
|
250
326
|
const src = (target.getAttribute('src') || '').toLowerCase();
|
|
251
327
|
const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', ');
|
|
@@ -354,6 +430,9 @@ export function extractElementData(el: Element, config?: ElementExtractionConfig
|
|
|
354
430
|
allAttrs[cfg.attrs.area] = areaHints.join('|');
|
|
355
431
|
allAttrs[cfg.attrs.context] = findContextLabel(el);
|
|
356
432
|
allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|');
|
|
433
|
+
const hitTarget = inspectHitTarget(el, rect);
|
|
434
|
+
allAttrs[cfg.attrs.hit] = hitTarget.hit;
|
|
435
|
+
if (hitTarget.coveredBy) allAttrs[cfg.attrs.coveredBy] = hitTarget.coveredBy;
|
|
357
436
|
|
|
358
437
|
return {
|
|
359
438
|
tag: el.tagName.toLowerCase(),
|
|
@@ -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/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 || '';
|
|
@@ -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
|
-
};
|