explorbot 0.2.2 → 0.2.4

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 (214) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +59 -38
  3. package/boat/api-tester/src/apibot.ts +4 -2
  4. package/boat/api-tester/src/cli.ts +2 -2
  5. package/boat/api-tester/src/config.ts +43 -10
  6. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  7. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  8. package/boat/doc-collector/src/cli.ts +1 -0
  9. package/boat/doc-collector/src/config.ts +4 -2
  10. package/boat/doc-collector/src/docs-renderer.ts +18 -4
  11. package/boat/doc-collector/src/state-diagram.ts +61 -14
  12. package/boat/prima/bin/prima-cli.ts +5 -0
  13. package/boat/prima/package.json +16 -0
  14. package/boat/prima/src/activity-line.ts +33 -0
  15. package/boat/prima/src/cli.ts +251 -0
  16. package/boat/prima/src/envelope.ts +169 -0
  17. package/boat/prima/src/prima.ts +1124 -0
  18. package/boat/prima/src/pw-parser.ts +27 -0
  19. package/boat/prima/src/pw-registry.ts +74 -0
  20. package/boat/prima/src/session-log.ts +126 -0
  21. package/dist/bin/explorbot-cli.js +51 -32
  22. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  23. package/dist/boat/api-tester/src/apibot.js +3 -2
  24. package/dist/boat/api-tester/src/cli.js +2 -2
  25. package/dist/boat/api-tester/src/config.js +40 -10
  26. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  27. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  28. package/dist/boat/doc-collector/src/cli.js +1 -0
  29. package/dist/boat/doc-collector/src/config.js +4 -2
  30. package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
  31. package/dist/boat/doc-collector/src/state-diagram.js +57 -13
  32. package/dist/boat/prima/bin/prima-cli.js +4 -0
  33. package/dist/boat/prima/src/activity-line.js +30 -0
  34. package/dist/boat/prima/src/cli.js +220 -0
  35. package/dist/boat/prima/src/envelope.js +142 -0
  36. package/dist/boat/prima/src/prima.js +1031 -0
  37. package/dist/boat/prima/src/pw-parser.js +30 -0
  38. package/dist/boat/prima/src/pw-registry.js +65 -0
  39. package/dist/boat/prima/src/session-log.js +108 -0
  40. package/dist/models.json +3 -0
  41. package/dist/package.json +8 -3
  42. package/dist/rules/navigator/verification-actions.md +20 -0
  43. package/dist/src/action-result.d.ts +7 -0
  44. package/dist/src/action-result.js +4 -0
  45. package/dist/src/action.d.ts +7 -2
  46. package/dist/src/action.js +41 -6
  47. package/dist/src/ai/captain/mixin.js +3 -4
  48. package/dist/src/ai/captain/web-mode.js +7 -4
  49. package/dist/src/ai/captain.js +2 -0
  50. package/dist/src/ai/navigator.d.ts +11 -0
  51. package/dist/src/ai/navigator.js +21 -12
  52. package/dist/src/ai/pilot.d.ts +4 -0
  53. package/dist/src/ai/pilot.js +48 -2
  54. package/dist/src/ai/planner.d.ts +1 -0
  55. package/dist/src/ai/planner.js +6 -0
  56. package/dist/src/ai/provider.js +2 -2
  57. package/dist/src/ai/rerunner.js +2 -1
  58. package/dist/src/ai/researcher.js +2 -2
  59. package/dist/src/ai/rules.js +17 -10
  60. package/dist/src/ai/task-agent.js +5 -2
  61. package/dist/src/ai/tester.d.ts +1 -0
  62. package/dist/src/ai/tester.js +33 -20
  63. package/dist/src/ai/tools.d.ts +14 -0
  64. package/dist/src/ai/tools.js +113 -36
  65. package/dist/src/application-spec-contract.d.ts +8 -0
  66. package/dist/src/application-spec-contract.js +8 -0
  67. package/dist/src/application-spec.d.ts +15 -0
  68. package/dist/src/application-spec.js +71 -0
  69. package/dist/src/browser-server.d.ts +12 -6
  70. package/dist/src/browser-server.js +74 -19
  71. package/dist/src/commands/clean-command.js +2 -7
  72. package/dist/src/commands/init-command.d.ts +5 -0
  73. package/dist/src/commands/init-command.js +119 -1
  74. package/dist/src/commands/navigate-command.js +1 -1
  75. package/dist/src/commands/research-command.js +1 -1
  76. package/dist/src/commands/sites-command.d.ts +6 -0
  77. package/dist/src/commands/sites-command.js +23 -0
  78. package/dist/src/components/InitWizard.d.ts +10 -0
  79. package/dist/src/components/InitWizard.js +133 -0
  80. package/dist/src/components/InputReadline.d.ts +1 -0
  81. package/dist/src/components/InputReadline.js +7 -4
  82. package/dist/src/config.d.ts +25 -5
  83. package/dist/src/config.js +158 -40
  84. package/dist/src/execution-controller.d.ts +2 -0
  85. package/dist/src/execution-controller.js +6 -0
  86. package/dist/src/explorbot.d.ts +11 -1
  87. package/dist/src/explorbot.js +27 -6
  88. package/dist/src/explorer.d.ts +4 -1
  89. package/dist/src/explorer.js +42 -9
  90. package/dist/src/global-config.d.ts +22 -0
  91. package/dist/src/global-config.js +117 -0
  92. package/dist/src/knowledge-tracker.d.ts +5 -1
  93. package/dist/src/knowledge-tracker.js +14 -1
  94. package/dist/src/playwright-recorder.js +30 -0
  95. package/dist/src/remote.d.ts +54 -0
  96. package/dist/src/remote.js +229 -0
  97. package/dist/src/reporter.d.ts +1 -0
  98. package/dist/src/reporter.js +7 -1
  99. package/dist/src/state-manager.d.ts +2 -1
  100. package/dist/src/stats.d.ts +1 -0
  101. package/dist/src/stats.js +1 -0
  102. package/dist/src/test-plan.d.ts +2 -0
  103. package/dist/src/test-plan.js +7 -0
  104. package/dist/src/utils/aria.d.ts +2 -8
  105. package/dist/src/utils/aria.js +69 -40
  106. package/dist/src/utils/cli-name.js +6 -2
  107. package/dist/src/utils/html.js +1 -0
  108. package/dist/src/utils/logger.d.ts +6 -0
  109. package/dist/src/utils/logger.js +24 -0
  110. package/dist/src/utils/page-readiness.js +18 -1
  111. package/dist/src/utils/test-files.js +1 -2
  112. package/dist/src/utils/url-matcher.d.ts +1 -0
  113. package/dist/src/utils/url-matcher.js +12 -0
  114. package/dist/src/utils/web-element.d.ts +2 -0
  115. package/dist/src/utils/web-element.js +8 -0
  116. package/dist/src/utils/web-sandbox.d.ts +1 -1
  117. package/dist/src/utils/web-sandbox.js +2 -3
  118. package/docs/api-testing/basics.md +90 -0
  119. package/docs/api-testing/planning.md +57 -0
  120. package/docs/api-testing/running-tests.md +55 -0
  121. package/docs/assets/cloud-report.png +0 -0
  122. package/docs/assets/html-report.png +0 -0
  123. package/docs/assets/langfuse-trace.png +0 -0
  124. package/docs/assets/successful-explore-run.png +0 -0
  125. package/docs/basics/getting-started.md +140 -0
  126. package/docs/basics/prerequisites.md +63 -0
  127. package/docs/basics/providers.md +362 -0
  128. package/docs/basics/running.md +78 -0
  129. package/docs/contributing/ai-integration-tests.md +57 -0
  130. package/docs/contributing/contributing.md +90 -0
  131. package/docs/contributing/demo-videos.md +36 -0
  132. package/docs/contributing/npm-package.md +138 -0
  133. package/docs/contributing/observability.md +227 -0
  134. package/docs/contributing/regression-tests.md +103 -0
  135. package/docs/contributing/testing.md +95 -0
  136. package/docs/doc-collection/basics.md +128 -0
  137. package/docs/doc-collection/crawling.md +67 -0
  138. package/docs/doc-collection/interactive-mode.md +99 -0
  139. package/docs/index.json +86 -0
  140. package/docs/reference/commands.md +994 -0
  141. package/docs/reference/configuration.md +569 -0
  142. package/docs/reference/scripting.md +303 -0
  143. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  144. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  145. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  146. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  147. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  148. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  149. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  150. package/docs/web-testing/agents.md +158 -0
  151. package/docs/web-testing/automated-tests.md +134 -0
  152. package/docs/web-testing/basics.md +91 -0
  153. package/docs/web-testing/customization.md +131 -0
  154. package/docs/web-testing/hooks.md +238 -0
  155. package/docs/web-testing/page-interaction.md +84 -0
  156. package/docs/web-testing/planner.md +122 -0
  157. package/docs/web-testing/rerun.md +164 -0
  158. package/docs/web-testing/researcher.md +380 -0
  159. package/docs/workflow/agentic-usage.md +233 -0
  160. package/docs/workflow/application-spec.md +73 -0
  161. package/docs/workflow/ci.md +202 -0
  162. package/docs/workflow/knowledge.md +310 -0
  163. package/docs/workflow/planning-styles.md +67 -0
  164. package/docs/workflow/reporting.md +133 -0
  165. package/docs/workflow/test-plans.md +90 -0
  166. package/models.json +3 -0
  167. package/package.json +8 -3
  168. package/rules/navigator/verification-actions.md +20 -0
  169. package/src/action-result.ts +11 -0
  170. package/src/action.ts +47 -7
  171. package/src/ai/captain/mixin.ts +3 -3
  172. package/src/ai/captain/web-mode.ts +7 -4
  173. package/src/ai/captain.ts +3 -0
  174. package/src/ai/navigator.ts +26 -15
  175. package/src/ai/pilot.ts +53 -2
  176. package/src/ai/planner.ts +7 -0
  177. package/src/ai/provider.ts +2 -4
  178. package/src/ai/rerunner.ts +2 -1
  179. package/src/ai/researcher.ts +2 -2
  180. package/src/ai/rules.ts +17 -10
  181. package/src/ai/task-agent.ts +4 -2
  182. package/src/ai/tester.ts +32 -18
  183. package/src/ai/tools.ts +132 -36
  184. package/src/application-spec-contract.ts +10 -0
  185. package/src/application-spec.ts +87 -0
  186. package/src/browser-server.ts +74 -19
  187. package/src/commands/clean-command.ts +1 -6
  188. package/src/commands/init-command.ts +146 -1
  189. package/src/commands/navigate-command.ts +1 -1
  190. package/src/commands/research-command.ts +1 -1
  191. package/src/commands/sites-command.ts +27 -0
  192. package/src/components/InitWizard.tsx +166 -0
  193. package/src/components/InputReadline.tsx +8 -4
  194. package/src/config.ts +177 -43
  195. package/src/execution-controller.ts +8 -0
  196. package/src/explorbot.ts +34 -7
  197. package/src/explorer.ts +47 -9
  198. package/src/global-config.ts +148 -0
  199. package/src/knowledge-tracker.ts +17 -1
  200. package/src/playwright-recorder.ts +23 -0
  201. package/src/remote.ts +238 -0
  202. package/src/reporter.ts +7 -1
  203. package/src/state-manager.ts +2 -1
  204. package/src/stats.ts +1 -0
  205. package/src/test-plan.ts +9 -0
  206. package/src/utils/aria.ts +65 -45
  207. package/src/utils/cli-name.ts +5 -2
  208. package/src/utils/html.ts +1 -0
  209. package/src/utils/logger.ts +24 -1
  210. package/src/utils/page-readiness.ts +24 -1
  211. package/src/utils/test-files.ts +1 -2
  212. package/src/utils/url-matcher.ts +13 -0
  213. package/src/utils/web-element.ts +9 -0
  214. package/src/utils/web-sandbox.ts +3 -4
