explorbot 0.1.32-beta.1 → 0.2.1
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 +35 -19
- package/bin/explorbot-cli.ts +33 -10
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +26 -28
- package/boat/doc-collector/src/ai/tools.ts +74 -16
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +30 -9
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +34 -2
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +29 -9
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
- package/dist/boat/doc-collector/src/ai/tools.js +53 -16
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +27 -8
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +30 -1
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/models.json +30 -0
- package/dist/package.json +23 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +13 -12
- package/dist/src/action.js +37 -113
- 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 -29
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +17 -45
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +2 -17
- package/dist/src/ai/driller.js +26 -50
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +4 -3
- package/dist/src/ai/historian/playwright.d.ts +4 -1
- package/dist/src/ai/historian/playwright.js +4 -3
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +10 -8
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +8 -6
- package/dist/src/ai/navigator.js +34 -65
- package/dist/src/ai/pilot.d.ts +13 -8
- package/dist/src/ai/pilot.js +53 -59
- package/dist/src/ai/planner.d.ts +5 -4
- package/dist/src/ai/planner.js +66 -52
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -137
- package/dist/src/ai/quartermaster.d.ts +22 -22
- package/dist/src/ai/quartermaster.js +8 -14
- package/dist/src/ai/rerunner.d.ts +3 -13
- package/dist/src/ai/rerunner.js +23 -44
- package/dist/src/ai/researcher/cache.js +11 -18
- 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 +8 -11
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +7 -3
- package/dist/src/ai/researcher.js +40 -106
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +19 -6
- package/dist/src/ai/task-agent.js +37 -33
- package/dist/src/ai/tester.d.ts +6 -14
- package/dist/src/ai/tester.js +46 -111
- package/dist/src/ai/tools.d.ts +26 -14
- package/dist/src/ai/tools.js +138 -132
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +8 -6
- 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 +2 -2
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/experience-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/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +5 -3
- 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/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +3 -3
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +156 -10
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +82 -155
- package/dist/src/explorbot.d.ts +21 -13
- package/dist/src/explorbot.js +84 -80
- package/dist/src/explorer.d.ts +66 -102
- package/dist/src/explorer.js +332 -623
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +10 -30
- package/dist/src/state-manager.js +11 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/hooks-runner.js +2 -4
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- 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 +3 -1
- package/dist/src/utils/web-element.js +32 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/models.json +30 -0
- package/package.json +23 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +38 -132
- 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 -30
- package/src/ai/captain.ts +17 -42
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +53 -83
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +5 -3
- package/src/ai/historian/playwright.ts +7 -4
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +6 -8
- package/src/ai/navigator.ts +39 -70
- package/src/ai/pilot.ts +59 -61
- package/src/ai/planner.ts +76 -62
- package/src/ai/provider.ts +85 -146
- package/src/ai/quartermaster.ts +39 -45
- package/src/ai/rerunner.ts +27 -50
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +10 -11
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +9 -3
- package/src/ai/researcher.ts +42 -119
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +45 -38
- package/src/ai/tester.ts +51 -129
- package/src/ai/tools.ts +145 -153
- package/src/command-handler.ts +1 -1
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +8 -6
- 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 +2 -2
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +5 -3
- 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/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +3 -3
- package/src/config.ts +185 -11
- package/src/experience-tracker.ts +89 -152
- package/src/explorbot.ts +88 -83
- package/src/explorer.ts +402 -693
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +20 -155
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/hooks-runner.ts +2 -4
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- 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 +34 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/README.md
CHANGED
|
@@ -14,7 +14,9 @@ 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
|
-
|
|
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
|
+
|
|
19
|
+
New here? Read the [Getting Started guide](docs/basics/getting-started.md).
|
|
18
20
|
|
|
19
21
|
## Use Cases
|
|
20
22
|
|
|
@@ -54,11 +56,11 @@ Cheap, fast workers do the clicking and reading; smart managers make the calls
|
|
|
54
56
|
|
|
55
57
|
| | | |
|
|
56
58
|
|---|---|---|
|
|
57
|
-
| [Researcher](docs/
|
|
58
|
-
| [Pilot](docs/
|
|
59
|
-
| [Analyst](docs/
|
|
59
|
+
| [Researcher](docs/web-testing/agents.md) | [Planner](docs/web-testing/agents.md) | [Tester](docs/web-testing/agents.md) |
|
|
60
|
+
| [Pilot](docs/web-testing/agents.md) | [Captain](docs/web-testing/agents.md) | [Navigator](docs/web-testing/agents.md) |
|
|
61
|
+
| [Analyst](docs/web-testing/agents.md) | [Historian](docs/web-testing/agents.md) | [Fisherman](docs/web-testing/agents.md) |
|
|
60
62
|
|
|
61
|
-
See [Agents](docs/
|
|
63
|
+
See [Agents](docs/web-testing/agents.md) for what each one does.
|
|
62
64
|
|
|
63
65
|
## Core Philosophy
|
|
64
66
|
|
|
@@ -74,7 +76,7 @@ See [Agents](docs/reference/agents.md) for what each one does.
|
|
|
74
76
|
|
|
75
77
|
When tuned, Explorbot **runs autonomously for hours**, trying new scenarios on its own. The more it runs, the more it learns.
|
|
76
78
|
|
|
77
|
-
##
|
|
79
|
+
## Tests, reports, videos
|
|
78
80
|
|
|
79
81
|
Every run leaves behind:
|
|
80
82
|
|
|
@@ -83,7 +85,7 @@ Every run leaves behind:
|
|
|
83
85
|
- **Videos** — step-by-step screencasts of every run.
|
|
84
86
|
- **Experience** — what Explorbot learned, reused to test smarter next time.
|
|
85
87
|
|
|
86
|
-
See [Automated Tests](docs/
|
|
88
|
+
See [Automated Tests](docs/web-testing/automated-tests.md) for the test output and [Reporting](docs/workflow/reporting.md) for reports.
|
|
87
89
|
|
|
88
90
|
## It works with your suite
|
|
89
91
|
|
|
@@ -92,9 +94,9 @@ 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
|
-
- A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/
|
|
99
|
+
- A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/basics/prerequisites.md)
|
|
98
100
|
|
|
99
101
|
If your CI runs Playwright, it runs Explorbot. No GPUs, no special runners.
|
|
100
102
|
|
|
@@ -123,35 +125,49 @@ npx explorbot start /admin/users
|
|
|
123
125
|
|
|
124
126
|
Type `/explore`, and Explorbot runs its loop on its own — research, plan, test, repeat — learning from every run.
|
|
125
127
|
|
|
126
|
-
That's the gist. The [**Getting Started guide**](docs/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
|
|
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.
|
|
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).
|
|
127
142
|
|
|
128
143
|
## Teaching Explorbot
|
|
129
144
|
|
|
130
145
|
Explorbot gets better when you tell it about your app:
|
|
131
146
|
|
|
132
|
-
- **Knowledge** — credentials, form rules, navigation quirks. See [Knowledge](docs/
|
|
147
|
+
- **Knowledge** — credentials, form rules, navigation quirks. See [Knowledge](docs/workflow/knowledge.md).
|
|
133
148
|
- **Rules** — per-agent, per-page instructions. See [Configuration](docs/reference/configuration.md#rules).
|
|
134
149
|
- **Experience** — learned automatically from what works.
|
|
135
150
|
|
|
136
|
-
Handling logins, cookie banners, modals, and test data takes a few lines — see [Customization](docs/
|
|
151
|
+
Handling logins, cookie banners, modals, and test data takes a few lines — see [Customization](docs/web-testing/customization.md).
|
|
137
152
|
|
|
138
153
|
## It also tests REST APIs
|
|
139
154
|
|
|
140
|
-
Point Explorbot at an OpenAPI spec and it plans and runs API tests too. See [API Testing](docs/
|
|
155
|
+
Point Explorbot at an OpenAPI spec and it plans and runs API tests too. See [API Testing](docs/api-testing/basics.md).
|
|
141
156
|
|
|
142
157
|
## Keep going
|
|
143
158
|
|
|
144
|
-
When you're ready to go deeper, the [full documentation](docs/
|
|
159
|
+
When you're ready to go deeper, the [full documentation](docs/) covers everything, starting with the [Getting Started guide](docs/basics/getting-started.md).
|
|
145
160
|
|
|
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.
|
|
@@ -163,7 +179,7 @@ No. With fast open models (e.g. `openai/gpt-oss-20b` on OpenRouter or Groq), exp
|
|
|
163
179
|
Not yet.
|
|
164
180
|
|
|
165
181
|
**Can I build my own agents with it?**
|
|
166
|
-
Yes, use the programmatic API. See [Scripting](docs/
|
|
182
|
+
Yes, use the programmatic API. See [Scripting](docs/reference/scripting.md).
|
|
167
183
|
|
|
168
184
|
**Can I do the same in Cursor with Playwright MCP?**
|
|
169
185
|
Good luck running it on CI!
|
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
|
|
|
@@ -509,9 +507,10 @@ program
|
|
|
509
507
|
path: options.path || process.cwd(),
|
|
510
508
|
});
|
|
511
509
|
|
|
510
|
+
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
511
|
+
const tracker = new KnowledgeTracker();
|
|
512
|
+
|
|
512
513
|
if (url && description) {
|
|
513
|
-
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
|
|
514
|
-
const tracker = new KnowledgeTracker();
|
|
515
514
|
const result = tracker.addKnowledge(url, description);
|
|
516
515
|
const action = result.isNewFile ? 'Created' : 'Updated';
|
|
517
516
|
console.log(`Knowledge ${action} in: ${result.filename}`);
|
|
@@ -519,7 +518,7 @@ program
|
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
const AddKnowledge = (await import('../src/components/AddKnowledge.js')).default;
|
|
522
|
-
render(React.createElement(AddKnowledge, { initialUrl: url || '' }), {
|
|
521
|
+
render(React.createElement(AddKnowledge, { initialUrl: url || '', knowledgeTracker: tracker }), {
|
|
523
522
|
exitOnCtrlC: false,
|
|
524
523
|
patchConsole: false,
|
|
525
524
|
});
|
|
@@ -689,7 +688,7 @@ program
|
|
|
689
688
|
path: options.path,
|
|
690
689
|
config: options.config,
|
|
691
690
|
headless: true,
|
|
692
|
-
session: options.session
|
|
691
|
+
session: options.session,
|
|
693
692
|
};
|
|
694
693
|
|
|
695
694
|
const explorBot = new ExplorBot(mainOptions);
|
|
@@ -719,11 +718,11 @@ addCommonOptions(program.command('shell <url> <command>').description('Execute a
|
|
|
719
718
|
await explorBot.start();
|
|
720
719
|
await explorBot.agentNavigator().visit(url);
|
|
721
720
|
|
|
722
|
-
const action = explorBot.getExplorer().
|
|
721
|
+
const action = explorBot.getExplorer().action();
|
|
723
722
|
await action.execute(command);
|
|
724
723
|
|
|
725
724
|
log('Command executed successfully');
|
|
726
|
-
const state = explorBot.
|
|
725
|
+
const state = explorBot.stateManager().getCurrentState();
|
|
727
726
|
if (state) log(`URL: ${state.url}`);
|
|
728
727
|
|
|
729
728
|
await explorBot.stop();
|
|
@@ -875,4 +874,28 @@ import { createDocsCommands } from '../boat/doc-collector/src/cli.ts';
|
|
|
875
874
|
program.addCommand(createApiCommands('api'));
|
|
876
875
|
program.addCommand(createDocsCommands('docs'));
|
|
877
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
|
+
|
|
878
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,23 +2,26 @@ 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';
|
|
8
|
-
import { collectDocInteractions } from './tools.ts';
|
|
9
|
+
import { type CaptureInteractionState, type DocStateTransition, collectDocInteractions } from './tools.ts';
|
|
9
10
|
|
|
10
11
|
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
|
-
async document(state: WebPageState, research: string): Promise<PageDocumentation> {
|
|
24
|
+
async document(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
|
|
22
25
|
const interactiveEnabled = this.config.docs?.interactive === true && this.explorer;
|
|
23
26
|
if (!interactiveEnabled) {
|
|
24
27
|
tag('info').log('Documentarian: Using static mode (interactive disabled or no explorer)');
|
|
@@ -26,7 +29,7 @@ class Documentarian {
|
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
tag('info').log('Documentarian: Using interactive mode with tools');
|
|
29
|
-
return this.documentWithInteraction(state, research);
|
|
32
|
+
return this.documentWithInteraction(state, research, captureState);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
private async documentStatic(state: WebPageState, research: string): Promise<PageDocumentation> {
|
|
@@ -41,12 +44,13 @@ class Documentarian {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
private async documentWithInteraction(state: WebPageState, research: string): Promise<PageDocumentation> {
|
|
47
|
+
private async documentWithInteraction(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
|
|
48
|
+
let meaningfulInteractions: StateTransition[] = [];
|
|
45
49
|
try {
|
|
46
50
|
tag('info').log('Starting interactive exploration...');
|
|
47
51
|
|
|
48
|
-
const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config);
|
|
49
|
-
|
|
52
|
+
const deterministicInteractions = await collectDocInteractions(this.explorer!, this.stateManager!, state, research, this.config, captureState);
|
|
53
|
+
meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
|
|
50
54
|
if (meaningfulInteractions.length > 0) {
|
|
51
55
|
tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
|
|
52
56
|
return await this.generateDocumentationWithInteractions(state, research, meaningfulInteractions);
|
|
@@ -61,7 +65,20 @@ class Documentarian {
|
|
|
61
65
|
return this.documentStatic(state, research);
|
|
62
66
|
} catch (error) {
|
|
63
67
|
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
-
tag('warning').log(`Interactive documentation failed: ${message}
|
|
68
|
+
tag('warning').log(`Interactive documentation failed: ${message}.`);
|
|
69
|
+
if (meaningfulInteractions.length > 0) {
|
|
70
|
+
tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
|
|
71
|
+
return this.normalizeDocumentation(
|
|
72
|
+
{
|
|
73
|
+
summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
|
|
74
|
+
can: [],
|
|
75
|
+
might: [],
|
|
76
|
+
interactions: meaningfulInteractions,
|
|
77
|
+
},
|
|
78
|
+
state,
|
|
79
|
+
research
|
|
80
|
+
);
|
|
81
|
+
}
|
|
65
82
|
return this.documentStatic(state, research);
|
|
66
83
|
}
|
|
67
84
|
}
|
|
@@ -354,26 +371,7 @@ const pageDocumentationSchema = z.object({
|
|
|
354
371
|
interactions: z.array(stateTransitionSchema).nullable(),
|
|
355
372
|
});
|
|
356
373
|
|
|
357
|
-
type StateTransition =
|
|
358
|
-
action: string;
|
|
359
|
-
before: string;
|
|
360
|
-
after: string;
|
|
361
|
-
targetUrl?: string | null;
|
|
362
|
-
discoveredUrls?: string[] | null;
|
|
363
|
-
newCapabilities?: string[] | null;
|
|
364
|
-
element?: {
|
|
365
|
-
role: string;
|
|
366
|
-
name: string;
|
|
367
|
-
section: string;
|
|
368
|
-
container?: string | null;
|
|
369
|
-
locator?: string | null;
|
|
370
|
-
} | null;
|
|
371
|
-
changes?: {
|
|
372
|
-
urlChanged: boolean;
|
|
373
|
-
newElements: number;
|
|
374
|
-
removedElements: number;
|
|
375
|
-
} | null;
|
|
376
|
-
};
|
|
374
|
+
type StateTransition = DocStateTransition;
|
|
377
375
|
type PageDocumentation = Omit<z.infer<typeof pageDocumentationSchema>, 'interactions'> & {
|
|
378
376
|
interactions?: StateTransition[];
|
|
379
377
|
qualityNotes?: string[];
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { detectFocusArea } from '../../../../src/utils/aria.ts';
|
|
4
5
|
import type { DocbotConfig } from '../config.ts';
|
|
5
6
|
|
|
6
7
|
export interface DocStateTransition {
|
|
@@ -12,6 +13,8 @@ export interface DocStateTransition {
|
|
|
12
13
|
newCapabilities?: string[];
|
|
13
14
|
element?: InteractionElement;
|
|
14
15
|
changes?: InteractionChanges;
|
|
16
|
+
targetState?: InteractionState;
|
|
17
|
+
screenshot?: InteractionScreenshot;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
interface InteractionCandidate {
|
|
@@ -35,6 +38,19 @@ interface InteractionChanges {
|
|
|
35
38
|
removedElements: number;
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
export interface InteractionState {
|
|
42
|
+
kind: 'page' | 'dialog' | 'modal' | 'section';
|
|
43
|
+
label: string;
|
|
44
|
+
url: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InteractionScreenshot {
|
|
48
|
+
title: string;
|
|
49
|
+
relativePath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type CaptureInteractionState = (state: WebPageState, transition: DocStateTransition) => Promise<InteractionScreenshot | null>;
|
|
53
|
+
|
|
38
54
|
const DEFAULT_MAX_PRIMARY_CANDIDATES = 3;
|
|
39
55
|
const DEFAULT_MAX_INTERACTIONS = 5;
|
|
40
56
|
const MAX_LINKS = 15;
|
|
@@ -42,14 +58,14 @@ const DEFAULT_WAIT_MS = 700;
|
|
|
42
58
|
const TAB_WAIT_MS = 500;
|
|
43
59
|
const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
|
|
44
60
|
|
|
45
|
-
export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}): Promise<DocStateTransition[]> {
|
|
61
|
+
export async function collectDocInteractions(explorer: Explorer, stateManager: StateManager, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
46
62
|
const sections = parseResearchSections(research);
|
|
47
63
|
const transitions: DocStateTransition[] = [];
|
|
48
64
|
const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
|
|
49
65
|
const tabGroup = findTabGroup(sections);
|
|
50
66
|
|
|
51
67
|
if (tabGroup) {
|
|
52
|
-
transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions)));
|
|
68
|
+
transitions.push(...(await exploreTabGroup(explorer, stateManager, tabGroup, state.url, maxInteractions, captureState)));
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
for (const candidate of findActionCandidates(sections, config)) {
|
|
@@ -57,7 +73,7 @@ export async function collectDocInteractions(explorer: Explorer, state: WebPageS
|
|
|
57
73
|
break;
|
|
58
74
|
}
|
|
59
75
|
|
|
60
|
-
const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS);
|
|
76
|
+
const transition = await executeInteraction(explorer, stateManager, candidate, state.url, DEFAULT_WAIT_MS, captureState);
|
|
61
77
|
if (!transition) {
|
|
62
78
|
continue;
|
|
63
79
|
}
|
|
@@ -76,7 +92,7 @@ export function pickDocActionCandidates(research: string, config: DocbotConfig =
|
|
|
76
92
|
}));
|
|
77
93
|
}
|
|
78
94
|
|
|
79
|
-
async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number): Promise<DocStateTransition[]> {
|
|
95
|
+
async function exploreTabGroup(explorer: Explorer, stateManager: StateManager, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
|
|
80
96
|
const transitions: DocStateTransition[] = [];
|
|
81
97
|
|
|
82
98
|
for (const element of tabGroup.elements) {
|
|
@@ -86,6 +102,7 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
86
102
|
|
|
87
103
|
const transition = await executeInteraction(
|
|
88
104
|
explorer,
|
|
105
|
+
stateManager,
|
|
89
106
|
{
|
|
90
107
|
element,
|
|
91
108
|
container: tabGroup.container,
|
|
@@ -93,7 +110,8 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
93
110
|
sectionName: tabGroup.sectionName,
|
|
94
111
|
},
|
|
95
112
|
restoreUrl,
|
|
96
|
-
TAB_WAIT_MS
|
|
113
|
+
TAB_WAIT_MS,
|
|
114
|
+
captureState
|
|
97
115
|
);
|
|
98
116
|
if (!transition) {
|
|
99
117
|
continue;
|
|
@@ -106,8 +124,8 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
|
|
|
106
124
|
return transitions;
|
|
107
125
|
}
|
|
108
126
|
|
|
109
|
-
async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number): Promise<DocStateTransition | null> {
|
|
110
|
-
const beforeState =
|
|
127
|
+
async function executeInteraction(explorer: Explorer, stateManager: StateManager, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
|
|
128
|
+
const beforeState = stateManager.getCurrentState();
|
|
111
129
|
if (!beforeState) {
|
|
112
130
|
return null;
|
|
113
131
|
}
|
|
@@ -119,7 +137,7 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
119
137
|
|
|
120
138
|
await wait(waitMs);
|
|
121
139
|
|
|
122
|
-
const afterState =
|
|
140
|
+
const afterState = stateManager.getCurrentState();
|
|
123
141
|
if (!afterState) {
|
|
124
142
|
return null;
|
|
125
143
|
}
|
|
@@ -132,7 +150,14 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
132
150
|
removedElements: ariaChanges.removedCount,
|
|
133
151
|
});
|
|
134
152
|
|
|
135
|
-
if (
|
|
153
|
+
if (captureState && isMeaningfulStateTransition(transition)) {
|
|
154
|
+
const screenshot = await captureState(afterState, transition);
|
|
155
|
+
if (screenshot) {
|
|
156
|
+
transition.screenshot = screenshot;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (urlChanged || ariaChanges.newCount > 0) {
|
|
136
161
|
await restoreInteractionState(explorer, restoreUrl);
|
|
137
162
|
}
|
|
138
163
|
|
|
@@ -140,10 +165,10 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
140
165
|
}
|
|
141
166
|
|
|
142
167
|
async function attemptInteraction(explorer: Explorer, candidate: InteractionCandidate): Promise<boolean> {
|
|
143
|
-
const action = explorer.
|
|
168
|
+
const action = explorer.action();
|
|
144
169
|
|
|
145
170
|
for (const command of buildClickCommands(candidate.element, candidate.container)) {
|
|
146
|
-
const success = await action.attempt(command, buildPurpose(candidate)
|
|
171
|
+
const success = await action.attempt(command, buildPurpose(candidate));
|
|
147
172
|
if (success) {
|
|
148
173
|
return true;
|
|
149
174
|
}
|
|
@@ -154,16 +179,16 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
|
|
|
154
179
|
|
|
155
180
|
async function restoreInteractionState(explorer: Explorer, restoreUrl: string, primaryCommand?: string | null): Promise<void> {
|
|
156
181
|
if (primaryCommand) {
|
|
157
|
-
const action = explorer.
|
|
158
|
-
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}
|
|
182
|
+
const action = explorer.action();
|
|
183
|
+
const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
|
|
159
184
|
if (restored) {
|
|
160
185
|
await wait(TAB_WAIT_MS);
|
|
161
186
|
return;
|
|
162
187
|
}
|
|
163
188
|
}
|
|
164
189
|
|
|
165
|
-
const action = explorer.
|
|
166
|
-
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}
|
|
190
|
+
const action = explorer.action();
|
|
191
|
+
await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
function buildTransition(candidate: InteractionCandidate, beforeState: WebPageState, afterState: WebPageState, changes: InteractionChanges): DocStateTransition {
|
|
@@ -175,6 +200,7 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
|
|
|
175
200
|
newCapabilities: collectDiscoveryNotes(afterState, changes),
|
|
176
201
|
element: buildInteractionElement(candidate),
|
|
177
202
|
changes,
|
|
203
|
+
targetState: describeTargetState(beforeState, afterState, candidate),
|
|
178
204
|
};
|
|
179
205
|
|
|
180
206
|
if (changes.urlChanged) {
|
|
@@ -184,6 +210,38 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
|
|
|
184
210
|
return transition;
|
|
185
211
|
}
|
|
186
212
|
|
|
213
|
+
function describeTargetState(beforeState: WebPageState, afterState: WebPageState, candidate: InteractionCandidate): InteractionState {
|
|
214
|
+
const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
|
|
215
|
+
const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
|
|
216
|
+
if (afterFocus.detected && (!beforeFocus.detected || beforeFocus.name !== afterFocus.name)) {
|
|
217
|
+
return {
|
|
218
|
+
kind: afterFocus.type || 'dialog',
|
|
219
|
+
label: afterFocus.name || candidate.element.name.trim(),
|
|
220
|
+
url: afterState.url,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const beforePath = beforeState.url.split('?')[0].split('#')[0];
|
|
225
|
+
const afterPath = afterState.url.split('?')[0].split('#')[0];
|
|
226
|
+
const headings = collectHeadings(afterState);
|
|
227
|
+
let kind: InteractionState['kind'] = 'page';
|
|
228
|
+
if (beforePath === afterPath) {
|
|
229
|
+
kind = 'section';
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
kind,
|
|
233
|
+
label: headings[0] || afterState.title || candidate.element.name.trim(),
|
|
234
|
+
url: afterState.url,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isMeaningfulStateTransition(transition: DocStateTransition): boolean {
|
|
239
|
+
if (transition.targetUrl || transition.changes?.urlChanged) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return (transition.changes?.newElements || 0) > 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
187
245
|
function buildInteractionElement(candidate: InteractionCandidate): InteractionElement {
|
|
188
246
|
const element: InteractionElement = {
|
|
189
247
|
role: candidate.role,
|
|
@@ -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
|
}
|