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.
Files changed (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. 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: string[] = [];
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: string[] = [];
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: string[] = [];
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) + content + result.slice(range.start + range.length);
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
 
@@ -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));
@@ -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()
@@ -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
- };