explorbot 0.1.10 → 0.1.12

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 (90) hide show
  1. package/README.md +37 -1
  2. package/bin/explorbot-cli.ts +27 -18
  3. package/dist/bin/explorbot-cli.js +26 -18
  4. package/dist/package.json +3 -3
  5. package/dist/rules/navigator/output.md +9 -0
  6. package/dist/rules/navigator/verification-actions.md +2 -0
  7. package/dist/src/action-result.js +23 -1
  8. package/dist/src/action.js +51 -42
  9. package/dist/src/ai/bosun.js +11 -1
  10. package/dist/src/ai/conversation.js +39 -0
  11. package/dist/src/ai/historian/codeceptjs.js +109 -0
  12. package/dist/src/ai/historian/experience.js +321 -0
  13. package/dist/src/ai/historian/mixin.js +2 -0
  14. package/dist/src/ai/historian/playwright.js +145 -0
  15. package/dist/src/ai/historian/screencast.js +121 -0
  16. package/dist/src/ai/historian/utils.js +18 -0
  17. package/dist/src/ai/historian.js +21 -405
  18. package/dist/src/ai/navigator.js +82 -29
  19. package/dist/src/ai/pilot.js +232 -13
  20. package/dist/src/ai/planner.js +29 -9
  21. package/dist/src/ai/provider.js +54 -17
  22. package/dist/src/ai/researcher.js +41 -32
  23. package/dist/src/ai/rules.js +26 -14
  24. package/dist/src/ai/tester.js +90 -26
  25. package/dist/src/ai/tools.js +13 -7
  26. package/dist/src/browser-server.js +16 -3
  27. package/dist/src/commands/add-rule-command.js +11 -8
  28. package/dist/src/commands/clean-command.js +2 -1
  29. package/dist/src/commands/explore-command.js +43 -15
  30. package/dist/src/commands/init-command.js +9 -8
  31. package/dist/src/commands/plan-command.js +32 -0
  32. package/dist/src/commands/plan-save-command.js +19 -7
  33. package/dist/src/commands/rerun-command.js +4 -0
  34. package/dist/src/components/App.js +15 -5
  35. package/dist/src/execution-controller.js +13 -2
  36. package/dist/src/experience-tracker.js +20 -64
  37. package/dist/src/explorbot.js +8 -8
  38. package/dist/src/explorer.js +11 -3
  39. package/dist/src/observability.js +50 -99
  40. package/dist/src/playwright-recorder.js +309 -0
  41. package/dist/src/reporter.js +4 -1
  42. package/dist/src/test-plan.js +12 -0
  43. package/dist/src/utils/aria.js +37 -1
  44. package/dist/src/utils/error-page.js +20 -7
  45. package/dist/src/utils/next-steps.js +37 -0
  46. package/dist/src/utils/strings.js +15 -0
  47. package/package.json +3 -3
  48. package/rules/navigator/output.md +9 -0
  49. package/rules/navigator/verification-actions.md +2 -0
  50. package/src/action-result.ts +26 -1
  51. package/src/action.ts +49 -41
  52. package/src/ai/bosun.ts +11 -1
  53. package/src/ai/conversation.ts +37 -0
  54. package/src/ai/historian/codeceptjs.ts +130 -0
  55. package/src/ai/historian/experience.ts +384 -0
  56. package/src/ai/historian/mixin.ts +4 -0
  57. package/src/ai/historian/playwright.ts +169 -0
  58. package/src/ai/historian/screencast.ts +133 -0
  59. package/src/ai/historian/utils.ts +23 -0
  60. package/src/ai/historian.ts +37 -473
  61. package/src/ai/navigator.ts +82 -29
  62. package/src/ai/pilot.ts +237 -14
  63. package/src/ai/planner.ts +29 -9
  64. package/src/ai/provider.ts +51 -17
  65. package/src/ai/researcher.ts +45 -33
  66. package/src/ai/rules.ts +27 -14
  67. package/src/ai/tester.ts +94 -26
  68. package/src/ai/tools.ts +47 -25
  69. package/src/browser-server.ts +17 -3
  70. package/src/commands/add-rule-command.ts +11 -7
  71. package/src/commands/clean-command.ts +2 -1
  72. package/src/commands/explore-command.ts +46 -14
  73. package/src/commands/init-command.ts +9 -8
  74. package/src/commands/plan-command.ts +35 -0
  75. package/src/commands/plan-save-command.ts +18 -7
  76. package/src/commands/rerun-command.ts +5 -0
  77. package/src/components/App.tsx +16 -5
  78. package/src/config.ts +12 -1
  79. package/src/execution-controller.ts +14 -3
  80. package/src/experience-tracker.ts +21 -72
  81. package/src/explorbot.ts +8 -8
  82. package/src/explorer.ts +13 -3
  83. package/src/observability.ts +50 -109
  84. package/src/playwright-recorder.ts +305 -0
  85. package/src/reporter.ts +4 -1
  86. package/src/test-plan.ts +12 -0
  87. package/src/utils/aria.ts +38 -1
  88. package/src/utils/error-page.ts +22 -7
  89. package/src/utils/next-steps.ts +51 -0
  90. package/src/utils/strings.ts +17 -0
