explorbot 0.1.12 → 0.1.15

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 (75) hide show
  1. package/bin/explorbot-cli.ts +21 -21
  2. package/dist/bin/explorbot-cli.js +3 -3
  3. package/dist/package.json +4 -2
  4. package/dist/rules/researcher/container-rules.md +2 -0
  5. package/dist/src/action-result.js +2 -1
  6. package/dist/src/action.js +3 -8
  7. package/dist/src/ai/captain.js +0 -2
  8. package/dist/src/ai/conversation.js +20 -4
  9. package/dist/src/ai/driller.js +1108 -0
  10. package/dist/src/ai/historian/utils.js +8 -1
  11. package/dist/src/ai/pilot.js +214 -267
  12. package/dist/src/ai/provider.js +25 -12
  13. package/dist/src/ai/quartermaster.js +2 -2
  14. package/dist/src/ai/rules.js +5 -5
  15. package/dist/src/ai/session-analyst.js +122 -0
  16. package/dist/src/ai/tester.js +69 -22
  17. package/dist/src/ai/tools.js +19 -4
  18. package/dist/src/commands/base-command.js +6 -6
  19. package/dist/src/commands/drill-command.js +3 -2
  20. package/dist/src/commands/exit-command.js +1 -0
  21. package/dist/src/commands/explore-command.js +9 -2
  22. package/dist/src/components/AddRule.js +1 -1
  23. package/dist/src/components/StatusPane.js +6 -1
  24. package/dist/src/experience-tracker.js +9 -0
  25. package/dist/src/explorbot.js +48 -8
  26. package/dist/src/explorer.js +11 -13
  27. package/dist/src/reporter.js +105 -4
  28. package/dist/src/state-manager.js +4 -3
  29. package/dist/src/stats.js +7 -1
  30. package/dist/src/test-plan.js +47 -3
  31. package/dist/src/utils/aria.js +354 -529
  32. package/dist/src/utils/hooks-runner.js +2 -8
  33. package/dist/src/utils/html.js +371 -0
  34. package/dist/src/utils/unique-names.js +12 -1
  35. package/dist/src/utils/url-matcher.js +6 -1
  36. package/dist/src/utils/web-element.js +27 -24
  37. package/dist/src/utils/xpath.js +1 -1
  38. package/package.json +4 -2
  39. package/rules/researcher/container-rules.md +2 -0
  40. package/src/action-result.ts +2 -1
  41. package/src/action.ts +3 -10
  42. package/src/ai/captain.ts +0 -2
  43. package/src/ai/conversation.ts +21 -4
  44. package/src/ai/driller.ts +1194 -0
  45. package/src/ai/historian/utils.ts +8 -1
  46. package/src/ai/pilot.ts +215 -265
  47. package/src/ai/provider.ts +24 -12
  48. package/src/ai/quartermaster.ts +2 -2
  49. package/src/ai/rules.ts +5 -5
  50. package/src/ai/session-analyst.ts +139 -0
  51. package/src/ai/tester.ts +63 -20
  52. package/src/ai/tools.ts +18 -4
  53. package/src/commands/base-command.ts +6 -6
  54. package/src/commands/drill-command.ts +3 -2
  55. package/src/commands/exit-command.ts +1 -0
  56. package/src/commands/explore-command.ts +10 -2
  57. package/src/components/AddRule.tsx +1 -1
  58. package/src/components/StatusPane.tsx +6 -3
  59. package/src/config.ts +4 -0
  60. package/src/experience-tracker.ts +9 -0
  61. package/src/explorbot.ts +55 -10
  62. package/src/explorer.ts +10 -12
  63. package/src/reporter.ts +108 -4
  64. package/src/state-manager.ts +4 -3
  65. package/src/stats.ts +10 -1
  66. package/src/test-plan.ts +62 -3
  67. package/src/utils/aria.ts +367 -537
  68. package/src/utils/hooks-runner.ts +2 -6
  69. package/src/utils/html.ts +381 -0
  70. package/src/utils/unique-names.ts +13 -0
  71. package/src/utils/url-matcher.ts +5 -1
  72. package/src/utils/web-element.ts +31 -28
  73. package/src/utils/xpath.ts +1 -1
  74. package/dist/src/ai/bosun.js +0 -456
  75. package/src/ai/bosun.ts +0 -571
@@ -586,32 +586,32 @@ addCommonOptions(program.command('research <url>').description('Research a page
586
586
  }
587
587
  );
588
588
 
