explorbot 0.2.3 → 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.
- package/README.md +1 -1
- package/bin/explorbot-cli.ts +7 -1
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +115 -86
- package/boat/prima/src/envelope.ts +80 -52
- package/boat/prima/src/prima.ts +547 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +7 -1
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +97 -77
- package/dist/boat/prima/src/envelope.js +70 -44
- package/dist/boat/prima/src/prima.js +515 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +36 -1
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +7 -0
- package/dist/src/ai/navigator.js +10 -6
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +1 -1
- package/dist/src/ai/tester.js +33 -33
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +13 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +3 -1
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +38 -2
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +14 -8
- package/src/ai/pilot.ts +53 -2
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +32 -33
- package/src/ai/tools.ts +132 -36
- package/src/config.ts +15 -4
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +4 -2
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ EXPLORBOT_KNOWLEDGE="Log in as admin@example.com / secret123" \
|
|
|
138
138
|
npx explorbot explore /admin/users --max-tests 3
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
Output lands in a per-host
|
|
141
|
+
Output lands in a per-host site directory, `~/.explorbot/sites/<host>/`, so runs against the same app collect in one place and nothing is written to your project. Set `EXPLORBOT_EPHEMERAL=1` to keep nothing between runs. See [Agentic Usage](docs/workflow/agentic-usage.md).
|
|
142
142
|
|
|
143
143
|
## Teaching Explorbot
|
|
144
144
|
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { App } from '../src/components/App.js';
|
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
12
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
13
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
14
|
+
import { remote } from '../src/remote.js';
|
|
14
15
|
import { Stats } from '../src/stats.js';
|
|
15
16
|
import { Plan } from '../src/test-plan.js';
|
|
16
17
|
import { getCliName } from '../src/utils/cli-name.ts';
|
|
@@ -26,8 +27,9 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
|
|
|
26
27
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
|
|
27
28
|
|
|
28
29
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
30
|
+
remote.registerOption(program);
|
|
29
31
|
|
|
30
|
-
if (!process.env.EXPLORBOT_NO_BANNER) {
|
|
32
|
+
if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
|
|
31
33
|
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
32
34
|
}
|
|
33
35
|
|
|
@@ -99,6 +101,10 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
async function showStatsAndExit(code: number): Promise<never> {
|
|
104
|
+
if (remote.isAttached()) {
|
|
105
|
+
await remote.close(code);
|
|
106
|
+
process.exit(code);
|
|
107
|
+
}
|
|
102
108
|
if (Stats.hasActivity()) {
|
|
103
109
|
await new Promise<void>((resolve) => {
|
|
104
110
|
const { unmount } = render(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import { parseEnv } from 'node:util';
|
|
4
5
|
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot } from '../../../src/config.ts';
|
|
5
6
|
import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
|
|
@@ -213,10 +214,11 @@ export class ApibotConfigParser {
|
|
|
213
214
|
|
|
214
215
|
private async loadConfigModule(configPath: string): Promise<any> {
|
|
215
216
|
const ext = configPath.split('.').pop();
|
|
217
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
216
218
|
|
|
217
219
|
if (ext === 'ts') {
|
|
218
220
|
try {
|
|
219
|
-
return await import(
|
|
221
|
+
return await import(moduleUrl);
|
|
220
222
|
} catch {
|
|
221
223
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
222
224
|
return require(configPath);
|
|
@@ -224,7 +226,7 @@ export class ApibotConfigParser {
|
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
if (ext === 'js' || ext === 'mjs') {
|
|
227
|
-
return await import(
|
|
229
|
+
return await import(moduleUrl);
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
const content = readFileSync(configPath, 'utf8');
|
|
@@ -67,17 +67,32 @@ class Documentarian {
|
|
|
67
67
|
const message = error instanceof Error ? error.message : String(error);
|
|
68
68
|
tag('warning').log(`Interactive documentation failed: ${message}.`);
|
|
69
69
|
if (meaningfulInteractions.length > 0) {
|
|
70
|
-
tag('info').log(`
|
|
71
|
-
return this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
tag('info').log(`Retrying static documentation while preserving ${meaningfulInteractions.length} observed interaction(s).`);
|
|
71
|
+
return this.documentStatic(state, research)
|
|
72
|
+
.then((documentation) =>
|
|
73
|
+
this.normalizeDocumentation(
|
|
74
|
+
{
|
|
75
|
+
...documentation,
|
|
76
|
+
interactions: meaningfulInteractions,
|
|
77
|
+
},
|
|
78
|
+
state,
|
|
79
|
+
research
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
.catch((fallbackError) => {
|
|
83
|
+
const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
84
|
+
tag('warning').log(`Static documentation fallback failed: ${fallbackMessage}. Preserving observed interactions without AI summary.`);
|
|
85
|
+
return this.normalizeDocumentation(
|
|
86
|
+
{
|
|
87
|
+
summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
|
|
88
|
+
can: [],
|
|
89
|
+
might: [],
|
|
90
|
+
interactions: meaningfulInteractions,
|
|
91
|
+
},
|
|
92
|
+
state,
|
|
93
|
+
research
|
|
94
|
+
);
|
|
95
|
+
});
|
|
81
96
|
}
|
|
82
97
|
return this.documentStatic(state, research);
|
|
83
98
|
}
|
|
@@ -113,13 +128,13 @@ class Documentarian {
|
|
|
113
128
|
},
|
|
114
129
|
];
|
|
115
130
|
|
|
116
|
-
const response = await this.provider.generateObject(messages,
|
|
131
|
+
const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
|
|
117
132
|
agentName: 'documentarian',
|
|
118
133
|
});
|
|
119
134
|
|
|
120
135
|
return this.normalizeDocumentation(
|
|
121
136
|
{
|
|
122
|
-
...(response.object as
|
|
137
|
+
...(response.object as GeneratedPageDocumentation),
|
|
123
138
|
interactions,
|
|
124
139
|
},
|
|
125
140
|
state,
|
|
@@ -139,11 +154,11 @@ class Documentarian {
|
|
|
139
154
|
},
|
|
140
155
|
];
|
|
141
156
|
|
|
142
|
-
const response = await this.provider.generateObject(messages,
|
|
157
|
+
const response = await this.provider.generateObject(messages, generatedPageDocumentationSchema, undefined, {
|
|
143
158
|
agentName: 'documentarian',
|
|
144
159
|
});
|
|
145
160
|
|
|
146
|
-
return this.normalizeDocumentation(response.object as
|
|
161
|
+
return this.normalizeDocumentation(response.object as GeneratedPageDocumentation, state, research);
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
private getSystemPrompt(): string {
|
|
@@ -259,7 +274,7 @@ class Documentarian {
|
|
|
259
274
|
return message.includes('Failed to generate JSON') || message.includes('Failed to validate JSON') || message.includes('failed_generation') || message.includes('No object generated') || message.includes('response did not match schema');
|
|
260
275
|
}
|
|
261
276
|
|
|
262
|
-
private normalizeDocumentation(documentation: PageDocumentation, _state: WebPageState, _research: string): PageDocumentation {
|
|
277
|
+
private normalizeDocumentation(documentation: GeneratedPageDocumentation & Partial<Pick<PageDocumentation, 'interactions'>>, _state: WebPageState, _research: string): PageDocumentation {
|
|
263
278
|
const normalized = { ...documentation };
|
|
264
279
|
if (!normalized.interactions) {
|
|
265
280
|
normalized.interactions = undefined;
|
|
@@ -267,10 +282,10 @@ class Documentarian {
|
|
|
267
282
|
|
|
268
283
|
const qualityNotes = this.evaluateDocumentationQuality(normalized);
|
|
269
284
|
|
|
270
|
-
return {
|
|
285
|
+
return pageDocumentationSchema.parse({
|
|
271
286
|
...normalized,
|
|
272
287
|
qualityNotes,
|
|
273
|
-
};
|
|
288
|
+
});
|
|
274
289
|
}
|
|
275
290
|
|
|
276
291
|
private evaluateDocumentationQuality(documentation: PageDocumentation): string[] {
|
|
@@ -346,39 +361,54 @@ const stateTransitionSchema = z.object({
|
|
|
346
361
|
action: z.string(),
|
|
347
362
|
before: z.string(),
|
|
348
363
|
after: z.string(),
|
|
349
|
-
targetUrl: z.string().
|
|
350
|
-
discoveredUrls: z.array(z.string()).
|
|
351
|
-
newCapabilities: z.array(z.string()).
|
|
364
|
+
targetUrl: z.string().optional(),
|
|
365
|
+
discoveredUrls: z.array(z.string()).optional(),
|
|
366
|
+
newCapabilities: z.array(z.string()).optional(),
|
|
352
367
|
element: z
|
|
353
368
|
.object({
|
|
354
369
|
role: z.string(),
|
|
355
370
|
name: z.string(),
|
|
356
371
|
section: z.string(),
|
|
357
|
-
container: z.string().
|
|
358
|
-
locator: z.string().
|
|
372
|
+
container: z.string().optional(),
|
|
373
|
+
locator: z.string().optional(),
|
|
359
374
|
})
|
|
360
|
-
.
|
|
375
|
+
.optional(),
|
|
361
376
|
changes: z
|
|
362
377
|
.object({
|
|
363
378
|
urlChanged: z.boolean(),
|
|
364
379
|
newElements: z.number(),
|
|
365
380
|
removedElements: z.number(),
|
|
366
381
|
})
|
|
367
|
-
.
|
|
382
|
+
.optional(),
|
|
383
|
+
targetState: z
|
|
384
|
+
.object({
|
|
385
|
+
kind: z.enum(['page', 'dialog', 'modal', 'section']),
|
|
386
|
+
label: z.string(),
|
|
387
|
+
url: z.string(),
|
|
388
|
+
})
|
|
389
|
+
.optional(),
|
|
390
|
+
screenshot: z
|
|
391
|
+
.object({
|
|
392
|
+
title: z.string(),
|
|
393
|
+
relativePath: z.string(),
|
|
394
|
+
})
|
|
395
|
+
.optional(),
|
|
368
396
|
});
|
|
369
397
|
|
|
370
|
-
const
|
|
398
|
+
const generatedPageDocumentationSchema = z.object({
|
|
371
399
|
summary: z.string(),
|
|
372
400
|
can: z.array(capabilitySchema),
|
|
373
401
|
might: z.array(capabilitySchema),
|
|
374
|
-
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const pageDocumentationSchema = generatedPageDocumentationSchema.extend({
|
|
405
|
+
interactions: z.array(stateTransitionSchema).optional(),
|
|
406
|
+
qualityNotes: z.array(z.string()).optional(),
|
|
375
407
|
});
|
|
376
408
|
|
|
377
409
|
type StateTransition = DocStateTransition;
|
|
378
|
-
type
|
|
379
|
-
|
|
380
|
-
qualityNotes?: string[];
|
|
381
|
-
};
|
|
410
|
+
type GeneratedPageDocumentation = z.infer<typeof generatedPageDocumentationSchema>;
|
|
411
|
+
type PageDocumentation = z.infer<typeof pageDocumentationSchema>;
|
|
382
412
|
|
|
383
413
|
export { Documentarian };
|
|
384
414
|
export type { PageDocumentation, StateTransition };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import { parseEnv } from 'node:util';
|
|
4
5
|
import { ConfigParser } from '../../../src/config.ts';
|
|
5
6
|
|
|
@@ -90,10 +91,11 @@ class DocbotConfigParser {
|
|
|
90
91
|
|
|
91
92
|
private async loadConfigModule(configPath: string): Promise<any> {
|
|
92
93
|
const ext = configPath.split('.').pop();
|
|
94
|
+
const moduleUrl = pathToFileURL(resolve(configPath)).href;
|
|
93
95
|
|
|
94
96
|
if (ext === 'ts') {
|
|
95
97
|
try {
|
|
96
|
-
return await import(
|
|
98
|
+
return await import(moduleUrl);
|
|
97
99
|
} catch {
|
|
98
100
|
const require = (await import('node:module')).createRequire(import.meta.url);
|
|
99
101
|
return require(configPath);
|
|
@@ -101,7 +103,7 @@ class DocbotConfigParser {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
if (ext === 'js' || ext === 'mjs') {
|
|
104
|
-
return await import(
|
|
106
|
+
return await import(moduleUrl);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
return JSON.parse(readFileSync(configPath, 'utf8'));
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { type ActivityEntry, addActivityListener, removeActivityListener } from '../../../src/activity.ts';
|
|
3
|
+
import { isVerboseMode } from '../../../src/utils/logger.ts';
|
|
4
|
+
|
|
5
|
+
const RESET_LINE = '\r\u001b[2K';
|
|
6
|
+
|
|
7
|
+
const stream = process.stderr;
|
|
8
|
+
let tracking = false;
|
|
9
|
+
|
|
10
|
+
export function trackActivityLine(): void {
|
|
11
|
+
if (tracking) return;
|
|
12
|
+
if (!stream.isTTY) return;
|
|
13
|
+
if (isVerboseMode()) return;
|
|
14
|
+
|
|
15
|
+
tracking = true;
|
|
16
|
+
addActivityListener(writeActivityLine);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearActivityLine(): void {
|
|
20
|
+
if (!tracking) return;
|
|
21
|
+
|
|
22
|
+
tracking = false;
|
|
23
|
+
removeActivityListener(writeActivityLine);
|
|
24
|
+
stream.write(RESET_LINE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeActivityLine(activity: ActivityEntry | null): void {
|
|
28
|
+
if (!activity) return;
|
|
29
|
+
|
|
30
|
+
const width = (stream.columns || 80) - 2;
|
|
31
|
+
const message = Array.from(activity.message.replace(/\s+/g, ' ').trim()).slice(0, width).join('');
|
|
32
|
+
stream.write(`${RESET_LINE}${chalk.gray(message)}`);
|
|
33
|
+
}
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -2,83 +2,71 @@ import { Command } from 'commander';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { keepServerRunning } from '../../../src/browser-server.ts';
|
|
4
4
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
5
|
-
import {
|
|
5
|
+
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
6
|
+
import { clearActivityLine, trackActivityLine } from './activity-line.ts';
|
|
6
7
|
import { type EnvelopeData, renderEnvelope } from './envelope.ts';
|
|
7
8
|
import { Prima, type PrimaOptions } from './prima.ts';
|
|
8
9
|
|
|
9
10
|
const helpContract = dedent`
|
|
10
|
-
Prima
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
prima browser start a prima-owned browser instead, when no session is open
|
|
59
|
-
--instance <name> which prima-owned browser you talk to; parallel work
|
|
60
|
-
needs one each
|
|
61
|
-
--session [file] cookies and storage persisted across processes; ignored
|
|
62
|
-
while attached, the attached session keeps its own
|
|
63
|
-
Prima never launches a browser implicitly and never closes an attached one - it
|
|
64
|
-
disconnects. browser list shows both kinds; ### Instance names the one you are on.
|
|
65
|
-
Every browser is reached over a Playwright browser-server endpoint, which needs the
|
|
66
|
-
Node build - run prima as "npx explorbot prima ..." or through the published prima
|
|
67
|
-
bin; from source under Bun the connection does not open.
|
|
68
|
-
When no AI model is usable pw still works; for everything else drive
|
|
69
|
-
playwright-cli directly.
|
|
70
|
-
Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
|
|
71
|
-
you pass.
|
|
11
|
+
Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
|
|
12
|
+
|
|
13
|
+
playwright-cli open <url> starts the session
|
|
14
|
+
prima <command> ... drives it
|
|
15
|
+
playwright-cli close ends it
|
|
16
|
+
|
|
17
|
+
One call takes a whole job:
|
|
18
|
+
|
|
19
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
20
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
21
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const checkHelp = dedent`
|
|
25
|
+
check takes an outcome rather than a click path, and works out how to reach it.
|
|
26
|
+
--expected one outcome the run must reach, repeatable for several. Without it the
|
|
27
|
+
scenario text is the single expected outcome. Each comes back under
|
|
28
|
+
### Expected outcomes as PASSED, FAILED or not verified - "not verified"
|
|
29
|
+
means the run never checked it, which is not the same as false.
|
|
30
|
+
Page problems seen on the way appear under ### Answer, not as step failures.
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const doHelp = dedent`
|
|
34
|
+
Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
|
|
35
|
+
with what proved it. One that could not be carried out fails the command and says why.
|
|
36
|
+
Nothing runs past the last instruction given. A whole remaining sequence in one call is
|
|
37
|
+
what makes this tier cheap.
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const verifyHelp = dedent`
|
|
41
|
+
Reports each assertion it could express as PASSED or FAILED with its playwright form,
|
|
42
|
+
and gives no overall verdict - read the lines and decide. "none ran" means the claim
|
|
43
|
+
could not be expressed, which is not the same as false.
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
const reportHelp = dedent`
|
|
47
|
+
Commands are logged as they run, so the report needs no browser and outlives the session.
|
|
48
|
+
The most recent session is reported unless --pw-session names another.
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const sessionHelp = dedent`
|
|
52
|
+
--endpoint <ep> attach to a browser server endpoint directly, skipping discovery
|
|
53
|
+
--instance <name> which prima-owned browser you talk to; parallel work needs one each
|
|
54
|
+
--session [file] cookies and storage persisted across processes; ignored while
|
|
55
|
+
attached, since the attached session keeps its own
|
|
56
|
+
--framework parsed but not active yet; reported code is CodeceptJS either way
|
|
57
|
+
DEBUG='explorbot:*' in front of a command prints the log of everything it does.
|
|
58
|
+
When no AI model is usable pw still works; for everything else drive playwright-cli.
|
|
72
59
|
`;
|
|
73
60
|
|
|
74
|
-
|
|
61
|
+
let rootOptions: () => any = () => ({});
|
|
62
|
+
|
|
63
|
+
function buildOptions(subcommand: any): PrimaOptions {
|
|
64
|
+
const options = { ...rootOptions(), ...stripEmpty(subcommand) };
|
|
75
65
|
return {
|
|
76
|
-
verbose: options.verbose || options.debug,
|
|
77
66
|
config: options.config,
|
|
78
67
|
path: options.path,
|
|
79
68
|
instance: options.instance,
|
|
80
69
|
session: options.session,
|
|
81
|
-
heal: options.heal,
|
|
82
70
|
ephemeral: options.ephemeral,
|
|
83
71
|
framework: options.framework,
|
|
84
72
|
noVision: options.vision === false,
|
|
@@ -91,30 +79,39 @@ function buildOptions(options: any): PrimaOptions {
|
|
|
91
79
|
};
|
|
92
80
|
}
|
|
93
81
|
|
|
82
|
+
function stripEmpty(options: any): any {
|
|
83
|
+
const present: any = {};
|
|
84
|
+
for (const [key, value] of Object.entries(options || {})) {
|
|
85
|
+
if (value === undefined) continue;
|
|
86
|
+
present[key] = value;
|
|
87
|
+
}
|
|
88
|
+
return present;
|
|
89
|
+
}
|
|
90
|
+
|
|
94
91
|
function addCommonOptions(cmd: Command): Command {
|
|
95
92
|
return cmd
|
|
96
|
-
.option('-v, --verbose', 'Enable verbose logging')
|
|
97
|
-
.option('--debug', 'Enable debug logging (same as --verbose)')
|
|
98
93
|
.option('-c, --config <path>', 'Path to explorbot configuration file')
|
|
99
94
|
.option('-p, --path <path>', 'Working directory path')
|
|
100
95
|
.option('-i, --instance <name>', 'Browser instance to drive')
|
|
101
96
|
.option('--session [file]', 'Persist cookies and storage to a session file')
|
|
102
|
-
.option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
|
|
103
97
|
.option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
|
|
104
98
|
.option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
|
|
105
99
|
.option('--url <url>', 'Page to open when the session has no page yet')
|
|
106
100
|
.option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
|
|
107
|
-
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
101
|
+
.option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
|
|
102
|
+
.addHelpText('after', `\n${sessionHelp}`);
|
|
108
103
|
}
|
|
109
104
|
|
|
110
105
|
function primaFor(options: any): Prima {
|
|
111
|
-
setPreserveConsoleLogs(true);
|
|
112
106
|
if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
|
|
113
107
|
return new Prima(buildOptions(options));
|
|
114
108
|
}
|
|
115
109
|
|
|
116
|
-
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData
|
|
110
|
+
async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, record = true): Promise<void> {
|
|
111
|
+
setQuietMode(!isVerboseMode());
|
|
112
|
+
trackActivityLine();
|
|
117
113
|
const prima = primaFor(options);
|
|
114
|
+
const startedAt = Date.now();
|
|
118
115
|
|
|
119
116
|
let envelope: EnvelopeData;
|
|
120
117
|
try {
|
|
@@ -124,6 +121,8 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
|
|
|
124
121
|
envelope = await prima.toolFailureEnvelope(command, error);
|
|
125
122
|
}
|
|
126
123
|
|
|
124
|
+
if (record) prima.record(envelope, Date.now() - startedAt);
|
|
125
|
+
clearActivityLine();
|
|
127
126
|
console.log(renderEnvelope(envelope));
|
|
128
127
|
await prima.stop().catch(() => {});
|
|
129
128
|
process.exit(envelope.ok ? 0 : 1);
|
|
@@ -143,32 +142,38 @@ async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>)
|
|
|
143
142
|
|
|
144
143
|
export function createPrimaCommands(name = 'prima'): Command {
|
|
145
144
|
const cmd = new Command(name);
|
|
146
|
-
cmd.description('
|
|
145
|
+
cmd.description('Tests and drives a web app through described behaviour instead of locators: one command carries a whole scenario, verifies it, and reports the proof');
|
|
146
|
+
cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
|
|
147
|
+
cmd.option('--url <url>', 'Page to open when the session has no page yet');
|
|
147
148
|
cmd.addHelpText('after', `\n${helpContract}`);
|
|
149
|
+
rootOptions = () => cmd.opts();
|
|
148
150
|
|
|
149
151
|
addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
|
|
150
152
|
await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
|
|
151
153
|
});
|
|
152
154
|
|
|
153
|
-
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await runPrima(options, `click ${target}`, (prima) => prima.click(target));
|
|
159
|
-
});
|
|
155
|
+
addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
|
|
156
|
+
.addHelpText('after', `\n${doHelp}`)
|
|
157
|
+
.action(async (instructions, options) => {
|
|
158
|
+
await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
|
|
159
|
+
});
|
|
160
160
|
|
|
161
|
-
addCommonOptions(cmd.command('
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
addCommonOptions(cmd.command('check <scenario>').description('Run a scenario end to end as a test, with its own verification, and report the steps it took'))
|
|
162
|
+
.option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value: string, all: string[]) => [...all, value], [])
|
|
163
|
+
.addHelpText('after', `\n${checkHelp}`)
|
|
164
|
+
.action(async (scenario, options) => {
|
|
165
|
+
await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
|
|
166
|
+
});
|
|
164
167
|
|
|
165
168
|
addCommonOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot')).action(async (question, options) => {
|
|
166
169
|
await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
|
|
167
170
|
});
|
|
168
171
|
|
|
169
|
-
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
|
|
173
|
+
.addHelpText('after', `\n${verifyHelp}`)
|
|
174
|
+
.action(async (assertion, options) => {
|
|
175
|
+
await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
|
|
176
|
+
});
|
|
172
177
|
|
|
173
178
|
addCommonOptions(
|
|
174
179
|
cmd.command('research').description('Map the current page and return verified locators').option('--data', 'Include data extraction in the map').option('--deep', 'Expand hidden elements for a deeper map').option('--fresh', 'Ignore the cached map and research the page again')
|
|
@@ -181,6 +186,30 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
181
186
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
182
187
|
});
|
|
183
188
|
|
|
189
|
+
addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
|
|
190
|
+
setQuietMode(!isVerboseMode());
|
|
191
|
+
const prima = primaFor(options);
|
|
192
|
+
console.log(await prima.config().catch((error: unknown) => browserErrorMessage(error)));
|
|
193
|
+
await prima.stop().catch(() => {});
|
|
194
|
+
process.exit(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
|
|
198
|
+
await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
|
|
202
|
+
.addHelpText('after', `\n${reportHelp}`)
|
|
203
|
+
.action(async (options) => {
|
|
204
|
+
setQuietMode(!isVerboseMode());
|
|
205
|
+
console.log(
|
|
206
|
+
await primaFor(options)
|
|
207
|
+
.report()
|
|
208
|
+
.catch((error: unknown) => browserErrorMessage(error))
|
|
209
|
+
);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
184
213
|
const browser = cmd.command('browser').description('Manage the browsers prima drives');
|
|
185
214
|
|
|
186
215
|
addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
|