@@ -0,0 +1,30 @@
1
+ const FUNCTION_SHAPE = /^(async\s+)?(function\b|\()/;
2
+ export function isFunctionExpression(expr) {
3
+ const trimmed = expr.trim();
4
+ if (!trimmed)
5
+ return { valid: false, error: 'empty expression; pass a function like ({ page }) => ...' };
6
+ if (!FUNCTION_SHAPE.test(trimmed))
7
+ return { valid: false, error: 'expression must be a function like ({ page }) => ... destructuring the playwright objects it needs' };
8
+ try {
9
+ new Function(`return (${trimmed})`);
10
+ }
11
+ catch (e) {
12
+ return { valid: false, error: `not a valid function expression: ${e.message}` };
13
+ }
14
+ return { valid: true };
15
+ }
16
+ export function toCodeceptWrapper(expr) {
17
+ return `return I.usePlaywrightTo('pw', async (playwright) => (${expr.trim()})(playwright))`;
18
+ }
19
+ export function takePwValue(held) {
20
+ if (held === undefined || held === null)
21
+ return undefined;
22
+ if (typeof held === 'string')
23
+ return held;
24
+ try {
25
+ return JSON.stringify(held, null, 2);
26
+ }
27
+ catch {
28
+ return String(held);
29
+ }
30
+ }
@@ -0,0 +1,65 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ const DEFAULT_TITLE = 'default';
5
+ const DEFAULT_BROWSER = 'chromium';
6
+ export function registryDir() {
7
+ if (process.platform === 'darwin')
8
+ return path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright', 'b');
9
+ if (process.platform === 'win32')
10
+ return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'ms-playwright', 'b');
11
+ return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'ms-playwright', 'b');
12
+ }
13
+ export function readDescriptors(dir = registryDir()) {
14
+ const descriptors = [];
15
+ for (const fileName of listFiles(dir)) {
16
+ const descriptor = parseDescriptor(path.join(dir, fileName));
17
+ if (descriptor)
18
+ descriptors.push(descriptor);
19
+ }
20
+ return descriptors;
21
+ }
22
+ export function selectDescriptor(descriptors, opts = {}) {
23
+ const candidates = descriptors;
24
+ if (!candidates.length)
25
+ return { candidates };
26
+ if (opts.title) {
27
+ const titled = candidates.find((descriptor) => descriptor.title === opts.title);
28
+ if (titled)
29
+ return { match: titled, candidates };
30
+ return { candidates };
31
+ }
32
+ const preferred = candidates.find((descriptor) => descriptor.title === DEFAULT_TITLE);
33
+ if (preferred)
34
+ return { match: preferred, candidates };
35
+ if (candidates.length === 1)
36
+ return { match: candidates[0], candidates };
37
+ return { candidates };
38
+ }
39
+ function listFiles(dir) {
40
+ try {
41
+ return readdirSync(dir);
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ function parseDescriptor(file) {
48
+ let data;
49
+ try {
50
+ data = JSON.parse(readFileSync(file, 'utf8'));
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ if (!data?.endpoint || !data?.title)
56
+ return null;
57
+ return {
58
+ file,
59
+ title: data.title,
60
+ endpoint: data.endpoint,
61
+ workspaceDir: data.workspaceDir || '',
62
+ browserName: data.browser?.browserName || DEFAULT_BROWSER,
63
+ playwrightLib: data.playwrightLib || '',
64
+ };
65
+ }
@@ -0,0 +1,108 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { outputPath } from "../../../src/config.js";
4
+ import { safeFilename } from "../../../src/utils/strings.js";
5
+ import { renderEnvelope } from "./envelope.js";
6
+ const EXPECTATION_STATUS = { passed: 'passed', failed: 'failed', unverified: 'skipped' };
7
+ export function sessionsDir() {
8
+ return outputPath('prima', 'sessions');
9
+ }
10
+ export function sessionFile(key) {
11
+ return path.join(sessionsDir(), `${safeFilename(key)}.jsonl`);
12
+ }
13
+ export function latestSessionFile() {
14
+ const dir = sessionsDir();
15
+ if (!existsSync(dir))
16
+ return null;
17
+ const files = readdirSync(dir)
18
+ .filter((name) => name.endsWith('.jsonl'))
19
+ .map((name) => path.join(dir, name))
20
+ .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
21
+ return files[0] || null;
22
+ }
23
+ export function recordCommand(file, session, envelope, durationMs) {
24
+ mkdirSync(path.dirname(file), { recursive: true });
25
+ if (recordedEndpoint(file) !== session.endpoint)
26
+ writeFileSync(file, jsonLine({ action: 'createRun', endpoint: session.endpoint, params: { title: session.title } }), 'utf-8');
27
+ appendFileSync(file, jsonLine({ action: 'addTest', testId: commandTest(envelope, durationMs) }), 'utf-8');
28
+ }
29
+ export function readSession(file) {
30
+ const entries = readEntries(file);
31
+ const run = entries.find((entry) => entry.action === 'createRun');
32
+ const tests = entries.filter((entry) => entry.action === 'addTest').map((entry) => entry.testId);
33
+ return { title: run?.params?.title || 'prima session', tests };
34
+ }
35
+ function commandTest(envelope, durationMs) {
36
+ let status = 'failed';
37
+ if (envelope.ok)
38
+ status = 'passed';
39
+ const test = {
40
+ rid: envelope.status,
41
+ title: envelope.command,
42
+ suite_title: 'Prima session',
43
+ status,
44
+ file: '<prima>',
45
+ description: [`page: ${envelope.page.url}`, envelope.stepFiles && `artifacts: ${envelope.stepFiles}`].filter(Boolean).join('\n'),
46
+ code: (envelope.used || []).join('\n'),
47
+ steps: commandSteps(envelope),
48
+ logs: renderEnvelope(envelope),
49
+ time: Math.round(durationMs),
50
+ };
51
+ if (envelope.failure)
52
+ test.error = { message: envelope.failure.error };
53
+ return test;
54
+ }
55
+ function commandSteps(envelope) {
56
+ const steps = [];
57
+ for (const step of envelope.steps || []) {
58
+ const entry = { category: 'user', title: step.label, status: 'passed', duration: 0, log: step.proof };
59
+ if (!step.ok) {
60
+ entry.status = 'failed';
61
+ entry.error = step.proof;
62
+ }
63
+ steps.push(entry);
64
+ }
65
+ for (const expectation of envelope.expectations || []) {
66
+ const entry = { category: 'user', title: `expected: ${expectation.text}`, status: EXPECTATION_STATUS[expectation.status], duration: 0 };
67
+ if (expectation.status === 'failed')
68
+ entry.error = 'the run did not reach this outcome';
69
+ steps.push(entry);
70
+ }
71
+ for (const assertion of envelope.assertions || []) {
72
+ const entry = { category: 'user', title: assertion.code, status: 'passed', duration: 0, log: assertion.proof.join('\n') };
73
+ if (!assertion.passed) {
74
+ entry.status = 'failed';
75
+ entry.error = 'the assertion did not hold on the page';
76
+ }
77
+ steps.push(entry);
78
+ }
79
+ return steps;
80
+ }
81
+ function recordedEndpoint(file) {
82
+ const [first] = readEntries(file);
83
+ return first?.endpoint || null;
84
+ }
85
+ function readEntries(file) {
86
+ if (!existsSync(file))
87
+ return [];
88
+ const entries = [];
89
+ for (const line of readFileSync(file, 'utf-8').split('\n')) {
90
+ if (!line.trim())
91
+ continue;
92
+ const entry = parseLine(line);
93
+ if (entry)
94
+ entries.push(entry);
95
+ }
96
+ return entries;
97
+ }
98
+ function parseLine(line) {
99
+ try {
100
+ return JSON.parse(line);
101
+ }
102
+ catch {
103
+ return null;
104
+ }
105
+ }
106
+ function jsonLine(entry) {
107
+ return `${JSON.stringify(entry)}\n`;
108
+ }
package/dist/models.json CHANGED
@@ -4,6 +4,9 @@
4
4
  "visionModel": "google/gemma-4-31b-it:nitro",
5
5
  "agenticModel": "google/gemma-4-31b-it:nitro"
6
6
  },
7
+ "poolside": {
8
+ "model": "poolside/laguna-xs-2.1"
9
+ },
7
10
  "groq": {
8
11
  "model": "openai/gpt-oss-20b",
9
12
  "visionModel": "qwen/qwen3.6-27b",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -14,10 +14,12 @@
14
14
  }
15
15
  },
16
16
  "bin": {
17
- "explorbot": "./dist/bin/explorbot-cli.js"
17
+ "explorbot": "./dist/bin/explorbot-cli.js",
18
+ "prima": "./dist/boat/prima/bin/prima-cli.js"
18
19
  },
19
20
  "files": [
20
21
  "dist/",
22
+ "docs/",
21
23
  "src/**/*.ts",
22
24
  "src/**/*.tsx",
23
25
  "bin/**/*.ts",
@@ -25,6 +27,9 @@
25
27
  "boat/doc-collector/src/**/*.ts",
26
28
  "boat/doc-collector/bin/**/*.ts",
27
29
  "boat/doc-collector/package.json",
30
+ "boat/prima/src/**/*.ts",
31
+ "boat/prima/bin/**/*.ts",
32
+ "boat/prima/package.json",
28
33
  "rules/",
29
34
  "assets/sample-files/",
30
35
  "models.json"
@@ -115,7 +120,7 @@
115
120
  "ora-classic": "^5.4.2",
116
121
  "parse5": "^8.0.0",
117
122
  "pixelmatch": "^7.2.0",
118
- "playwright": "^1.60",
123
+ "playwright": "^1.62",
119
124
  "pngjs": "^7.0.0",
120
125
  "react": "^19.1.1",
121
126
  "sambanova-ai-provider": "^1.2.2",
@@ -105,9 +105,29 @@ Checks that page source does NOT contain expected text.
105
105
  I.dontSeeInSource('error-class');
106
106
  </example>
107
107
 
108
+ ### Asserting the state of a control
109
+
110
+ State means disabled, checked, readonly, required, selected, expanded. A presence assertion says
111
+ nothing about it, so assert it with an attribute selector:
112
+
113
+ <example>
114
+ I.seeElement('button[aria-label="Submit"][disabled]');
115
+ I.dontSeeElement('button[aria-label="Submit"][disabled]');
116
+ I.seeElement('input[name="accept"][checked]');
117
+ I.seeElement('button[aria-label="Details"][aria-expanded="true"]');
118
+ </example>
119
+
120
+ Use I.seeElement for the state you expect and I.dontSeeElement for the state you expect to be
121
+ absent — that pair expresses both directions.
122
+
123
+ I.seeAttributesOnElements(<locator>, { disabled: true }) also exists, but only takes a state that
124
+ must be PRESENT and does not resolve reliably against a role/text locator. Prefer the selector form above.
125
+
108
126
  <verification_rules>
109
127
  Be strict in assertions to avoid false positives.
110
128
  Prefer I.seeElement() with ARIA locators - most reliable.
129
+ For a claim about a control's state, assert it with an attribute selector — presence of the element is not evidence of its state.
130
+ If no assertion above can express the claim, say so instead of proposing an assertion that checks something weaker.
111
131
  I.see() and I.dontSee() MUST include context parameter.
112
132
  For input field values, ALWAYS use I.seeInField() — never check value via CSS attribute selectors or I.seeInSource.
113
133
  Prefer text locators (label, name, placeholder) for form fields: I.seeInField('Search', 'value') over I.seeInField('input[name="search"]', 'value').
@@ -24,6 +24,7 @@ interface ActionResultData extends WebPageState {
24
24
  }>;
25
25
  ariaSnapshot?: string | null;
26
26
  ariaSnapshotFile?: string;
27
+ focusedElement?: FocusedElement | null;
27
28
  iframeURL?: string;
28
29
  links?: Link[];
29
30
  }
@@ -69,6 +70,7 @@ export declare class ActionResult implements ActionResultData {
69
70
  snapshotCache: TTLCache<string>;
70
71
  readonly logFile: string | undefined;
71
72
  readonly ariaSnapshotFile: string | undefined;
73
+ readonly focusedElement: FocusedElement | null;
72
74
  _ariaSnapshot: string | null | undefined;
73
75
  _lastExtractedHtml: string | undefined;
74
76
  notes: string[];
@@ -128,4 +130,9 @@ export declare class Diff {
128
130
  get htmlDiff(): HtmlDiffResult | null;
129
131
  calculate(): Promise<void>;
130
132
  }
133
+ export interface FocusedElement {
134
+ role: string;
135
+ name: string;
136
+ value?: string;
137
+ }
131
138
  export {};
@@ -30,6 +30,7 @@ export class ActionResult {
30
30
  snapshotCache = new TTLCache();
31
31
  logFile = undefined;
32
32
  ariaSnapshotFile = undefined;
33
+ focusedElement = null;
33
34
  _ariaSnapshot = undefined;
34
35
  _lastExtractedHtml = undefined;
35
36
  notes = [];
@@ -61,6 +62,9 @@ export class ActionResult {
61
62
  if (data.ariaSnapshotFile !== undefined) {
62
63
  this.ariaSnapshotFile = data.ariaSnapshotFile;
63
64
  }
65
+ if (data.focusedElement !== undefined) {
66
+ this.focusedElement = data.focusedElement;
67
+ }
64
68
  // Store HTML in a private property if provided
65
69
  if (data.html !== undefined) {
66
70
  this._html = data.html;
@@ -15,6 +15,7 @@ declare class Action {
15
15
  name: string;
16
16
  args: any[];
17
17
  }>;
18
+ lastValue: unknown;
18
19
  recorder?: PlaywrightRecorder;
19
20
  recovery: RecoveryRunner;
20
21
  mainDocumentStatus: number | undefined;
@@ -24,7 +25,7 @@ declare class Action {
24
25
  includeScreenshot?: boolean;
25
26
  codeBlock?: string;
26
27
  }): Promise<ActionResult>;
27
- execute(code: string): Promise<Action>;
28
+ execute(code: string, opts?: ExecuteOptions): Promise<Action>;
28
29
  captureOnce({ includeScreenshot, codeBlock }?: {
29
30
  includeScreenshot?: boolean;
30
31
  codeBlock?: string;
@@ -40,12 +41,16 @@ declare class Action {
40
41
  id?: string;
41
42
  }>>;
42
43
  captureBrowserLogs(): Promise<any[]>;
43
- executeOnce(code: string): Promise<Action>;
44
+ executeOnce(code: string, verbatim?: boolean): Promise<Action>;
44
45
  attempt(codeBlock: string, originalMessage?: string): Promise<boolean>;
45
46
  exitIframe(): Promise<void>;
46
47
  getActor(): CodeceptJS.I;
47
48
  getActionResult(): ActionResult | null;
49
+ restorePageTimeout(): void;
48
50
  waitForPageReadiness(page: any): Promise<void>;
49
51
  }
50
52
  export default Action;
51
53
  export type RecoveryRunner = <T>(fn: () => Promise<T>) => Promise<T>;
54
+ export interface ExecuteOptions {
55
+ verbatim?: boolean;
56
+ }
@@ -15,6 +15,8 @@ import { safeFilename } from "./utils/strings.js";
15
15
  import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
16
16
  const debugLog = createDebug('explorbot:action');
17
17
  const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
18
+ const DEFAULT_ACTION_TIMEOUT = 3000;
19
+ const DEFAULT_PAGE_TIMEOUT = 3000;
18
20
  class Action {
19
21
  actor;
20
22
  stateManager;
@@ -26,6 +28,7 @@ class Action {
26
28
  playwrightHelper;
27
29
  playwrightGroupId = null;
28
30
  assertionSteps = [];
31
+ lastValue;
29
32
  recorder;
30
33
  recovery;
31
34
  mainDocumentStatus = undefined;
@@ -57,8 +60,8 @@ class Action {
57
60
  async capturePageState(opts = {}) {
58
61
  return this.recovery(() => this.captureOnce(opts));
59
62
  }
60
- async execute(code) {
61
- return this.recovery(() => this.executeOnce(code));
63
+ async execute(code, opts = {}) {
64
+ return this.recovery(() => this.executeOnce(code, opts.verbatim));
62
65
  }
63
66
  async captureOnce({ includeScreenshot = false, codeBlock } = {}) {
64
67
  try {
@@ -120,9 +123,11 @@ class Action {
120
123
  const iframeSnapshots = await this.captureIframeSnapshots(html);
121
124
  let ariaSnapshot = null;
122
125
  let ariaSnapshotFile = undefined;
126
+ let focusedElement = null;
123
127
  try {
124
128
  const page = this.playwrightHelper.page;
125
129
  ariaSnapshot = await page.locator('body').ariaSnapshot();
130
+ focusedElement = await page.evaluate(readFocusedElement);
126
131
  }
127
132
  catch (err) {
128
133
  debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
@@ -145,6 +150,7 @@ class Action {
145
150
  iframeSnapshots,
146
151
  ariaSnapshot,
147
152
  ariaSnapshotFile,
153
+ focusedElement,
148
154
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
149
155
  });
150
156
  this.stateManager.updateState(result, codeBlock);
@@ -251,7 +257,7 @@ class Action {
251
257
  return [];
252
258
  }
253
259
  }
254
- async executeOnce(code) {
260
+ async executeOnce(code, verbatim = false) {
255
261
  let error = null;
256
262
  setActivity('🔎 Browsing...', 'action');
257
263
  let codeString = code.replace(/^\(I\) => /, '').trim();
@@ -265,23 +271,26 @@ class Action {
265
271
  const tracer = trace.getTracer('ai');
266
272
  const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
267
273
  setStepSpanParent(stepSpan);
268
- const sanitizedCode = sanitizeCodeBlock(codeString);
269
- const isPlaywright = hasPlaywrightCommands(sanitizedCode);
274
+ const sanitizedCode = verbatim ? codeString : sanitizeCodeBlock(codeString);
275
+ const isPlaywright = !verbatim && hasPlaywrightCommands(sanitizedCode);
270
276
  try {
271
277
  debugLog('Executing action:', codeString);
272
278
  if (!sanitizedCode) {
273
279
  throw new Error('No valid I.* or page.* commands found in code block');
274
280
  }
281
+ this.playwrightHelper?.page?.setDefaultTimeout(this.config.action?.timeout ?? DEFAULT_ACTION_TIMEOUT);
275
282
  if (isPlaywright) {
276
283
  const page = this.playwrightHelper.page;
277
284
  await playwrightSandbox(page, sanitizedCode);
278
285
  await sleep(this.config.action?.delay || 500);
279
286
  }
280
287
  else {
281
- codeceptJSSandbox(this.actor, sanitizedCode);
288
+ const returned = codeceptJSSandbox(this.actor, sanitizedCode);
282
289
  await recorder.add(() => sleep(this.config.action?.delay || 500));
283
290
  await recorder.promise();
291
+ this.lastValue = await returned;
284
292
  }
293
+ this.restorePageTimeout();
285
294
  if (executedSteps.length > 0) {
286
295
  codeString = executedSteps.join('\n');
287
296
  }
@@ -300,6 +309,7 @@ class Action {
300
309
  throw err;
301
310
  }
302
311
  finally {
312
+ this.restorePageTimeout();
303
313
  detachMainDocumentResponse();
304
314
  if (groupId)
305
315
  await this.recorder.endAction();
@@ -346,6 +356,9 @@ class Action {
346
356
  getActionResult() {
347
357
  return this.actionResult;
348
358
  }
359
+ restorePageTimeout() {
360
+ this.playwrightHelper?.page?.setDefaultTimeout(this.config.playwright.timeout ?? DEFAULT_PAGE_TIMEOUT);
361
+ }
349
362
  async waitForPageReadiness(page) {
350
363
  await waitForPageReadiness(page, {
351
364
  timeout: this.config.playwright.waitForTimeout,
@@ -408,3 +421,25 @@ const detachStepLogger = (listener) => {
408
421
  codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
409
422
  codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
410
423
  };
424
+ const readFocusedElement = () => {
425
+ const el = document.activeElement;
426
+ if (!el || el === document.body)
427
+ return null;
428
+ const tag = el.tagName.toLowerCase();
429
+ const textish = new Set(['text', 'search', 'email', 'password', 'url', 'tel', 'number']);
430
+ let role = el.getAttribute('role') || tag;
431
+ if (tag === 'textarea' || el.isContentEditable)
432
+ role = 'textbox';
433
+ if (tag === 'input' && textish.has(el.type))
434
+ role = 'textbox';
435
+ if (tag === 'select')
436
+ role = 'combobox';
437
+ if (tag === 'a')
438
+ role = 'link';
439
+ const label = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.labels?.[0]?.textContent || el.textContent || '';
440
+ const focused = { role, name: label.trim().slice(0, 80) };
441
+ const value = el.value ?? el.textContent;
442
+ if (typeof value === 'string' && value)
443
+ focused.value = value.slice(0, 200);
444
+ return focused;
445
+ };
@@ -1,10 +1,9 @@
1
- import { dirname } from 'node:path';
2
1
  import { ConfigParser } from "../../config.js";
3
2
  import { createDebug } from '../../utils/logger.js';
4
3
  export const debugLog = createDebug('explorbot:captain');
5
4
  export function resolveProjectRoot() {
6
- const configPath = ConfigParser.getInstance().getConfigPath();
7
- if (!configPath)
5
+ const configParser = ConfigParser.getInstance();
6
+ if (!configParser.getConfigPath())
8
7
  return null;
9
- return dirname(configPath);
8
+ return configParser.getProjectRoot();
10
9
  }
@@ -16,7 +16,7 @@ export function WithWebMode(Base) {
16
16
  navigator: ctx.explorBot.agentNavigator(),
17
17
  });
18
18
  const { see, context, visualClick, learnExperience } = agentTools;
19
- return {
19
+ const tools = {
20
20
  navigate: tool({
21
21
  description: 'Navigate to a URL or page description using AI-powered navigation.',
22
22
  inputSchema: z.object({
@@ -25,7 +25,7 @@ export function WithWebMode(Base) {
25
25
  execute: async ({ destination }) => {
26
26
  try {
27
27
  debugLog('navigate', destination);
28
- await ctx.explorBot.agentNavigator().visit(destination);
28
+ await ctx.explorBot.visit(destination);
29
29
  const stateManager = ctx.explorBot.stateManager();
30
30
  const state = stateManager.getCurrentState();
31
31
  return { success: true, url: state?.url, title: state?.title };
@@ -114,11 +114,14 @@ export function WithWebMode(Base) {
114
114
  },
115
115
  }),
116
116
  ...codeceptTools,
117
- see,
118
117
  context,
119
- visualClick,
120
118
  learnExperience,
121
119
  };
120
+ if (see)
121
+ tools.see = see;
122
+ if (visualClick)
123
+ tools.visualClick = visualClick;
124
+ return tools;
122
125
  }
123
126
  webModePrompt() {
124
127
  return dedent `
@@ -15,6 +15,7 @@ import { WithWebMode } from "./captain/web-mode.js";
15
15
  import { toolExecutionLabel } from './conversation.js';
16
16
  import { Researcher } from "./researcher.js";
17
17
  import { TaskAgent } from "./task-agent.js";
18
+ import { withdrawVisionTools } from "./tools.js";
18
19
  const MAX_STEPS = 15;
19
20
  const CaptainBase = WithTestMode(WithWebMode(WithIdleMode(TaskAgent)));
20
21
  export class Captain extends CaptainBase {
@@ -389,6 +390,7 @@ export class Captain extends CaptainBase {
389
390
  stop();
390
391
  return;
391
392
  }
393
+ withdrawVisionTools(tools);
392
394
  const currentState = stateManager.getCurrentState();
393
395
  if (!currentState && this.getMode() !== 'idle') {
394
396
  stop();
@@ -32,6 +32,10 @@ declare class Navigator implements Agent {
32
32
  resolveState(message: string, actionResult: ActionResult, opts?: {
33
33
  action?: Action;
34
34
  expectedUrl?: string;
35
+ onAttempt?: (attempt: {
36
+ code: string;
37
+ error?: string;
38
+ }) => void;
35
39
  }): Promise<boolean>;
36
40
  buildExperienceTools(): {
37
41
  learnExperience: unknown;
@@ -46,6 +50,8 @@ declare class Navigator implements Agent {
46
50
  } | null>;
47
51
  verifyState(message: string, actionResult: ActionResult): Promise<{
48
52
  verified: boolean;
53
+ inexpressible: boolean;
54
+ results: AssertionResult[];
49
55
  successfulCodes: string[];
50
56
  assertionSteps: Array<{
51
57
  name: string;
@@ -55,4 +61,9 @@ declare class Navigator implements Agent {
55
61
  }>;
56
62
  checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
57
63
  }
64
+ export type AssertionResult = {
65
+ code: string;
66
+ passed: boolean;
67
+ proof: string[];
68
+ };
58
69
  export { Navigator };