589
- addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(
590
- async (url, options) => {
591
- try {
592
- const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
593
- await explorBot.start();
589
+ addCommonOptions(
590
+ program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')
591
+ ).action(async (url, options) => {
592
+ try {
593
+ const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
594
+ await explorBot.start();
594
595
 
595
- await explorBot.visit(url);
596
+ await explorBot.visit(url);
596
597
 
597
- const plan = await explorBot.agentBosun().drill({
598
- knowledgePath: options.knowledge,
599
- maxComponents: Number.parseInt(options.max, 10),
600
- interactive: false,
601
- });
598
+ const plan = await explorBot.agentDriller().drill({
599
+ knowledgePath: options.knowledge,
600
+ maxComponents: Number.parseInt(options.maxComponents || '30', 10),
601
+ interactive: false,
602
+ });
602
603
 
603
- console.log(`\nDrill completed: ${plan.tests.length} components`);
604
- console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
605
- console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
604
+ console.log(`\nDrill completed: ${plan.tests.length} components`);
605
+ console.log(`Successful: ${plan.tests.filter((t) => t.isSuccessful).length}`);
606
+ console.log(`Failed: ${plan.tests.filter((t) => t.hasFailed).length}`);
606
607
 
607
- await explorBot.stop();
608
- await showStatsAndExit(0);
609
- } catch (error) {
610
- console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
611
- await showStatsAndExit(1);
612
- }
608
+ await explorBot.stop();
609
+ await showStatsAndExit(0);
610
+ } catch (error) {
611
+ console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
612
+ await showStatsAndExit(1);
613
613
  }
614
- );
614
+ });
615
615
 
616
616
  program
617
617
  .command('context <url>')
@@ -531,14 +531,14 @@ addCommonOptions(program.command('research <url>').description('Research a page
531
531
  await showStatsAndExit(1);
532
532
  }
533
533
  });
