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.
Files changed (222) hide show
  1. package/README.md +35 -19
  2. package/bin/explorbot-cli.ts +33 -10
  3. package/boat/api-tester/src/config.ts +45 -3
  4. package/boat/doc-collector/src/ai/documentarian.ts +26 -28
  5. package/boat/doc-collector/src/ai/tools.ts +74 -16
  6. package/boat/doc-collector/src/cli.ts +1 -6
  7. package/boat/doc-collector/src/docbot.ts +30 -9
  8. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  9. package/boat/doc-collector/src/screenshots.ts +34 -2
  10. package/boat/doc-collector/src/state-diagram.ts +281 -0
  11. package/dist/bin/explorbot-cli.js +29 -9
  12. package/dist/boat/api-tester/src/config.js +43 -4
  13. package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
  14. package/dist/boat/doc-collector/src/ai/tools.js +53 -16
  15. package/dist/boat/doc-collector/src/cli.js +1 -5
  16. package/dist/boat/doc-collector/src/docbot.js +27 -8
  17. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  18. package/dist/boat/doc-collector/src/screenshots.js +30 -1
  19. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  20. package/dist/models.json +30 -0
  21. package/dist/package.json +23 -3
  22. package/dist/src/action-result.d.ts +8 -10
  23. package/dist/src/action-result.js +72 -164
  24. package/dist/src/action.d.ts +13 -12
  25. package/dist/src/action.js +37 -113
  26. package/dist/src/ai/agent.d.ts +17 -0
  27. package/dist/src/ai/captain/idle-mode.js +1 -1
  28. package/dist/src/ai/captain/test-mode.js +1 -1
  29. package/dist/src/ai/captain/web-mode.js +18 -29
  30. package/dist/src/ai/captain.d.ts +1 -3
  31. package/dist/src/ai/captain.js +17 -45
  32. package/dist/src/ai/conversation.d.ts +2 -5
  33. package/dist/src/ai/conversation.js +9 -28
  34. package/dist/src/ai/driller.d.ts +2 -17
  35. package/dist/src/ai/driller.js +26 -50
  36. package/dist/src/ai/experience-compactor.d.ts +2 -2
  37. package/dist/src/ai/experience-compactor.js +16 -33
  38. package/dist/src/ai/fisherman-tools.js +1 -1
  39. package/dist/src/ai/fisherman.d.ts +0 -1
  40. package/dist/src/ai/fisherman.js +0 -3
  41. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  42. package/dist/src/ai/historian/codeceptjs.js +4 -3
  43. package/dist/src/ai/historian/playwright.d.ts +4 -1
  44. package/dist/src/ai/historian/playwright.js +4 -3
  45. package/dist/src/ai/historian/screencast.d.ts +2 -1
  46. package/dist/src/ai/historian/screencast.js +2 -2
  47. package/dist/src/ai/historian/utils.d.ts +0 -1
  48. package/dist/src/ai/historian/utils.js +0 -1
  49. package/dist/src/ai/historian.d.ts +10 -8
  50. package/dist/src/ai/historian.js +1 -5
  51. package/dist/src/ai/navigator.d.ts +8 -6
  52. package/dist/src/ai/navigator.js +34 -65
  53. package/dist/src/ai/pilot.d.ts +13 -8
  54. package/dist/src/ai/pilot.js +53 -59
  55. package/dist/src/ai/planner.d.ts +5 -4
  56. package/dist/src/ai/planner.js +66 -52
  57. package/dist/src/ai/provider.d.ts +4 -1
  58. package/dist/src/ai/provider.js +77 -137
  59. package/dist/src/ai/quartermaster.d.ts +22 -22
  60. package/dist/src/ai/quartermaster.js +8 -14
  61. package/dist/src/ai/rerunner.d.ts +3 -13
  62. package/dist/src/ai/rerunner.js +23 -44
  63. package/dist/src/ai/researcher/cache.js +11 -18
  64. package/dist/src/ai/researcher/coordinates.js +4 -3
  65. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
  66. package/dist/src/ai/researcher/deep-analysis.js +8 -11
  67. package/dist/src/ai/researcher/locators.d.ts +9 -2
  68. package/dist/src/ai/researcher/locators.js +65 -8
  69. package/dist/src/ai/researcher/parser.js +1 -1
  70. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  71. package/dist/src/ai/researcher/research-result.js +0 -19
  72. package/dist/src/ai/researcher/sections.d.ts +2 -0
  73. package/dist/src/ai/researcher/sections.js +7 -3
  74. package/dist/src/ai/researcher.js +40 -106
  75. package/dist/src/ai/rules.d.ts +1 -0
  76. package/dist/src/ai/rules.js +22 -15
  77. package/dist/src/ai/session-analyst.js +8 -5
  78. package/dist/src/ai/task-agent.d.ts +19 -6
  79. package/dist/src/ai/task-agent.js +37 -33
  80. package/dist/src/ai/tester.d.ts +6 -14
  81. package/dist/src/ai/tester.js +46 -111
  82. package/dist/src/ai/tools.d.ts +26 -14
  83. package/dist/src/ai/tools.js +138 -132
  84. package/dist/src/command-handler.js +1 -1
  85. package/dist/src/commands/compact-command.d.ts +1 -1
  86. package/dist/src/commands/compact-command.js +6 -6
  87. package/dist/src/commands/context-aria-command.js +1 -1
  88. package/dist/src/commands/context-command.js +8 -6
  89. package/dist/src/commands/context-data-command.js +2 -2
  90. package/dist/src/commands/context-experience-command.js +2 -2
  91. package/dist/src/commands/context-html-command.js +2 -2
  92. package/dist/src/commands/context-knowledge-command.js +2 -2
  93. package/dist/src/commands/drill-command.js +1 -1
  94. package/dist/src/commands/experience-command.js +1 -1
  95. package/dist/src/commands/explore-command.js +4 -3
  96. package/dist/src/commands/freesail-command.js +2 -2
  97. package/dist/src/commands/knows-command.js +1 -1
  98. package/dist/src/commands/learn-command.js +5 -3
  99. package/dist/src/commands/path-command.js +1 -1
  100. package/dist/src/commands/research-command.js +1 -1
  101. package/dist/src/commands/test-command.js +1 -1
  102. package/dist/src/components/AddKnowledge.d.ts +2 -0
  103. package/dist/src/components/AddKnowledge.js +3 -7
  104. package/dist/src/components/App.js +3 -3
  105. package/dist/src/config.d.ts +19 -0
  106. package/dist/src/config.js +156 -10
  107. package/dist/src/experience-tracker.d.ts +11 -6
  108. package/dist/src/experience-tracker.js +82 -155
  109. package/dist/src/explorbot.d.ts +21 -13
  110. package/dist/src/explorbot.js +84 -80
  111. package/dist/src/explorer.d.ts +66 -102
  112. package/dist/src/explorer.js +332 -623
  113. package/dist/src/knowledge-tracker.d.ts +2 -2
  114. package/dist/src/knowledge-tracker.js +38 -45
  115. package/dist/src/state-manager.d.ts +10 -30
  116. package/dist/src/state-manager.js +11 -129
  117. package/dist/src/utils/aria.d.ts +6 -1
  118. package/dist/src/utils/aria.js +4 -1
  119. package/dist/src/utils/cache.d.ts +15 -0
  120. package/dist/src/utils/cache.js +34 -0
  121. package/dist/src/utils/context-formatter.d.ts +6 -0
  122. package/dist/src/utils/context-formatter.js +15 -27
  123. package/dist/src/utils/hooks-runner.js +2 -4
  124. package/dist/src/utils/html.d.ts +5 -0
  125. package/dist/src/utils/html.js +71 -0
  126. package/dist/src/utils/markdown-files.d.ts +10 -0
  127. package/dist/src/utils/markdown-files.js +21 -0
  128. package/dist/src/utils/markdown-query.d.ts +6 -0
  129. package/dist/src/utils/markdown-query.js +14 -1
  130. package/dist/src/utils/page-readiness.d.ts +1 -0
  131. package/dist/src/utils/page-readiness.js +1 -1
  132. package/dist/src/utils/secrets.js +2 -2
  133. package/dist/src/utils/strings.d.ts +2 -0
  134. package/dist/src/utils/strings.js +10 -0
  135. package/dist/src/utils/test-files.js +1 -1
  136. package/dist/src/utils/web-annotate.d.ts +5 -0
  137. package/dist/src/utils/web-annotate.js +58 -0
  138. package/dist/src/utils/web-eidx.d.ts +2 -0
  139. package/dist/src/utils/web-eidx.js +20 -0
  140. package/dist/src/utils/web-element.d.ts +3 -1
  141. package/dist/src/utils/web-element.js +32 -0
  142. package/dist/src/utils/web-sandbox.d.ts +4 -0
  143. package/dist/src/utils/web-sandbox.js +37 -0
  144. package/models.json +30 -0
  145. package/package.json +23 -3
  146. package/src/action-result.ts +70 -173
  147. package/src/action.ts +38 -132
  148. package/src/ai/agent.ts +20 -0
  149. package/src/ai/captain/idle-mode.ts +1 -1
  150. package/src/ai/captain/test-mode.ts +1 -1
  151. package/src/ai/captain/web-mode.ts +18 -30
  152. package/src/ai/captain.ts +17 -42
  153. package/src/ai/conversation.ts +9 -32
  154. package/src/ai/driller.ts +53 -83
  155. package/src/ai/experience-compactor.ts +16 -35
  156. package/src/ai/fisherman-tools.ts +1 -1
  157. package/src/ai/fisherman.ts +0 -4
  158. package/src/ai/historian/codeceptjs.ts +5 -3
  159. package/src/ai/historian/playwright.ts +7 -4
  160. package/src/ai/historian/screencast.ts +4 -3
  161. package/src/ai/historian/utils.ts +0 -1
  162. package/src/ai/historian.ts +6 -8
  163. package/src/ai/navigator.ts +39 -70
  164. package/src/ai/pilot.ts +59 -61
  165. package/src/ai/planner.ts +76 -62
  166. package/src/ai/provider.ts +85 -146
  167. package/src/ai/quartermaster.ts +39 -45
  168. package/src/ai/rerunner.ts +27 -50
  169. package/src/ai/researcher/cache.ts +10 -16
  170. package/src/ai/researcher/coordinates.ts +4 -3
  171. package/src/ai/researcher/deep-analysis.ts +10 -11
  172. package/src/ai/researcher/locators.ts +66 -9
  173. package/src/ai/researcher/parser.ts +1 -1
  174. package/src/ai/researcher/research-result.ts +0 -18
  175. package/src/ai/researcher/sections.ts +9 -3
  176. package/src/ai/researcher.ts +42 -119
  177. package/src/ai/rules.ts +24 -13
  178. package/src/ai/session-analyst.ts +8 -5
  179. package/src/ai/task-agent.ts +45 -38
  180. package/src/ai/tester.ts +51 -129
  181. package/src/ai/tools.ts +145 -153
  182. package/src/command-handler.ts +1 -1
  183. package/src/commands/compact-command.ts +7 -8
  184. package/src/commands/context-aria-command.ts +1 -1
  185. package/src/commands/context-command.ts +8 -6
  186. package/src/commands/context-data-command.ts +2 -2
  187. package/src/commands/context-experience-command.ts +2 -2
  188. package/src/commands/context-html-command.ts +2 -2
  189. package/src/commands/context-knowledge-command.ts +2 -2
  190. package/src/commands/drill-command.ts +1 -1
  191. package/src/commands/experience-command.ts +1 -1
  192. package/src/commands/explore-command.ts +4 -3
  193. package/src/commands/freesail-command.ts +2 -2
  194. package/src/commands/knows-command.ts +1 -1
  195. package/src/commands/learn-command.ts +5 -3
  196. package/src/commands/path-command.ts +1 -1
  197. package/src/commands/research-command.ts +1 -1
  198. package/src/commands/test-command.ts +1 -1
  199. package/src/components/AddKnowledge.tsx +5 -7
  200. package/src/components/App.tsx +3 -3
  201. package/src/config.ts +185 -11
  202. package/src/experience-tracker.ts +89 -152
  203. package/src/explorbot.ts +88 -83
  204. package/src/explorer.ts +402 -693
  205. package/src/knowledge-tracker.ts +42 -49
  206. package/src/state-manager.ts +20 -155
  207. package/src/utils/aria.ts +11 -2
  208. package/src/utils/cache.ts +40 -0
  209. package/src/utils/context-formatter.ts +12 -15
  210. package/src/utils/hooks-runner.ts +2 -4
  211. package/src/utils/html.ts +79 -0
  212. package/src/utils/markdown-files.ts +30 -0
  213. package/src/utils/markdown-query.ts +15 -1
  214. package/src/utils/page-readiness.ts +1 -1
  215. package/src/utils/secrets.ts +2 -3
  216. package/src/utils/strings.ts +12 -0
  217. package/src/utils/test-files.ts +1 -1
  218. package/src/utils/web-annotate.ts +64 -0
  219. package/src/utils/web-eidx.ts +21 -0
  220. package/src/utils/web-element.ts +34 -0
  221. package/src/utils/web-sandbox.ts +43 -0
  222. 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
