explorbot 0.5.7 → 0.5.9
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/bin/explorbot-cli.ts +25 -3
- package/boat/prima/README.md +3 -0
- package/boat/prima/src/cli.ts +7 -1
- package/boat/prima/src/envelope.ts +18 -4
- package/boat/prima/src/help.ts +15 -4
- package/boat/prima/src/prima.ts +181 -25
- package/dist/bin/explorbot-cli.js +31 -3
- package/dist/boat/prima/src/cli.js +6 -1
- package/dist/boat/prima/src/envelope.js +16 -3
- package/dist/boat/prima/src/help.js +14 -4
- package/dist/boat/prima/src/prima.js +191 -27
- package/dist/package.json +1 -1
- package/dist/rules/navigator/verification-actions.md +5 -3
- package/dist/rules/planner/ignored-pages.md +6 -0
- package/dist/rules/quartermaster/ux/accessibility.md +8 -0
- package/dist/rules/quartermaster/ux/charts.md +6 -0
- package/dist/rules/quartermaster/ux/copy.md +9 -0
- package/dist/rules/quartermaster/ux/forms.md +7 -0
- package/dist/rules/quartermaster/ux/interaction.md +7 -0
- package/dist/rules/quartermaster/ux/motion.md +5 -0
- package/dist/rules/quartermaster/ux/navigation.md +7 -0
- package/dist/rules/quartermaster/ux/readability.md +6 -0
- package/dist/rules/quartermaster/ux/responsive.md +7 -0
- package/dist/src/ai/judge.d.ts +3 -1
- package/dist/src/ai/judge.js +13 -3
- package/dist/src/ai/navigator.js +3 -2
- package/dist/src/ai/pilot.js +10 -4
- package/dist/src/ai/planner.js +9 -1
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +27 -5
- package/dist/src/ai/quartermaster.d.ts +31 -3
- package/dist/src/ai/quartermaster.js +262 -17
- package/dist/src/ai/tester.js +6 -3
- package/dist/src/commands/explore-command.js +7 -0
- package/dist/src/commands/init-command.d.ts +1 -0
- package/dist/src/commands/init-command.js +87 -36
- package/dist/src/components/InitWizard.d.ts +5 -1
- package/dist/src/components/InitWizard.js +29 -11
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +29 -7
- package/dist/src/explorbot.d.ts +1 -1
- package/dist/src/explorbot.js +8 -13
- package/dist/src/explorer.js +1 -0
- package/dist/src/global-config.js +6 -2
- package/dist/src/utils/ux-probe.d.ts +25 -0
- package/dist/src/utils/ux-probe.js +155 -0
- package/docs/basics/getting-started.md +23 -10
- package/docs/basics/providers.md +2 -0
- package/docs/reference/commands.md +5 -2
- package/docs/reference/configuration.md +1 -0
- package/package.json +1 -1
- package/rules/navigator/verification-actions.md +5 -3
- package/rules/planner/ignored-pages.md +6 -0
- package/rules/quartermaster/ux/accessibility.md +8 -0
- package/rules/quartermaster/ux/charts.md +6 -0
- package/rules/quartermaster/ux/copy.md +9 -0
- package/rules/quartermaster/ux/forms.md +7 -0
- package/rules/quartermaster/ux/interaction.md +7 -0
- package/rules/quartermaster/ux/motion.md +5 -0
- package/rules/quartermaster/ux/navigation.md +7 -0
- package/rules/quartermaster/ux/readability.md +6 -0
- package/rules/quartermaster/ux/responsive.md +7 -0
- package/src/ai/judge.ts +11 -3
- package/src/ai/navigator.ts +2 -2
- package/src/ai/pilot.ts +10 -4
- package/src/ai/planner.ts +10 -1
- package/src/ai/provider.ts +29 -6
- package/src/ai/quartermaster.ts +300 -18
- package/src/ai/tester.ts +5 -3
- package/src/commands/explore-command.ts +8 -0
- package/src/commands/init-command.ts +105 -38
- package/src/components/InitWizard.tsx +43 -11
- package/src/config.ts +32 -7
- package/src/explorbot.ts +9 -13
- package/src/explorer.ts +1 -0
- package/src/global-config.ts +5 -2
- package/src/utils/ux-probe.ts +163 -0
package/bin/explorbot-cli.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { RecommendedModelsCommand } from '../src/commands/recommended-models-com
|
|
|
13
13
|
import { App } from '../src/components/App.js';
|
|
14
14
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
15
15
|
import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
16
|
-
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
16
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS, outputPath } from '../src/config.js';
|
|
17
17
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
18
18
|
import { remote } from '../src/remote.js';
|
|
19
19
|
import { Stats } from '../src/stats.js';
|
|
@@ -123,6 +123,18 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
function printPageContext(explorBot?: ExplorBot): void {
|
|
127
|
+
const state = explorBot?.stateManager().getCurrentState();
|
|
128
|
+
if (!state) return;
|
|
129
|
+
const lines = [`Last page: ${state.fullUrl || state.url}`];
|
|
130
|
+
if (state.title) lines.push(`Title: ${state.title}`);
|
|
131
|
+
if (state.screenshotFile) lines.push(`Screenshot: file://${outputPath('states', state.screenshotFile)}`);
|
|
132
|
+
if (state.htmlFile) lines.push(`HTML: file://${outputPath('states', state.htmlFile)}`);
|
|
133
|
+
if (state.ariaSnapshotFile) lines.push(`ARIA: file://${outputPath('states', state.ariaSnapshotFile)}`);
|
|
134
|
+
if (state.ariaSnapshot) lines.push('', state.ariaSnapshot);
|
|
135
|
+
console.error(lines.join('\n'));
|
|
136
|
+
}
|
|
137
|
+
|
|
126
138
|
async function showStatsAndExit(code: number): Promise<never> {
|
|
127
139
|
if (remote.isAttached()) {
|
|
128
140
|
await remote.close(code);
|
|
@@ -165,10 +177,11 @@ addCommonOptions(
|
|
|
165
177
|
.option('--configure <spec>', 'Reuse spec: keys new|from|style|subpages|pick_by|priority, e.g. "new:25%;pick_by=random;priority=critical,high"')
|
|
166
178
|
.option('--dry-run', 'Mark picked tests as skipped without executing or generating new ones')
|
|
167
179
|
).action(async (explorePath, options) => {
|
|
180
|
+
let explorBot: ExplorBot | undefined;
|
|
168
181
|
try {
|
|
169
182
|
const urlFilter = new UrlFilter(options.urlFilter);
|
|
170
183
|
const sitemapUrls = await readCliSitemap(explorePath, options.sitemap, urlFilter);
|
|
171
|
-
|
|
184
|
+
explorBot = new ExplorBot(buildExplorBotOptions(explorePath || sitemapUrls?.[0], options));
|
|
172
185
|
await explorBot.start();
|
|
173
186
|
if (explorePath) await explorBot.visit(explorePath);
|
|
174
187
|
const { ExploreCommand } = await import('../src/commands/explore-command.js');
|
|
@@ -188,6 +201,7 @@ addCommonOptions(
|
|
|
188
201
|
await showStatsAndExit(0);
|
|
189
202
|
} catch (error) {
|
|
190
203
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
204
|
+
printPageContext(explorBot);
|
|
191
205
|
await showStatsAndExit(1);
|
|
192
206
|
}
|
|
193
207
|
});
|
|
@@ -471,7 +485,13 @@ addCommonOptions(
|
|
|
471
485
|
.join(' ');
|
|
472
486
|
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
473
487
|
const cmd = new FreesailCommand(explorBot);
|
|
474
|
-
|
|
488
|
+
try {
|
|
489
|
+
await cmd.execute(args);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
492
|
+
printPageContext(explorBot);
|
|
493
|
+
await showStatsAndExit(1);
|
|
494
|
+
}
|
|
475
495
|
});
|
|
476
496
|
|
|
477
497
|
program
|
|
@@ -502,6 +522,7 @@ program
|
|
|
502
522
|
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
503
523
|
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
504
524
|
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
525
|
+
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
505
526
|
.action(async (options) => {
|
|
506
527
|
try {
|
|
507
528
|
const { runInit } = await import('../src/commands/init-command.js');
|
|
@@ -512,6 +533,7 @@ program
|
|
|
512
533
|
global: options.global,
|
|
513
534
|
provider: options.provider,
|
|
514
535
|
apiKey: options.apiKey,
|
|
536
|
+
url: options.url,
|
|
515
537
|
});
|
|
516
538
|
} catch (error) {
|
|
517
539
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
package/boat/prima/README.md
CHANGED
|
@@ -41,6 +41,7 @@ playwright-cli close
|
|
|
41
41
|
| `verify <assertion>` | Assert a statement about the current page |
|
|
42
42
|
| `ask <question>` | Answer a question about the current page |
|
|
43
43
|
| `research` | Map the current page and return verified locators |
|
|
44
|
+
| `ux [focus]` | Review the area being worked on for UX issues at the current width, 768px and 375px |
|
|
44
45
|
| `go <target>` | Navigate to a url, a path, or a page described in plain words |
|
|
45
46
|
| `pw <fn>` | Run a Playwright function expression against the open page |
|
|
46
47
|
| `status <hash>` | Show the artifacts and page detail recorded for an earlier command |
|
|
@@ -52,6 +53,8 @@ playwright-cli close
|
|
|
52
53
|
|
|
53
54
|
`do` accounts for every instruction you gave: each is reported as ok, FAIL or ??. Nothing runs past the last instruction.
|
|
54
55
|
|
|
56
|
+
`ux` reviews one area, not the whole page. A named focus is shown first with at most one click, which may open a dialog, tab or section or go to another page; nothing is submitted, and when no click leads to it the command fails without reviewing. The area reviewed is the dialog or region open on the page, else the focus you named, else the area your session's commands on this page acted in, else the main content. It measures target sizes, spacing, overflow, text size, zoom and motion at each width, runs axe on the area, and asks the model to judge copy, forms and navigation against the rules in `rules/quartermaster/ux/`. Findings are ranked Critical to Low. It reports problems only: no replacement wording and no colors or styles, since the project owns those. Override a rule by placing a file of the same name in `./rules/quartermaster/ux/`, or add your own with `ai.agents.quartermaster.rules`.
|
|
57
|
+
|
|
55
58
|
## AI model
|
|
56
59
|
|
|
57
60
|
Prima needs a model, and takes it from the environment. There is no `init` to run and nothing is written for you.
|
package/boat/prima/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
|
4
4
|
import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
|
|
5
5
|
import { clearActivityLine, trackActivityLine } from './activity-line.ts';
|
|
6
6
|
import { type EnvelopeData, renderEnvelope } from './envelope.ts';
|
|
7
|
-
import { askHelp, checkHelp, doHelp, helpContract, reportHelp, researchHelp, sessionHelp, statusHelp, verifyHelp } from './help.ts';
|
|
7
|
+
import { askHelp, checkHelp, doHelp, helpContract, reportHelp, researchHelp, sessionHelp, statusHelp, uxHelp, verifyHelp } from './help.ts';
|
|
8
8
|
import { Prima, type PrimaOptions } from './prima.ts';
|
|
9
9
|
|
|
10
10
|
let rootOptions: () => any = () => ({});
|
|
@@ -129,6 +129,12 @@ export function createPrimaCommands(name = 'prima'): Command {
|
|
|
129
129
|
await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
+
addBrowserOptions(cmd.command('ux [focus]').description('Review the area being worked on for UX issues, across breakpoints'))
|
|
133
|
+
.addHelpText('after', `\n${uxHelp}`)
|
|
134
|
+
.action(async (focus, options) => {
|
|
135
|
+
await runPrima(options, ['ux', focus].filter(Boolean).join(' '), (prima) => prima.ux(focus));
|
|
136
|
+
});
|
|
137
|
+
|
|
132
138
|
addBrowserOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
|
|
133
139
|
if (URL.canParse(target)) options.baseUrl = target;
|
|
134
140
|
await runPrima(options, `go ${target}`, (prima) => prima.go(target));
|
|
@@ -29,12 +29,14 @@ export interface EnvelopeData {
|
|
|
29
29
|
used?: string[];
|
|
30
30
|
page: { url: string; previousUrl?: string; redirectedFrom?: string; title: string; state: string; visits: number };
|
|
31
31
|
changes?: string | null;
|
|
32
|
-
steps?: Array<{ label: string; ok: boolean; unconfirmed?: boolean; proof: string }>;
|
|
32
|
+
steps?: Array<{ label: string; ok: boolean; unconfirmed?: boolean; proof: string; state?: number }>;
|
|
33
|
+
states?: Array<{ url: string; aria: string }>;
|
|
33
34
|
expectations?: SettledExpectation[];
|
|
34
35
|
warning?: string;
|
|
35
36
|
stepFiles?: string;
|
|
36
37
|
value?: string;
|
|
37
38
|
answer?: string;
|
|
39
|
+
pageIssues?: string[];
|
|
38
40
|
research?: string;
|
|
39
41
|
assertions?: Array<{ code: string; passed: boolean; proof: string[]; error?: string }>;
|
|
40
42
|
failure?: { error: string; compactAria?: string };
|
|
@@ -44,7 +46,7 @@ export interface EnvelopeData {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export function renderEnvelope(data: EnvelopeData): string {
|
|
47
|
-
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
49
|
+
const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderStates(data), renderExpectations(data), renderWarning(data), renderOutcome(data), renderPageIssues(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
|
|
48
50
|
return sections.filter((section) => section).join('\n\n');
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -118,13 +120,20 @@ function renderSteps(data: EnvelopeData): string | null {
|
|
|
118
120
|
let mark = 'FAIL';
|
|
119
121
|
if (step.ok) mark = 'ok ';
|
|
120
122
|
if (step.unconfirmed) mark = '?? ';
|
|
121
|
-
|
|
123
|
+
let label = step.label;
|
|
124
|
+
if (step.state) label += ` // state ${step.state}`;
|
|
125
|
+
lines.push(`${index + 1}. ${mark} ${label}`);
|
|
122
126
|
for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
123
127
|
});
|
|
124
128
|
if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{${Object.values(STEP_FILES).join(',')}}`);
|
|
125
129
|
return section('Steps', lines.join('\n'));
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
function renderStates(data: EnvelopeData): string | null {
|
|
133
|
+
if (!data.states?.length) return null;
|
|
134
|
+
return section('States', data.states.map((state, index) => `${index + 1}. ${state.url}\n aria: ${state.aria}`).join('\n'));
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
function renderExpectations(data: EnvelopeData): string | null {
|
|
129
138
|
if (!data.expectations?.length) return null;
|
|
130
139
|
|
|
@@ -132,7 +141,7 @@ function renderExpectations(data: EnvelopeData): string | null {
|
|
|
132
141
|
data.expectations.forEach((expectation, index) => {
|
|
133
142
|
lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
|
|
134
143
|
if (expectation.resolution) lines.push(` resolved: ${expectation.resolution}`);
|
|
135
|
-
if (
|
|
144
|
+
if (expectation.status === 'passed' && !expectation.resolution) return;
|
|
136
145
|
for (const line of (expectation.evidence || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
|
|
137
146
|
});
|
|
138
147
|
return section('Expected outcomes', lines.join('\n'));
|
|
@@ -168,6 +177,11 @@ function renderOutcome(data: EnvelopeData): string | null {
|
|
|
168
177
|
return section('Assertions', lines.join('\n'));
|
|
169
178
|
}
|
|
170
179
|
|
|
180
|
+
function renderPageIssues(data: EnvelopeData): string | null {
|
|
181
|
+
if (!data.pageIssues?.length) return null;
|
|
182
|
+
return section('Page issues', ['Noticed on the page while running, unrelated to the steps or the outcome:', ...data.pageIssues.map((issue) => `- ${issue}`)].join('\n'));
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
function renderFailure(data: EnvelopeData): Array<string | null> {
|
|
172
186
|
if (!data.failure) return [];
|
|
173
187
|
return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
|
package/boat/prima/src/help.ts
CHANGED
|
@@ -25,15 +25,17 @@ export const checkHelp = dedent`
|
|
|
25
25
|
--expected one required outcome, repeatable for several. Without it the
|
|
26
26
|
scenario text is the outcome. Each returns under
|
|
27
27
|
### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
|
|
28
|
-
"not verified" means never checked, not false
|
|
28
|
+
"not verified" means never checked, not false; the line under it names
|
|
29
|
+
what kept the run from checking it.
|
|
29
30
|
Proof is a full-page screenshot: what a user sees counts, the log only shows actions.
|
|
30
31
|
When screenshot and log disagree, the page structure breaks the tie: the outcome
|
|
31
32
|
becomes PASSED or FAILED with a "resolved:" line naming the side that was wrong.
|
|
32
33
|
CONTRADICTION remains when it cannot be broken, or when the page holds something
|
|
33
34
|
it does not display; judge the html, aria and screenshot under ### Artifacts yourself.
|
|
34
|
-
ok is false when any outcome FAILED or CONTRADICTED,
|
|
35
|
-
reported as such rather than as an app failure.
|
|
36
|
-
|
|
35
|
+
ok is false when any outcome FAILED or CONTRADICTED, when none was verified, or when
|
|
36
|
+
the run could not finish, reported as such rather than as an app failure.
|
|
37
|
+
### Steps lists only the steps that failed.
|
|
38
|
+
Side issues found on the way go under ### Page issues, not as step failures.
|
|
37
39
|
`;
|
|
38
40
|
|
|
39
41
|
export const doHelp = dedent`
|
|
@@ -56,6 +58,15 @@ export const researchHelp = dedent`
|
|
|
56
58
|
The map is saved per page state and joins later commands there, so one research run pays for all that follow it.
|
|
57
59
|
`;
|
|
58
60
|
|
|
61
|
+
export const uxHelp = dedent`
|
|
62
|
+
Reviews one area, not the page. A named focus is shown first with at most one click,
|
|
63
|
+
which may open a dialog, tab or section or go to another page; nothing is submitted. The area reviewed is the dialog or region open on the page,
|
|
64
|
+
else the named focus, else where the session's commands on this page acted, else main.
|
|
65
|
+
Measures it at the current width, 768px and 375px, then restores the viewport.
|
|
66
|
+
Findings come under ### Answer by priority; the review reports problems and never
|
|
67
|
+
proposes wording or visual style. ok stays true whatever it finds.
|
|
68
|
+
`;
|
|
69
|
+
|
|
59
70
|
export const statusHelp = dedent`
|
|
60
71
|
Reads recorded files, so it needs no browser and outlives the session.
|
|
61
72
|
The hash is matched across all recorded sites. ### Artifacts lists every kept
|
package/boat/prima/src/prima.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { findSiteWith, listSites } from '../../../src/global-config.ts';
|
|
|
20
20
|
import { Reporter } from '../../../src/reporter.ts';
|
|
21
21
|
import type { WebPageState } from '../../../src/state-manager.ts';
|
|
22
22
|
import { Stats } from '../../../src/stats.ts';
|
|
23
|
-
import { Task, Test
|
|
23
|
+
import { Task, Test } from '../../../src/test-plan.ts';
|
|
24
24
|
import { ariaRefSelector, ariaRefSnapshot, describeRef, parseAriaRefs, refIsGone } from '../../../src/utils/aria-ref.ts';
|
|
25
25
|
import { compactAriaSnapshot } from '../../../src/utils/aria.ts';
|
|
26
26
|
import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
|
|
@@ -37,6 +37,7 @@ const ITERATIONS_PER_INSTRUCTION = 2;
|
|
|
37
37
|
const MAX_INSTRUCTION_ITERATIONS = 24;
|
|
38
38
|
const CONTEXT_HTML_CAP = 6000;
|
|
39
39
|
const MAX_TOOL_ROUNDTRIPS = 5;
|
|
40
|
+
const UX_SESSION_COMMANDS = 8;
|
|
40
41
|
const AI_AGENT_NAME = 'prima';
|
|
41
42
|
const CONNECT_TIMEOUT = 3000;
|
|
42
43
|
const requireLib = createRequire(import.meta.url);
|
|
@@ -197,11 +198,13 @@ export class Prima {
|
|
|
197
198
|
const unreported = this.openInstructions(ledger);
|
|
198
199
|
if (!unreported || nudged) break;
|
|
199
200
|
nudged = true;
|
|
201
|
+
let reminder = 'Do not act again on anything you have already carried out.';
|
|
202
|
+
if (!trace.length) reminder = 'Nothing has been carried out yet: text in a reply is never executed, only tool calls are. Act with tools.';
|
|
200
203
|
conversation.addUserText(dedent`
|
|
201
204
|
These instructions are still unreported:
|
|
202
205
|
${unreported}
|
|
203
206
|
|
|
204
|
-
Report each one with completed() or blocked().
|
|
207
|
+
Report each one with completed() or blocked(). ${reminder}
|
|
205
208
|
`);
|
|
206
209
|
continue;
|
|
207
210
|
}
|
|
@@ -257,7 +260,8 @@ export class Prima {
|
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
if (!trace.length && unfinished.length === ledger.length) {
|
|
260
|
-
|
|
263
|
+
let reason = 'No action was performed for these instructions on the current page.';
|
|
264
|
+
if (narration) reason += `\nThe model replied with text instead of calling a tool:\n${narration}`;
|
|
261
265
|
return this.failureEnvelope(command, reason, previousState);
|
|
262
266
|
}
|
|
263
267
|
|
|
@@ -382,22 +386,65 @@ export class Prima {
|
|
|
382
386
|
const guard = await this.aiGuard(command);
|
|
383
387
|
if (guard) return guard;
|
|
384
388
|
|
|
385
|
-
const previousState = await this.baselineState();
|
|
386
389
|
const outcomes = expected.length ? expected : [scenario];
|
|
390
|
+
const preparation = await this.preparationSteps(scenario, outcomes);
|
|
391
|
+
let prepared: EnvelopeData | null = null;
|
|
392
|
+
if (preparation.length) {
|
|
393
|
+
prepared = await this.do(preparation, command);
|
|
394
|
+
if (!prepared.ok) {
|
|
395
|
+
prepared.failure = { error: `the check never ran, because preparing the page for it failed:\n${prepared.failure?.error || ''}` };
|
|
396
|
+
return prepared;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const previousState = await this.baselineState();
|
|
387
401
|
const test = new Test(scenario, 'normal', outcomes, previousState?.url || this.options.url || '');
|
|
388
402
|
const tester = this.bot.agentTester();
|
|
389
403
|
|
|
390
|
-
|
|
404
|
+
const states: Array<{ url: string; aria: string }> = [];
|
|
405
|
+
const visited = new Map<string, number>();
|
|
406
|
+
const timeline: Array<{ time: number; state: number }> = [];
|
|
407
|
+
const trackState = (state?: WebPageState | null) => {
|
|
408
|
+
if (!state?.hash) return;
|
|
409
|
+
let number = visited.get(state.hash);
|
|
410
|
+
if (!number) {
|
|
411
|
+
number = visited.size + 1;
|
|
412
|
+
visited.set(state.hash, number);
|
|
413
|
+
const aria = path.join(this.statusDir(), `state-${number}.aria.yml`);
|
|
414
|
+
mkdirSync(this.statusDir(), { recursive: true });
|
|
415
|
+
writeFileSync(aria, state.ariaSnapshot || '', 'utf-8');
|
|
416
|
+
states.push({ url: state.url, aria });
|
|
417
|
+
}
|
|
418
|
+
timeline.push({ time: performance.now(), state: number });
|
|
419
|
+
};
|
|
420
|
+
trackState(previousState);
|
|
421
|
+
const stopTracking = this.bot.stateManager().onStateChange((transition) => trackState(transition.toState));
|
|
422
|
+
|
|
423
|
+
await tester.test(test, { startOnCurrentPage: true }).finally(stopTracking);
|
|
391
424
|
|
|
392
425
|
const notes = Object.values(test.notes || {}) as Array<{ message: string; status?: string; log?: string; observation?: boolean }>;
|
|
393
426
|
const result = await this.capturedResult(this.bot.stateManager().getCurrentState(), { screenshot: this.visionEnabled() });
|
|
394
427
|
const envelope = await this.reportEnvelope(command, result, previousState, {});
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
428
|
+
const repeats = new Map<string, { message: string; ok: boolean; count: number; proof: string; state?: number }>();
|
|
429
|
+
for (const [startedAt, step] of Object.entries(test.steps || {})) {
|
|
430
|
+
const ok = step.status !== 'failed';
|
|
431
|
+
const state = timeline.findLast((entry) => entry.time <= Number.parseFloat(startedAt))?.state;
|
|
432
|
+
const key = `${ok} ${state} ${step.text}`;
|
|
433
|
+
let proof = '';
|
|
434
|
+
if (!ok) proof = step.error || '';
|
|
435
|
+
repeats.set(key, { message: step.text, ok, count: (repeats.get(key)?.count || 0) + 1, proof, state });
|
|
436
|
+
}
|
|
437
|
+
const preparedSteps = (prepared?.steps || []).map((step) => ({ ...step, label: `prepare: ${step.label}` }));
|
|
438
|
+
envelope.steps = [
|
|
439
|
+
...preparedSteps,
|
|
440
|
+
...[...repeats.values()].map(({ message, ok, count, proof, state }) => {
|
|
441
|
+
let label = message;
|
|
442
|
+
if (count > 1) label += ` (×${count})`;
|
|
443
|
+
return { label, ok, proof, state };
|
|
444
|
+
}),
|
|
445
|
+
];
|
|
446
|
+
if (prepared?.stepFiles) envelope.stepFiles = prepared.stepFiles;
|
|
447
|
+
if (states.length) envelope.states = states;
|
|
401
448
|
|
|
402
449
|
envelope.expectations = await this.bot.agentPilot().settleExpectations(test, result);
|
|
403
450
|
|
|
@@ -407,9 +454,12 @@ export class Prima {
|
|
|
407
454
|
|
|
408
455
|
const unreached = envelope.expectations.filter((expectation) => expectation.failed);
|
|
409
456
|
const contradicted = envelope.expectations.filter((expectation) => expectation.contradicted);
|
|
410
|
-
envelope.
|
|
457
|
+
const unverified = envelope.expectations.filter((expectation) => expectation.status === 'unverified');
|
|
458
|
+
const nothingVerified = unverified.length === envelope.expectations.length;
|
|
459
|
+
envelope.ok = !unreached.length && !contradicted.length && !nothingVerified;
|
|
411
460
|
|
|
412
461
|
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
462
|
+
if (nothingVerified) problems.push(...unverified.map((expectation) => `not verified: ${expectation.text}`));
|
|
413
463
|
if (problems.length) envelope.failure = { error: problems.join('\n') };
|
|
414
464
|
|
|
415
465
|
if (!test.hasFinished || test.isSkipped) {
|
|
@@ -418,7 +468,7 @@ export class Prima {
|
|
|
418
468
|
}
|
|
419
469
|
|
|
420
470
|
const observations = notes.filter((note) => note.observation).map((note) => note.message);
|
|
421
|
-
if (observations.length) envelope.
|
|
471
|
+
if (observations.length) envelope.pageIssues = observations;
|
|
422
472
|
return envelope;
|
|
423
473
|
}
|
|
424
474
|
|
|
@@ -466,6 +516,29 @@ export class Prima {
|
|
|
466
516
|
return this.reportEnvelope(command, result, previousState, { research: dropVolatileColumns(uiMap) });
|
|
467
517
|
}
|
|
468
518
|
|
|
519
|
+
async ux(focus?: string): Promise<EnvelopeData> {
|
|
520
|
+
const command = ['ux', focus].filter(Boolean).join(' ');
|
|
521
|
+
const guard = await this.aiGuard(command);
|
|
522
|
+
if (guard) return guard;
|
|
523
|
+
|
|
524
|
+
const previousState = await this.baselineState();
|
|
525
|
+
let clicked: string[] = [];
|
|
526
|
+
if (focus) {
|
|
527
|
+
const shown = await this.showFocus(focus, previousState);
|
|
528
|
+
if (!shown) return this.failureEnvelope(command, `Could not show "${focus}": it is not on this page and no single click from here leads to it, so no review was made.`, previousState);
|
|
529
|
+
clicked = shown;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const result = await this.capturedResult(this.bot.stateManager().getCurrentState());
|
|
533
|
+
let auditFocus = focus;
|
|
534
|
+
if (result.overlay.isOpen && result.overlay.describe() !== previousState?.overlay?.describe()) auditFocus = undefined;
|
|
535
|
+
const answer = await this.bot.agentQuartermaster().auditUx(result, { focus: auditFocus, commands: this.sessionCommandsOn(result.url) });
|
|
536
|
+
const envelope = await this.reportEnvelope(command, result, previousState, { answer });
|
|
537
|
+
if (clicked.length) envelope.steps = clicked.map((code) => ({ label: code, ok: true, proof: '' }));
|
|
538
|
+
if (!this.visionEnabled()) envelope.warning = 'No screenshot backed this review, so visual checks were skipped. Set ai.visionModel to include them.';
|
|
539
|
+
return envelope;
|
|
540
|
+
}
|
|
541
|
+
|
|
469
542
|
async go(target: string): Promise<EnvelopeData> {
|
|
470
543
|
const command = `go ${target}`;
|
|
471
544
|
const code = `I.amOnPage('${target}')`;
|
|
@@ -487,7 +560,7 @@ export class Prima {
|
|
|
487
560
|
let arrived = true;
|
|
488
561
|
|
|
489
562
|
try {
|
|
490
|
-
arrived = await this.bot.agentNavigator().visit(target, { recover: !
|
|
563
|
+
arrived = await this.bot.agentNavigator().visit(target, { recover: !this.aiUnavailable() });
|
|
491
564
|
} catch (error) {
|
|
492
565
|
navigationError = error;
|
|
493
566
|
}
|
|
@@ -498,7 +571,11 @@ export class Prima {
|
|
|
498
571
|
if (isUrl) used.push(code);
|
|
499
572
|
const result = await this.capturedResult(this.bot.stateManager().getCurrentState());
|
|
500
573
|
const envelope = await this.successEnvelope(command, used, result, previousState);
|
|
501
|
-
if (
|
|
574
|
+
if (arrived) return envelope;
|
|
575
|
+
|
|
576
|
+
envelope.page.redirectedFrom = target;
|
|
577
|
+
envelope.ok = false;
|
|
578
|
+
envelope.failure = { error: `${target} was not reached: the page redirected to ${result.url} and no AI model is available to recover (log in, follow knowledge)` };
|
|
502
579
|
return envelope;
|
|
503
580
|
}
|
|
504
581
|
|
|
@@ -516,7 +593,7 @@ export class Prima {
|
|
|
516
593
|
const config = await this.loadConfig();
|
|
517
594
|
const parser = ConfigParser.getInstance();
|
|
518
595
|
|
|
519
|
-
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json });
|
|
596
|
+
return ConfigCommand.render(config, { configPath: parser.getConfigPath(), siteConfigPath: parser.getSiteConfigPath(), root: parser.getProjectRoot(), json });
|
|
520
597
|
}
|
|
521
598
|
|
|
522
599
|
record(envelope: EnvelopeData, durationMs: number): void {
|
|
@@ -870,6 +947,42 @@ export class Prima {
|
|
|
870
947
|
});
|
|
871
948
|
}
|
|
872
949
|
|
|
950
|
+
private async preparationSteps(scenario: string, outcomes: string[]): Promise<string[]> {
|
|
951
|
+
const result = await this.capturedResult(await this.baselineState());
|
|
952
|
+
const page = compactAriaSnapshot(result.ariaSnapshot, true).slice(0, JUDGE_PAGE_CAP);
|
|
953
|
+
|
|
954
|
+
const judge = this.bot.judge?.();
|
|
955
|
+
if (judge) {
|
|
956
|
+
const question = 'Before this scenario can start, the current page needs actions the scenario does not perform itself: reaching the page where it starts, or setting up a precondition it relies on.';
|
|
957
|
+
const needed = await judge.decide(question, null, { scenario, outcomes, url: result.url, page });
|
|
958
|
+
if (needed.rejected) return [];
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const prompt = dedent`
|
|
962
|
+
<scenario>
|
|
963
|
+
${scenario}
|
|
964
|
+
</scenario>
|
|
965
|
+
|
|
966
|
+
<expected_outcomes>
|
|
967
|
+
${outcomes.map((outcome) => `- ${outcome}`).join('\n')}
|
|
968
|
+
</expected_outcomes>
|
|
969
|
+
|
|
970
|
+
<page>
|
|
971
|
+
url: ${result.url}
|
|
972
|
+
${page}
|
|
973
|
+
</page>
|
|
974
|
+
|
|
975
|
+
A check of this scenario runs next, starting from the current page. Decide whether the page must be prepared first.
|
|
976
|
+
Preparation is only what the scenario assumes but does not do: reaching the page where it starts when this is a different one,
|
|
977
|
+
or setting up a precondition it relies on. Never include the actions the scenario describes — the check performs and judges those.
|
|
978
|
+
List the preparation as short imperative instructions, in order. Return an empty list when the scenario can start here.
|
|
979
|
+
`;
|
|
980
|
+
const schema = z.object({ steps: z.array(z.string()).describe('preparation instructions, in order; empty when none are needed') });
|
|
981
|
+
const provider = this.bot.getProvider();
|
|
982
|
+
const response = await provider.generateObject([{ role: 'user', content: prompt }], schema, provider.getAgenticModel?.(AI_AGENT_NAME), { agentName: AI_AGENT_NAME }).catch(() => null);
|
|
983
|
+
return response?.object?.steps || [];
|
|
984
|
+
}
|
|
985
|
+
|
|
873
986
|
private async pageContext(result: ActionResult): Promise<string> {
|
|
874
987
|
const experience = this.bot.experienceTracker?.()?.renderExperienceTocFor?.(result) || '';
|
|
875
988
|
const map = getPreviousResearch(result.baseHash);
|
|
@@ -922,13 +1035,8 @@ export class Prima {
|
|
|
922
1035
|
const pick = await judge.decide(`Which listed control leads to: ${target}`, [...labels, UNDECIDED], { target });
|
|
923
1036
|
if (!pick.value) return null;
|
|
924
1037
|
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
if (await refIsGone(explorer, ref)) return null;
|
|
928
|
-
|
|
929
|
-
const named = await describeRef(explorer, ref);
|
|
930
|
-
const run = `I.usePlaywrightTo(${JSON.stringify(`go to ${target}`)}, async ({ page }) => page.locator(${JSON.stringify(ariaRefSelector(ref))}).click())`;
|
|
931
|
-
if (!(await explorer.action().attempt(run, `go ${target}`))) return null;
|
|
1038
|
+
const code = await this.clickRef(refs[labels.indexOf(pick.value)].ref, `go to ${target}`);
|
|
1039
|
+
if (!code) return null;
|
|
932
1040
|
|
|
933
1041
|
const newState = this.bot.stateManager().getCurrentState();
|
|
934
1042
|
if (!newState) return null;
|
|
@@ -938,11 +1046,59 @@ export class Prima {
|
|
|
938
1046
|
const arrived = await judge.decide(`The page now shows: ${target}`, null, { target, page });
|
|
939
1047
|
if (arrived.rejected) return null;
|
|
940
1048
|
|
|
941
|
-
let code = run;
|
|
942
|
-
if (named) code = `I.click(${JSON.stringify(named)})`;
|
|
943
1049
|
return this.successEnvelope(`go ${target}`, [code], await this.capturedResult(newState), previousState);
|
|
944
1050
|
}
|
|
945
1051
|
|
|
1052
|
+
private async showFocus(focus: string, state: WebPageState | null): Promise<string[] | null> {
|
|
1053
|
+
if (!state) return null;
|
|
1054
|
+
const snapshot = (await this.refAriaSnapshot(ActionResult.fromState(state))) || '';
|
|
1055
|
+
const refs = parseAriaRefs(snapshot).filter((entry) => entry.name);
|
|
1056
|
+
const schema = z.object({
|
|
1057
|
+
shown: z.boolean().describe('whether the page already shows the area'),
|
|
1058
|
+
ref: z.enum(['none', ...refs.map((entry) => entry.ref)]).describe('when it is not shown, the one control whose click leads to it, or none'),
|
|
1059
|
+
});
|
|
1060
|
+
const prompt = dedent`
|
|
1061
|
+
A UX review needs this area on screen: ${focus}
|
|
1062
|
+
Say whether the page already shows it. If not, pick the one control whose single click opens, reveals or navigates to it.
|
|
1063
|
+
Its label may differ from how the area was named: judge by what the control leads to.
|
|
1064
|
+
Answer none when no control plausibly leads there.
|
|
1065
|
+
|
|
1066
|
+
<page_aria>
|
|
1067
|
+
${snapshot.slice(0, JUDGE_PAGE_CAP)}
|
|
1068
|
+
</page_aria>
|
|
1069
|
+
`;
|
|
1070
|
+
const response = await this.bot
|
|
1071
|
+
.getProvider()
|
|
1072
|
+
.generateObject([{ role: 'user', content: prompt }], schema, undefined, { agentName: AI_AGENT_NAME })
|
|
1073
|
+
.catch(() => null);
|
|
1074
|
+
if (response?.object?.shown) return [];
|
|
1075
|
+
const ref = response?.object?.ref;
|
|
1076
|
+
if (!ref || ref === 'none') return null;
|
|
1077
|
+
const code = await this.clickRef(ref, `show ${focus}`);
|
|
1078
|
+
if (!code) return null;
|
|
1079
|
+
return [code];
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private async clickRef(ref: string, purpose: string): Promise<string | null> {
|
|
1083
|
+
const explorer = this.bot.getExplorer();
|
|
1084
|
+
if (await refIsGone(explorer, ref)) return null;
|
|
1085
|
+
|
|
1086
|
+
const named = await describeRef(explorer, ref);
|
|
1087
|
+
const run = `I.usePlaywrightTo(${JSON.stringify(purpose)}, async ({ page }) => page.locator(${JSON.stringify(ariaRefSelector(ref))}).click())`;
|
|
1088
|
+
if (!(await explorer.action().attempt(run, purpose))) return null;
|
|
1089
|
+
if (named) return `I.click(${JSON.stringify(named)})`;
|
|
1090
|
+
return run;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
private sessionCommandsOn(url: string): string[] {
|
|
1094
|
+
if (!this.session) return [];
|
|
1095
|
+
const { tests } = readSession(sessionFile(this.session.key));
|
|
1096
|
+
return tests
|
|
1097
|
+
.filter((test) => test.description?.split('\n')[0] === `page: ${url}` && !test.title.startsWith('ux'))
|
|
1098
|
+
.map((test) => test.title)
|
|
1099
|
+
.slice(-UX_SESSION_COMMANDS);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
946
1102
|
private executedCodes(code: unknown): string[] {
|
|
947
1103
|
if (typeof code !== 'string') return [];
|
|
948
1104
|
return code
|
|
@@ -13,7 +13,7 @@ import { RecommendedModelsCommand } from '../src/commands/recommended-models-com
|
|
|
13
13
|
import { App } from '../src/components/App.js';
|
|
14
14
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
15
15
|
import { decisionModelOption, knowledgeOption, wsOption } from '../src/commands/options/index.js';
|
|
16
|
-
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
16
|
+
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS, outputPath } from '../src/config.js';
|
|
17
17
|
import { ExplorBot } from '../src/explorbot.js';
|
|
18
18
|
import { remote } from '../src/remote.js';
|
|
19
19
|
import { Stats } from '../src/stats.js';
|
|
@@ -94,6 +94,23 @@ async function startTUI(explorBot) {
|
|
|
94
94
|
await cleanup();
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
+
function printPageContext(explorBot) {
|
|
98
|
+
const state = explorBot?.stateManager().getCurrentState();
|
|
99
|
+
if (!state)
|
|
100
|
+
return;
|
|
101
|
+
const lines = [`Last page: ${state.fullUrl || state.url}`];
|
|
102
|
+
if (state.title)
|
|
103
|
+
lines.push(`Title: ${state.title}`);
|
|
104
|
+
if (state.screenshotFile)
|
|
105
|
+
lines.push(`Screenshot: file://${outputPath('states', state.screenshotFile)}`);
|
|
106
|
+
if (state.htmlFile)
|
|
107
|
+
lines.push(`HTML: file://${outputPath('states', state.htmlFile)}`);
|
|
108
|
+
if (state.ariaSnapshotFile)
|
|
109
|
+
lines.push(`ARIA: file://${outputPath('states', state.ariaSnapshotFile)}`);
|
|
110
|
+
if (state.ariaSnapshot)
|
|
111
|
+
lines.push('', state.ariaSnapshot);
|
|
112
|
+
console.error(lines.join('\n'));
|
|
113
|
+
}
|
|
97
114
|
async function showStatsAndExit(code) {
|
|
98
115
|
if (remote.isAttached()) {
|
|
99
116
|
await remote.close(code);
|
|
@@ -130,10 +147,11 @@ addCommonOptions(program
|
|
|
130
147
|
.option('--focus <feature>', 'Focus area for exploration')
|
|
131
148
|
.option('--configure <spec>', 'Reuse spec: keys new|from|style|subpages|pick_by|priority, e.g. "new:25%;pick_by=random;priority=critical,high"')
|
|
132
149
|
.option('--dry-run', 'Mark picked tests as skipped without executing or generating new ones')).action(async (explorePath, options) => {
|
|
150
|
+
let explorBot;
|
|
133
151
|
try {
|
|
134
152
|
const urlFilter = new UrlFilter(options.urlFilter);
|
|
135
153
|
const sitemapUrls = await readCliSitemap(explorePath, options.sitemap, urlFilter);
|
|
136
|
-
|
|
154
|
+
explorBot = new ExplorBot(buildExplorBotOptions(explorePath || sitemapUrls?.[0], options));
|
|
137
155
|
await explorBot.start();
|
|
138
156
|
if (explorePath)
|
|
139
157
|
await explorBot.visit(explorePath);
|
|
@@ -162,6 +180,7 @@ addCommonOptions(program
|
|
|
162
180
|
}
|
|
163
181
|
catch (error) {
|
|
164
182
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
183
|
+
printPageContext(explorBot);
|
|
165
184
|
await showStatsAndExit(1);
|
|
166
185
|
}
|
|
167
186
|
});
|
|
@@ -423,7 +442,14 @@ addCommonOptions(program
|
|
|
423
442
|
.join(' ');
|
|
424
443
|
const { FreesailCommand } = await import('../src/commands/freesail-command.js');
|
|
425
444
|
const cmd = new FreesailCommand(explorBot);
|
|
426
|
-
|
|
445
|
+
try {
|
|
446
|
+
await cmd.execute(args);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
450
|
+
printPageContext(explorBot);
|
|
451
|
+
await showStatsAndExit(1);
|
|
452
|
+
}
|
|
427
453
|
});
|
|
428
454
|
program
|
|
429
455
|
.command('config [url]')
|
|
@@ -452,6 +478,7 @@ program
|
|
|
452
478
|
.option('-g, --global', 'Configure explorbot in ~/.explorbot to run from anywhere')
|
|
453
479
|
.option('--provider <name>', `AI provider for the global config: ${Object.keys(PROVIDERS).join(', ')}`)
|
|
454
480
|
.option('--api-key <key>', 'API key stored in ~/.explorbot/.env')
|
|
481
|
+
.option('--url <url>', 'Web application URL written to web.url in the local config')
|
|
455
482
|
.action(async (options) => {
|
|
456
483
|
try {
|
|
457
484
|
const { runInit } = await import('../src/commands/init-command.js');
|
|
@@ -462,6 +489,7 @@ program
|
|
|
462
489
|
global: options.global,
|
|
463
490
|
provider: options.provider,
|
|
464
491
|
apiKey: options.apiKey,
|
|
492
|
+
url: options.url,
|
|
465
493
|
});
|
|
466
494
|
}
|
|
467
495
|
catch (error) {
|
|
@@ -4,7 +4,7 @@ import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
|
4
4
|
import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
|
|
5
5
|
import { clearActivityLine, trackActivityLine } from "./activity-line.js";
|
|
6
6
|
import { renderEnvelope } from "./envelope.js";
|
|
7
|
-
import { askHelp, checkHelp, doHelp, helpContract, reportHelp, researchHelp, sessionHelp, statusHelp, verifyHelp } from "./help.js";
|
|
7
|
+
import { askHelp, checkHelp, doHelp, helpContract, reportHelp, researchHelp, sessionHelp, statusHelp, uxHelp, verifyHelp } from "./help.js";
|
|
8
8
|
import { Prima } from "./prima.js";
|
|
9
9
|
let rootOptions = () => ({});
|
|
10
10
|
function buildOptions(subcommand) {
|
|
@@ -120,6 +120,11 @@ export function createPrimaCommands(name = 'prima') {
|
|
|
120
120
|
.action(async (options) => {
|
|
121
121
|
await runPrima(options, 'research', (prima) => prima.research({ data: options.data, deep: options.deep, fresh: options.fresh }));
|
|
122
122
|
});
|
|
123
|
+
addBrowserOptions(cmd.command('ux [focus]').description('Review the area being worked on for UX issues, across breakpoints'))
|
|
124
|
+
.addHelpText('after', `\n${uxHelp}`)
|
|
125
|
+
.action(async (focus, options) => {
|
|
126
|
+
await runPrima(options, ['ux', focus].filter(Boolean).join(' '), (prima) => prima.ux(focus));
|
|
127
|
+
});
|
|
123
128
|
addBrowserOptions(cmd.command('go <target>').description('Navigate to a url, a path, or a page described in plain words')).action(async (target, options) => {
|
|
124
129
|
if (URL.canParse(target))
|
|
125
130
|
options.baseUrl = target;
|