package/src/utils/aria.ts CHANGED
@@ -644,9 +644,18 @@ const resolveDisplayName = (node: AriaNode): string | undefined => {
644
644
  return undefined;
645
645
  };
646
646
 
647
+ const SIBLING_COLLAPSE_THRESHOLD = 50;
648
+ const SIBLING_COLLAPSE_KEEP_EACH_SIDE = 5;
649
+
647
650
  const serializeAriaNodes = (nodes: AriaNode[], depth = 0): string => {
648
651
  const lines: string[] = [];
649
- for (const node of nodes) {
652
+ const collapsed = collapseSimilarSiblingRuns(nodes, depth);
653
+ for (const entry of collapsed) {
654
+ if (entry.placeholder) {
655
+ lines.push(entry.placeholder);
656
+ continue;
657
+ }
658
+ const node = entry.node!;
650
659
  const indent = ' '.repeat(depth);
651
660
  let line = `${indent}- ${renderNodeLine(node.role, resolveDisplayName(node), node.attributes, node.value)}`;
652
661
  if (node.children.length > 0) {
@@ -660,6 +669,34 @@ const serializeAriaNodes = (nodes: AriaNode[], depth = 0): string => {
660
669
  return lines.join('\n');
661
670
  };
662
671
 
672
+ type SerializeEntry = { node?: AriaNode; placeholder?: string };
673
+
674
+ const collapseSimilarSiblingRuns = (nodes: AriaNode[], depth: number): SerializeEntry[] => {
675
+ const result: SerializeEntry[] = [];
676
+ let i = 0;
677
+ while (i < nodes.length) {
678
+ const role = nodes[i].role;
679
+ let j = i;
680
+ while (j < nodes.length && nodes[j].role === role) j++;
681
+ const runLength = j - i;
682
+ if (runLength > SIBLING_COLLAPSE_THRESHOLD) {
683
+ for (let k = i; k < i + SIBLING_COLLAPSE_KEEP_EACH_SIDE; k++) {
684
+ result.push({ node: nodes[k] });
685
+ }
686
+ const omitted = runLength - SIBLING_COLLAPSE_KEEP_EACH_SIDE * 2;
687
+ const indent = ' '.repeat(depth);
688
+ result.push({ placeholder: `${indent}- ...${omitted} similar "${role}" items omitted...` });
689
+ for (let k = j - SIBLING_COLLAPSE_KEEP_EACH_SIDE; k < j; k++) {
690
+ result.push({ node: nodes[k] });
691
+ }
692
+ } else {
693
+ for (let k = i; k < j; k++) result.push({ node: nodes[k] });
694
+ }
695
+ i = j;
696
+ }
697
+ return result;
698
+ };
699
+
663
700
  export const compactAriaSnapshot = (snapshot: string | null, keepNamed = false): string => {
664
701
  if (!snapshot) return '';
665
702
  const nodes = parseAriaSnapshot(snapshot, keepNamed);
@@ -4,22 +4,37 @@ import { isBodyEmpty } from './html.js';
4
4
  const HTTP_ERRORS = ['400 Bad Request', '401 Unauthorized', '403 Forbidden', '404 Not Found', '405 Method Not Allowed', '408 Request Timeout', '500 Internal Server Error', '502 Bad Gateway', '503 Service Unavailable', '504 Gateway Timeout'];
5
5
 
6
6
  const SMALL_PAGE_THRESHOLD = 500;
7
+ const LOADING_WORD = /\bloading\b/i;
7
8
 
8
- export function isErrorPage(actionResult: ActionResult): boolean {
9
- const checkFields = [actionResult.title, actionResult.h1, actionResult.h2].filter(Boolean) as string[];
9
+ export type PageCondition = 'ok' | 'loading' | 'error';
10
+
11
+ export function detectPageCondition(actionResult: ActionResult): PageCondition {
12
+ const headingFields = [actionResult.title, actionResult.h1, actionResult.h2].filter(Boolean) as string[];
10
13
 
11
- for (const field of checkFields) {
14
+ for (const field of headingFields) {
12
15
  for (const error of HTTP_ERRORS) {
13
- if (field.toLowerCase().includes(error.toLowerCase())) return true;
16
+ if (field.toLowerCase().includes(error.toLowerCase())) return 'error';
14
17
  }
15
18
  }
16
19
 
17
- if (!actionResult.html || isBodyEmpty(actionResult.html)) return true;
20
+ const aria = actionResult.ariaSnapshot || '';
21
+ if (/\bprogressbar\b/i.test(aria)) return 'loading';
22
+ if (/\[busy\]/.test(aria)) return 'loading';
23
+
24
+ for (const field of headingFields) {
25
+ if (LOADING_WORD.test(field)) return 'loading';
26
+ }
27
+
28
+ if (!actionResult.html || isBodyEmpty(actionResult.html)) return 'loading';
18
29
 
19
30
  const bodyMatch = actionResult.html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
20
- if (bodyMatch && bodyMatch[1].trim().length < SMALL_PAGE_THRESHOLD) return true;
31
+ if (bodyMatch && bodyMatch[1].trim().length < SMALL_PAGE_THRESHOLD) return 'loading';
32
+
33
+ return 'ok';
34
+ }
21
35
 
22
- return false;
36
+ export function isErrorPage(actionResult: ActionResult): boolean {
37
+ return detectPageCondition(actionResult) === 'error';
23
38
  }
24
39
 
25
40
  export class ErrorPageError extends Error {
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import { tag } from './logger.js';
3
+
4
+ export interface NextStepCommand {
5
+ label: string;
6
+ command: string;
7
+ }
8
+
9
+ export interface NextStepSection {
10
+ label: string;
11
+ path?: string;
12
+ commands?: NextStepCommand[];
13
+ }
14
+
15
+ export function relativeToCwd(absPath: string): string {
16
+ const rel = path.relative(process.cwd(), absPath);
17
+ return rel || '.';
18
+ }
19
+
20
+ export function printNextSteps(sections: NextStepSection[]): void {
21
+ if (sections.length === 0) return;
22
+
23
+ const blocks: string[] = [];
24
+ for (const section of sections) {
25
+ const lines: string[] = [];
26
+ const headerPath = section.path ? relativeToCwd(section.path) : '';
27
+ lines.push(headerPath ? `${section.label}: ${headerPath}` : section.label);
28
+
29
+ const commands = section.commands || [];
30
+ if (commands.length > 0) {
31
+ const labeled = commands.filter((c) => c.label);
32
+ const maxLabel = labeled.length > 0 ? Math.max(...labeled.map((c) => c.label.length)) : 0;
33
+ for (const cmd of commands) {
34
+ if (!cmd.label) {
35
+ lines.push(` ${cmd.command}`);
36
+ continue;
37
+ }
38
+ const padded = `${cmd.label}:`.padEnd(maxLabel + 2);
39
+ lines.push(` ${padded} ${cmd.command}`);
40
+ }
41
+ }
42
+ blocks.push(lines.join('\n'));
43
+ }
44
+
45
+ for (let i = 0; i < blocks.length; i++) {
46
+ if (i > 0) tag('info').log('');
47
+ for (const line of blocks[i].split('\n')) {
48
+ tag('info').log(line);
49
+ }
50
+ }
51
+ }
@@ -1,3 +1,5 @@
1
+ import { createHash } from 'node:crypto';
2
+
1
3
  export function truncateJson(input: any): string {
2
4
  if (!input) return '';
3
5
  const str = JSON.stringify(input);
@@ -11,3 +13,18 @@ export function sanitizeFilename(name: string): string {
11
13
  .replace(/^_+|_+$/g, '')
12
14
  .slice(0, 50);
13
15
  }
16
+
17
+ export function safeFilename(name: string, ext = '', maxBytes = 240): string {
18
+ const sanitized = name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
19
+ const extBytes = Buffer.byteLength(ext, 'utf8');
20
+ const budget = maxBytes - extBytes;
21
+ if (Buffer.byteLength(sanitized, 'utf8') <= budget) return sanitized + ext;
22
+
23
+ const hash = createHash('sha1').update(name).digest('hex').slice(0, 8);
24
+ const suffix = `_${hash}`;
25
+ let truncated = sanitized;
26
+ while (Buffer.byteLength(truncated + suffix, 'utf8') > budget && truncated.length > 0) {
27
+ truncated = truncated.slice(0, -1);
28
+ }
29
+ return truncated + suffix + ext;
30
+ }