- New here? Read the [Getting Started guide](docs/getting-started.md).
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/reference/agents.md) | [Planner](docs/reference/agents.md) | [Tester](docs/reference/agents.md) |
58
- | [Pilot](docs/reference/agents.md) | [Captain](docs/reference/agents.md) | [Navigator](docs/reference/agents.md) |
59
- | [Analyst](docs/reference/agents.md) | [Historian](docs/reference/agents.md) | [Fisherman](docs/reference/agents.md) |
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/reference/agents.md) for what each one does.
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
- ## What you get
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/guides/automated-tests.md) for the test output and [Reporting](docs/guides/reporting.md) for reports.
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/reference/prerequisites.md)
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/guides/knowledge.md).
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/guides/customization.md).
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/guides/api-testing.md).
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/README.md) covers everything, starting with the [Getting Started guide](docs/getting-started.md).
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
- No, Explorbot is a separate application designed for constant testing. Cursor, Codex, and Claude Code are coding agents not relevant here.
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. Their models are too slow for the way Explorbot works. Use pay-per-token providers like Groq and OpenRouter.
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/contributing/scripting.md).
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!
@@ -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: sessionFile,
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 === true ? 'output/session.json' : 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().createAction();
721
+ const action = explorBot.getExplorer().action();
723
722
  await action.execute(command);
724
723
 
725
724
  log('Command executed successfully');
726
- const state = explorBot.getExplorer().getStateManager().getCurrentState();
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
- if (options?.path) process.chdir(originalCwd);
58
- throw new Error('No configuration file found. Create apibot.config.js or apibot.config.ts');
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
- const meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
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}. Falling back to static.`);
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 = explorer.getStateManager().getCurrentState();
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 = explorer.getStateManager().getCurrentState();
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 (urlChanged) {
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.createAction();
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), false);
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.createAction();
158
- const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`, false);
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.createAction();
166
- await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`, false);
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
  }