534
- addCommonOptions(program.command('drill <url>').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max <count>', 'Maximum number of components to drill', '20')).action(async (url, options) => {
534
+ addCommonOptions(program.command('drill <url>').alias('driller').description('Drill all components on a page to learn interactions').option('--knowledge <path>', 'Save learned interactions to knowledge file at this URL path').option('--max-components <count>', 'Maximum number of components to drill')).action(async (url, options) => {
535
535
  try {
536
536
  const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
537
537
  await explorBot.start();
538
538
  await explorBot.visit(url);
539
- const plan = await explorBot.agentBosun().drill({
539
+ const plan = await explorBot.agentDriller().drill({
540
540
  knowledgePath: options.knowledge,
541
- maxComponents: Number.parseInt(options.max, 10),
541
+ maxComponents: Number.parseInt(options.maxComponents || '30', 10),
542
542
  interactive: false,
543
543
  });
544
544
  console.log(`\nDrill completed: ${plan.tests.length} components`);
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.12",
3
+ "version": "0.1.15",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -67,6 +67,7 @@
67
67
  "@ai-sdk/openai": "^3.0",
68
68
  "@axe-core/playwright": "^4.11.0",
69
69
  "@codeceptjs/reflection": "^0.5.2",
70
+ "@faker-js/faker": "^10.4.0",
70
71
  "@inkjs/ui": "^2.0.0",
71
72
  "@langfuse/otel": "^4.5.1",
72
73
  "@openrouter/ai-sdk-provider": "^2.3.3",
@@ -78,7 +79,7 @@
78
79
  "@opentelemetry/sdk-trace-base": "^2.2.0",
79
80
  "@opentelemetry/semantic-conventions": "^1.38.0",
80
81
  "@scalar/openapi-parser": "^0.25.6",
81
- "@testomatio/reporter": "^2.7.6",
82
+ "@testomatio/reporter": "^2.7.9-beta.3-markdown",
82
83
  "ai": "^6.0.6",
83
84
  "axe-core": "^4.11.1",
84
85
  "bash-tool": "^1.3.15",
@@ -109,6 +110,7 @@
109
110
  "strip-ansi": "^7.1.2",
110
111
  "turndown": "^7.2.1",
111
112
  "unique-names-generator": "^4.7.1",
113
+ "yaml": "^2.8.3",
112
114
  "yargs": "^17.7.2",
113
115
  "zod": "^4.1.8"
114
116
  },
@@ -5,6 +5,8 @@ Container CSS must be a SINGLE semantic selector — one class, one id, or one a
5
5
  - VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
6
6
 
7
7
  The container must uniquely identify a semantic wrapper, not a path through the DOM.
8
+
9
+ Iframes are not sections — list each iframe as a single row of type `iframe` inside the section that contains it. Never use an iframe selector as a section's `> Container:`.
8
10
  </container_rules>
9
11
 
10
12
  <css_selector_rules>
@@ -371,8 +371,9 @@ export class ActionResult {
371
371
  try {
372
372
  const urlObj = new URL(this.url);
373
373
  const path = urlObj.pathname.replace(/\/$/, '') || '/';
374
+ const search = urlObj.search || '';
374
375
  const hash = urlObj.hash || '';
375
- return path + hash;
376
+ return path + search + hash;
376
377
  }
377
378
  catch {
378
379
  // If URL parsing fails, assume it's already a relative URL
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { faker } from '@faker-js/faker';
3
4
  import { context, trace } from '@opentelemetry/api';
4
5
  import { highlight } from 'cli-highlight';
5
6
  import { container, recorder } from 'codeceptjs';
@@ -218,8 +219,8 @@ class Action {
218
219
  await sleep(this.config.action?.delay || 500);
219
220
  }
220
221
  else {
221
- const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', sanitizedCode);
222
- codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
222
+ const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker', sanitizedCode);
223
+ codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
223
224
  await recorder.add(() => sleep(this.config.action?.delay || 500));
224
225
  await recorder.promise();
225
226
  }
@@ -324,12 +325,6 @@ class Action {
324
325
  }
325
326
  catch (error) {
326
327
  this.lastError = error;
327
- if (error && typeof error === 'object') {
328
- const errorObj = error;
329
- if (typeof errorObj.fetchDetails === 'function') {
330
- await errorObj.fetchDetails();
331
- }
332
- }
333
328
  debugLog(`Attempt failed: ${codeBlock}: ${errorToString(error) || this.lastError?.toString()}`);
334
329
  return false;
335
330
  }
@@ -174,8 +174,6 @@ export class Captain extends CaptainBase {
174
174
  ${knowledge}
175
175
 
176
176
  ${experience}
177
-
178
- Use runCommand("/research") if you need deeper page understanding or UI element mapping.
179
177
  `;
180
178
  }
181
179
  planSummary() {
@@ -8,6 +8,7 @@ export class Conversation {
8
8
  messages;
9
9
  model;
10
10
  telemetryFunctionId;
11
+ protectedPrefixCount = 0;
11
12
  autoTrimRules;
12
13
  constructor(messages = [], model, telemetryFunctionId) {
13
14
  this.id = this.generateId();
@@ -16,6 +17,9 @@ export class Conversation {
16
17
  this.telemetryFunctionId = telemetryFunctionId;
17
18
  this.autoTrimRules = new Map();
18
19
  }
20
+ protectPrefix(count) {
21
+ this.protectedPrefixCount = count;
22
+ }
19
23
  addUserText(text) {
20
24
  this.messages.push({
21
25
  role: 'user',
@@ -63,8 +67,10 @@ export class Conversation {
63
67
  const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
64
68
  const regex = new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g');
65
69
  const replacementText = `<${tagName}>${replacement}</${tagName}>`;
70
+ const start = this.protectedPrefixCount;
66
71
  if (keepLast === 0) {
67
- for (const message of this.messages) {
72
+ for (let i = start; i < this.messages.length; i++) {
73
+ const message = this.messages[i];
68
74
  if (typeof message.content === 'string') {
69
75
  message.content = message.content.replace(regex, replacementText);
70
76
  }
@@ -72,7 +78,7 @@ export class Conversation {
72
78
  return;
73
79
  }
74
80
  const allMatches = [];
75
- for (let i = 0; i < this.messages.length; i++) {
81
+ for (let i = start; i < this.messages.length; i++) {
76
82
  const message = this.messages[i];
77
83
  if (typeof message.content === 'string') {
78
84
  const matches = [...message.content.matchAll(new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g'))];
@@ -86,7 +92,7 @@ export class Conversation {
86
92
  const keepCount = Math.min(keepLast, allMatches.length);
87
93
  const keepMatches = allMatches.slice(-keepCount);
88
94
  const keepSet = new Set(keepMatches.map((m) => `${m.messageIndex}:${m.startIndex}`));
89
- for (let i = 0; i < this.messages.length; i++) {
95
+ for (let i = start; i < this.messages.length; i++) {
90
96
  const message = this.messages[i];
91
97
  if (typeof message.content === 'string') {
92
98
  const matches = [...message.content.matchAll(new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g'))];
@@ -109,7 +115,7 @@ export class Conversation {
109
115
  }
110
116
  compactToolResults(keepLastN) {
111
117
  const toolMessageIndexes = [];
112
- for (let i = 0; i < this.messages.length; i++) {
118
+ for (let i = this.protectedPrefixCount; i < this.messages.length; i++) {
113
119
  if (this.messages[i].role === 'tool')
114
120
  toolMessageIndexes.push(i);
115
121
  }
@@ -144,6 +150,16 @@ export class Conversation {
144
150
  }
145
151
  }
146
152
  }
153
+ markLastMessageCacheable() {
154
+ const last = this.messages[this.messages.length - 1];
155
+ if (!last)
156
+ return;
157
+ last.providerOptions = {
158
+ ...last.providerOptions,
159
+ anthropic: { cacheControl: { type: 'ephemeral' } },
160
+ bedrock: { cachePoint: { type: 'default' } },
161
+ };
162
+ }
147
163
  hasTag(tagName, lastN) {
148
164
  const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
149
165
  const regex = new RegExp(`<${escapedTag}>`, 'g');