explorbot 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/explorbot-cli.ts +2 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/README.md +31 -4
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/explorbot-cli.js +2 -1
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +4 -3
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +53 -8
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +13 -0
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/options/decision-model-option.d.ts +6 -0
- package/dist/src/commands/options/decision-model-option.js +10 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +11 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +8 -2
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/docs/api-testing/planning.md +1 -1
- package/docs/basics/providers.md +37 -0
- package/docs/index.json +1 -0
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +2 -0
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/docs/web-testing/decisions.md +108 -0
- package/docs/workflow/agentic-usage.md +1 -0
- package/package.json +4 -3
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +52 -8
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +13 -0
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/options/decision-model-option.ts +11 -0
- package/src/commands/options/index.ts +2 -0
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +11 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +6 -1
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logos/explorbot/explorbot-for-dark-theme.svg">
|
|
4
|
+
<img src="assets/logos/explorbot/explorbot-for-light-theme.svg" alt="Explorbot" width="560">
|
|
5
|
+
</picture>
|
|
3
6
|
</p>
|
|
4
7
|
|
|
5
8
|
<p align="center"><b>The vibe-testing agent for web applications.</b></p>
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { HelpJsonCommand } from '../src/commands/help-json-command.js';
|
|
|
12
12
|
import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
|
|
13
13
|
import { App } from '../src/components/App.js';
|
|
14
14
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
15
|
-
import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
15
|
+
import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
16
16
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
17
17
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
18
18
|
import { remote } from '../src/remote.js';
|
|
@@ -33,6 +33,7 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as stri
|
|
|
33
33
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
34
34
|
wsOption.register(program);
|
|
35
35
|
knowledgeOption.register(program);
|
|
36
|
+
decisionModelOption.register(program);
|
|
36
37
|
|
|
37
38
|
process.on('uncaughtException', async (error) => {
|
|
38
39
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
package/bin/mdq.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runMdq } from '../src/utils/mdq/cli.ts';
|
|
3
|
+
|
|
4
|
+
async function readStdin(): Promise<string> {
|
|
5
|
+
if (process.stdin.isTTY) return '';
|
|
6
|
+
process.stdin.setEncoding('utf8');
|
|
7
|
+
let text = '';
|
|
8
|
+
for await (const chunk of process.stdin) text += chunk;
|
|
9
|
+
return text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const result = await runMdq(process.argv.slice(2), readStdin);
|
|
13
|
+
if (result.output) {
|
|
14
|
+
let text = result.output;
|
|
15
|
+
if (!text.endsWith('\n')) text = `${text}\n`;
|
|
16
|
+
process.stdout.write(text);
|
|
17
|
+
}
|
|
18
|
+
process.exit(result.code);
|
|
@@ -8,6 +8,7 @@ import { Observability } from '../../../../src/observability.ts';
|
|
|
8
8
|
import { Plan, Test } from '../../../../src/test-plan.ts';
|
|
9
9
|
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
10
10
|
import { RulesLoader } from '../../../../src/utils/rules-loader.ts';
|
|
11
|
+
import { isSecretName, registerSecret } from '../../../../src/utils/secrets.ts';
|
|
11
12
|
import type { ApiClient } from '../api-client.ts';
|
|
12
13
|
import type { ApibotConfig } from '../config.ts';
|
|
13
14
|
import { getActiveStyle, getStyles } from './chief/styles.ts';
|
|
@@ -28,6 +29,21 @@ const ApiTasksSchema = z.object({
|
|
|
28
29
|
.describe('List of testing scenarios'),
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
const NamedValueSchema = z.object({ name: z.string(), value: z.string() });
|
|
33
|
+
|
|
34
|
+
const RequestDefaultsSchema = z.object({
|
|
35
|
+
files: z
|
|
36
|
+
.array(
|
|
37
|
+
z.object({
|
|
38
|
+
file: z.number().describe('Number of the knowledge file the values come from'),
|
|
39
|
+
headers: z.array(NamedValueSchema).describe('Headers to send, each value exactly as it goes on the wire'),
|
|
40
|
+
query: z.array(NamedValueSchema).describe('Query parameters to send'),
|
|
41
|
+
body: z.array(NamedValueSchema).describe('Fields a request body must contain, each value as a JSON literal'),
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.describe('One entry per knowledge file that names values to send with requests'),
|
|
45
|
+
});
|
|
46
|
+
|
|
31
47
|
const ChiefBase = WithSessionDedup(Object as unknown as new (...args: any[]) => object);
|
|
32
48
|
|
|
33
49
|
export class Chief extends ChiefBase {
|
|
@@ -37,6 +53,7 @@ export class Chief extends ChiefBase {
|
|
|
37
53
|
private knowledgeTracker?: KnowledgeTracker;
|
|
38
54
|
currentPlan: Plan | null = null;
|
|
39
55
|
private lastStyleName = '';
|
|
56
|
+
private readKnowledgeFiles = new Set<string>();
|
|
40
57
|
|
|
41
58
|
MIN_TASKS = 3;
|
|
42
59
|
MAX_TASKS = 10;
|
|
@@ -56,6 +73,7 @@ export class Chief extends ChiefBase {
|
|
|
56
73
|
debugLog('Sending planning prompt to AI provider');
|
|
57
74
|
|
|
58
75
|
await Observability.run(`chief: ${endpoint}`, { tags: ['chief'], sessionId: endpoint }, async () => {
|
|
76
|
+
await this.prepareRequestDefaults(endpoint);
|
|
59
77
|
const sampleData = await this.collectSampleData(endpoint);
|
|
60
78
|
const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
|
|
61
79
|
|
|
@@ -127,6 +145,52 @@ export class Chief extends ChiefBase {
|
|
|
127
145
|
}
|
|
128
146
|
}
|
|
129
147
|
|
|
148
|
+
private async prepareRequestDefaults(endpoint: string): Promise<void> {
|
|
149
|
+
if (!this.apiClient) return;
|
|
150
|
+
const files = (this.knowledgeTracker?.getEndpointKnowledge(endpoint) || []).filter((knowledge) => !this.readKnowledgeFiles.has(knowledge.filePath));
|
|
151
|
+
if (!files.length) return;
|
|
152
|
+
|
|
153
|
+
const knowledgeFiles = files.map((knowledge, index) => `<file number="${index + 1}" endpoint="${knowledge.endpoint}">\n${knowledge.content}\n</file>`).join('\n\n');
|
|
154
|
+
const prompt = dedent`
|
|
155
|
+
<task>
|
|
156
|
+
Extract the values these knowledge files say must be sent with API requests:
|
|
157
|
+
- headers: authentication, API keys and other headers, written exactly as sent (include the auth scheme when one is named)
|
|
158
|
+
- query: query parameters requests need
|
|
159
|
+
- body: fields request bodies must contain, each value written as a JSON literal
|
|
160
|
+
Copy values exactly as written. Never invent a value. Skip facts that are not about what to send.
|
|
161
|
+
</task>
|
|
162
|
+
|
|
163
|
+
<knowledge_files>
|
|
164
|
+
${knowledgeFiles}
|
|
165
|
+
</knowledge_files>
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const result = await this.provider.generateObject([{ role: 'user', content: prompt }], RequestDefaultsSchema, this.provider.getModelForAgent('chief'), { agentName: 'chief' }).catch((error: Error) => {
|
|
169
|
+
tag('warning').log(`Could not read request defaults from knowledge: ${error.message}`);
|
|
170
|
+
return null;
|
|
171
|
+
});
|
|
172
|
+
if (!result) return;
|
|
173
|
+
for (const knowledge of files) this.readKnowledgeFiles.add(knowledge.filePath);
|
|
174
|
+
|
|
175
|
+
const entries: z.infer<typeof RequestDefaultsSchema>['files'] = result.object?.files || [];
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const knowledge = files[entry.file - 1];
|
|
178
|
+
if (!knowledge?.endpoint) continue;
|
|
179
|
+
|
|
180
|
+
const headers = Object.fromEntries(entry.headers.map(({ name, value }) => [name, value]));
|
|
181
|
+
const query = Object.fromEntries(entry.query.map(({ name, value }) => [name, value]));
|
|
182
|
+
const body = Object.fromEntries(entry.body.map(({ name, value }) => [name, parseJsonLiteral(value)]));
|
|
183
|
+
const sent = Object.entries({ headers, query, body }).filter(([, values]) => Object.keys(values).length);
|
|
184
|
+
if (!sent.length) continue;
|
|
185
|
+
|
|
186
|
+
for (const [name, value] of sent.flatMap(([, values]) => Object.entries(values))) {
|
|
187
|
+
if (isSecretName(name)) registerSecret(String(value));
|
|
188
|
+
}
|
|
189
|
+
this.apiClient.addRequestDefaults({ pattern: knowledge.endpoint, headers, query, body });
|
|
190
|
+
tag('info').log(`Request defaults from knowledge for ${knowledge.endpoint}: ${sent.map(([kind, values]) => `${kind} ${Object.keys(values).join(', ')}`).join('; ')}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
130
194
|
private async collectSampleData(endpoint: string): Promise<string> {
|
|
131
195
|
if (!this.apiClient) return '';
|
|
132
196
|
|
|
@@ -341,3 +405,11 @@ export class Chief extends ChiefBase {
|
|
|
341
405
|
`;
|
|
342
406
|
}
|
|
343
407
|
}
|
|
408
|
+
|
|
409
|
+
function parseJsonLiteral(value: string): unknown {
|
|
410
|
+
try {
|
|
411
|
+
return JSON.parse(value);
|
|
412
|
+
} catch {
|
|
413
|
+
return value;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { ApiClient as BaseApiClient } from '../../../src/api/api-client.ts';
|
|
2
|
+
import type { RequestResult } from '../../../src/api/request-result.ts';
|
|
3
|
+
import { extractStatePath, matchesUrl } from '../../../src/utils/url-matcher.ts';
|
|
2
4
|
import type { HookFn } from './config.ts';
|
|
3
5
|
|
|
6
|
+
const BODY_METHODS = ['POST', 'PUT', 'PATCH'];
|
|
7
|
+
|
|
4
8
|
export class ApiClient extends BaseApiClient {
|
|
5
9
|
private bootstrapHook?: HookFn;
|
|
6
10
|
private teardownHook?: HookFn;
|
|
11
|
+
private requestDefaults: RequestDefaults[] = [];
|
|
7
12
|
|
|
8
13
|
constructor(baseEndpoint: string, defaultHeaders: Record<string, string> = {}, hooks?: { bootstrap?: HookFn; teardown?: HookFn }) {
|
|
9
14
|
super(baseEndpoint, defaultHeaders);
|
|
@@ -25,4 +30,36 @@ export class ApiClient extends BaseApiClient {
|
|
|
25
30
|
const ctx = { headers: this.getHeaders(), baseEndpoint: this.getBaseEndpoint() };
|
|
26
31
|
await this.teardownHook(ctx);
|
|
27
32
|
}
|
|
33
|
+
|
|
34
|
+
addRequestDefaults(defaults: RequestDefaults): void {
|
|
35
|
+
this.requestDefaults.push(defaults);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override async request(opts: Parameters<BaseApiClient['request']>[0]): Promise<RequestResult> {
|
|
39
|
+
const path = extractStatePath(opts.path);
|
|
40
|
+
const matching = this.requestDefaults.filter((defaults) => matchesUrl(defaults.pattern, path));
|
|
41
|
+
if (!matching.length) return super.request(opts);
|
|
42
|
+
|
|
43
|
+
const knowledgeHeaders: Record<string, string> = {};
|
|
44
|
+
for (const defaults of matching) {
|
|
45
|
+
for (const [name, value] of Object.entries(defaults.headers)) knowledgeHeaders[name.toLowerCase()] = value;
|
|
46
|
+
}
|
|
47
|
+
for (const name of Object.keys({ ...this.getHeaders(), ...opts.headers })) delete knowledgeHeaders[name.toLowerCase()];
|
|
48
|
+
|
|
49
|
+
const headers = { ...knowledgeHeaders, ...this.getHeaders(), ...opts.headers };
|
|
50
|
+
const queryParams = Object.assign({}, ...matching.map((defaults) => defaults.query), opts.queryParams);
|
|
51
|
+
let body = opts.body;
|
|
52
|
+
if (BODY_METHODS.includes(opts.method.toUpperCase()) && body && typeof body === 'object' && !Array.isArray(body)) {
|
|
53
|
+
body = Object.assign({}, ...matching.map((defaults) => defaults.body), body);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return super.request({ ...opts, headers, queryParams, body });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RequestDefaults {
|
|
61
|
+
pattern: string;
|
|
62
|
+
headers: Record<string, string>;
|
|
63
|
+
query: Record<string, string>;
|
|
64
|
+
body: Record<string, unknown>;
|
|
28
65
|
}
|
package/boat/prima/README.md
CHANGED
|
@@ -61,22 +61,49 @@ prima-cli browser stop
|
|
|
61
61
|
|
|
62
62
|
Prima needs a model, and takes it from the environment. There is no `init` to run and nothing is written for you.
|
|
63
63
|
|
|
64
|
+
The setup we recommend runs everything through OpenRouter with one key: GPT-OSS 120B served by Groq as the main model, and TypeSafe's Jev as the [decision model](https://github.com/testomatio/explorbot/blob/main/docs/web-testing/decisions.md), which answers narrow yes/no and pick-one questions so the main model is called less often.
|
|
65
|
+
|
|
64
66
|
```bash
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b:nitro
|
|
68
|
+
PRIMA_CLI_DECISION_MODEL=openrouter/typesafe/jev-1.13
|
|
69
|
+
OPENROUTER_API_KEY=your-key
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
The provider comes from
|
|
72
|
+
The provider comes from each model name, and `:nitro` asks OpenRouter for its fastest host, which is Groq for this model. Jev is optional: leave `PRIMA_CLI_DECISION_MODEL` unset and prima never calls it, or pass `--decision-model` for a single run.
|
|
73
|
+
|
|
74
|
+
Prima is usually run by a coding agent, so the best home for these variables is the agent's own config, where they reach every command the agent runs.
|
|
75
|
+
|
|
76
|
+
Claude Code, in `~/.claude/settings.json`:
|
|
70
77
|
|
|
71
78
|
```json
|
|
72
79
|
{
|
|
73
80
|
"env": {
|
|
74
|
-
"PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b",
|
|
81
|
+
"PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b:nitro",
|
|
82
|
+
"PRIMA_CLI_DECISION_MODEL": "openrouter/typesafe/jev-1.13",
|
|
75
83
|
"OPENROUTER_API_KEY": "your-key"
|
|
76
84
|
}
|
|
77
85
|
}
|
|
78
86
|
```
|
|
79
87
|
|
|
88
|
+
Codex, in `~/.codex/config.toml`:
|
|
89
|
+
|
|
90
|
+
```toml
|
|
91
|
+
[shell_environment_policy]
|
|
92
|
+
set = { PRIMA_CLI_AI_MODEL = "openrouter/openai/gpt-oss-120b:nitro", PRIMA_CLI_DECISION_MODEL = "openrouter/typesafe/jev-1.13", OPENROUTER_API_KEY = "your-key" }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`set` is applied after Codex filters the inherited environment, so the key survives even where names containing `KEY` are stripped.
|
|
96
|
+
|
|
97
|
+
To run prima yourself, or from any agent that inherits your shell, export them in `~/.bashrc` or `~/.zshrc`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b:nitro
|
|
101
|
+
export PRIMA_CLI_DECISION_MODEL=openrouter/typesafe/jev-1.13
|
|
102
|
+
export OPENROUTER_API_KEY=your-key
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Fish uses `set -gx` in `~/.config/fish/config.fish` instead of `export`.
|
|
106
|
+
|
|
80
107
|
Screenshot analysis needs its own model — one is never guessed from the main model. Set `PRIMA_CLI_VISION_MODEL`, or pass `--vision-model` for a single run, as `--model` does for the main one. Without it prima still runs, but settles `check` outcomes from the run log rather than from the page as seen.
|
|
81
108
|
|
|
82
109
|
Every `PRIMA_CLI_*` variable mirrors the `EXPLORBOT_*` one of the same name and wins over it, so prima can be pointed at a different provider, model or URL without disturbing an explorbot setup on the same machine. `PRIMA_CLI_AI_MODEL`, `PRIMA_CLI_URL`, `PRIMA_CLI_VISION_MODEL` and the rest all work this way.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { knowledgeOption } from '../../../src/commands/options/index.ts';
|
|
2
|
+
import { decisionModelOption, knowledgeOption } from '../../../src/commands/options/index.ts';
|
|
3
3
|
import { createPrimaCommands } from '../src/cli.ts';
|
|
4
4
|
|
|
5
5
|
const program = createPrimaCommands('prima');
|
|
6
6
|
knowledgeOption.register(program);
|
|
7
|
+
decisionModelOption.register(program);
|
|
7
8
|
program.parse();
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as playwright from 'playwright';
|
|
|
8
8
|
import type { Browser } from 'playwright';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { ActionResult } from '../../../src/action-result.ts';
|
|
11
|
+
import { JUDGE_PAGE_CAP, UNDECIDED } from '../../../src/ai/judge.ts';
|
|
11
12
|
import { getPreviousResearch } from '../../../src/ai/researcher/cache.ts';
|
|
12
13
|
import { actionRule, locatorRule } from '../../../src/ai/rules.ts';
|
|
13
14
|
import { createAgentTools, createCodeceptJSTools, createRefTools } from '../../../src/ai/tools.ts';
|
|
@@ -20,7 +21,7 @@ import { Reporter } from '../../../src/reporter.ts';
|
|
|
20
21
|
import type { WebPageState } from '../../../src/state-manager.ts';
|
|
21
22
|
import { Stats } from '../../../src/stats.ts';
|
|
22
23
|
import { Task, Test, TestResult } from '../../../src/test-plan.ts';
|
|
23
|
-
import { ariaRefSnapshot } from '../../../src/utils/aria-ref.ts';
|
|
24
|
+
import { ariaRefSelector, ariaRefSnapshot, describeRef, parseAriaRefs, refIsGone } from '../../../src/utils/aria-ref.ts';
|
|
24
25
|
import { compactAriaSnapshot } from '../../../src/utils/aria.ts';
|
|
25
26
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
26
27
|
import { pluralize } from '../../../src/utils/logger.ts';
|
|
@@ -57,7 +58,8 @@ function dropVolatileColumns(markdown: string): string {
|
|
|
57
58
|
const divider = `|${columns.map(() => '------').join('|')}|`;
|
|
58
59
|
const body = rows.map((row) => `| ${columns.map((name) => row[name] || '-').join(' | ')} |`);
|
|
59
60
|
return [header, divider, ...body, ''].join('\n');
|
|
60
|
-
})
|
|
61
|
+
})
|
|
62
|
+
.toString();
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function cap(text: string, max: number): string {
|
|
@@ -446,6 +448,12 @@ export class Prima {
|
|
|
446
448
|
}
|
|
447
449
|
|
|
448
450
|
const previousState = this.bot.stateManager().getCurrentState();
|
|
451
|
+
|
|
452
|
+
if (!isUrl) {
|
|
453
|
+
const judged = await this.judgedGo(target, previousState);
|
|
454
|
+
if (judged) return judged;
|
|
455
|
+
}
|
|
456
|
+
|
|
449
457
|
let navigationError: unknown = null;
|
|
450
458
|
|
|
451
459
|
try {
|
|
@@ -925,6 +933,37 @@ export class Prima {
|
|
|
925
933
|
return snapshot || result.ariaSnapshot;
|
|
926
934
|
}
|
|
927
935
|
|
|
936
|
+
private async judgedGo(target: string, previousState: WebPageState | null): Promise<EnvelopeData | null> {
|
|
937
|
+
if (!previousState) return null;
|
|
938
|
+
const judge = this.bot.judge?.();
|
|
939
|
+
if (!judge) return null;
|
|
940
|
+
|
|
941
|
+
const refs = parseAriaRefs((await this.refAriaSnapshot(ActionResult.fromState(previousState))) || '').filter((entry) => entry.name);
|
|
942
|
+
const labels = refs.map((entry) => `${entry.role} named "${entry.name}"`);
|
|
943
|
+
const pick = await judge.decide(`Which listed control leads to: ${target}`, [...labels, UNDECIDED], { target });
|
|
944
|
+
if (!pick.value) return null;
|
|
945
|
+
|
|
946
|
+
const ref = refs[labels.indexOf(pick.value)].ref;
|
|
947
|
+
const explorer = this.bot.getExplorer();
|
|
948
|
+
if (await refIsGone(explorer, ref)) return null;
|
|
949
|
+
|
|
950
|
+
const named = await describeRef(explorer, ref);
|
|
951
|
+
const run = `I.usePlaywrightTo(${JSON.stringify(`go to ${target}`)}, async ({ page }) => page.locator(${JSON.stringify(ariaRefSelector(ref))}).click())`;
|
|
952
|
+
if (!(await explorer.action().attempt(run, `go ${target}`))) return null;
|
|
953
|
+
|
|
954
|
+
const newState = this.bot.stateManager().getCurrentState();
|
|
955
|
+
if (!newState) return null;
|
|
956
|
+
if (newState.hash === previousState.hash && newState.url === previousState.url) return null;
|
|
957
|
+
|
|
958
|
+
const page = ((await this.refAriaSnapshot(ActionResult.fromState(newState))) || '').slice(0, JUDGE_PAGE_CAP);
|
|
959
|
+
const arrived = await judge.decide(`The page now shows: ${target}`, null, { target, page });
|
|
960
|
+
if (arrived.rejected) return null;
|
|
961
|
+
|
|
962
|
+
let code = run;
|
|
963
|
+
if (named) code = `I.click(${JSON.stringify(named)})`;
|
|
964
|
+
return this.successEnvelope(`go ${target}`, [code], await this.capturedResult(newState), previousState);
|
|
965
|
+
}
|
|
966
|
+
|
|
928
967
|
private executedCodes(code: unknown): string[] {
|
|
929
968
|
if (typeof code !== 'string') return [];
|
|
930
969
|
return code
|
|
@@ -12,7 +12,7 @@ import { HelpJsonCommand } from '../src/commands/help-json-command.js';
|
|
|
12
12
|
import { RecommendedModelsCommand } from '../src/commands/recommended-models-command.js';
|
|
13
13
|
import { App } from '../src/components/App.js';
|
|
14
14
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
15
|
-
import { knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
15
|
+
import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
16
16
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
17
17
|
import { ExplorBot } from '../src/explorbot.js';
|
|
18
18
|
import { remote } from '../src/remote.js';
|
|
@@ -30,6 +30,7 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
|
30
30
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
31
31
|
wsOption.register(program);
|
|
32
32
|
knowledgeOption.register(program);
|
|
33
|
+
decisionModelOption.register(program);
|
|
33
34
|
process.on('uncaughtException', async (error) => {
|
|
34
35
|
tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
|
|
35
36
|
await flushTelemetry();
|
package/dist/bin/mdq.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runMdq } from "../src/utils/mdq/cli.js";
|
|
3
|
+
async function readStdin() {
|
|
4
|
+
if (process.stdin.isTTY)
|
|
5
|
+
return '';
|
|
6
|
+
process.stdin.setEncoding('utf8');
|
|
7
|
+
let text = '';
|
|
8
|
+
for await (const chunk of process.stdin)
|
|
9
|
+
text += chunk;
|
|
10
|
+
return text;
|
|
11
|
+
}
|
|
12
|
+
const result = await runMdq(process.argv.slice(2), readStdin);
|
|
13
|
+
if (result.output) {
|
|
14
|
+
let text = result.output;
|
|
15
|
+
if (!text.endsWith('\n'))
|
|
16
|
+
text = `${text}\n`;
|
|
17
|
+
process.stdout.write(text);
|
|
18
|
+
}
|
|
19
|
+
process.exit(result.code);
|
|
@@ -6,6 +6,7 @@ import { Observability } from "../../../../src/observability.js";
|
|
|
6
6
|
import { Plan, Test } from "../../../../src/test-plan.js";
|
|
7
7
|
import { createDebug, tag } from "../../../../src/utils/logger.js";
|
|
8
8
|
import { RulesLoader } from "../../../../src/utils/rules-loader.js";
|
|
9
|
+
import { isSecretName, registerSecret } from "../../../../src/utils/secrets.js";
|
|
9
10
|
import { getActiveStyle, getStyles } from "./chief/styles.js";
|
|
10
11
|
const debugLog = createDebug('explorbot:chief');
|
|
11
12
|
const ApiTasksSchema = z.object({
|
|
@@ -19,6 +20,17 @@ const ApiTasksSchema = z.object({
|
|
|
19
20
|
}))
|
|
20
21
|
.describe('List of testing scenarios'),
|
|
21
22
|
});
|
|
23
|
+
const NamedValueSchema = z.object({ name: z.string(), value: z.string() });
|
|
24
|
+
const RequestDefaultsSchema = z.object({
|
|
25
|
+
files: z
|
|
26
|
+
.array(z.object({
|
|
27
|
+
file: z.number().describe('Number of the knowledge file the values come from'),
|
|
28
|
+
headers: z.array(NamedValueSchema).describe('Headers to send, each value exactly as it goes on the wire'),
|
|
29
|
+
query: z.array(NamedValueSchema).describe('Query parameters to send'),
|
|
30
|
+
body: z.array(NamedValueSchema).describe('Fields a request body must contain, each value as a JSON literal'),
|
|
31
|
+
}))
|
|
32
|
+
.describe('One entry per knowledge file that names values to send with requests'),
|
|
33
|
+
});
|
|
22
34
|
const ChiefBase = WithSessionDedup(Object);
|
|
23
35
|
export class Chief extends ChiefBase {
|
|
24
36
|
provider;
|
|
@@ -27,6 +39,7 @@ export class Chief extends ChiefBase {
|
|
|
27
39
|
knowledgeTracker;
|
|
28
40
|
currentPlan = null;
|
|
29
41
|
lastStyleName = '';
|
|
42
|
+
readKnowledgeFiles = new Set();
|
|
30
43
|
MIN_TASKS = 3;
|
|
31
44
|
MAX_TASKS = 10;
|
|
32
45
|
constructor(provider, config, apiClient, knowledgeTracker) {
|
|
@@ -42,6 +55,7 @@ export class Chief extends ChiefBase {
|
|
|
42
55
|
tag('info').log(`Planning style: ${opts.style}`);
|
|
43
56
|
debugLog('Sending planning prompt to AI provider');
|
|
44
57
|
await Observability.run(`chief: ${endpoint}`, { tags: ['chief'], sessionId: endpoint }, async () => {
|
|
58
|
+
await this.prepareRequestDefaults(endpoint);
|
|
45
59
|
const sampleData = await this.collectSampleData(endpoint);
|
|
46
60
|
const conversation = this.buildConversation(endpoint, opts?.style, sampleData);
|
|
47
61
|
const knowledge = this.knowledgeTracker?.renderEndpointKnowledge(endpoint);
|
|
@@ -105,6 +119,53 @@ export class Chief extends ChiefBase {
|
|
|
105
119
|
existing.add(test.scenario.toLowerCase());
|
|
106
120
|
}
|
|
107
121
|
}
|
|
122
|
+
async prepareRequestDefaults(endpoint) {
|
|
123
|
+
if (!this.apiClient)
|
|
124
|
+
return;
|
|
125
|
+
const files = (this.knowledgeTracker?.getEndpointKnowledge(endpoint) || []).filter((knowledge) => !this.readKnowledgeFiles.has(knowledge.filePath));
|
|
126
|
+
if (!files.length)
|
|
127
|
+
return;
|
|
128
|
+
const knowledgeFiles = files.map((knowledge, index) => `<file number="${index + 1}" endpoint="${knowledge.endpoint}">\n${knowledge.content}\n</file>`).join('\n\n');
|
|
129
|
+
const prompt = dedent `
|
|
130
|
+
<task>
|
|
131
|
+
Extract the values these knowledge files say must be sent with API requests:
|
|
132
|
+
- headers: authentication, API keys and other headers, written exactly as sent (include the auth scheme when one is named)
|
|
133
|
+
- query: query parameters requests need
|
|
134
|
+
- body: fields request bodies must contain, each value written as a JSON literal
|
|
135
|
+
Copy values exactly as written. Never invent a value. Skip facts that are not about what to send.
|
|
136
|
+
</task>
|
|
137
|
+
|
|
138
|
+
<knowledge_files>
|
|
139
|
+
${knowledgeFiles}
|
|
140
|
+
</knowledge_files>
|
|
141
|
+
`;
|
|
142
|
+
const result = await this.provider.generateObject([{ role: 'user', content: prompt }], RequestDefaultsSchema, this.provider.getModelForAgent('chief'), { agentName: 'chief' }).catch((error) => {
|
|
143
|
+
tag('warning').log(`Could not read request defaults from knowledge: ${error.message}`);
|
|
144
|
+
return null;
|
|
145
|
+
});
|
|
146
|
+
if (!result)
|
|
147
|
+
return;
|
|
148
|
+
for (const knowledge of files)
|
|
149
|
+
this.readKnowledgeFiles.add(knowledge.filePath);
|
|
150
|
+
const entries = result.object?.files || [];
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const knowledge = files[entry.file - 1];
|
|
153
|
+
if (!knowledge?.endpoint)
|
|
154
|
+
continue;
|
|
155
|
+
const headers = Object.fromEntries(entry.headers.map(({ name, value }) => [name, value]));
|
|
156
|
+
const query = Object.fromEntries(entry.query.map(({ name, value }) => [name, value]));
|
|
157
|
+
const body = Object.fromEntries(entry.body.map(({ name, value }) => [name, parseJsonLiteral(value)]));
|
|
158
|
+
const sent = Object.entries({ headers, query, body }).filter(([, values]) => Object.keys(values).length);
|
|
159
|
+
if (!sent.length)
|
|
160
|
+
continue;
|
|
161
|
+
for (const [name, value] of sent.flatMap(([, values]) => Object.entries(values))) {
|
|
162
|
+
if (isSecretName(name))
|
|
163
|
+
registerSecret(String(value));
|
|
164
|
+
}
|
|
165
|
+
this.apiClient.addRequestDefaults({ pattern: knowledge.endpoint, headers, query, body });
|
|
166
|
+
tag('info').log(`Request defaults from knowledge for ${knowledge.endpoint}: ${sent.map(([kind, values]) => `${kind} ${Object.keys(values).join(', ')}`).join('; ')}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
108
169
|
async collectSampleData(endpoint) {
|
|
109
170
|
if (!this.apiClient)
|
|
110
171
|
return '';
|
|
@@ -305,3 +366,11 @@ export class Chief extends ChiefBase {
|
|
|
305
366
|
`;
|
|
306
367
|
}
|
|
307
368
|
}
|
|
369
|
+
function parseJsonLiteral(value) {
|
|
370
|
+
try {
|
|
371
|
+
return JSON.parse(value);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ApiClient as BaseApiClient } from "../../../src/api/api-client.js";
|
|
2
|
+
import { extractStatePath, matchesUrl } from "../../../src/utils/url-matcher.js";
|
|
3
|
+
const BODY_METHODS = ['POST', 'PUT', 'PATCH'];
|
|
2
4
|
export class ApiClient extends BaseApiClient {
|
|
3
5
|
bootstrapHook;
|
|
4
6
|
teardownHook;
|
|
7
|
+
requestDefaults = [];
|
|
5
8
|
constructor(baseEndpoint, defaultHeaders = {}, hooks) {
|
|
6
9
|
super(baseEndpoint, defaultHeaders);
|
|
7
10
|
this.bootstrapHook = hooks?.bootstrap;
|
|
@@ -22,4 +25,27 @@ export class ApiClient extends BaseApiClient {
|
|
|
22
25
|
const ctx = { headers: this.getHeaders(), baseEndpoint: this.getBaseEndpoint() };
|
|
23
26
|
await this.teardownHook(ctx);
|
|
24
27
|
}
|
|
28
|
+
addRequestDefaults(defaults) {
|
|
29
|
+
this.requestDefaults.push(defaults);
|
|
30
|
+
}
|
|
31
|
+
async request(opts) {
|
|
32
|
+
const path = extractStatePath(opts.path);
|
|
33
|
+
const matching = this.requestDefaults.filter((defaults) => matchesUrl(defaults.pattern, path));
|
|
34
|
+
if (!matching.length)
|
|
35
|
+
return super.request(opts);
|
|
36
|
+
const knowledgeHeaders = {};
|
|
37
|
+
for (const defaults of matching) {
|
|
38
|
+
for (const [name, value] of Object.entries(defaults.headers))
|
|
39
|
+
knowledgeHeaders[name.toLowerCase()] = value;
|
|
40
|
+
}
|
|
41
|
+
for (const name of Object.keys({ ...this.getHeaders(), ...opts.headers }))
|
|
42
|
+
delete knowledgeHeaders[name.toLowerCase()];
|
|
43
|
+
const headers = { ...knowledgeHeaders, ...this.getHeaders(), ...opts.headers };
|
|
44
|
+
const queryParams = Object.assign({}, ...matching.map((defaults) => defaults.query), opts.queryParams);
|
|
45
|
+
let body = opts.body;
|
|
46
|
+
if (BODY_METHODS.includes(opts.method.toUpperCase()) && body && typeof body === 'object' && !Array.isArray(body)) {
|
|
47
|
+
body = Object.assign({}, ...matching.map((defaults) => defaults.body), body);
|
|
48
|
+
}
|
|
49
|
+
return super.request({ ...opts, headers, queryParams, body });
|
|
50
|
+
}
|
|
25
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { knowledgeOption } from "../../../src/commands/options/index.js";
|
|
2
|
+
import { decisionModelOption, knowledgeOption } from "../../../src/commands/options/index.js";
|
|
3
3
|
import { createPrimaCommands } from "../src/cli.js";
|
|
4
4
|
const program = createPrimaCommands('prima');
|
|
5
5
|
knowledgeOption.register(program);
|
|
6
|
+
decisionModelOption.register(program);
|
|
6
7
|
program.parse();
|
|
@@ -7,6 +7,7 @@ import dedent from 'dedent';
|
|
|
7
7
|
import * as playwright from 'playwright';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { ActionResult } from "../../../src/action-result.js";
|
|
10
|
+
import { JUDGE_PAGE_CAP, UNDECIDED } from "../../../src/ai/judge.js";
|
|
10
11
|
import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
|
|
11
12
|
import { actionRule, locatorRule } from "../../../src/ai/rules.js";
|
|
12
13
|
import { createAgentTools, createCodeceptJSTools, createRefTools } from "../../../src/ai/tools.js";
|
|
@@ -18,7 +19,7 @@ import { findSiteWith, listSites } from "../../../src/global-config.js";
|
|
|
18
19
|
import { Reporter } from "../../../src/reporter.js";
|
|
19
20
|
import { Stats } from "../../../src/stats.js";
|
|
20
21
|
import { Task, Test, TestResult } from "../../../src/test-plan.js";
|
|
21
|
-
import { ariaRefSnapshot } from "../../../src/utils/aria-ref.js";
|
|
22
|
+
import { ariaRefSelector, ariaRefSnapshot, describeRef, parseAriaRefs, refIsGone } from "../../../src/utils/aria-ref.js";
|
|
22
23
|
import { compactAriaSnapshot } from "../../../src/utils/aria.js";
|
|
23
24
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
24
25
|
import { pluralize } from "../../../src/utils/logger.js";
|
|
@@ -52,7 +53,8 @@ function dropVolatileColumns(markdown) {
|
|
|
52
53
|
const divider = `|${columns.map(() => '------').join('|')}|`;
|
|
53
54
|
const body = rows.map((row) => `| ${columns.map((name) => row[name] || '-').join(' | ')} |`);
|
|
54
55
|
return [header, divider, ...body, ''].join('\n');
|
|
55
|
-
})
|
|
56
|
+
})
|
|
57
|
+
.toString();
|
|
56
58
|
}
|
|
57
59
|
function cap(text, max) {
|
|
58
60
|
if (text.length <= max)
|
|
@@ -405,6 +407,11 @@ export class Prima {
|
|
|
405
407
|
return guard;
|
|
406
408
|
}
|
|
407
409
|
const previousState = this.bot.stateManager().getCurrentState();
|
|
410
|
+
if (!isUrl) {
|
|
411
|
+
const judged = await this.judgedGo(target, previousState);
|
|
412
|
+
if (judged)
|
|
413
|
+
return judged;
|
|
414
|
+
}
|
|
408
415
|
let navigationError = null;
|
|
409
416
|
try {
|
|
410
417
|
await this.bot.agentNavigator().visit(target);
|
|
@@ -868,6 +875,39 @@ export class Prima {
|
|
|
868
875
|
const snapshot = await Promise.resolve(this.bot.getExplorer()?.withPage?.(ariaRefSnapshot)).catch(() => null);
|
|
869
876
|
return snapshot || result.ariaSnapshot;
|
|
870
877
|
}
|
|
878
|
+
async judgedGo(target, previousState) {
|
|
879
|
+
if (!previousState)
|
|
880
|
+
return null;
|
|
881
|
+
const judge = this.bot.judge?.();
|
|
882
|
+
if (!judge)
|
|
883
|
+
return null;
|
|
884
|
+
const refs = parseAriaRefs((await this.refAriaSnapshot(ActionResult.fromState(previousState))) || '').filter((entry) => entry.name);
|
|
885
|
+
const labels = refs.map((entry) => `${entry.role} named "${entry.name}"`);
|
|
886
|
+
const pick = await judge.decide(`Which listed control leads to: ${target}`, [...labels, UNDECIDED], { target });
|
|
887
|
+
if (!pick.value)
|
|
888
|
+
return null;
|
|
889
|
+
const ref = refs[labels.indexOf(pick.value)].ref;
|
|
890
|
+
const explorer = this.bot.getExplorer();
|
|
891
|
+
if (await refIsGone(explorer, ref))
|
|
892
|
+
return null;
|
|
893
|
+
const named = await describeRef(explorer, ref);
|
|
894
|
+
const run = `I.usePlaywrightTo(${JSON.stringify(`go to ${target}`)}, async ({ page }) => page.locator(${JSON.stringify(ariaRefSelector(ref))}).click())`;
|
|
895
|
+
if (!(await explorer.action().attempt(run, `go ${target}`)))
|
|
896
|
+
return null;
|
|
897
|
+
const newState = this.bot.stateManager().getCurrentState();
|
|
898
|
+
if (!newState)
|
|
899
|
+
return null;
|
|
900
|
+
if (newState.hash === previousState.hash && newState.url === previousState.url)
|
|
901
|
+
return null;
|
|
902
|
+
const page = ((await this.refAriaSnapshot(ActionResult.fromState(newState))) || '').slice(0, JUDGE_PAGE_CAP);
|
|
903
|
+
const arrived = await judge.decide(`The page now shows: ${target}`, null, { target, page });
|
|
904
|
+
if (arrived.rejected)
|
|
905
|
+
return null;
|
|
906
|
+
let code = run;
|
|
907
|
+
if (named)
|
|
908
|
+
code = `I.click(${JSON.stringify(named)})`;
|
|
909
|
+
return this.successEnvelope(`go ${target}`, [code], await this.capturedResult(newState), previousState);
|
|
910
|
+
}
|
|
871
911
|
executedCodes(code) {
|
|
872
912
|
if (typeof code !== 'string')
|
|
873
913
|
return [];
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"check": "biome check .",
|
|
61
61
|
"check:fix": "biome check --write .",
|
|
62
62
|
"langfuse:export": "bun run .claude/skills/explorbot-debug/langfuse-export.ts",
|
|
63
|
-
"build:prima": "bun run scripts/build-prima-npm.ts"
|
|
63
|
+
"build:prima": "bun run scripts/build-prima-npm.ts",
|
|
64
|
+
"build:mdq": "bun run scripts/build-mdq-npm.ts"
|
|
64
65
|
},
|
|
65
66
|
"keywords": [
|
|
66
67
|
"cli",
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
"bash-tool": "^1.3.15",
|
|
102
103
|
"chalk": "^5.6.2",
|
|
103
104
|
"cli-highlight": "^2.1.11",
|
|
104
|
-
"codeceptjs": "^4.2.0
|
|
105
|
+
"codeceptjs": "^4.2.0",
|
|
105
106
|
"commander": "^14.0.1",
|
|
106
107
|
"debug": "^4.4.3",
|
|
107
108
|
"dedent": "^1.6.0",
|