explorbot 0.1.6 → 0.1.8

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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import chalk from 'chalk';
5
6
  import { Command } from 'commander';
6
7
  import figureSet from 'figures';
@@ -20,7 +21,13 @@ import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
20
21
  const program = new Command();
21
22
  const cli = getCliName();
22
23
 
23
- program.name(cli).description('AI-powered web exploration tool');
24
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
25
+ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
26
+
27
+ program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
28
+ program.hook('preAction', () => {
29
+ console.log(chalk.dim(`${cli} v${pkgVersion}`));
30
+ });
24
31
 
25
32
  interface CLIOptions {
26
33
  verbose?: boolean;
@@ -113,7 +120,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
113
120
  await startTUI(explorBot);
114
121
  });
115
122
 
116
- addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
123
+ addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run').option('--focus <feature>', 'Focus area for exploration')).action(async (explorePath, options) => {
117
124
  try {
118
125
  const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
119
126
  await explorBot.start();
@@ -121,7 +128,9 @@ addCommonOptions(program.command('explore <path>').description('Explore a page a
121
128
  const { ExploreCommand } = await import('../src/commands/explore-command.js');
122
129
  const cmd = new ExploreCommand(explorBot);
123
130
  if (options.maxTests) cmd.maxTests = Number.parseInt(options.maxTests, 10);
124
- await cmd.execute('');
131
+ const execArgs: string[] = [];
132
+ if (options.focus) execArgs.push('--focus', `"${options.focus}"`);
133
+ await cmd.execute(execArgs.join(' '));
125
134
  await explorBot.stop();
126
135
  await showStatsAndExit(0);
127
136
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import chalk from 'chalk';
5
6
  import { Command } from 'commander';
6
7
  import figureSet from 'figures';
@@ -18,7 +19,12 @@ import { jsonToTable } from '../src/utils/markdown-parser.js';
18
19
  import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
19
20
  const program = new Command();
20
21
  const cli = getCliName();
21
- program.name(cli).description('AI-powered web exploration tool');
22
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
23
+ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
24
+ program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
25
+ program.hook('preAction', () => {
26
+ console.log(chalk.dim(`${cli} v${pkgVersion}`));
27
+ });
22
28
  function buildExplorBotOptions(from, options) {
23
29
  return {
24
30
  from,
@@ -86,7 +92,7 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
86
92
  await explorBot.start();
87
93
  await startTUI(explorBot);
88
94
  });
89
- addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run')).action(async (explorePath, options) => {
95
+ addCommonOptions(program.command('explore <path>').description('Explore a page autonomously and run invented scenarios').option('--max-tests <count>', 'Maximum number of tests to run').option('--focus <feature>', 'Focus area for exploration')).action(async (explorePath, options) => {
90
96
  try {
91
97
  const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
92
98
  await explorBot.start();
@@ -95,7 +101,10 @@ addCommonOptions(program.command('explore <path>').description('Explore a page a
95
101
  const cmd = new ExploreCommand(explorBot);
96
102
  if (options.maxTests)
97
103
  cmd.maxTests = Number.parseInt(options.maxTests, 10);
98
- await cmd.execute('');
104
+ const execArgs = [];
105
+ if (options.focus)
106
+ execArgs.push('--focus', `"${options.focus}"`);
107
+ await cmd.execute(execArgs.join(' '));
99
108
  await explorBot.stop();
100
109
  await showStatsAndExit(0);
101
110
  }
@@ -0,0 +1,145 @@
1
+ {
2
+ "name": "explorbot",
3
+ "version": "0.1.8",
4
+ "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
+ "license": "Elastic-2.0",
6
+ "type": "module",
7
+ "main": "dist/src/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "bun": "./src/index.tsx",
11
+ "import": "./dist/src/index.js"
12
+ }
13
+ },
14
+ "bin": {
15
+ "explorbot": "./dist/bin/explorbot-cli.js"
16
+ },
17
+ "files": [
18
+ "dist/",
19
+ "src/**/*.ts",
20
+ "src/**/*.tsx",
21
+ "bin/**/*.ts",
22
+ "boat/api-tester/src/**/*.ts",
23
+ "rules/",
24
+ "assets/sample-files/"
25
+ ],
26
+ "scripts": {
27
+ "build": "bun run src/index.tsx build && bun run build:bin",
28
+ "build:bin": "bun build bin/explorbot-cli.ts --outdir bin --target node --external commander --format esm",
29
+ "build:npm": "bash scripts/build-npm.sh",
30
+ "prepublishOnly": "npm run build:npm",
31
+ "dev": "bun run src/index.tsx",
32
+ "start": "node dist/index.js",
33
+ "init": "bun run src/index.tsx init",
34
+ "test": "codeceptjs run",
35
+ "test:headless": "codeceptjs run --headless",
36
+ "test:ui": "bun test tests/ui",
37
+ "test:unit": "bun test tests/unit",
38
+ "test:node": "node --test tests/node/*.mjs",
39
+ "test:unit:coverage": "bun test tests/unit --coverage && find . -name '.lcov.info*.tmp' -type f -delete 2>/dev/null || true",
40
+ "test:coverage": "bun test tests/unit --coverage --coverage-reporter=text && find . -name '.lcov.info*.tmp' -type f -delete 2>/dev/null || true",
41
+ "test:coverage:html": "echo 'Run: bun test tests/unit --coverage for detailed coverage report'",
42
+ "test:coverage:summary": "bun test tests/unit --coverage --coverage-reporter=text | tail -n 20 && find . -name '.lcov.info*.tmp' -type f -delete 2>/dev/null || true",
43
+ "test:coverage:clean": "find . -name '.lcov.info*.tmp' -type f -delete 2>/dev/null || true",
44
+ "format": "biome format --write .",
45
+ "format:check": "biome format .",
46
+ "lint": "biome lint .",
47
+ "lint:fix": "biome lint --write .",
48
+ "check": "biome check .",
49
+ "check:fix": "biome check --write .",
50
+ "langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts"
51
+ },
52
+ "keywords": [
53
+ "cli",
54
+ "react",
55
+ "ink",
56
+ "codeceptjs",
57
+ "playwright"
58
+ ],
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/testomatio/explorbot"
62
+ },
63
+ "author": "",
64
+ "dependencies": {
65
+ "@ai-sdk/anthropic": "^3.0",
66
+ "@ai-sdk/groq": "^3.0",
67
+ "@ai-sdk/openai": "^3.0",
68
+ "@axe-core/playwright": "^4.11.0",
69
+ "@codeceptjs/reflection": "^0.5.2",
70
+ "@inkjs/ui": "^2.0.0",
71
+ "@langfuse/otel": "^4.5.1",
72
+ "@openrouter/ai-sdk-provider": "^2.3.3",
73
+ "@opentelemetry/api": "^1.9.0",
74
+ "@opentelemetry/auto-instrumentations-node": "^0.67.3",
75
+ "@opentelemetry/instrumentation": "^0.208.0",
76
+ "@opentelemetry/resources": "^2.2.0",
77
+ "@opentelemetry/sdk-node": "^0.208.0",
78
+ "@opentelemetry/sdk-trace-base": "^2.2.0",
79
+ "@opentelemetry/semantic-conventions": "^1.38.0",
80
+ "@scalar/openapi-parser": "^0.25.6",
81
+ "@testomatio/reporter": "^2.7.6",
82
+ "ai": "^6.0.6",
83
+ "axe-core": "^4.11.1",
84
+ "bash-tool": "^1.3.15",
85
+ "cli-highlight": "^2.1.11",
86
+ "codeceptjs": "4.0.0-rc.11",
87
+ "commander": "^14.0.1",
88
+ "debug": "^4.4.3",
89
+ "dedent": "^1.6.0",
90
+ "expect": "^30.3.0",
91
+ "figures": "^6.1.0",
92
+ "gray-matter": "^4.0.3",
93
+ "html-minifier-next": "^2.1.5",
94
+ "ink": "^6.3.1",
95
+ "ink-big-text": "^2.0.0",
96
+ "ink-select-input": "^6.2.0",
97
+ "ink-text-input": "^6.0.0",
98
+ "jsdom": "^28.0.0",
99
+ "jsonpath-plus": "^10.4.0",
100
+ "just-bash": "^2.14.0",
101
+ "langfuse": "^3.5.0",
102
+ "marked": "^16.2.0",
103
+ "marked-terminal": "^7.3.0",
104
+ "micromatch": "^4.0.8",
105
+ "ora-classic": "^5.4.2",
106
+ "parse5": "^8.0.0",
107
+ "playwright": "^1.40.0",
108
+ "react": "^19.1.1",
109
+ "strip-ansi": "^7.1.2",
110
+ "turndown": "^7.2.1",
111
+ "unique-names-generator": "^4.7.1",
112
+ "yargs": "^17.7.2",
113
+ "zod": "^4.1.8"
114
+ },
115
+ "devDependencies": {
116
+ "@biomejs/biome": "^1.5.3",
117
+ "@copilotkit/aimock": "^1.14.0",
118
+ "@testing-library/react": "^16.3.0",
119
+ "@types/debug": "^4.1.12",
120
+ "@types/jsdom": "^27.0.0",
121
+ "@types/micromatch": "^4.0.9",
122
+ "@types/react": "^18.2.0",
123
+ "@types/yargs": "^17.0.24",
124
+ "bunosh": "^0.4.0",
125
+ "ink-testing-library": "^4.0.0",
126
+ "langwatch": "^0.10.0",
127
+ "msw": "^2.11.3",
128
+ "typescript": "^5.0.0",
129
+ "vitest": "^3.2.4"
130
+ },
131
+ "engines": {
132
+ "node": ">=24.0.0"
133
+ },
134
+ "overrides": {
135
+ "has-flag": "4.0.0",
136
+ "supports-color": "7.2.0"
137
+ },
138
+ "resolutions": {
139
+ "marked-terminal": {
140
+ "supports-hyperlinks": "2.3.0"
141
+ },
142
+ "has-flag": "4.0.0",
143
+ "supports-color": "7.2.0"
144
+ }
145
+ }
@@ -131,7 +131,15 @@ export const fileUploadRule = dedent `
131
131
  export const protectionRule = dedent `
132
132
  <important>
133
133
  Do not sign out current user of the application.
134
- Do not change current user account settings
134
+ Do not change current user account settings.
135
+
136
+ Pre-existing data on the page belongs to the application, not the test.
137
+ Items that were not created inside the current test scenario must not be deleted, removed, emptied, reset, archived, or otherwise destroyed.
138
+ If a scenario needs to verify destructive behaviour, the same scenario must first create a disposable target and then destroy that specific target — never operate on data that was already there when the test started.
139
+
140
+ The resource that the current page URL represents is "under test".
141
+ The test must not destroy the resource it is running against — doing so invalidates every subsequent scenario that starts on the same URL.
142
+ Do not propose or perform delete/remove/archive actions on the entity that owns the current URL; propose such actions only on disposable children created within the scenario itself.
135
143
  </important>
136
144
  `;
137
145
  export const focusedElementRule = dedent `
@@ -12,7 +12,7 @@ import { detectFocusArea, extractFocusedElement } from "../utils/aria.js";
12
12
  import { HooksRunner } from "../utils/hooks-runner.js";
13
13
  import { createDebug, tag } from "../utils/logger.js";
14
14
  import { loop } from "../utils/loop.js";
15
- import { actionRule, focusedElementRule, locatorRule, multipleTabsRule, sectionContextRule } from "./rules.js";
15
+ import { actionRule, focusedElementRule, locatorRule, multipleTabsRule, protectionRule, sectionContextRule } from "./rules.js";
16
16
  import { TaskAgent } from "./task-agent.js";
17
17
  import { createCodeceptJSTools, createSpecialContextTools } from "./tools.js";
18
18
  const debugLog = createDebug('explorbot:tester');
@@ -97,6 +97,22 @@ export class Tester extends TaskAgent {
97
97
  this.explorer.getStateManager().clearHistory();
98
98
  this.resetFailureCount();
99
99
  this.pilot?.reset();
100
+ const requestStore = this.explorer.getRequestStore();
101
+ requestStore?.clear();
102
+ const offFailedRequest = requestStore?.onFailedRequest((r) => {
103
+ task.addNote(`Network error: ${r.method} ${r.path} → ${r.status}`, TestResult.FAILED);
104
+ });
105
+ const page = this.explorer.playwrightHelper?.page;
106
+ const onPageError = (err) => {
107
+ task.addNote(`Console error: ${err.message}`, TestResult.FAILED);
108
+ };
109
+ const onConsoleMessage = (msg) => {
110
+ if (msg.type() !== 'error')
111
+ return;
112
+ task.addNote(`Console error: ${msg.text()}`, TestResult.FAILED);
113
+ };
114
+ page?.on('pageerror', onPageError);
115
+ page?.on('console', onConsoleMessage);
100
116
  const initialState = ActionResult.fromState(state);
101
117
  const conversation = this.provider.startConversation(this.getSystemMessage(), 'tester');
102
118
  this.currentConversation = conversation;
@@ -295,6 +311,9 @@ export class Tester extends TaskAgent {
295
311
  }
296
312
  await this.getQuartermaster().analyzeSession(task, initialState, conversation);
297
313
  offStateChange();
314
+ offFailedRequest?.();
315
+ page?.off('pageerror', onPageError);
316
+ page?.off('console', onConsoleMessage);
298
317
  await this.finishTest(task);
299
318
  await this.explorer.stopTest(task, {
300
319
  startUrl: task.startUrl,
@@ -606,6 +625,8 @@ export class Tester extends TaskAgent {
606
625
  When creating or editing items via form() or type() you should include ${task.sessionName} in the value (if it is not restricted by the application logic)
607
626
  Initial page URL: ${actionResult.url}
608
627
 
628
+ ${protectionRule}
629
+
609
630
  ${this.buildDeletionScope(task)}
610
631
 
611
632
  ${this.buildAvailableFiles()}
@@ -394,7 +394,7 @@ export const detectFocusArea = (snapshot) => {
394
394
  if (result)
395
395
  return result;
396
396
  const fallback = findOverlayByCloseButton(nodes);
397
- if (fallback && fallback.name)
397
+ if (fallback?.name)
398
398
  return fallback;
399
399
  return { detected: false, type: null, name: null };
400
400
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
package/src/ai/rules.ts CHANGED
@@ -135,7 +135,15 @@ export const fileUploadRule = dedent`
135
135
  export const protectionRule = dedent`
136
136
  <important>
137
137
  Do not sign out current user of the application.
138
- Do not change current user account settings
138
+ Do not change current user account settings.
139
+
140
+ Pre-existing data on the page belongs to the application, not the test.
141
+ Items that were not created inside the current test scenario must not be deleted, removed, emptied, reset, archived, or otherwise destroyed.
142
+ If a scenario needs to verify destructive behaviour, the same scenario must first create a disposable target and then destroy that specific target — never operate on data that was already there when the test started.
143
+
144
+ The resource that the current page URL represents is "under test".
145
+ The test must not destroy the resource it is running against — doing so invalidates every subsequent scenario that starts on the same URL.
146
+ Do not propose or perform delete/remove/archive actions on the entity that owns the current URL; propose such actions only on disposable children created within the scenario itself.
139
147
  </important>
140
148
  `;
141
149
 
package/src/ai/tester.ts CHANGED
@@ -17,13 +17,13 @@ import { codeToMarkdown } from '../utils/html.ts';
17
17
  import { createDebug, tag } from '../utils/logger.ts';
18
18
  import { loop } from '../utils/loop.ts';
19
19
  import type { Agent } from './agent.ts';
20
+ import type { Captain } from './captain.ts';
20
21
  import type { Conversation } from './conversation.ts';
21
22
  import { Navigator } from './navigator.ts';
22
- import type { Captain } from './captain.ts';
23
23
  import type { Pilot } from './pilot.ts';
24
24
  import { Provider } from './provider.ts';
25
25
  import { Researcher } from './researcher.ts';
26
- import { actionRule, focusedElementRule, locatorRule, multipleTabsRule, sectionContextRule } from './rules.ts';
26
+ import { actionRule, focusedElementRule, locatorRule, multipleTabsRule, protectionRule, sectionContextRule } from './rules.ts';
27
27
  import { TaskAgent } from './task-agent.ts';
28
28
  import { createCodeceptJSTools, createSpecialContextTools } from './tools.ts';
29
29
 
@@ -125,6 +125,23 @@ export class Tester extends TaskAgent implements Agent {
125
125
  this.resetFailureCount();
126
126
  this.pilot?.reset();
127
127
 
128
+ const requestStore = this.explorer.getRequestStore();
129
+ requestStore?.clear();
130
+ const offFailedRequest = requestStore?.onFailedRequest((r) => {
131
+ task.addNote(`Network error: ${r.method} ${r.path} → ${r.status}`, TestResult.FAILED);
132
+ });
133
+
134
+ const page = this.explorer.playwrightHelper?.page;
135
+ const onPageError = (err: Error) => {
136
+ task.addNote(`Console error: ${err.message}`, TestResult.FAILED);
137
+ };
138
+ const onConsoleMessage = (msg: any) => {
139
+ if (msg.type() !== 'error') return;
140
+ task.addNote(`Console error: ${msg.text()}`, TestResult.FAILED);
141
+ };
142
+ page?.on('pageerror', onPageError);
143
+ page?.on('console', onConsoleMessage);
144
+
128
145
  const initialState = ActionResult.fromState(state);
129
146
 
130
147
  const conversation = this.provider.startConversation(this.getSystemMessage(), 'tester');
@@ -355,6 +372,9 @@ export class Tester extends TaskAgent implements Agent {
355
372
  await this.getQuartermaster().analyzeSession(task, initialState, conversation);
356
373
 
357
374
  offStateChange();
375
+ offFailedRequest?.();
376
+ page?.off('pageerror', onPageError);
377
+ page?.off('console', onConsoleMessage);
358
378
  await this.finishTest(task);
359
379
  await this.explorer.stopTest(task, {
360
380
  startUrl: task.startUrl,
@@ -691,6 +711,8 @@ export class Tester extends TaskAgent implements Agent {
691
711
  When creating or editing items via form() or type() you should include ${task.sessionName} in the value (if it is not restricted by the application logic)
692
712
  Initial page URL: ${actionResult.url}
693
713
 
714
+ ${protectionRule}
715
+
694
716
  ${this.buildDeletionScope(task)}
695
717
 
696
718
  ${this.buildAvailableFiles()}
package/src/utils/aria.ts CHANGED
@@ -421,7 +421,7 @@ export const detectFocusArea = (snapshot: string | null): FocusAreaResult => {
421
421
  if (result) return result;
422
422
 
423
423
  const fallback = findOverlayByCloseButton(nodes);
424
- if (fallback && fallback.name) return fallback;
424
+ if (fallback?.name) return fallback;
425
425
 
426
426
  return { detected: false, type: null, name: null };
427
427
  };