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
@@ -1,456 +0,0 @@
1
- import { tool } from 'ai';
2
- import dedent from 'dedent';
3
- import { z } from 'zod';
4
- import { ActionResult } from "../action-result.js";
5
- import { setActivity } from "../activity.js";
6
- import { Observability } from "../observability.js";
7
- import { Plan, Task, Test, TestResult } from "../test-plan.js";
8
- import { getCliName } from "../utils/cli-name.js";
9
- import { HooksRunner } from "../utils/hooks-runner.js";
10
- import { createDebug, tag } from "../utils/logger.js";
11
- import { loop, pause } from "../utils/loop.js";
12
- import { printNextSteps } from "../utils/next-steps.js";
13
- import { locatorRule } from "./rules.js";
14
- import { TaskAgent, isInteractive } from "./task-agent.js";
15
- import { createCodeceptJSTools } from "./tools.js";
16
- const debugLog = createDebug('explorbot:bosun');
17
- export class Bosun extends TaskAgent {
18
- ACTION_TOOLS = ['click', 'pressKey', 'form'];
19
- emoji = '⚓';
20
- explorer;
21
- provider;
22
- researcher;
23
- navigator;
24
- hooksRunner;
25
- currentPlan;
26
- currentConversation = null;
27
- allResults = [];
28
- agentTools;
29
- MAX_ITERATIONS = 50;
30
- constructor(explorer, provider, researcher, navigator, agentTools) {
31
- super();
32
- this.explorer = explorer;
33
- this.provider = provider;
34
- this.researcher = researcher;
35
- this.navigator = navigator;
36
- this.hooksRunner = new HooksRunner(explorer, explorer.getConfig());
37
- this.agentTools = agentTools;
38
- }
39
- getNavigator() {
40
- return this.navigator;
41
- }
42
- getExperienceTracker() {
43
- return this.explorer.getStateManager().getExperienceTracker();
44
- }
45
- getKnowledgeTracker() {
46
- return this.explorer.getKnowledgeTracker();
47
- }
48
- getProvider() {
49
- return this.provider;
50
- }
51
- getSystemMessage() {
52
- const currentUrl = this.explorer.getStateManager().getCurrentState()?.url;
53
- const customPrompt = this.provider.getSystemPromptForAgent('bosun', currentUrl);
54
- return dedent `
55
- <role>
56
- You are a senior QA automation engineer focused on learning how to interact with UI components.
57
- Your goal is to systematically discover all possible interactions with each component and document what works.
58
- </role>
59
-
60
- <approach>
61
- 1. Review the UI map to understand all available components
62
- 2. Create a plan listing all components to drill using drill_plan tool
63
- 3. For each component, try appropriate interactions using click, form tools
64
- 4. Use drill_record to document successful interactions
65
- 5. If an interaction fails multiple times, use drill_ask for help (in interactive mode)
66
- 6. Call drill_finish when all components have been tested
67
- </approach>
68
-
69
- <rules>
70
- - Focus on one component at a time
71
- - Try multiple locator strategies if one fails
72
- - Document what each interaction does (opens modal, navigates, etc.)
73
- - Skip decorative or non-interactive elements
74
- - Restore page state after each interaction (press Escape or navigate back)
75
- </rules>
76
-
77
- ${locatorRule}
78
-
79
- ${customPrompt || ''}
80
- `;
81
- }
82
- async drill(opts = {}) {
83
- const { knowledgePath, maxComponents = 20, interactive = isInteractive() } = opts;
84
- const state = this.explorer.getStateManager().getCurrentState();
85
- if (!state)
86
- throw new Error('No page state available');
87
- const sessionName = `bosun_${Date.now().toString(36)}`;
88
- this.allResults = [];
89
- return Observability.run(`bosun: ${state.url}`, { tags: ['bosun'], sessionId: sessionName }, async () => {
90
- tag('info').log(`Bosun starting drill on ${state.url}`);
91
- setActivity(`${this.emoji} Researching page for drilling...`, 'action');
92
- await this.hooksRunner.runBeforeHook('bosun', state.url);
93
- const research = await this.researcher.research(state, { screenshot: true, force: true });
94
- this.currentPlan = new Plan(`Drill: ${state.url}`);
95
- this.currentPlan.url = state.url;
96
- const conversation = this.provider.startConversation(this.getSystemMessage(), 'bosun');
97
- this.currentConversation = conversation;
98
- const initialPrompt = await this.buildInitialPrompt(state, research, maxComponents);
99
- conversation.addUserText(initialPrompt);
100
- const drillTask = new Task(`Drill session: ${state.url}`, state.url);
101
- const codeceptjsTools = createCodeceptJSTools(this.explorer, drillTask);
102
- const drillFlowTools = this.createDrillFlowTools(state, interactive);
103
- const tools = {
104
- ...codeceptjsTools,
105
- ...drillFlowTools,
106
- ...this.agentTools,
107
- };
108
- let drillFinished = false;
109
- await loop(async ({ stop, iteration }) => {
110
- debugLog(`Drill iteration ${iteration}`);
111
- setActivity(`${this.emoji} Drilling components...`, 'action');
112
- const currentState = ActionResult.fromState(this.explorer.getStateManager().getCurrentState());
113
- if (iteration > 1) {
114
- conversation.cleanupTag('page_aria', '...cleaned aria snapshot...', 2);
115
- const contextUpdate = await this.buildContextUpdate(currentState);
116
- conversation.addUserText(contextUpdate);
117
- }
118
- const result = await this.provider.invokeConversation(conversation, tools, {
119
- maxToolRoundtrips: 5,
120
- toolChoice: 'required',
121
- });
122
- if (!result)
123
- throw new Error('Failed to get response from provider');
124
- const toolExecutions = result.toolExecutions || [];
125
- this.trackToolExecutions(toolExecutions);
126
- for (const execution of toolExecutions) {
127
- if (execution.wasSuccessful && this.ACTION_TOOLS.includes(execution.toolName)) {
128
- const componentName = execution.input?.explanation || 'unknown';
129
- this.allResults.push({
130
- component: componentName,
131
- action: execution.toolName,
132
- result: 'success',
133
- description: execution.output?.message || 'Action completed',
134
- code: execution.output?.code,
135
- });
136
- }
137
- }
138
- const finishExecution = toolExecutions.find((e) => e.toolName === 'drill_finish');
139
- if (finishExecution) {
140
- drillFinished = true;
141
- stop();
142
- return;
143
- }
144
- if (iteration >= this.MAX_ITERATIONS) {
145
- tag('warning').log('Max iterations reached');
146
- stop();
147
- }
148
- }, {
149
- maxAttempts: this.MAX_ITERATIONS,
150
- interruptPrompt: 'Drill interrupted. Enter instruction (or "stop" to end):',
151
- observability: {
152
- agent: 'bosun',
153
- sessionId: sessionName,
154
- },
155
- catch: async ({ error, stop }) => {
156
- tag('error').log(`Drill error: ${error}`);
157
- stop();
158
- },
159
- });
160
- await this.saveToExperience(state, this.allResults);
161
- if (knowledgePath) {
162
- await this.saveToKnowledge(knowledgePath, state, this.allResults);
163
- }
164
- await this.hooksRunner.runAfterHook('bosun', state.url);
165
- this.logSummary();
166
- return this.currentPlan;
167
- });
168
- }
169
- async buildInitialPrompt(state, research, maxComponents) {
170
- const actionResult = ActionResult.fromState(state);
171
- const knowledge = this.getKnowledge(actionResult);
172
- const experience = this.getExperience(actionResult);
173
- return dedent `
174
- <task>
175
- Drill all interactive components on this page to learn how to interact with them.
176
- Maximum components to drill: ${maxComponents}
177
- </task>
178
-
179
- <page>
180
- URL: ${state.url}
181
- Title: ${state.title || 'Unknown'}
182
- </page>
183
-
184
- <page_ui_map>
185
- ${research}
186
- </page_ui_map>
187
-
188
- <page_aria>
189
- ${actionResult.getInteractiveARIA()}
190
- </page_aria>
191
-
192
- ${knowledge}
193
- ${experience}
194
-
195
- <instructions>
196
- 1. First, call drill_plan to create a list of components to test
197
- 2. Then systematically test each component using click or form tools
198
- 3. Use drill_record to save observations about what each component does
199
- 4. Press Escape or use drill_restore to reset state between tests
200
- 5. Call drill_finish when all components have been tested
201
- </instructions>
202
- `;
203
- }
204
- async buildContextUpdate(currentState) {
205
- const remainingComponents = this.currentPlan?.tests.filter((t) => !t.hasFinished).length || 0;
206
- return dedent `
207
- <context_update>
208
- Current URL: ${currentState.url}
209
- Components remaining: ${remainingComponents}
210
- Successful interactions so far: ${this.allResults.filter((r) => r.result === 'success').length}
211
- </context_update>
212
-
213
- <page_aria>
214
- ${currentState.getInteractiveARIA()}
215
- </page_aria>
216
-
217
- Continue drilling components. Test each one and record what it does.
218
- `;
219
- }
220
- createDrillFlowTools(originalState, interactive) {
221
- const originalUrl = originalState.url;
222
- return {
223
- drill_plan: tool({
224
- description: 'Create a plan of components to drill. Call this first to identify all testable components from the UI map.',
225
- inputSchema: z.object({
226
- components: z.array(z.object({
227
- name: z.string().describe('Display name of the component'),
228
- role: z.string().describe('ARIA role (button, link, textbox, combobox, etc.)'),
229
- locator: z.string().describe('Best locator for this component'),
230
- section: z.string().optional().describe('Section of the page where component is located'),
231
- })),
232
- }),
233
- execute: async ({ components }) => {
234
- for (const comp of components) {
235
- const task = new Test(`Learn: ${comp.name} (${comp.role})`, 'normal', [`Discover interactions for ${comp.name}`], originalUrl);
236
- task.component = comp;
237
- task.interactions = [];
238
- this.currentPlan.addTest(task);
239
- }
240
- tag('info').log(`Created drill plan with ${components.length} components`);
241
- return {
242
- success: true,
243
- message: `Plan created with ${components.length} components`,
244
- components: components.map((c) => `${c.name} (${c.role})`),
245
- instruction: 'Now test each component using click or form tools. Record observations with drill_record.',
246
- };
247
- },
248
- }),
249
- drill_record: tool({
250
- description: 'Record what a component does after testing it. Call this after each successful interaction.',
251
- inputSchema: z.object({
252
- component: z.string().describe('Component name that was tested'),
253
- action: z.string().describe('Action performed (click, form)'),
254
- result: z.string().describe('What happened (opened modal, navigated to X, showed dropdown, etc.)'),
255
- code: z.string().optional().describe('The CodeceptJS code that worked'),
256
- }),
257
- execute: async ({ component, action, result, code }) => {
258
- const task = this.findComponentTask(component);
259
- if (task) {
260
- task.addNote(`${action}: ${result}`, TestResult.PASSED);
261
- task.finish(TestResult.PASSED);
262
- }
263
- this.allResults.push({
264
- component,
265
- action,
266
- result: 'success',
267
- description: result,
268
- code,
269
- });
270
- tag('success').log(`${component}: ${action} -> ${result}`);
271
- return {
272
- success: true,
273
- recorded: `${component}: ${action} -> ${result}`,
274
- instruction: 'Continue testing other components or call drill_finish when done.',
275
- };
276
- },
277
- }),
278
- drill_restore: tool({
279
- description: 'Restore page state after testing a component. Use when page navigated away or modal opened.',
280
- inputSchema: z.object({
281
- reason: z.string().describe('Why restoration is needed'),
282
- }),
283
- execute: async ({ reason }) => {
284
- const currentState = this.explorer.getStateManager().getCurrentState();
285
- const action = this.explorer.createAction();
286
- if (currentState?.url !== originalUrl) {
287
- await action.execute(`I.amOnPage("${originalUrl}")`);
288
- return { success: true, action: 'navigated back', url: originalUrl };
289
- }
290
- await action.execute('I.pressKey("Escape")');
291
- return { success: true, action: 'pressed Escape' };
292
- },
293
- }),
294
- drill_skip: tool({
295
- description: 'Skip a component that cannot be drilled.',
296
- inputSchema: z.object({
297
- component: z.string().describe('Component to skip'),
298
- reason: z.string().describe('Why this component is being skipped'),
299
- }),
300
- execute: async ({ component, reason }) => {
301
- const task = this.findComponentTask(component);
302
- if (task) {
303
- task.addNote(`Skipped: ${reason}`, TestResult.FAILED);
304
- task.finish(TestResult.FAILED);
305
- }
306
- this.allResults.push({
307
- component,
308
- action: 'skip',
309
- result: 'unknown',
310
- description: reason,
311
- });
312
- tag('warning').log(`Skipped ${component}: ${reason}`);
313
- return { success: true, skipped: component, reason };
314
- },
315
- }),
316
- drill_ask: tool({
317
- description: 'Ask the user for help when stuck on a component. Only available in interactive mode.',
318
- inputSchema: z.object({
319
- component: z.string().describe('Component you need help with'),
320
- question: z.string().describe('What you need help with'),
321
- triedLocators: z.array(z.string()).optional().describe('Locators already tried'),
322
- }),
323
- execute: async ({ component, question, triedLocators }) => {
324
- if (!interactive) {
325
- return { success: false, message: 'Not in interactive mode. Skip this component.' };
326
- }
327
- let prompt = `Help needed for "${component}"\n${question}`;
328
- if (triedLocators?.length) {
329
- prompt += `\n\nAlready tried:\n${triedLocators.map((l) => ` - ${l}`).join('\n')}`;
330
- }
331
- prompt += '\n\nYour CodeceptJS command ("skip" to continue):';
332
- const userInput = await pause(prompt);
333
- if (!userInput || userInput.toLowerCase() === 'skip') {
334
- return { success: false, skipped: true, instruction: 'Use drill_skip to skip this component.' };
335
- }
336
- return {
337
- success: true,
338
- userSuggestion: userInput,
339
- instruction: `Try this command: ${userInput}`,
340
- };
341
- },
342
- }),
343
- drill_finish: tool({
344
- description: 'Finish the drill session. Call when all components have been tested.',
345
- inputSchema: z.object({
346
- summary: z.string().describe('Summary of what was learned during drilling'),
347
- }),
348
- execute: async ({ summary }) => {
349
- for (const test of this.currentPlan.tests) {
350
- if (!test.hasFinished) {
351
- test.addNote('Not tested');
352
- test.finish(TestResult.FAILED);
353
- }
354
- }
355
- tag('info').log(`Drill completed: ${summary}`);
356
- return {
357
- success: true,
358
- totalComponents: this.currentPlan.tests.length,
359
- successfulInteractions: this.allResults.filter((r) => r.result === 'success').length,
360
- summary,
361
- };
362
- },
363
- }),
364
- };
365
- }
366
- findComponentTask(componentName) {
367
- return this.currentPlan?.tests.find((t) => {
368
- const ct = t;
369
- return ct.component?.name === componentName || t.scenario.includes(componentName);
370
- });
371
- }
372
- async saveToExperience(state, results) {
373
- const experienceTracker = this.getExperienceTracker();
374
- const actionResult = ActionResult.fromState(state);
375
- const successfulInteractions = results.filter((r) => r.result === 'success' && r.code);
376
- for (const interaction of successfulInteractions) {
377
- experienceTracker.writeAction(actionResult, {
378
- title: `Drill ${interaction.action}: ${interaction.component}`,
379
- code: interaction.code,
380
- explanation: interaction.description,
381
- });
382
- }
383
- if (successfulInteractions.length > 0) {
384
- tag('success').log(`Saved ${successfulInteractions.length} interactions to experience`);
385
- }
386
- }
387
- async saveToKnowledge(knowledgePath, state, results) {
388
- const knowledgeTracker = this.getKnowledgeTracker();
389
- const successfulInteractions = results.filter((r) => r.result === 'success');
390
- if (successfulInteractions.length === 0) {
391
- tag('warning').log('No successful interactions to save to knowledge');
392
- return;
393
- }
394
- const content = this.generateKnowledgeContent(state, successfulInteractions);
395
- const result = knowledgeTracker.addKnowledge(knowledgePath, content);
396
- const cli = getCliName();
397
- const sections = [
398
- {
399
- label: 'Knowledge',
400
- path: result.filePath,
401
- commands: [{ label: 'View matches', command: `${cli} knows ${knowledgePath}` }],
402
- },
403
- ];
404
- printNextSteps(sections);
405
- }
406
- generateKnowledgeContent(state, interactions) {
407
- const lines = [];
408
- lines.push('# Component Interactions\n');
409
- lines.push(`Learned interactions from drilling ${state.url}\n`);
410
- const groupedByComponent = new Map();
411
- for (const interaction of interactions) {
412
- const existing = groupedByComponent.get(interaction.component) || [];
413
- existing.push(interaction);
414
- groupedByComponent.set(interaction.component, existing);
415
- }
416
- for (const [component, items] of groupedByComponent) {
417
- lines.push(`\n## ${component}\n`);
418
- for (const item of items) {
419
- lines.push(`- **${item.action}**: ${item.description}`);
420
- if (item.code) {
421
- lines.push('```js');
422
- lines.push(item.code);
423
- lines.push('```');
424
- }
425
- }
426
- }
427
- return lines.join('\n');
428
- }
429
- logSummary() {
430
- if (!this.currentPlan)
431
- return;
432
- const total = this.currentPlan.tests.length;
433
- const passed = this.currentPlan.tests.filter((t) => t.isSuccessful).length;
434
- const failed = this.currentPlan.tests.filter((t) => t.hasFailed).length;
435
- const successfulInteractions = this.allResults.filter((r) => r.result === 'success').length;
436
- tag('info').log('\nDrill Summary:');
437
- tag('info').log(` Total components: ${total}`);
438
- tag('success').log(` Successful: ${passed}`);
439
- if (failed > 0) {
440
- tag('warning').log(` Failed: ${failed}`);
441
- }
442
- tag('info').log(` Total interactions learned: ${successfulInteractions}`);
443
- for (const test of this.currentPlan.tests) {
444
- const componentTask = test;
445
- const status = test.isSuccessful ? '✓' : '✗';
446
- const successCount = componentTask.interactions?.filter((i) => i.result === 'success').length || 0;
447
- tag('step').log(` ${status} ${componentTask.component?.name || test.scenario}: ${successCount} interactions`);
448
- }
449
- }
450
- getCurrentPlan() {
451
- return this.currentPlan;
452
- }
453
- getConversation() {
454
- return this.currentConversation;
455
- }
456
- }