explorbot 0.2.0 → 0.2.2
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.
- package/README.md +21 -5
- package/bin/explorbot-cli.ts +29 -7
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +8 -2
- package/boat/doc-collector/src/ai/tools.ts +29 -16
- package/boat/doc-collector/src/cli.ts +2 -6
- package/boat/doc-collector/src/config.ts +2 -0
- package/boat/doc-collector/src/docbot.ts +42 -14
- package/boat/doc-collector/src/interaction-screenshots.ts +160 -0
- package/boat/doc-collector/src/screenshots.ts +24 -16
- package/dist/bin/explorbot-cli.js +26 -6
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +7 -2
- package/dist/boat/doc-collector/src/ai/tools.js +21 -15
- package/dist/boat/doc-collector/src/cli.js +2 -5
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +39 -13
- package/dist/boat/doc-collector/src/interaction-screenshots.js +156 -0
- package/dist/boat/doc-collector/src/screenshots.js +25 -16
- package/dist/models.json +30 -0
- package/dist/package.json +8 -2
- package/dist/src/action.d.ts +11 -3
- package/dist/src/action.js +22 -5
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -22
- package/dist/src/ai/captain.js +13 -26
- package/dist/src/ai/driller.d.ts +2 -13
- package/dist/src/ai/driller.js +23 -38
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.d.ts +3 -2
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/navigator.d.ts +6 -2
- package/dist/src/ai/navigator.js +26 -22
- package/dist/src/ai/pilot.d.ts +13 -6
- package/dist/src/ai/pilot.js +49 -44
- package/dist/src/ai/planner.d.ts +3 -2
- package/dist/src/ai/planner.js +19 -17
- package/dist/src/ai/provider.js +0 -1
- package/dist/src/ai/quartermaster.d.ts +3 -2
- package/dist/src/ai/quartermaster.js +4 -4
- package/dist/src/ai/rerunner.d.ts +2 -11
- package/dist/src/ai/rerunner.js +9 -22
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +6 -6
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +2 -2
- package/dist/src/ai/researcher.js +29 -54
- package/dist/src/ai/task-agent.d.ts +18 -5
- package/dist/src/ai/task-agent.js +35 -6
- package/dist/src/ai/tester.d.ts +6 -12
- package/dist/src/ai/tester.js +37 -51
- package/dist/src/ai/tools.d.ts +11 -10
- package/dist/src/ai/tools.js +24 -36
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +7 -5
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/learn-command.js +4 -4
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/App.js +1 -1
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +144 -10
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.d.ts +17 -8
- package/dist/src/explorbot.js +74 -36
- package/dist/src/explorer.d.ts +67 -91
- package/dist/src/explorer.js +327 -567
- package/dist/src/state-manager.d.ts +6 -1
- package/dist/src/state-manager.js +1 -0
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +1 -0
- package/dist/src/utils/web-element.js +29 -0
- package/models.json +30 -0
- package/package.json +8 -2
- package/src/action.ts +25 -5
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -23
- package/src/ai/captain.ts +12 -25
- package/src/ai/driller.ts +50 -67
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +4 -3
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian.ts +3 -2
- package/src/ai/navigator.ts +30 -25
- package/src/ai/pilot.ts +55 -47
- package/src/ai/planner.ts +22 -20
- package/src/ai/provider.ts +0 -1
- package/src/ai/quartermaster.ts +5 -4
- package/src/ai/rerunner.ts +10 -29
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +8 -6
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/sections.ts +4 -2
- package/src/ai/researcher.ts +32 -61
- package/src/ai/task-agent.ts +45 -11
- package/src/ai/tester.ts +40 -57
- package/src/ai/tools.ts +32 -53
- package/src/command-handler.ts +1 -1
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +7 -5
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/learn-command.ts +4 -4
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/App.tsx +1 -1
- package/src/config.ts +173 -11
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +78 -39
- package/src/explorer.ts +388 -626
- package/src/state-manager.ts +7 -1
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- package/src/utils/web-element.ts +30 -0
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npx explorbot start https://your-app.com
|
|
|
14
14
|
|
|
15
15
|
It runs with no babysitting and reports back what it finds. This is vibe-testing.
|
|
16
16
|
|
|
17
|
+
Explorbot works with any AI provider through the [Vercel AI SDK](https://sdk.vercel.ai/providers). See [`models.json`](models.json) for the current recommended provider and model setup, and [Providers](docs/basics/providers.md) for how to configure each one.
|
|
18
|
+
|
|
17
19
|
New here? Read the [Getting Started guide](docs/basics/getting-started.md).
|
|
18
20
|
|
|
19
21
|
## Use Cases
|
|
@@ -92,7 +94,7 @@ Explorbot won't replace your regression tests — it covers what they can't. You
|
|
|
92
94
|
## Requirements
|
|
93
95
|
|
|
94
96
|
- Node.js 24+ or **Bun**
|
|
95
|
-
- An **AI provider key** — OpenRouter recommended; Groq, Cerebras, OpenAI, Anthropic, and others via the [Vercel AI SDK](https://sdk.vercel.ai/providers)
|
|
97
|
+
- An **AI provider key** — OpenRouter recommended; Groq, Cerebras, [OpenAI](docs/basics/providers.md#openai), Anthropic, and others via the [Vercel AI SDK](https://sdk.vercel.ai/providers)
|
|
96
98
|
- A **modern terminal** — iTerm2, WARP, Kitty, Ghostty, or Windows Terminal with WSL
|
|
97
99
|
- A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/basics/prerequisites.md)
|
|
98
100
|
|
|
@@ -125,6 +127,19 @@ Type `/explore`, and Explorbot runs its loop on its own — research, plan, test
|
|
|
125
127
|
|
|
126
128
|
That's the gist. The [**Getting Started guide**](docs/basics/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
|
|
127
129
|
|
|
130
|
+
### Or skip the config file
|
|
131
|
+
|
|
132
|
+
For a CI job, a demo, or a coding agent, pass everything as environment variables. Name a provider and Explorbot picks its recommended models:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
EXPLORBOT_URL=https://app.example.com \
|
|
136
|
+
EXPLORBOT_AI_PROVIDER=openrouter \
|
|
137
|
+
EXPLORBOT_KNOWLEDGE="Log in as admin@example.com / secret123" \
|
|
138
|
+
npx explorbot explore /admin/users --max-tests 3
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Output lands in a temp directory and nothing is written to your project. See [Agentic Usage](docs/workflow/agentic-usage.md).
|
|
142
|
+
|
|
128
143
|
## Teaching Explorbot
|
|
129
144
|
|
|
130
145
|
Explorbot gets better when you tell it about your app:
|
|
@@ -146,12 +161,13 @@ When you're ready to go deeper, the [full documentation](docs/) covers everythin
|
|
|
146
161
|
## FAQ
|
|
147
162
|
|
|
148
163
|
**Can I run it in Cursor or Claude Code?**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
> However, Explorbot can be used as a subagent or terminal command controlled by a coding agent.
|
|
164
|
+
Not as a replacement — Explorbot is a separate application designed for constant testing, while Cursor, Codex, and Claude Code are coding agents. But a coding agent can drive Explorbot as a terminal command or subagent: it writes the test plan, Explorbot executes it against the real app. See [Agentic Usage](docs/workflow/agentic-usage.md).
|
|
152
165
|
|
|
153
166
|
**Can I bring a Cursor or OpenAI subscription?**
|
|
154
|
-
No.
|
|
167
|
+
No. Explorbot needs an API key, not a chat subscription. Use pay-per-token access — Groq, OpenRouter, or OpenAI's own API.
|
|
168
|
+
|
|
169
|
+
**Can I use OpenAI directly?**
|
|
170
|
+
Yes. Add your `OPENAI_API_KEY` and point the models at OpenAI — a nano-class model for `model` and `visionModel`, a stronger one for `agenticModel`. Expect it to run a bit slower than hosted OSS models on Groq or Cerebras. See [Providers](docs/basics/providers.md#openai) for the config.
|
|
155
171
|
|
|
156
172
|
**I want to use Opus!!!**
|
|
157
173
|
Opus is great for coding. Testing needs a simpler model that can safely consume lots of HTML tokens, fast. Save the expensive models for sophisticated decision-making.
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
-
import { ConfigParser } from '../src/config.js';
|
|
12
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
14
14
|
import { Stats } from '../src/stats.js';
|
|
15
15
|
import { Plan } from '../src/test-plan.js';
|
|
@@ -43,8 +43,6 @@ interface CLIOptions {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function buildExplorBotOptions(from: string | undefined, options: CLIOptions): ExplorBotOptions {
|
|
46
|
-
const sessionFile = options.session === true ? path.join(path.resolve(options.path || process.cwd()), 'output', 'session.json') : options.session;
|
|
47
|
-
|
|
48
46
|
return {
|
|
49
47
|
from,
|
|
50
48
|
verbose: options.verbose || options.debug,
|
|
@@ -53,7 +51,7 @@ function buildExplorBotOptions(from: string | undefined, options: CLIOptions): E
|
|
|
53
51
|
show: options.show,
|
|
54
52
|
headless: options.headless,
|
|
55
53
|
incognito: options.incognito,
|
|
56
|
-
session:
|
|
54
|
+
session: options.session,
|
|
57
55
|
} as ExplorBotOptions;
|
|
58
56
|
}
|
|
59
57
|
|
|
@@ -690,7 +688,7 @@ program
|
|
|
690
688
|
path: options.path,
|
|
691
689
|
config: options.config,
|
|
692
690
|
headless: true,
|
|
693
|
-
session: options.session
|
|
691
|
+
session: options.session,
|
|
694
692
|
};
|
|
695
693
|
|
|
696
694
|
const explorBot = new ExplorBot(mainOptions);
|
|
@@ -720,11 +718,11 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
720
718
|
await explorBot.start();
|
|
721
719
|
await explorBot.agentNavigator().visit(url);
|
|
722
720
|
|
|
723
|
-
const action = explorBot.getExplorer().
|
|
721
|
+
const action = explorBot.getExplorer().action();
|
|
724
722
|
await action.execute(command);
|
|
725
723
|
|
|
726
724
|
log('Command executed successfully');
|
|
727
|
-
const state = explorBot.
|
|
725
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
728
726
|
if (state) log(`URL: ${state.url}`);
|
|
729
727
|
|
|
730
728
|
await explorBot.stop();
|
|
@@ -876,4 +874,28 @@ import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
|
876
874
|
program.addCommand(createApiCommands('api'));
|
|
877
875
|
program.addCommand(createDocsCommands('docs'));
|
|
878
876
|
|
|
877
|
+
const envHelp = () => {
|
|
878
|
+
const width = Math.max(...EXPLORBOT_ENV_VARS.map((v) => v.name.length));
|
|
879
|
+
const rows = EXPLORBOT_ENV_VARS.map((v) => ` ${v.name.padEnd(width)} ${v.description}`).join('\n');
|
|
880
|
+
|
|
881
|
+
return `
|
|
882
|
+
Environment variables (config-free one-liner mode):
|
|
883
|
+
Set EXPLORBOT_AI_PROVIDER to run without an explorbot.config.js. A config file always wins.
|
|
884
|
+
|
|
885
|
+
${rows}
|
|
886
|
+
|
|
887
|
+
Providers: ${Object.keys(PROVIDERS).join(', ')}
|
|
888
|
+
Example:
|
|
889
|
+
EXPLORBOT_URL=https://app.example.com EXPLORBOT_AI_PROVIDER=openrouter \\
|
|
890
|
+
${cli} explore /login --max-tests 3
|
|
891
|
+
`;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const addEnvHelp = (cmd: Command) => {
|
|
895
|
+
cmd.addHelpText('after', envHelp);
|
|
896
|
+
for (const sub of cmd.commands) addEnvHelp(sub);
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
addEnvHelp(program);
|
|
900
|
+
|
|
879
901
|
program.parse();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
3
|
import { parseEnv } from 'node:util';
|
|
4
|
-
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS } from '../../../src/config.ts';
|
|
4
|
+
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS, createModel, materializeKnowledge, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
|
|
5
5
|
|
|
6
6
|
export type { AIConfig };
|
|
7
7
|
|
|
@@ -54,8 +54,11 @@ export class ApibotConfigParser {
|
|
|
54
54
|
|
|
55
55
|
const resolvedPath = options?.config || this.findConfigFile();
|
|
56
56
|
if (!resolvedPath) {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
try {
|
|
58
|
+
return await this.loadEnvConfig();
|
|
59
|
+
} finally {
|
|
60
|
+
if (options?.path && originalCwd !== process.cwd()) process.chdir(originalCwd);
|
|
61
|
+
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
try {
|
|
@@ -123,6 +126,45 @@ export class ApibotConfigParser {
|
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
private async loadEnvConfig(): Promise<ApibotConfig> {
|
|
130
|
+
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
131
|
+
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
132
|
+
if (!provider && !modelSpec) {
|
|
133
|
+
throw new Error('No configuration file found. Create apibot.config.js or set EXPLORBOT_URL and EXPLORBOT_AI_PROVIDER environment variables');
|
|
134
|
+
}
|
|
135
|
+
if (modelSpec && !provider && !modelSpec.includes('/')) {
|
|
136
|
+
throw new Error('EXPLORBOT_AI_MODEL needs a provider — set EXPLORBOT_AI_PROVIDER, or write it as "provider/model-id"');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const baseEndpoint = process.env.EXPLORBOT_URL;
|
|
140
|
+
if (!baseEndpoint) {
|
|
141
|
+
throw new Error('No API endpoint to test. Set EXPLORBOT_URL to the API base endpoint');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const outputRoot = resolveOutputRoot();
|
|
145
|
+
materializeKnowledge(outputRoot);
|
|
146
|
+
|
|
147
|
+
const api: ApiConfig = { baseEndpoint };
|
|
148
|
+
if (process.env.EXPLORBOT_API_SPEC) {
|
|
149
|
+
api.spec = [process.env.EXPLORBOT_API_SPEC];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let model: any;
|
|
153
|
+
if (provider && modelSpec) model = await createModel(provider, modelSpec);
|
|
154
|
+
if (provider && !modelSpec) model = await resolveModel(provider, 'model');
|
|
155
|
+
if (!provider) model = await resolveModel(modelSpec!, 'model');
|
|
156
|
+
|
|
157
|
+
this.config = {
|
|
158
|
+
ai: { model },
|
|
159
|
+
api,
|
|
160
|
+
dirs: { output: '.', knowledge: 'knowledge' },
|
|
161
|
+
};
|
|
162
|
+
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
163
|
+
this.validateConfig(this.config);
|
|
164
|
+
|
|
165
|
+
return this.config;
|
|
166
|
+
}
|
|
167
|
+
|
|
126
168
|
private findConfigFile(): string | null {
|
|
127
169
|
const apibotPaths = ['apibot.config.js', 'apibot.config.mjs', 'apibot.config.ts'];
|
|
128
170
|
for (const p of apibotPaths) {
|
|
@@ -2,6 +2,7 @@ import dedent from 'dedent';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
4
4
|
import type Explorer from '../../../../src/explorer.ts';
|
|
5
|
+
import type { StateManager } from '../../../../src/state-manager.ts';
|
|
5
6
|
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
6
7
|
import { tag } from '../../../../src/utils/logger.ts';
|
|
7
8
|
import type { DocbotConfig } from '../config.ts';
|
|
@@ -11,11 +12,13 @@ class Documentarian {
|
|
|
11
12
|
private provider: AIProvider;
|
|
12
13
|
private config: DocbotConfig;
|
|
13
14
|
private explorer?: Explorer;
|
|
15
|
+
private stateManager?: StateManager;
|
|
14
16
|
|
|
15
|
-
constructor(provider: AIProvider, config: DocbotConfig = {}, explorer?: Explorer) {
|
|
17
|
+
constructor(provider: AIProvider, config: DocbotConfig = {}, explorer?: Explorer, stateManager?: StateManager) {
|
|
16
18
|
this.provider = provider;
|
|
17
19
|
this.config = config;
|
|
18
20
|
this.explorer = explorer;
|
|
21
|
+
this.stateManager = stateManager;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
async document(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
|
|
@@ -46,7 +49,7 @@ class Documentarian {
|
|
|
46
49
|
try {
|
|
47
50
|
tag('info').log('Starting interactive exploration...');
|
|
48
51
|
|
|
49
|
-
const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config, captureState);
|
|
52
|
+
const deterministicInteractions = await collectDocInteractions(this.explorer!, this.stateManager!, state, research, this.config, captureState);
|
|
50
53
|
meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
|
|
51
54
|
if (meaningfulInteractions.length > 0) {
|
|
52
55
|
tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
|
|
@@ -91,6 +94,9 @@ class Documentarian {
|
|
|
91
94
|
if ((interaction.changes?.newElements || 0) > 0) {
|
|
92
95
|
return true;
|
|
93
96
|
}
|
|
97
|
+
if ((interaction.changes?.removedElements || 0) > 0) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
94
100
|
return (interaction.discoveredUrls || []).length > 0;
|
|
95
101
|
});
|
|
96
102
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ResearchElement, parseResearchSections } from '../../../../src/ai/researcher/parser.ts';
|
|
2
2
|
import type Explorer from '../../../../src/explorer.ts';
|
|
3
|
-
import type { WebPageState } from '../../../../src/state-manager.ts';
|
|
3
|
+
import type { StateManager, WebPageState } from '../../../../src/state-manager.ts';
|
|
4
4
|
import { detectFocusArea } from '../../../../src/utils/aria.ts';
|
|
5
5
|
import type { DocbotConfig } from '../config.ts';
|
|
6
6
|
|
|
@@ -49,7 +49,10 @@ export interface InteractionScreenshot {
|
|
|
49
49
|
relativePath: string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export
|
|
52
|
+
export interface CaptureInteractionState {
|
|
53
|
+
before(): Promise<Buffer | null>;
|
|
54
|
+
after(beforeScreenshot: Buffer | null, state: WebPageState, transition: DocStateTransition): Promise<InteractionScreenshot | null>;
|
|
55
|
+
}
|
|
53
56
|
|
|
54
57
|
const DEFAULT_MAX_PRIMARY_CANDIDATES = 3;
|
|
55
58
|
const DEFAULT_MAX_INTERACTIONS = 5;
|
|
@@ -58,14 +61,14 @@ const DEFAULT_WAIT_MS = 700;
|
|
|
58
61
|
const TAB_WAIT_MS = 500;
|
|
59
62
|
const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
|
|
60
63
|
|
|
61
|
-
export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
64
|
+
export async function collectDocInteractions(explorer: Explorer, stateManager: StateManager, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
62
65
|
const sections = parseResearchSections(research);
|
|
63
66
|
const transitions: DocStateTransition[] = [];
|
|
64
67
|
const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
|
|
65
68
|
const tabGroup = findTabGroup(sections);
|
|
66
69
|
|
|
67
70
|
if (tabGroup) {
|
|
68
|
-
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions, captureState)));
|
|
71
|
+
transitions.push(...(await exploreTabGroup(explorer, stateManager, tabGroup, state.url, maxInteractions, captureState)));
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
for (const candidate of findActionCandidates(sections, config)) {
|
|
@@ -73,7 +76,7 @@ export async function collectDocInteractions(explorer: Explorer, state: WebPageS
|
|
|
73
76
|
break;
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
79
|
+
const transition = await executeInteraction(explorer, stateManager, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
77
80
|
if (!transition) {
|
|
78
81
|
continue;
|
|
79
82
|
}
|
|
@@ -92,7 +95,7 @@ export function pickDocActionCandidates(research: string, config: DocbotConfig =
|
|
|
92
95
|
}));
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
98
|
+
async function exploreTabGroup(explorer: Explorer, stateManager: StateManager, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
96
99
|
const transitions: DocStateTransition[] = [];
|
|
97
100
|
|
|
98
101
|
for (const element of tabGroup.elements) {
|
|
@@ -102,6 +105,7 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
102
105
|
|
|
103
106
|
const transition = await executeInteraction(
|
|
104
107
|
explorer,
|
|
108
|
+
stateManager,
|
|
105
109
|
{
|
|
106
110
|
element,
|
|
107
111
|
container: tabGroup.container,
|
|
@@ -123,12 +127,14 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
123
127
|
return transitions;
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
|
|
127
|
-
const beforeState =
|
|
130
|
+
async function executeInteraction(explorer: Explorer, stateManager: StateManager, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
|
|
131
|
+
const beforeState = stateManager.getCurrentState();
|
|
128
132
|
if (!beforeState) {
|
|
129
133
|
return null;
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
const beforeScreenshot = await captureState?.before();
|
|
137
|
+
|
|
132
138
|
const executed = await attemptInteraction(explorer, candidate);
|
|
133
139
|
if (!executed) {
|
|
134
140
|
return null;
|
|
@@ -136,7 +142,7 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
136
142
|
|
|
137
143
|
await wait(waitMs);
|
|
138
144
|
|
|
139
|
-
const afterState =
|
|
145
|
+
const afterState = stateManager.getCurrentState();
|
|
140
146
|
if (!afterState) {
|
|
141
147
|
return null;
|
|
142
148
|
}
|
|
@@ -150,13 +156,13 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
150
156
|
});
|
|
151
157
|
|
|
152
158
|
if (captureState && isMeaningfulStateTransition(transition)) {
|
|
153
|
-
const screenshot = await captureState(afterState, transition);
|
|
159
|
+
const screenshot = await captureState.after(beforeScreenshot ?? null, afterState, transition);
|
|
154
160
|
if (screenshot) {
|
|
155
161
|
transition.screenshot = screenshot;
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
159
|
-
if (urlChanged || ariaChanges.newCount > 0) {
|
|
165
|
+
if (urlChanged || ariaChanges.newCount > 0 || ariaChanges.removedCount > 0) {
|
|
160
166
|
await restoreInteractionState(explorer, restoreUrl);
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -164,7 +170,7 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
async function attemptInteraction(explorer: Explorer, candidate: InteractionCandidate): Promise<boolean> {
|
|
167
|
-
const action = explorer.
|
|
173
|
+
const action = explorer.action();
|
|
168
174
|
|
|
169
175
|
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
170
176
|
const success = await action.attempt(command, buildPurpose(candidate));
|
|
@@ -178,7 +184,7 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
178
184
|
|
|
179
185
|
async function restoreInteractionState(explorer: Explorer, restoreUrl: string, primaryCommand?: string | null): Promise<void> {
|
|
180
186
|
if (primaryCommand) {
|
|
181
|
-
const action = explorer.
|
|
187
|
+
const action = explorer.action();
|
|
182
188
|
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
|
|
183
189
|
if (restored) {
|
|
184
190
|
await wait(TAB_WAIT_MS);
|
|
@@ -186,16 +192,23 @@ async function restoreInteractionState(explorer: Explorer, restoreUrl: string, p
|
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
const action = explorer.
|
|
195
|
+
const action = explorer.action();
|
|
190
196
|
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
|
|
191
197
|
}
|
|
192
198
|
|
|
193
199
|
function buildTransition(candidate: InteractionCandidate, beforeState: WebPageState, afterState: WebPageState, changes: InteractionChanges): DocStateTransition {
|
|
200
|
+
const existingUrls = new Set(collectLinks(beforeState).map((link) => link.url));
|
|
194
201
|
const transition: DocStateTransition = {
|
|
195
202
|
action: describeAction(candidate),
|
|
196
203
|
before: summarizeInteractiveState(beforeState),
|
|
197
204
|
after: summarizeInteractiveState(afterState),
|
|
198
|
-
discoveredUrls:
|
|
205
|
+
discoveredUrls: [
|
|
206
|
+
...new Set(
|
|
207
|
+
collectLinks(afterState)
|
|
208
|
+
.map((link) => link.url)
|
|
209
|
+
.filter((url) => !existingUrls.has(url))
|
|
210
|
+
),
|
|
211
|
+
],
|
|
199
212
|
newCapabilities: collectDiscoveryNotes(afterState, changes),
|
|
200
213
|
element: buildInteractionElement(candidate),
|
|
201
214
|
changes,
|
|
@@ -238,7 +251,7 @@ function isMeaningfulStateTransition(transition: DocStateTransition): boolean {
|
|
|
238
251
|
if (transition.targetUrl || transition.changes?.urlChanged) {
|
|
239
252
|
return true;
|
|
240
253
|
}
|
|
241
|
-
return (transition.changes?.newElements || 0) > 0;
|
|
254
|
+
return (transition.changes?.newElements || 0) > 0 || (transition.changes?.removedElements || 0) > 0;
|
|
242
255
|
}
|
|
243
256
|
|
|
244
257
|
function buildInteractionElement(candidate: InteractionCandidate): InteractionElement {
|
|
@@ -5,11 +5,6 @@ import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
|
|
|
5
5
|
import { DocBot, type DocbotOptions } from './docbot.ts';
|
|
6
6
|
|
|
7
7
|
function buildOptions(options: any): DocbotOptions {
|
|
8
|
-
let session = options.session;
|
|
9
|
-
if (options.session === true) {
|
|
10
|
-
session = 'output/session.json';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
return {
|
|
14
9
|
verbose: options.verbose || options.debug,
|
|
15
10
|
config: options.config,
|
|
@@ -17,7 +12,7 @@ function buildOptions(options: any): DocbotOptions {
|
|
|
17
12
|
show: options.show,
|
|
18
13
|
headless: options.headless,
|
|
19
14
|
incognito: options.incognito,
|
|
20
|
-
session,
|
|
15
|
+
session: options.session,
|
|
21
16
|
docsConfig: options.docsConfig,
|
|
22
17
|
};
|
|
23
18
|
}
|
|
@@ -96,6 +91,7 @@ export function createDocsCommands(name = 'docs'): Command {
|
|
|
96
91
|
output: 'docs',
|
|
97
92
|
screenshot: true,
|
|
98
93
|
interactive: false,
|
|
94
|
+
ignoreErrors: true,
|
|
99
95
|
collapseDynamicPages: true,
|
|
100
96
|
scope: 'site',
|
|
101
97
|
includePaths: [],
|
|
@@ -115,6 +115,7 @@ class DocbotConfigParser {
|
|
|
115
115
|
output: 'docs',
|
|
116
116
|
screenshot: true,
|
|
117
117
|
interactive: false,
|
|
118
|
+
ignoreErrors: true,
|
|
118
119
|
collapseDynamicPages: true,
|
|
119
120
|
scope: 'site',
|
|
120
121
|
includePaths: [],
|
|
@@ -151,6 +152,7 @@ interface DocbotConfig {
|
|
|
151
152
|
maxPages?: number;
|
|
152
153
|
output?: string;
|
|
153
154
|
screenshot?: boolean;
|
|
155
|
+
ignoreErrors?: boolean | string[];
|
|
154
156
|
prompt?: string;
|
|
155
157
|
collapseDynamicPages?: boolean;
|
|
156
158
|
scope?: 'site' | 'section' | 'subtree';
|
|
@@ -6,12 +6,13 @@ import { normalizeUrl } from '../../../src/state-manager.ts';
|
|
|
6
6
|
import { tag } from '../../../src/utils/logger.ts';
|
|
7
7
|
import { sanitizeFilename } from '../../../src/utils/strings.ts';
|
|
8
8
|
import { Documentarian, type PageDocumentation } from './ai/documentarian.ts';
|
|
9
|
+
import type { DocStateTransition } from './ai/tools.ts';
|
|
9
10
|
import { type DocbotConfig, DocbotConfigParser } from './config.ts';
|
|
10
11
|
import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
|
|
11
|
-
import { renderMermaidBody } from './state-diagram.ts';
|
|
12
12
|
import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
|
|
13
13
|
import { extractResearchNavigationTargets } from './research-navigation.ts';
|
|
14
|
-
import { type DocumentationScreenshot, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
14
|
+
import { type DocumentationScreenshot, captureBeforeInteraction, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
|
|
15
|
+
import { renderMermaidBody } from './state-diagram.ts';
|
|
15
16
|
|
|
16
17
|
class DocBot {
|
|
17
18
|
private explorBot: ExplorBot;
|
|
@@ -43,7 +44,7 @@ class DocBot {
|
|
|
43
44
|
config: this.options.docsConfig,
|
|
44
45
|
path: this.options.path,
|
|
45
46
|
});
|
|
46
|
-
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer());
|
|
47
|
+
this.documentarian = new Documentarian(this.explorBot.getProvider(), this.config, this.explorBot.getExplorer(), this.explorBot.stateManager());
|
|
47
48
|
this.ensureDirectory(this.configParser.getOutputDir());
|
|
48
49
|
this.ensureDirectory(this.getPagesDir());
|
|
49
50
|
}
|
|
@@ -76,7 +77,7 @@ class DocBot {
|
|
|
76
77
|
continue;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
const stateManager = this.explorBot.
|
|
80
|
+
const stateManager = this.explorBot.stateManager();
|
|
80
81
|
if (stateManager.hasVisitedState(target)) {
|
|
81
82
|
continue;
|
|
82
83
|
}
|
|
@@ -113,16 +114,18 @@ class DocBot {
|
|
|
113
114
|
force: true,
|
|
114
115
|
});
|
|
115
116
|
const pagePath = this.getPageFilePath(state.url);
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
const captureState = this.shouldUseScreenshots()
|
|
118
|
+
? {
|
|
119
|
+
before: () => captureBeforeInteraction(this.explorBot.getExplorer()),
|
|
120
|
+
after: (beforeScreenshot: Buffer | null, interactionState: WebPageState, transition: DocStateTransition) =>
|
|
121
|
+
captureInteractionScreenshot(this.explorBot.getExplorer(), beforeScreenshot, interactionState, transition, {
|
|
122
|
+
pageFilePath: pagePath,
|
|
123
|
+
screenshotsDir: this.getScreenshotsDir(),
|
|
124
|
+
config: this.config,
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
: undefined;
|
|
128
|
+
const documentation = await this.documentarian.document(state, research, captureState);
|
|
126
129
|
const lowSignalReason = this.getLowSignalReason(documentation, research);
|
|
127
130
|
if (lowSignalReason) {
|
|
128
131
|
skipped.push({
|
|
@@ -163,6 +166,9 @@ class DocBot {
|
|
|
163
166
|
}
|
|
164
167
|
} catch (error) {
|
|
165
168
|
const reason = error instanceof Error ? error.message : String(error);
|
|
169
|
+
if (!this.shouldIgnoreError(error)) {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
166
172
|
tag('warning').log(`Skipping ${target}: ${reason}`);
|
|
167
173
|
skipped.push({
|
|
168
174
|
url: target,
|
|
@@ -410,6 +416,28 @@ class DocBot {
|
|
|
410
416
|
return `low-signal page: only ${documentation.can.length} proven actions and ${interactiveCount} interactive elements`;
|
|
411
417
|
}
|
|
412
418
|
|
|
419
|
+
private shouldIgnoreError(error: unknown): boolean {
|
|
420
|
+
const ignoreErrors = this.config.docs?.ignoreErrors;
|
|
421
|
+
if (ignoreErrors === undefined || ignoreErrors === true) return true;
|
|
422
|
+
if (ignoreErrors === false) return false;
|
|
423
|
+
|
|
424
|
+
const details = [error instanceof Error ? error.name : '', error instanceof Error ? error.message : String(error)];
|
|
425
|
+
if (typeof error === 'object' && error && 'code' in error) {
|
|
426
|
+
details.push(String(error.code));
|
|
427
|
+
}
|
|
428
|
+
const normalized = details
|
|
429
|
+
.join(' ')
|
|
430
|
+
.toLowerCase()
|
|
431
|
+
.replaceAll(/[\W_]+/g, ' ');
|
|
432
|
+
return ignoreErrors.some((pattern) => {
|
|
433
|
+
const normalizedPattern = pattern
|
|
434
|
+
.trim()
|
|
435
|
+
.toLowerCase()
|
|
436
|
+
.replaceAll(/[\W_]+/g, ' ');
|
|
437
|
+
return normalizedPattern.length > 0 && normalized.includes(normalizedPattern);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
413
441
|
private countInteractiveElements(research: string): number {
|
|
414
442
|
const matches = [...research.matchAll(/\((\d+) elements?\)/g)];
|
|
415
443
|
return matches.reduce((sum, match) => sum + Number.parseInt(match[1], 10), 0);
|