explorbot 0.5.4 → 0.5.5
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/boat/prima/README.md +5 -10
- package/boat/prima/package.json +1 -1
- package/boat/prima/src/cli.ts +8 -50
- package/boat/prima/src/prima.ts +21 -77
- package/dist/boat/prima/src/cli.js +9 -45
- package/dist/boat/prima/src/prima.js +21 -73
- package/dist/package.json +1 -1
- package/dist/src/ai/navigator.js +8 -2
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +7 -0
- package/dist/src/utils/error-page.d.ts +1 -0
- package/dist/src/utils/error-page.js +8 -0
- package/dist/src/utils/mdq/query.d.ts +4 -1
- package/dist/src/utils/mdq/query.js +48 -7
- package/docs/reference/commands.md +4 -6
- package/package.json +1 -1
- package/src/ai/navigator.ts +8 -2
- package/src/ai/rules.ts +8 -0
- package/src/utils/error-page.ts +8 -0
- package/src/utils/mdq/query.ts +48 -7
package/boat/prima/README.md
CHANGED
|
@@ -17,25 +17,20 @@ npm install -g prima-cli # or install it
|
|
|
17
17
|
|
|
18
18
|
Prima also ships inside [explorbot](https://www.npmjs.com/package/explorbot), so `npx explorbot prima <command>` runs the same tool if you already have it.
|
|
19
19
|
|
|
20
|
-
Requires Node.js
|
|
20
|
+
Requires Node.js 22.13+. Playwright browsers come from `npx playwright install chromium`.
|
|
21
21
|
|
|
22
22
|
## Session
|
|
23
23
|
|
|
24
|
-
Prima
|
|
24
|
+
Prima drives a browser opened by `playwright-cli` and never launches one itself:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
playwright-cli open https://app.example.com
|
|
28
|
+
prima-cli browser
|
|
28
29
|
prima-cli check "the settings page saves a changed theme"
|
|
29
30
|
playwright-cli close
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
prima-cli browser start --url https://app.example.com
|
|
36
|
-
prima-cli do "open the account menu" "choose the settings entry"
|
|
37
|
-
prima-cli browser stop
|
|
38
|
-
```
|
|
33
|
+
`browser` checks that a `playwright-cli` session is open and fails with the command to open one when it is not.
|
|
39
34
|
|
|
40
35
|
## Commands
|
|
41
36
|
|
|
@@ -50,7 +45,7 @@ prima-cli browser stop
|
|
|
50
45
|
| `pw <fn>` | Run a Playwright function expression against the open page |
|
|
51
46
|
| `status <hash>` | Show the artifacts and page detail recorded for an earlier command |
|
|
52
47
|
| `report` | Turn every command of a session into one html and markdown report |
|
|
53
|
-
| `browser
|
|
48
|
+
| `browser` | Check that a playwright-cli session is open, and fail with the command to open one when it is not |
|
|
54
49
|
| `config` | Show models, config file and paths used by this run |
|
|
55
50
|
|
|
56
51
|
`check` takes an outcome rather than a click path. It runs on the page you are already on and never reloads it, so an open dialog survives the check. Each `--expected` outcome comes back as PASSED, FAILED, CONTRADICTION or not verified — settled against a screenshot of the whole page, because what a user can see is the proof.
|
package/boat/prima/package.json
CHANGED
package/boat/prima/src/cli.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { keepServerRunning } from '../../../src/browser-server.ts';
|
|
3
2
|
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
4
3
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
5
4
|
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
@@ -22,8 +21,6 @@ function buildOptions(subcommand: any): PrimaOptions {
|
|
|
22
21
|
noVision: options.vision === false,
|
|
23
22
|
url: options.url,
|
|
24
23
|
baseUrl: options.baseUrl,
|
|
25
|
-
show: options.show,
|
|
26
|
-
headless: options.headless,
|
|
27
24
|
endpoint: options.endpoint,
|
|
28
25
|
pwSession: options.pwSession,
|
|
29
26
|
};
|
|
@@ -89,18 +86,6 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
|
|
|
89
86
|
process.exit(envelope.ok ? 0 : 1);
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>): Promise<void> {
|
|
93
|
-
let ok = false;
|
|
94
|
-
try {
|
|
95
|
-
ok = await run(primaFor(options));
|
|
96
|
-
} catch (error) {
|
|
97
|
-
console.error(browserErrorMessage(error));
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
process.exit(ok ? 0 : 1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
89
|
export function createPrimaCommands(name = 'prima'): Command {
|
|
105
90
|
const cmd = new Command(name);
|
|
106
91
|
cmd.description('Drives a web app through described behaviour, not locators');
|
|
@@ -179,41 +164,14 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
179
164
|
process.exit(0);
|
|
180
165
|
});
|
|
181
166
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
console.log(await prima.browserStatus());
|
|
191
|
-
return keepServerRunning(() => prima.browserStop());
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
addBrowserOptions(browser.command('stop').description('Stop the browser of this instance'))
|
|
196
|
-
.option('--all', 'Stop every running instance')
|
|
197
|
-
.action(async (options) => {
|
|
198
|
-
await runBrowser(options, async (prima) => {
|
|
199
|
-
const stopped = await prima.browserStop(options.all);
|
|
200
|
-
console.log(await prima.browserStatus());
|
|
201
|
-
return stopped;
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
addBrowserOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
|
|
206
|
-
await runBrowser(options, async (prima) => {
|
|
207
|
-
console.log(await prima.browserStatus());
|
|
208
|
-
return true;
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
addBrowserOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
|
|
213
|
-
await runBrowser(options, async (prima) => {
|
|
214
|
-
console.log(await prima.browserList());
|
|
215
|
-
return true;
|
|
216
|
-
});
|
|
167
|
+
addBrowserOptions(cmd.command('browser').description('Check that a playwright-cli session is open for prima to drive')).action(async (options) => {
|
|
168
|
+
try {
|
|
169
|
+
console.log(await primaFor(options).browserCheck());
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(browserErrorMessage(error));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
217
175
|
});
|
|
218
176
|
|
|
219
177
|
return cmd;
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { JUDGE_PAGE_CAP, UNDECIDED } from '../../../src/ai/judge.ts';
|
|
|
12
12
|
import { getPreviousResearch } from '../../../src/ai/researcher/cache.ts';
|
|
13
13
|
import { actionRule, locatorRule } from '../../../src/ai/rules.ts';
|
|
14
14
|
import { createAgentTools, createCodeceptJSTools, createRefTools } from '../../../src/ai/tools.ts';
|
|
15
|
-
import { getAliveEndpoint,
|
|
15
|
+
import { getAliveEndpoint, listInstances } from '../../../src/browser-server.ts';
|
|
16
16
|
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
17
17
|
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, type ExplorbotConfig, outputPath } from '../../../src/config.ts';
|
|
18
18
|
import { ExplorBot } from '../../../src/explorbot.ts';
|
|
@@ -73,7 +73,6 @@ export class Prima {
|
|
|
73
73
|
private artifactsDir?: string;
|
|
74
74
|
private hash?: string;
|
|
75
75
|
private sessionUrl?: string;
|
|
76
|
-
private server: { close: () => Promise<void> } | null = null;
|
|
77
76
|
private attached: string | null = null;
|
|
78
77
|
private session: SessionRun | null = null;
|
|
79
78
|
|
|
@@ -503,25 +502,12 @@ export class Prima {
|
|
|
503
502
|
return envelope;
|
|
504
503
|
}
|
|
505
504
|
|
|
506
|
-
async
|
|
507
|
-
const config = await this.loadConfig();
|
|
508
|
-
let show = config.playwright.show || false;
|
|
509
|
-
if (this.options.show) show = true;
|
|
510
|
-
if (this.options.headless) show = false;
|
|
511
|
-
this.server = await this.launchOwnServer({ browser: config.playwright.browser, show }, this.instanceName());
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
async browserStop(all = false): Promise<boolean> {
|
|
505
|
+
async browserCheck(): Promise<string> {
|
|
515
506
|
await this.loadConfig();
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (!
|
|
519
|
-
|
|
520
|
-
let stopped = false;
|
|
521
|
-
for (const instance of listInstances()) {
|
|
522
|
-
if (await this.stopInstance(instance.name)) stopped = true;
|
|
523
|
-
}
|
|
524
|
-
return stopped;
|
|
507
|
+
const { match, candidates, browser } = await this.discover();
|
|
508
|
+
await browser?.close().catch(() => {});
|
|
509
|
+
if (!match) throw this.missingSessionError(candidates);
|
|
510
|
+
return `browser: ${this.attachmentLabel(match)} at ${match.endpoint}`;
|
|
525
511
|
}
|
|
526
512
|
|
|
527
513
|
async config(json?: boolean): Promise<string> {
|
|
@@ -572,37 +558,6 @@ export class Prima {
|
|
|
572
558
|
].join('\n');
|
|
573
559
|
}
|
|
574
560
|
|
|
575
|
-
async browserStatus(): Promise<string> {
|
|
576
|
-
await this.loadConfig();
|
|
577
|
-
const info = await this.instanceInfo();
|
|
578
|
-
const endpoint = await getAliveEndpoint(info.name);
|
|
579
|
-
const others = info.others.map((other) => other.name).join(', ') || 'none';
|
|
580
|
-
const lines = [`instance: ${info.name} (${info.tabs} ${pluralize(info.tabs, 'tab')}) | other instances: ${others}`];
|
|
581
|
-
if (!endpoint) lines.push('browser: not running');
|
|
582
|
-
if (endpoint) lines.push(`browser: running at ${endpoint}`);
|
|
583
|
-
return lines.join('\n');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async browserList(): Promise<string> {
|
|
587
|
-
await this.loadConfig();
|
|
588
|
-
|
|
589
|
-
const lines: string[] = [];
|
|
590
|
-
for (const instance of listInstances()) {
|
|
591
|
-
const endpoint = await getAliveEndpoint(instance.name);
|
|
592
|
-
if (!endpoint) continue;
|
|
593
|
-
lines.push(`prima --instance ${instance.name} ${endpoint}`);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const discovery = await this.discover();
|
|
597
|
-
await discovery.browser?.close().catch(() => {});
|
|
598
|
-
for (const descriptor of discovery.candidates) {
|
|
599
|
-
lines.push(`playwright-cli --pw-session ${descriptor.title} ${descriptor.endpoint}`);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (!lines.length) return 'no browser instances running';
|
|
603
|
-
return lines.join('\n');
|
|
604
|
-
}
|
|
605
|
-
|
|
606
561
|
async instanceInfo(): Promise<InstanceInfo> {
|
|
607
562
|
const name = this.instanceName();
|
|
608
563
|
const others = listInstances()
|
|
@@ -666,22 +621,27 @@ export class Prima {
|
|
|
666
621
|
const { match, candidates, browser } = discovered || (await this.discover());
|
|
667
622
|
if (match && (await this.attachToEndpoint(match, browser))) return;
|
|
668
623
|
|
|
669
|
-
if (!match && candidates.length)
|
|
624
|
+
if (!match && candidates.length) throw this.missingSessionError(candidates);
|
|
625
|
+
|
|
626
|
+
if (await this.connectOwnInstance()) return;
|
|
627
|
+
|
|
628
|
+
throw this.missingSessionError(candidates);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private missingSessionError(candidates: PwServerDescriptor[]): Error {
|
|
632
|
+
if (candidates.length) {
|
|
670
633
|
const titles = candidates.map((candidate) => candidate.title).join(', ');
|
|
671
|
-
|
|
634
|
+
return new Error(dedent`
|
|
672
635
|
Several playwright-cli sessions are open: ${titles}
|
|
673
636
|
Pick one with --pw-session <title>.
|
|
674
637
|
`);
|
|
675
638
|
}
|
|
676
639
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
playwright-cli open <url> prima attaches to this workspace session by default
|
|
683
|
-
prima browser start starts a prima-owned browser instead
|
|
684
|
-
Prima never launches a browser implicitly.
|
|
640
|
+
return new Error(dedent`
|
|
641
|
+
No playwright-cli session is open for prima to drive.
|
|
642
|
+
Start one first:
|
|
643
|
+
playwright-cli open ${this.options.url || '<url>'}
|
|
644
|
+
Prima attaches to this workspace session by default and never launches a browser itself.
|
|
685
645
|
`);
|
|
686
646
|
}
|
|
687
647
|
|
|
@@ -751,20 +711,6 @@ export class Prima {
|
|
|
751
711
|
return true;
|
|
752
712
|
}
|
|
753
713
|
|
|
754
|
-
private async launchOwnServer(opts: { browser?: string; show?: boolean }, instance: string): Promise<{ close: () => Promise<void> }> {
|
|
755
|
-
return launchServer(opts, instance);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
private async stopInstance(instance: string): Promise<boolean> {
|
|
759
|
-
if (instance === this.instanceName()) {
|
|
760
|
-
const server = this.server;
|
|
761
|
-
this.server = null;
|
|
762
|
-
await server?.close();
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
return stopServer(instance);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
714
|
private isUrlTarget(target: string): boolean {
|
|
769
715
|
const value = target.trim();
|
|
770
716
|
if (value.startsWith('/')) return true;
|
|
@@ -1229,8 +1175,6 @@ export interface PrimaOptions {
|
|
|
1229
1175
|
noVision?: boolean;
|
|
1230
1176
|
url?: string;
|
|
1231
1177
|
baseUrl?: string;
|
|
1232
|
-
show?: boolean;
|
|
1233
|
-
headless?: boolean;
|
|
1234
1178
|
endpoint?: string;
|
|
1235
1179
|
pwSession?: string;
|
|
1236
1180
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { keepServerRunning } from "../../../src/browser-server.js";
|
|
3
2
|
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
4
3
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
5
4
|
import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
|
|
@@ -20,8 +19,6 @@ function buildOptions(subcommand) {
|
|
|
20
19
|
noVision: options.vision === false,
|
|
21
20
|
url: options.url,
|
|
22
21
|
baseUrl: options.baseUrl,
|
|
23
|
-
show: options.show,
|
|
24
|
-
headless: options.headless,
|
|
25
22
|
endpoint: options.endpoint,
|
|
26
23
|
pwSession: options.pwSession,
|
|
27
24
|
};
|
|
@@ -87,17 +84,6 @@ async function runPrima(options, command, run, opts = {}) {
|
|
|
87
84
|
await prima.stop().catch(() => { });
|
|
88
85
|
process.exit(envelope.ok ? 0 : 1);
|
|
89
86
|
}
|
|
90
|
-
async function runBrowser(options, run) {
|
|
91
|
-
let ok = false;
|
|
92
|
-
try {
|
|
93
|
-
ok = await run(primaFor(options));
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
console.error(browserErrorMessage(error));
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
process.exit(ok ? 0 : 1);
|
|
100
|
-
}
|
|
101
87
|
export function createPrimaCommands(name = 'prima') {
|
|
102
88
|
const cmd = new Command(name);
|
|
103
89
|
cmd.description('Drives a web app through described behaviour, not locators');
|
|
@@ -163,37 +149,15 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
163
149
|
.catch((error) => browserErrorMessage(error)));
|
|
164
150
|
process.exit(0);
|
|
165
151
|
});
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
addBrowserOptions(browser.command('stop').description('Stop the browser of this instance'))
|
|
178
|
-
.option('--all', 'Stop every running instance')
|
|
179
|
-
.action(async (options) => {
|
|
180
|
-
await runBrowser(options, async (prima) => {
|
|
181
|
-
const stopped = await prima.browserStop(options.all);
|
|
182
|
-
console.log(await prima.browserStatus());
|
|
183
|
-
return stopped;
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
addBrowserOptions(browser.command('status').description('Report the browser of this instance')).action(async (options) => {
|
|
187
|
-
await runBrowser(options, async (prima) => {
|
|
188
|
-
console.log(await prima.browserStatus());
|
|
189
|
-
return true;
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
addBrowserOptions(browser.command('list').description('List every browser instance that is running')).action(async (options) => {
|
|
193
|
-
await runBrowser(options, async (prima) => {
|
|
194
|
-
console.log(await prima.browserList());
|
|
195
|
-
return true;
|
|
196
|
-
});
|
|
152
|
+
addBrowserOptions(cmd.command('browser').description('Check that a playwright-cli session is open for prima to drive')).action(async (options) => {
|
|
153
|
+
try {
|
|
154
|
+
console.log(await primaFor(options).browserCheck());
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error(browserErrorMessage(error));
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
process.exit(0);
|
|
197
161
|
});
|
|
198
162
|
return cmd;
|
|
199
163
|
}
|
|
@@ -11,7 +11,7 @@ import { JUDGE_PAGE_CAP, UNDECIDED } from "../../../src/ai/judge.js";
|
|
|
11
11
|
import { getPreviousResearch } from "../../../src/ai/researcher/cache.js";
|
|
12
12
|
import { actionRule, locatorRule } from "../../../src/ai/rules.js";
|
|
13
13
|
import { createAgentTools, createCodeceptJSTools, createRefTools } from "../../../src/ai/tools.js";
|
|
14
|
-
import { getAliveEndpoint,
|
|
14
|
+
import { getAliveEndpoint, listInstances } from "../../../src/browser-server.js";
|
|
15
15
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
16
16
|
import { ConfigMissingError, ConfigParser, EXPLORBOT_ENV_VARS, outputPath } from "../../../src/config.js";
|
|
17
17
|
import { ExplorBot } from "../../../src/explorbot.js";
|
|
@@ -67,7 +67,6 @@ export class Prima {
|
|
|
67
67
|
artifactsDir;
|
|
68
68
|
hash;
|
|
69
69
|
sessionUrl;
|
|
70
|
-
server = null;
|
|
71
70
|
attached = null;
|
|
72
71
|
session = null;
|
|
73
72
|
constructor(options = {}) {
|
|
@@ -460,27 +459,13 @@ export class Prima {
|
|
|
460
459
|
envelope.page.redirectedFrom = target;
|
|
461
460
|
return envelope;
|
|
462
461
|
}
|
|
463
|
-
async
|
|
464
|
-
const config = await this.loadConfig();
|
|
465
|
-
let show = config.playwright.show || false;
|
|
466
|
-
if (this.options.show)
|
|
467
|
-
show = true;
|
|
468
|
-
if (this.options.headless)
|
|
469
|
-
show = false;
|
|
470
|
-
this.server = await this.launchOwnServer({ browser: config.playwright.browser, show }, this.instanceName());
|
|
471
|
-
}
|
|
472
|
-
async browserStop(all = false) {
|
|
462
|
+
async browserCheck() {
|
|
473
463
|
await this.loadConfig();
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (!
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
for (const instance of listInstances()) {
|
|
480
|
-
if (await this.stopInstance(instance.name))
|
|
481
|
-
stopped = true;
|
|
482
|
-
}
|
|
483
|
-
return stopped;
|
|
464
|
+
const { match, candidates, browser } = await this.discover();
|
|
465
|
+
await browser?.close().catch(() => { });
|
|
466
|
+
if (!match)
|
|
467
|
+
throw this.missingSessionError(candidates);
|
|
468
|
+
return `browser: ${this.attachmentLabel(match)} at ${match.endpoint}`;
|
|
484
469
|
}
|
|
485
470
|
async config(json) {
|
|
486
471
|
const [site] = listSites();
|
|
@@ -529,36 +514,6 @@ export class Prima {
|
|
|
529
514
|
`upload: TESTOMATIO=<apiKey> npx @testomatio/reporter replay ${file}`,
|
|
530
515
|
].join('\n');
|
|
531
516
|
}
|
|
532
|
-
async browserStatus() {
|
|
533
|
-
await this.loadConfig();
|
|
534
|
-
const info = await this.instanceInfo();
|
|
535
|
-
const endpoint = await getAliveEndpoint(info.name);
|
|
536
|
-
const others = info.others.map((other) => other.name).join(', ') || 'none';
|
|
537
|
-
const lines = [`instance: ${info.name} (${info.tabs} ${pluralize(info.tabs, 'tab')}) | other instances: ${others}`];
|
|
538
|
-
if (!endpoint)
|
|
539
|
-
lines.push('browser: not running');
|
|
540
|
-
if (endpoint)
|
|
541
|
-
lines.push(`browser: running at ${endpoint}`);
|
|
542
|
-
return lines.join('\n');
|
|
543
|
-
}
|
|
544
|
-
async browserList() {
|
|
545
|
-
await this.loadConfig();
|
|
546
|
-
const lines = [];
|
|
547
|
-
for (const instance of listInstances()) {
|
|
548
|
-
const endpoint = await getAliveEndpoint(instance.name);
|
|
549
|
-
if (!endpoint)
|
|
550
|
-
continue;
|
|
551
|
-
lines.push(`prima --instance ${instance.name} ${endpoint}`);
|
|
552
|
-
}
|
|
553
|
-
const discovery = await this.discover();
|
|
554
|
-
await discovery.browser?.close().catch(() => { });
|
|
555
|
-
for (const descriptor of discovery.candidates) {
|
|
556
|
-
lines.push(`playwright-cli --pw-session ${descriptor.title} ${descriptor.endpoint}`);
|
|
557
|
-
}
|
|
558
|
-
if (!lines.length)
|
|
559
|
-
return 'no browser instances running';
|
|
560
|
-
return lines.join('\n');
|
|
561
|
-
}
|
|
562
517
|
async instanceInfo() {
|
|
563
518
|
const name = this.instanceName();
|
|
564
519
|
const others = listInstances()
|
|
@@ -621,21 +576,25 @@ export class Prima {
|
|
|
621
576
|
const { match, candidates, browser } = discovered || (await this.discover());
|
|
622
577
|
if (match && (await this.attachToEndpoint(match, browser)))
|
|
623
578
|
return;
|
|
624
|
-
if (!match && candidates.length)
|
|
579
|
+
if (!match && candidates.length)
|
|
580
|
+
throw this.missingSessionError(candidates);
|
|
581
|
+
if (await this.connectOwnInstance())
|
|
582
|
+
return;
|
|
583
|
+
throw this.missingSessionError(candidates);
|
|
584
|
+
}
|
|
585
|
+
missingSessionError(candidates) {
|
|
586
|
+
if (candidates.length) {
|
|
625
587
|
const titles = candidates.map((candidate) => candidate.title).join(', ');
|
|
626
|
-
|
|
588
|
+
return new Error(dedent `
|
|
627
589
|
Several playwright-cli sessions are open: ${titles}
|
|
628
590
|
Pick one with --pw-session <title>.
|
|
629
591
|
`);
|
|
630
592
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
playwright-cli open <url> prima attaches to this workspace session by default
|
|
637
|
-
prima browser start starts a prima-owned browser instead
|
|
638
|
-
Prima never launches a browser implicitly.
|
|
593
|
+
return new Error(dedent `
|
|
594
|
+
No playwright-cli session is open for prima to drive.
|
|
595
|
+
Start one first:
|
|
596
|
+
playwright-cli open ${this.options.url || '<url>'}
|
|
597
|
+
Prima attaches to this workspace session by default and never launches a browser itself.
|
|
639
598
|
`);
|
|
640
599
|
}
|
|
641
600
|
async discover(descriptors = readDescriptors()) {
|
|
@@ -703,17 +662,6 @@ export class Prima {
|
|
|
703
662
|
this.session = { key: this.instanceName(), endpoint, title: `prima instance "${this.instanceName()}"` };
|
|
704
663
|
return true;
|
|
705
664
|
}
|
|
706
|
-
async launchOwnServer(opts, instance) {
|
|
707
|
-
return launchServer(opts, instance);
|
|
708
|
-
}
|
|
709
|
-
async stopInstance(instance) {
|
|
710
|
-
if (instance === this.instanceName()) {
|
|
711
|
-
const server = this.server;
|
|
712
|
-
this.server = null;
|
|
713
|
-
await server?.close();
|
|
714
|
-
}
|
|
715
|
-
return stopServer(instance);
|
|
716
|
-
}
|
|
717
665
|
isUrlTarget(target) {
|
|
718
666
|
const value = target.trim();
|
|
719
667
|
if (value.startsWith('/'))
|
package/dist/package.json
CHANGED
package/dist/src/ai/navigator.js
CHANGED
|
@@ -7,6 +7,7 @@ import { normalizeUrl } from '../state-manager.js';
|
|
|
7
7
|
import { isFatalBrowserError } from "../utils/browser-errors.js";
|
|
8
8
|
import { getCliName } from "../utils/cli-name.js";
|
|
9
9
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
10
|
+
import { isLoginPage } from "../utils/error-page.js";
|
|
10
11
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
11
12
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
12
13
|
import { loop, pause } from '../utils/loop.js';
|
|
@@ -15,7 +16,7 @@ import { normalizeInlineText } from "../utils/strings.js";
|
|
|
15
16
|
import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
16
17
|
import { JUDGE_PAGE_CAP, UNDECIDED } from "./judge.js";
|
|
17
18
|
import { Researcher } from "./researcher.js";
|
|
18
|
-
import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
19
|
+
import { actionRule, credentialsRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
19
20
|
import { isInteractive } from './task-agent.js';
|
|
20
21
|
import { createLearnExperienceTool } from "./tools.js";
|
|
21
22
|
const debugLog = createDebug('explorbot:navigator');
|
|
@@ -46,7 +47,7 @@ class Navigator {
|
|
|
46
47
|
NEVER use executeScript, executeAsyncScript, or any JS evaluation to change the URL, bypass redirects, or fake the page state.
|
|
47
48
|
If the target URL redirects to an authentication/login page, DO NOT try to force the original URL. Instead:
|
|
48
49
|
1. Look for credentials in the provided knowledge/hint context and perform a real login through the form.
|
|
49
|
-
2. If no credentials are available,
|
|
50
|
+
2. If no credentials are available, stop and name the credentials that are missing. Never guess them.
|
|
50
51
|
A redirect to /login, /sign_in, /auth, or similar is a signal that authentication is required — treat it as such, never as an obstacle to bypass.
|
|
51
52
|
</constraints>
|
|
52
53
|
`;
|
|
@@ -344,6 +345,9 @@ class Navigator {
|
|
|
344
345
|
if (!experience && !actionResult.isInsideIframe) {
|
|
345
346
|
experience = this.experienceTracker.renderExperienceFor(actionResult);
|
|
346
347
|
}
|
|
348
|
+
let credentials = '';
|
|
349
|
+
if (isLoginPage(actionResult))
|
|
350
|
+
credentials = credentialsRule;
|
|
347
351
|
return dedent `
|
|
348
352
|
<message>
|
|
349
353
|
${message}
|
|
@@ -369,6 +373,8 @@ class Navigator {
|
|
|
369
373
|
|
|
370
374
|
${unexpectedPopupRule}
|
|
371
375
|
|
|
376
|
+
${credentials}
|
|
377
|
+
|
|
372
378
|
${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
|
|
373
379
|
|
|
374
380
|
${experience}
|
package/dist/src/ai/rules.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const locatorRule: string;
|
|
|
3
3
|
export declare const drillLocatorRule: string;
|
|
4
4
|
export declare const fileUploadRule: string;
|
|
5
5
|
export declare const formRequirementsRule: string;
|
|
6
|
+
export declare const credentialsRule: string;
|
|
6
7
|
export declare const protectionRule: string;
|
|
7
8
|
export declare const dataProtectionRules: string;
|
|
8
9
|
export declare const capabilityGroundingRule: string;
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -138,6 +138,13 @@ export const formRequirementsRule = dedent `
|
|
|
138
138
|
Before filling a form that persists data (create/update), read each control's requirements (required, type/format, length, placeholder/aria-describedby hints) from <page_aria> and page HTML — call context() if not visible — and enter values that satisfy them. Search/filter/sort forms that only change the view do not need this.
|
|
139
139
|
</form_requirements>
|
|
140
140
|
`;
|
|
141
|
+
export const credentialsRule = dedent `
|
|
142
|
+
<credentials>
|
|
143
|
+
Sign in to an existing account only with credentials present in the provided context (message, knowledge, experience).
|
|
144
|
+
Never invent, guess, or try default, common, or placeholder credentials for an existing account.
|
|
145
|
+
If the required credentials are not provided, call stop() and name what is missing — do not submit the form.
|
|
146
|
+
</credentials>
|
|
147
|
+
`;
|
|
141
148
|
// in rage mode we do not protect from irreversible actions
|
|
142
149
|
export const protectionRule = dedent `
|
|
143
150
|
<important>
|
|
@@ -3,6 +3,7 @@ import type { WebPageState } from '../state-manager.js';
|
|
|
3
3
|
export type PageCondition = 'ok' | 'loading' | 'error';
|
|
4
4
|
export declare function detectPageCondition(actionResult: ActionResult): PageCondition;
|
|
5
5
|
export declare function isErrorPage(actionResult: ActionResult): boolean;
|
|
6
|
+
export declare function isLoginPage(actionResult: ActionResult): boolean;
|
|
6
7
|
export declare function getStateErrorPageError(state: WebPageState | null | undefined): ErrorPageError | null;
|
|
7
8
|
export declare class ErrorPageError extends Error {
|
|
8
9
|
readonly url: string;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
2
|
import { isBodyEmpty } from './html.js';
|
|
3
|
+
import { extractStatePath } from './url-matcher.js';
|
|
3
4
|
const HTTP_ERRORS = ['400 Bad Request', '401 Unauthorized', '403 Forbidden', '404 Not Found', '405 Method Not Allowed', '408 Request Timeout', '500 Internal Server Error', '502 Bad Gateway', '503 Service Unavailable', '504 Gateway Timeout'];
|
|
4
5
|
const SMALL_PAGE_THRESHOLD = 500;
|
|
5
6
|
const LOADING_WORD = /\bloading\b/i;
|
|
7
|
+
const PASSWORD_INPUT = /<input\b[^>]*\btype\s*=\s*["']?password\b/i;
|
|
8
|
+
const AUTH_PATH = /\b(log[-_]?in|sign[-_]?in|auth)\b/i;
|
|
6
9
|
export function detectPageCondition(actionResult) {
|
|
7
10
|
if (actionResult.httpStatus && actionResult.httpStatus >= 400)
|
|
8
11
|
return 'error';
|
|
@@ -32,6 +35,11 @@ export function detectPageCondition(actionResult) {
|
|
|
32
35
|
export function isErrorPage(actionResult) {
|
|
33
36
|
return detectPageCondition(actionResult) === 'error';
|
|
34
37
|
}
|
|
38
|
+
export function isLoginPage(actionResult) {
|
|
39
|
+
if (PASSWORD_INPUT.test(actionResult.html || ''))
|
|
40
|
+
return true;
|
|
41
|
+
return AUTH_PATH.test(extractStatePath(actionResult.url || '').split('?')[0]);
|
|
42
|
+
}
|
|
35
43
|
export function getStateErrorPageError(state) {
|
|
36
44
|
if (!state)
|
|
37
45
|
return null;
|
|
@@ -57,6 +57,8 @@ export declare class MarkdownQuery {
|
|
|
57
57
|
sectionsOf(candidates: MatchedRange[], segment: QuerySegment): MatchedRange[];
|
|
58
58
|
itemsOf(candidates: MatchedRange[]): MatchedRange[];
|
|
59
59
|
expandSections(matches: MatchedRange[]): MatchedRange[];
|
|
60
|
+
evaluate(candidates: MatchedRange[], expression: QueryExpression): MatchedRange[];
|
|
61
|
+
union(results: MatchedRange[][]): MatchedRange[];
|
|
60
62
|
narrow(matches: MatchedRange[], segment: QuerySegment): MatchedRange[];
|
|
61
63
|
filterByText(matches: MatchedRange[], segment: QuerySegment): MatchedRange[];
|
|
62
64
|
matchText(text: string, matcher: TextMatcher): boolean;
|
|
@@ -77,7 +79,7 @@ export declare class MdqOperationError extends MdqError {
|
|
|
77
79
|
constructor(message: string);
|
|
78
80
|
}
|
|
79
81
|
export declare function mdq(source: Markdown): MarkdownQuery;
|
|
80
|
-
export declare function parseQuery(input: string):
|
|
82
|
+
export declare function parseQuery(input: string): QueryExpression;
|
|
81
83
|
export declare function buildTokenIndex(source: string): MatchedRange[];
|
|
82
84
|
export type Markdown = string | MarkdownQuery;
|
|
83
85
|
export type Matcher = string | RegExp | ((text: string) => boolean);
|
|
@@ -92,6 +94,7 @@ export interface TextMatcher {
|
|
|
92
94
|
flags?: string;
|
|
93
95
|
predicate?: (text: string) => boolean;
|
|
94
96
|
}
|
|
97
|
+
export type QueryExpression = QuerySegment[][][];
|
|
95
98
|
export interface QuerySegment {
|
|
96
99
|
selector: SelectorType;
|
|
97
100
|
textMatch?: TextMatcher;
|
|
@@ -9,10 +9,13 @@ export class MarkdownQuery {
|
|
|
9
9
|
this.matches = matches || buildTokenIndex(source);
|
|
10
10
|
}
|
|
11
11
|
query(selector, matcher) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const expression = parseQuery(selector);
|
|
13
|
+
for (const chain of expression.flat()) {
|
|
14
|
+
const last = chain.at(-1);
|
|
15
|
+
if (matcher !== undefined && last)
|
|
16
|
+
last.textMatch = this.valueMatcher(matcher);
|
|
17
|
+
}
|
|
18
|
+
return new MarkdownQuery(this.source, this.evaluate(this.expandSections(this.matches), expression));
|
|
16
19
|
}
|
|
17
20
|
section(matcher, options) {
|
|
18
21
|
return this.query(`section${options?.depth || ''}`, matcher);
|
|
@@ -299,6 +302,27 @@ export class MarkdownQuery {
|
|
|
299
302
|
}
|
|
300
303
|
return expanded;
|
|
301
304
|
}
|
|
305
|
+
evaluate(candidates, expression) {
|
|
306
|
+
const results = [];
|
|
307
|
+
for (const operands of expression) {
|
|
308
|
+
const matched = operands.map((chain) => this.run(candidates, chain));
|
|
309
|
+
if (matched.some((ranges) => ranges.length === 0))
|
|
310
|
+
continue;
|
|
311
|
+
results.push(...matched);
|
|
312
|
+
}
|
|
313
|
+
return this.union(results);
|
|
314
|
+
}
|
|
315
|
+
union(results) {
|
|
316
|
+
if (results.length === 1)
|
|
317
|
+
return results[0];
|
|
318
|
+
const unique = new Map();
|
|
319
|
+
for (const range of results.flat()) {
|
|
320
|
+
const key = `${range.start}:${range.length}:${Boolean(range.innerTokens)}`;
|
|
321
|
+
if (!unique.has(key))
|
|
322
|
+
unique.set(key, range);
|
|
323
|
+
}
|
|
324
|
+
return [...unique.values()].sort((a, b) => a.start - b.start || b.length - a.length);
|
|
325
|
+
}
|
|
302
326
|
narrow(matches, segment) {
|
|
303
327
|
if (segment.index !== null) {
|
|
304
328
|
let index = segment.index;
|
|
@@ -390,10 +414,24 @@ export function mdq(source) {
|
|
|
390
414
|
return new MarkdownQuery(String(source));
|
|
391
415
|
}
|
|
392
416
|
export function parseQuery(input) {
|
|
393
|
-
const
|
|
417
|
+
const expression = [[[]]];
|
|
418
|
+
let chain = expression[0][0];
|
|
394
419
|
SEGMENT.lastIndex = 0;
|
|
395
420
|
while (SEGMENT.lastIndex < input.length) {
|
|
396
421
|
const at = SEGMENT.lastIndex;
|
|
422
|
+
OPERATOR.lastIndex = at;
|
|
423
|
+
const operator = OPERATOR.exec(input);
|
|
424
|
+
if (operator) {
|
|
425
|
+
if (chain.length === 0)
|
|
426
|
+
throw new MdqSelectorError(`Empty selector before ${operator[1]}`, at);
|
|
427
|
+
chain = [];
|
|
428
|
+
if (operator[1] === '||')
|
|
429
|
+
expression.push([chain]);
|
|
430
|
+
if (operator[1] === '&&')
|
|
431
|
+
expression[expression.length - 1].push(chain);
|
|
432
|
+
SEGMENT.lastIndex = OPERATOR.lastIndex;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
397
435
|
const match = SEGMENT.exec(input);
|
|
398
436
|
if (!match)
|
|
399
437
|
throw new MdqSelectorError(`Unexpected character "${input[at]}" in selector`, at);
|
|
@@ -424,9 +462,11 @@ export function parseQuery(input) {
|
|
|
424
462
|
if (to)
|
|
425
463
|
segment.slice.to = Number.parseInt(to, 10);
|
|
426
464
|
}
|
|
427
|
-
|
|
465
|
+
chain.push(segment);
|
|
428
466
|
}
|
|
429
|
-
|
|
467
|
+
if (expression.flat().length > 1 && chain.length === 0)
|
|
468
|
+
throw new MdqSelectorError('Empty selector after operator', input.length);
|
|
469
|
+
return expression;
|
|
430
470
|
}
|
|
431
471
|
export function buildTokenIndex(source) {
|
|
432
472
|
const { body, offset } = MarkdownEditor.splitFrontmatter(source);
|
|
@@ -449,3 +489,4 @@ export function buildTokenIndex(source) {
|
|
|
449
489
|
const SELECTOR_NAMES = new Set(['section', 'heading', 'paragraph', 'table', 'list', 'item', 'code', 'blockquote', 'hr', 'html', 'comment']);
|
|
450
490
|
const TOKEN_ALIASES = { item: 'list_item' };
|
|
451
491
|
const SEGMENT = /(\s*\.?)([A-Za-z]\w*)(?:\((!?)(~?)(?:"((?:[^"\\]|\\.)*)"|\/((?:[^/\\]|\\.)*)\/([a-z]*))\))?((?:\[[^\]]*\])*)\s*/y;
|
|
492
|
+
const OPERATOR = /\s*(&&|\|\|)\s*/y;
|
|
@@ -733,16 +733,16 @@ html: /home/you/.explorbot/sites/app.example.com/output/prima/2026-08-04T10-04-2
|
|
|
733
733
|
|
|
734
734
|
### Browsers and sessions
|
|
735
735
|
|
|
736
|
-
Prima never launches a browser
|
|
736
|
+
Prima never launches a browser. Open a playwright-cli session first, and check prima sees it:
|
|
737
737
|
|
|
738
738
|
```bash
|
|
739
739
|
playwright-cli open https://app.example.com # the session prima attaches to by default
|
|
740
|
-
npx explorbot prima browser
|
|
740
|
+
npx explorbot prima browser # prints the session, or fails with the command to open one
|
|
741
741
|
```
|
|
742
742
|
|
|
743
|
-
By default prima attaches to the playwright-cli browser of the current workspace and works on the tabs it already has open — driving the same session from both tools is the intended usage. Stopping prima disconnects from an attached browser; it never closes it.
|
|
743
|
+
By default prima attaches to the playwright-cli browser of the current workspace and works on the tabs it already has open — driving the same session from both tools is the intended usage. Stopping prima disconnects from an attached browser; it never closes it. The `### Instance` block names the browser you are on.
|
|
744
744
|
|
|
745
|
-
Prima reaches every browser over a Playwright browser-server endpoint — a playwright-cli session, an `--endpoint`, or
|
|
745
|
+
Prima reaches every browser over a Playwright browser-server endpoint — a playwright-cli session, an `--endpoint`, or an `explorbot browser start` instance — and that client needs the Node build, which is what `npx explorbot prima` and the published `prima` bin run on. Driving a browser by running the CLI from source under Bun does not connect.
|
|
746
746
|
|
|
747
747
|
Every command takes these:
|
|
748
748
|
|
|
@@ -769,8 +769,6 @@ A few commands add their own:
|
|
|
769
769
|
| `research` | `--data` | Include data extraction in the map |
|
|
770
770
|
| `research` | `--deep` | Expand hidden elements for a deeper map |
|
|
771
771
|
| `research` | `--fresh` | Ignore the cached map and research the page again |
|
|
772
|
-
| `browser start` | `-s, --show` / `--headless` | Launch the browser with or without a window |
|
|
773
|
-
| `browser stop` | `--all` | Stop every running instance |
|
|
774
772
|
|
|
775
773
|
Prima carries no logging flags of its own. `DEBUG` in front of a command prints everything the run does — config and browser attachment, every step as it executes, and the debug stream of the agents behind it:
|
|
776
774
|
|
package/package.json
CHANGED
package/src/ai/navigator.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { type StateManager, normalizeUrl } from '../state-manager.js';
|
|
|
13
13
|
import { isFatalBrowserError } from '../utils/browser-errors.ts';
|
|
14
14
|
import { getCliName } from '../utils/cli-name.ts';
|
|
15
15
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
16
|
+
import { isLoginPage } from '../utils/error-page.ts';
|
|
16
17
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
17
18
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
18
19
|
import { loop, pause } from '../utils/loop.js';
|
|
@@ -24,7 +25,7 @@ import type { Conversation } from './conversation.js';
|
|
|
24
25
|
import { type Decision, JUDGE_PAGE_CAP, type Judge, UNDECIDED } from './judge.ts';
|
|
25
26
|
import type { Provider } from './provider.js';
|
|
26
27
|
import { Researcher } from './researcher.ts';
|
|
27
|
-
import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
28
|
+
import { actionRule, credentialsRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
28
29
|
import { isInteractive } from './task-agent.js';
|
|
29
30
|
import { createLearnExperienceTool } from './tools.ts';
|
|
30
31
|
|
|
@@ -59,7 +60,7 @@ class Navigator implements Agent {
|
|
|
59
60
|
NEVER use executeScript, executeAsyncScript, or any JS evaluation to change the URL, bypass redirects, or fake the page state.
|
|
60
61
|
If the target URL redirects to an authentication/login page, DO NOT try to force the original URL. Instead:
|
|
61
62
|
1. Look for credentials in the provided knowledge/hint context and perform a real login through the form.
|
|
62
|
-
2. If no credentials are available,
|
|
63
|
+
2. If no credentials are available, stop and name the credentials that are missing. Never guess them.
|
|
63
64
|
A redirect to /login, /sign_in, /auth, or similar is a signal that authentication is required — treat it as such, never as an obstacle to bypass.
|
|
64
65
|
</constraints>
|
|
65
66
|
`;
|
|
@@ -381,6 +382,9 @@ class Navigator implements Agent {
|
|
|
381
382
|
experience = this.experienceTracker.renderExperienceFor(actionResult);
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
let credentials = '';
|
|
386
|
+
if (isLoginPage(actionResult)) credentials = credentialsRule;
|
|
387
|
+
|
|
384
388
|
return dedent`
|
|
385
389
|
<message>
|
|
386
390
|
${message}
|
|
@@ -406,6 +410,8 @@ class Navigator implements Agent {
|
|
|
406
410
|
|
|
407
411
|
${unexpectedPopupRule}
|
|
408
412
|
|
|
413
|
+
${credentials}
|
|
414
|
+
|
|
409
415
|
${RulesLoader.loadRules('navigator', ['multiple-locator', 'output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.MAX_ATTEMPTS))}
|
|
410
416
|
|
|
411
417
|
${experience}
|
package/src/ai/rules.ts
CHANGED
|
@@ -147,6 +147,14 @@ export const formRequirementsRule = dedent`
|
|
|
147
147
|
</form_requirements>
|
|
148
148
|
`;
|
|
149
149
|
|
|
150
|
+
export const credentialsRule = dedent`
|
|
151
|
+
<credentials>
|
|
152
|
+
Sign in to an existing account only with credentials present in the provided context (message, knowledge, experience).
|
|
153
|
+
Never invent, guess, or try default, common, or placeholder credentials for an existing account.
|
|
154
|
+
If the required credentials are not provided, call stop() and name what is missing — do not submit the form.
|
|
155
|
+
</credentials>
|
|
156
|
+
`;
|
|
157
|
+
|
|
150
158
|
// in rage mode we do not protect from irreversible actions
|
|
151
159
|
export const protectionRule = dedent`
|
|
152
160
|
<important>
|
package/src/utils/error-page.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
2
|
import type { WebPageState } from '../state-manager.js';
|
|
3
3
|
import { isBodyEmpty } from './html.js';
|
|
4
|
+
import { extractStatePath } from './url-matcher.js';
|
|
4
5
|
|
|
5
6
|
const HTTP_ERRORS = ['400 Bad Request', '401 Unauthorized', '403 Forbidden', '404 Not Found', '405 Method Not Allowed', '408 Request Timeout', '500 Internal Server Error', '502 Bad Gateway', '503 Service Unavailable', '504 Gateway Timeout'];
|
|
6
7
|
|
|
7
8
|
const SMALL_PAGE_THRESHOLD = 500;
|
|
8
9
|
const LOADING_WORD = /\bloading\b/i;
|
|
10
|
+
const PASSWORD_INPUT = /<input\b[^>]*\btype\s*=\s*["']?password\b/i;
|
|
11
|
+
const AUTH_PATH = /\b(log[-_]?in|sign[-_]?in|auth)\b/i;
|
|
9
12
|
|
|
10
13
|
export type PageCondition = 'ok' | 'loading' | 'error';
|
|
11
14
|
|
|
@@ -40,6 +43,11 @@ export function isErrorPage(actionResult: ActionResult): boolean {
|
|
|
40
43
|
return detectPageCondition(actionResult) === 'error';
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
export function isLoginPage(actionResult: ActionResult): boolean {
|
|
47
|
+
if (PASSWORD_INPUT.test(actionResult.html || '')) return true;
|
|
48
|
+
return AUTH_PATH.test(extractStatePath(actionResult.url || '').split('?')[0]);
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
export function getStateErrorPageError(state: WebPageState | null | undefined): ErrorPageError | null {
|
|
44
52
|
if (!state) return null;
|
|
45
53
|
const actionResult = ActionResult.fromState(state);
|
package/src/utils/mdq/query.ts
CHANGED
|
@@ -13,9 +13,12 @@ export class MarkdownQuery {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
query(selector: string, matcher?: Matcher): MarkdownQuery {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const expression = parseQuery(selector);
|
|
17
|
+
for (const chain of expression.flat()) {
|
|
18
|
+
const last = chain.at(-1);
|
|
19
|
+
if (matcher !== undefined && last) last.textMatch = this.valueMatcher(matcher);
|
|
20
|
+
}
|
|
21
|
+
return new MarkdownQuery(this.source, this.evaluate(this.expandSections(this.matches), expression));
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
section(matcher?: Matcher, options?: SelectorOptions): MarkdownQuery {
|
|
@@ -364,6 +367,27 @@ export class MarkdownQuery {
|
|
|
364
367
|
return expanded;
|
|
365
368
|
}
|
|
366
369
|
|
|
370
|
+
private evaluate(candidates: MatchedRange[], expression: QueryExpression): MatchedRange[] {
|
|
371
|
+
const results: MatchedRange[][] = [];
|
|
372
|
+
for (const operands of expression) {
|
|
373
|
+
const matched = operands.map((chain) => this.run(candidates, chain));
|
|
374
|
+
if (matched.some((ranges) => ranges.length === 0)) continue;
|
|
375
|
+
results.push(...matched);
|
|
376
|
+
}
|
|
377
|
+
return this.union(results);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private union(results: MatchedRange[][]): MatchedRange[] {
|
|
381
|
+
if (results.length === 1) return results[0];
|
|
382
|
+
|
|
383
|
+
const unique = new Map<string, MatchedRange>();
|
|
384
|
+
for (const range of results.flat()) {
|
|
385
|
+
const key = `${range.start}:${range.length}:${Boolean(range.innerTokens)}`;
|
|
386
|
+
if (!unique.has(key)) unique.set(key, range);
|
|
387
|
+
}
|
|
388
|
+
return [...unique.values()].sort((a, b) => a.start - b.start || b.length - a.length);
|
|
389
|
+
}
|
|
390
|
+
|
|
367
391
|
private narrow(matches: MatchedRange[], segment: QuerySegment): MatchedRange[] {
|
|
368
392
|
if (segment.index !== null) {
|
|
369
393
|
let index = segment.index;
|
|
@@ -449,12 +473,24 @@ export function mdq(source: Markdown): MarkdownQuery {
|
|
|
449
473
|
return new MarkdownQuery(String(source));
|
|
450
474
|
}
|
|
451
475
|
|
|
452
|
-
export function parseQuery(input: string):
|
|
453
|
-
const
|
|
476
|
+
export function parseQuery(input: string): QueryExpression {
|
|
477
|
+
const expression: QueryExpression = [[[]]];
|
|
478
|
+
let chain = expression[0][0];
|
|
454
479
|
SEGMENT.lastIndex = 0;
|
|
455
480
|
|
|
456
481
|
while (SEGMENT.lastIndex < input.length) {
|
|
457
482
|
const at = SEGMENT.lastIndex;
|
|
483
|
+
OPERATOR.lastIndex = at;
|
|
484
|
+
const operator = OPERATOR.exec(input);
|
|
485
|
+
if (operator) {
|
|
486
|
+
if (chain.length === 0) throw new MdqSelectorError(`Empty selector before ${operator[1]}`, at);
|
|
487
|
+
chain = [];
|
|
488
|
+
if (operator[1] === '||') expression.push([chain]);
|
|
489
|
+
if (operator[1] === '&&') expression[expression.length - 1].push(chain);
|
|
490
|
+
SEGMENT.lastIndex = OPERATOR.lastIndex;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
|
|
458
494
|
const match = SEGMENT.exec(input);
|
|
459
495
|
if (!match) throw new MdqSelectorError(`Unexpected character "${input[at]}" in selector`, at);
|
|
460
496
|
|
|
@@ -484,10 +520,11 @@ export function parseQuery(input: string): QuerySegment[] {
|
|
|
484
520
|
if (to) segment.slice.to = Number.parseInt(to, 10);
|
|
485
521
|
}
|
|
486
522
|
|
|
487
|
-
|
|
523
|
+
chain.push(segment);
|
|
488
524
|
}
|
|
489
525
|
|
|
490
|
-
|
|
526
|
+
if (expression.flat().length > 1 && chain.length === 0) throw new MdqSelectorError('Empty selector after operator', input.length);
|
|
527
|
+
return expression;
|
|
491
528
|
}
|
|
492
529
|
|
|
493
530
|
export function buildTokenIndex(source: string): MatchedRange[] {
|
|
@@ -516,6 +553,8 @@ const TOKEN_ALIASES: Record<string, string> = { item: 'list_item' };
|
|
|
516
553
|
|
|
517
554
|
const SEGMENT = /(\s*\.?)([A-Za-z]\w*)(?:\((!?)(~?)(?:"((?:[^"\\]|\\.)*)"|\/((?:[^/\\]|\\.)*)\/([a-z]*))\))?((?:\[[^\]]*\])*)\s*/y;
|
|
518
555
|
|
|
556
|
+
const OPERATOR = /\s*(&&|\|\|)\s*/y;
|
|
557
|
+
|
|
519
558
|
export type Markdown = string | MarkdownQuery;
|
|
520
559
|
|
|
521
560
|
export type Matcher = string | RegExp | ((text: string) => boolean);
|
|
@@ -534,6 +573,8 @@ export interface TextMatcher {
|
|
|
534
573
|
predicate?: (text: string) => boolean;
|
|
535
574
|
}
|
|
536
575
|
|
|
576
|
+
export type QueryExpression = QuerySegment[][][];
|
|
577
|
+
|
|
537
578
|
export interface QuerySegment {
|
|
538
579
|
selector: SelectorType;
|
|
539
580
|
textMatch?: TextMatcher;
|