explorbot 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/explorbot-cli.ts +59 -38
- package/boat/api-tester/src/apibot.ts +4 -2
- package/boat/api-tester/src/cli.ts +2 -2
- package/boat/api-tester/src/config.ts +43 -10
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +1 -0
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/doc-collector/src/docs-renderer.ts +18 -4
- package/boat/doc-collector/src/state-diagram.ts +61 -14
- package/boat/prima/bin/prima-cli.ts +5 -0
- package/boat/prima/package.json +16 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +251 -0
- package/boat/prima/src/envelope.ts +169 -0
- package/boat/prima/src/prima.ts +1124 -0
- package/boat/prima/src/pw-parser.ts +27 -0
- package/boat/prima/src/pw-registry.ts +74 -0
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +51 -32
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/apibot.js +3 -2
- package/dist/boat/api-tester/src/cli.js +2 -2
- package/dist/boat/api-tester/src/config.js +40 -10
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +1 -0
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/doc-collector/src/docs-renderer.js +17 -3
- package/dist/boat/doc-collector/src/state-diagram.js +57 -13
- package/dist/boat/prima/bin/prima-cli.js +4 -0
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +220 -0
- package/dist/boat/prima/src/envelope.js +142 -0
- package/dist/boat/prima/src/prima.js +1031 -0
- package/dist/boat/prima/src/pw-parser.js +30 -0
- package/dist/boat/prima/src/pw-registry.js +65 -0
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/models.json +3 -0
- package/dist/package.json +8 -3
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +7 -2
- package/dist/src/action.js +41 -6
- package/dist/src/ai/captain/mixin.js +3 -4
- package/dist/src/ai/captain/web-mode.js +7 -4
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +11 -0
- package/dist/src/ai/navigator.js +21 -12
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +48 -2
- package/dist/src/ai/planner.d.ts +1 -0
- package/dist/src/ai/planner.js +6 -0
- package/dist/src/ai/provider.js +2 -2
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher.js +2 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/task-agent.js +5 -2
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +33 -20
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +113 -36
- package/dist/src/application-spec-contract.d.ts +8 -0
- package/dist/src/application-spec-contract.js +8 -0
- package/dist/src/application-spec.d.ts +15 -0
- package/dist/src/application-spec.js +71 -0
- package/dist/src/browser-server.d.ts +12 -6
- package/dist/src/browser-server.js +74 -19
- package/dist/src/commands/clean-command.js +2 -7
- package/dist/src/commands/init-command.d.ts +5 -0
- package/dist/src/commands/init-command.js +119 -1
- package/dist/src/commands/navigate-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/sites-command.d.ts +6 -0
- package/dist/src/commands/sites-command.js +23 -0
- package/dist/src/components/InitWizard.d.ts +10 -0
- package/dist/src/components/InitWizard.js +133 -0
- package/dist/src/components/InputReadline.d.ts +1 -0
- package/dist/src/components/InputReadline.js +7 -4
- package/dist/src/config.d.ts +25 -5
- package/dist/src/config.js +158 -40
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +11 -1
- package/dist/src/explorbot.js +27 -6
- package/dist/src/explorer.d.ts +4 -1
- package/dist/src/explorer.js +42 -9
- package/dist/src/global-config.d.ts +22 -0
- package/dist/src/global-config.js +117 -0
- package/dist/src/knowledge-tracker.d.ts +5 -1
- package/dist/src/knowledge-tracker.js +14 -1
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +54 -0
- package/dist/src/remote.js +229 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +2 -0
- package/dist/src/test-plan.js +7 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/cli-name.js +6 -2
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +6 -0
- package/dist/src/utils/logger.js +24 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/test-files.js +1 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +12 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +86 -0
- package/docs/reference/commands.md +994 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/models.json +3 -0
- package/package.json +8 -3
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +47 -7
- package/src/ai/captain/mixin.ts +3 -3
- package/src/ai/captain/web-mode.ts +7 -4
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +26 -15
- package/src/ai/pilot.ts +53 -2
- package/src/ai/planner.ts +7 -0
- package/src/ai/provider.ts +2 -4
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher.ts +2 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/task-agent.ts +4 -2
- package/src/ai/tester.ts +32 -18
- package/src/ai/tools.ts +132 -36
- package/src/application-spec-contract.ts +10 -0
- package/src/application-spec.ts +87 -0
- package/src/browser-server.ts +74 -19
- package/src/commands/clean-command.ts +1 -6
- package/src/commands/init-command.ts +146 -1
- package/src/commands/navigate-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/sites-command.ts +27 -0
- package/src/components/InitWizard.tsx +166 -0
- package/src/components/InputReadline.tsx +8 -4
- package/src/config.ts +177 -43
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +34 -7
- package/src/explorer.ts +47 -9
- package/src/global-config.ts +148 -0
- package/src/knowledge-tracker.ts +17 -1
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +238 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +2 -1
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +9 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/cli-name.ts +5 -2
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +24 -1
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/test-files.ts +1 -2
- package/src/utils/url-matcher.ts +13 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/src/ai/task-agent.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ActionResult } from '../action-result.js';
|
|
2
2
|
import type { ExplorbotConfig } from '../config.ts';
|
|
3
|
+
import { executionController } from '../execution-controller.ts';
|
|
3
4
|
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
4
5
|
import type Explorer from '../explorer.ts';
|
|
5
6
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
@@ -12,7 +13,8 @@ import type { Provider } from './provider.js';
|
|
|
12
13
|
import { Quartermaster } from './quartermaster.js';
|
|
13
14
|
|
|
14
15
|
export function isInteractive(): boolean {
|
|
15
|
-
|
|
16
|
+
if (process.env.INK_RUNNING === 'true') return true;
|
|
17
|
+
return executionController.hasInputCallback();
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function createNullProxy<T extends object>(): T {
|
|
@@ -73,7 +75,7 @@ export abstract class TaskAgent {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
protected getKnowledge(actionResult: ActionResult): string {
|
|
76
|
-
return this.getKnowledgeTracker().
|
|
78
|
+
return this.getKnowledgeTracker().renderRelevantContext(actionResult);
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
protected getExperience(actionResult: ActionResult): string {
|
package/src/ai/tester.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Observability } from '../observability.ts';
|
|
|
11
11
|
import type { StateTransition } from '../state-manager.ts';
|
|
12
12
|
import { Stats } from '../stats.ts';
|
|
13
13
|
import { type Test, TestResult, type TestResultType } from '../test-plan.ts';
|
|
14
|
-
import {
|
|
14
|
+
import { compactAriaSnapshot, detectFocusArea } from '../utils/aria.ts';
|
|
15
15
|
import { ErrorPageError, isErrorPage } from '../utils/error-page.ts';
|
|
16
16
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
17
17
|
import { loop } from '../utils/loop.ts';
|
|
@@ -24,7 +24,7 @@ import { Provider } from './provider.ts';
|
|
|
24
24
|
import { Researcher } from './researcher.ts';
|
|
25
25
|
import { actionRule, capabilityGroundingRule, dataProtectionRules, focusedElementRule, formRequirementsRule, locatorRule, multipleTabsRule, sectionContextRule } from './rules.ts';
|
|
26
26
|
import { TaskAgent } from './task-agent.ts';
|
|
27
|
-
import { createCodeceptJSTools, createIframeTools } from './tools.ts';
|
|
27
|
+
import { createCodeceptJSTools, createIframeTools, withdrawVisionTools } from './tools.ts';
|
|
28
28
|
|
|
29
29
|
const debugLog = createDebug('explorbot:tester');
|
|
30
30
|
|
|
@@ -117,7 +117,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
117
117
|
const requestStore = this.requestStore;
|
|
118
118
|
requestStore.clear();
|
|
119
119
|
const offFailedRequest = requestStore.onFailedRequest((r) => {
|
|
120
|
-
task.
|
|
120
|
+
task.addObservation(`Network error: ${r.method} ${r.path} → ${r.status}`);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
const initialState = ActionResult.fromState(state);
|
|
@@ -245,6 +245,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
245
245
|
if (currentState.isInsideIframe) {
|
|
246
246
|
Object.assign(tools, createIframeTools(this.toolDeps));
|
|
247
247
|
}
|
|
248
|
+
withdrawVisionTools(tools);
|
|
248
249
|
|
|
249
250
|
debugLog(`Test ${task.scenario} iteration ${iteration}`);
|
|
250
251
|
|
|
@@ -406,6 +407,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
406
407
|
if (extensions >= this.MAX_EXTENSIONS) break;
|
|
407
408
|
|
|
408
409
|
extensions++;
|
|
410
|
+
this.stalledIterations = 0;
|
|
409
411
|
tag('info').log(`Pilot extending test (${extensions}/${this.MAX_EXTENSIONS})`);
|
|
410
412
|
conversation.cleanupTag('page_aria', '...trimmed...', 1);
|
|
411
413
|
conversation.cleanupTag('page_html', '...trimmed...', 0);
|
|
@@ -464,8 +466,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
464
466
|
this.stalledIterations++;
|
|
465
467
|
if (this.stalledIterations < this.MAX_STALLED_ITERATIONS) return false;
|
|
466
468
|
|
|
467
|
-
task.addNote('No browser progress
|
|
468
|
-
task.finish(TestResult.FAILED);
|
|
469
|
+
task.addNote('No further browser progress on unchanged page; requesting final review');
|
|
469
470
|
return true;
|
|
470
471
|
}
|
|
471
472
|
|
|
@@ -485,7 +486,8 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
485
486
|
`;
|
|
486
487
|
|
|
487
488
|
if (task.getPrintableNotes()) {
|
|
488
|
-
outcomeStatus
|
|
489
|
+
outcomeStatus += dedent`
|
|
490
|
+
|
|
489
491
|
Your current log:
|
|
490
492
|
<notes>
|
|
491
493
|
${task.notesToString()}
|
|
@@ -514,7 +516,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
514
516
|
|
|
515
517
|
const focusArea = detectFocusArea(currentState.ariaSnapshot);
|
|
516
518
|
|
|
517
|
-
const focusedElement =
|
|
519
|
+
const focusedElement = currentState.focusedElement;
|
|
518
520
|
if (focusedElement) {
|
|
519
521
|
const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
|
|
520
522
|
context += dedent`
|
|
@@ -596,7 +598,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
596
598
|
</page>
|
|
597
599
|
|
|
598
600
|
<page_aria>
|
|
599
|
-
${
|
|
601
|
+
${await this.interactiveAriaWithRefs(currentState)}
|
|
600
602
|
</page_aria>
|
|
601
603
|
${uiMapSection}
|
|
602
604
|
|
|
@@ -633,25 +635,32 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
633
635
|
</page>
|
|
634
636
|
|
|
635
637
|
<page_aria>
|
|
636
|
-
${
|
|
638
|
+
${await this.interactiveAriaWithRefs(currentState)}
|
|
637
639
|
</page_aria>
|
|
638
640
|
`;
|
|
639
641
|
}
|
|
640
642
|
|
|
643
|
+
private async interactiveAriaWithRefs(state: ActionResult): Promise<string> {
|
|
644
|
+
const withRefs = await Promise.resolve(this.explorer?.withPage?.((page: any) => page.locator('body').ariaSnapshot({ mode: 'ai' }))).catch(() => null);
|
|
645
|
+
if (!withRefs) return state.getInteractiveARIA();
|
|
646
|
+
return compactAriaSnapshot(withRefs, false);
|
|
647
|
+
}
|
|
648
|
+
|
|
641
649
|
private finishTest(task: Test): void {
|
|
642
|
-
if (!task.
|
|
643
|
-
task.finish(TestResult.
|
|
650
|
+
if (!task.result) {
|
|
651
|
+
if (task.hasAchievedAll()) task.finish(TestResult.PASSED);
|
|
652
|
+
else task.finish(TestResult.FAILED);
|
|
644
653
|
}
|
|
645
654
|
|
|
646
655
|
if (task.isSuccessful) {
|
|
647
656
|
tag('success').log(`Successful test: ${task.scenario}`);
|
|
648
|
-
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (task.isSkipped) {
|
|
649
660
|
tag('warning').log(`Skipped test: ${task.scenario}`);
|
|
650
|
-
|
|
651
|
-
tag('error').log(`Failed test: ${task.scenario}`);
|
|
652
|
-
} else {
|
|
653
|
-
tag('warning').log(`Test with no result: ${task.scenario}`);
|
|
661
|
+
return;
|
|
654
662
|
}
|
|
663
|
+
tag('error').log(`Failed test: ${task.scenario}`);
|
|
655
664
|
}
|
|
656
665
|
|
|
657
666
|
private async abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{ success: boolean }> {
|
|
@@ -795,6 +804,9 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
795
804
|
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
796
805
|
</expected_results>
|
|
797
806
|
|
|
807
|
+
An expected result counts as settled only when you record it back word for word as it is written above.
|
|
808
|
+
A note in your own wording is a general note and leaves that result unsettled.
|
|
809
|
+
|
|
798
810
|
Your goal is to perform actions on the web page and verify the expected outcomes.
|
|
799
811
|
Try to achieve as many goals as possible.
|
|
800
812
|
If goal is not achievable, log that and skip to next one.
|
|
@@ -1044,6 +1056,9 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
1044
1056
|
- You unsuccessfully tried multiple iterations and failed
|
|
1045
1057
|
- If the expected result was expected to fail, use status="success" instead
|
|
1046
1058
|
|
|
1059
|
+
When a note settles one of the expected results, that note must repeat the expected result word
|
|
1060
|
+
for word. Paraphrasing it leaves the expected result unsettled and it is reported as unverified.
|
|
1061
|
+
|
|
1047
1062
|
Example:
|
|
1048
1063
|
- record({ notes: ["clicked login button", "login form appeared", "fill credentials"], status: "success" })
|
|
1049
1064
|
`,
|
|
@@ -1128,8 +1143,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
1128
1143
|
this.stalledIterations++;
|
|
1129
1144
|
if (this.stalledIterations < this.MAX_STALLED_ITERATIONS) return false;
|
|
1130
1145
|
|
|
1131
|
-
task.addNote('No browser progress after repeated execution errors'
|
|
1132
|
-
task.finish(TestResult.FAILED);
|
|
1146
|
+
task.addNote('No browser progress after repeated execution errors; requesting final review');
|
|
1133
1147
|
return true;
|
|
1134
1148
|
}
|
|
1135
1149
|
|
package/src/ai/tools.ts
CHANGED
|
@@ -3,8 +3,9 @@ import dedent from 'dedent';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult, type PageDiff, type ToolResultMetadata } from '../action-result.ts';
|
|
5
5
|
import type { ExperienceTracker } from '../experience-tracker.ts';
|
|
6
|
+
import { Stats } from '../stats.ts';
|
|
6
7
|
import { type Task, TestResult } from '../test-plan.js';
|
|
7
|
-
import { LARGE_ARIA_CHANGE_THRESHOLD
|
|
8
|
+
import { LARGE_ARIA_CHANGE_THRESHOLD } from '../utils/aria.ts';
|
|
8
9
|
import { isFatalBrowserError } from '../utils/browser-errors.ts';
|
|
9
10
|
import { createDebug, tag } from '../utils/logger.js';
|
|
10
11
|
import { pause } from '../utils/loop.js';
|
|
@@ -33,6 +34,10 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps,
|
|
|
33
34
|
description: dedent`
|
|
34
35
|
Click an element by trying multiple CodeceptJS commands in order until one succeeds.
|
|
35
36
|
|
|
37
|
+
Use this only for elements the page context gives you no ref for. When the element shows a ref such as [ref=e14],
|
|
38
|
+
call clickRef with that ref instead — composing a locator for an element that already has a ref is wasted work,
|
|
39
|
+
and a locator can match several elements where a ref cannot.
|
|
40
|
+
|
|
36
41
|
Follow <locator_priority> from system prompt for locator selection.
|
|
37
42
|
|
|
38
43
|
I.click(locator) - click element matching locator
|
|
@@ -51,14 +56,13 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps,
|
|
|
51
56
|
commands: z.array(z.string()).describe(dedent`
|
|
52
57
|
FALLBACK LOCATORS for ONE element. All commands must click the SAME element.
|
|
53
58
|
Never mix different elements — use separate click() calls instead.
|
|
59
|
+
REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
|
|
54
60
|
Order by reliability:
|
|
55
|
-
1. I.click(text, container) -
|
|
61
|
+
1. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
|
|
56
62
|
2. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
|
|
57
63
|
3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
|
|
58
64
|
4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
|
|
59
65
|
5. I.clickXY(x, y) - coordinates fallback
|
|
60
|
-
IMPORTANT: Always include at least one command WITHOUT a container as fallback,
|
|
61
|
-
in case the element moved to a different section (e.g. I.click("Save") without container).
|
|
62
66
|
`),
|
|
63
67
|
explanation: z.string().describe('Why you are clicking this element'),
|
|
64
68
|
}),
|
|
@@ -132,13 +136,7 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps,
|
|
|
132
136
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, commands[0]);
|
|
133
137
|
await commitNote(activeNote, TestResult.FAILED, toolResult, action);
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
const lastError = attempts[attempts.length - 1]?.error || '';
|
|
137
|
-
if (lastError.includes('was not found') || lastError.includes('not found by text')) {
|
|
138
|
-
suggestion = 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
139
|
-
} else if (lastError.includes('Timeout') || lastError.includes('intercept')) {
|
|
140
|
-
suggestion = 'Element exists but could not be clicked (possibly covered by overlay or not interactable). Try closing overlapping panels first, or use visualClick().';
|
|
141
|
-
}
|
|
139
|
+
const suggestion = clickFailureSuggestion(attempts);
|
|
142
140
|
|
|
143
141
|
return failedToolResult(
|
|
144
142
|
'click',
|
|
@@ -153,6 +151,41 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps,
|
|
|
153
151
|
},
|
|
154
152
|
}),
|
|
155
153
|
|
|
154
|
+
clickRef: tool({
|
|
155
|
+
description: dedent`
|
|
156
|
+
Click an element by the ref the page context gave it, e.g. [ref=e14].
|
|
157
|
+
|
|
158
|
+
Prefer this over click() whenever the element you want carries a ref. A ref names one exact element, so it
|
|
159
|
+
cannot match several by mistake and never needs disambiguating — it is the fastest way to click.
|
|
160
|
+
Only pass a ref that appears in the page context you were given. Never invent or guess one.
|
|
161
|
+
If it reports the ref is gone, the page has been rebuilt: get fresh context and use the new ref.
|
|
162
|
+
`,
|
|
163
|
+
inputSchema: z.object({
|
|
164
|
+
ref: z.string().describe('The ref exactly as it appears in the page context, e.g. "e14"'),
|
|
165
|
+
element: z.string().describe('Role and name of the element you are clicking, for the record'),
|
|
166
|
+
}),
|
|
167
|
+
execute: async ({ ref, element }) => {
|
|
168
|
+
const activeNote = task.startNote(`Click ${element}`);
|
|
169
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
|
|
170
|
+
const action = explorer.action();
|
|
171
|
+
const named = await describeRef(explorer, ref);
|
|
172
|
+
const run = `I.usePlaywrightTo(${JSON.stringify(`click ${element}`)}, async ({ page }) => page.locator(${JSON.stringify(`aria-ref=${ref}`)}).click())`;
|
|
173
|
+
|
|
174
|
+
if (!(await action.attempt(run, `Click ${element}`))) {
|
|
175
|
+
activeNote.commit(TestResult.FAILED);
|
|
176
|
+
return failedToolResult('clickRef', `Ref ${ref} could not be clicked: ${errorText(action.lastError)}`, {
|
|
177
|
+
suggestion: 'The ref may belong to an older version of the page. Get fresh context and use the ref it gives, or fall back to click() with a locator.',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// a ref belongs to this session only, so the run is reported as the locator a later test can replay
|
|
182
|
+
const code = named ? `I.click(${JSON.stringify(named)})` : run;
|
|
183
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()!).toToolResult(previousState, code);
|
|
184
|
+
await commitNote(activeNote, TestResult.PASSED, toolResult, action);
|
|
185
|
+
return successToolResult('clickRef', { ...toolResult, code }, action);
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
|
|
156
189
|
hover: tool({
|
|
157
190
|
description: dedent`
|
|
158
191
|
Move the mouse cursor to an element to reveal hover-only controls.
|
|
@@ -299,15 +332,11 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps,
|
|
|
299
332
|
const focusFreeKeys = new Set(['Escape', 'Esc', 'Tab', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']);
|
|
300
333
|
const needsFocus = !focusFreeKeys.has(keyToUse) && !modifier;
|
|
301
334
|
|
|
302
|
-
if (needsFocus) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
308
|
-
suggestion: 'Click the target element first, then press the key.',
|
|
309
|
-
});
|
|
310
|
-
}
|
|
335
|
+
if (needsFocus && !(await hasFocusedElement(explorer))) {
|
|
336
|
+
activeNote.commit(TestResult.FAILED);
|
|
337
|
+
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
338
|
+
suggestion: 'Click the target element first, then press the key.',
|
|
339
|
+
});
|
|
311
340
|
}
|
|
312
341
|
|
|
313
342
|
const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
|
|
@@ -358,6 +387,8 @@ export function createCodeceptJSTools({ explorer, stateManager, ai }: ToolDeps,
|
|
|
358
387
|
Execute raw CodeceptJS code block with multiple commands.
|
|
359
388
|
USE THIS TOOL for typing text into fields: I.fillField, I.type
|
|
360
389
|
|
|
390
|
+
Do not put a click on a ref-bearing element in here — clickRef with its ref is cheaper and cannot mis-target.
|
|
391
|
+
|
|
361
392
|
Follow <actions> from system prompt for available commands.
|
|
362
393
|
Follow <locator_priority> from system prompt for locator selection.
|
|
363
394
|
|
|
@@ -539,26 +570,25 @@ export function createLearnExperienceTool({ getExperienceTracker, getState }: {
|
|
|
539
570
|
}
|
|
540
571
|
|
|
541
572
|
export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any {
|
|
542
|
-
let visionDisabled = false;
|
|
543
|
-
|
|
544
573
|
const tools: Record<string, any> = {
|
|
545
574
|
see: tool({
|
|
546
575
|
description: dedent`
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
576
|
+
Answer a question about the page from a screenshot, for things its structure cannot express:
|
|
577
|
+
layout and position, what an image or canvas depicts, colour, and whether something is covered or cut off.
|
|
578
|
+
This runs a second model and is the slowest tool here, so reach for it only when the question is genuinely visual.
|
|
579
|
+
Do NOT use it to confirm an action landed — every action already reports what changed on the page.
|
|
550
580
|
Input schema has exactly one field: request. Do not pass text, reason, assertion, or other fields.
|
|
551
581
|
|
|
552
582
|
<example>
|
|
553
|
-
request: "
|
|
554
|
-
result: "
|
|
583
|
+
request: "Is the save button covered by anything, and does the chart show any plotted data?"
|
|
584
|
+
result: "The save button is partly behind a cookie banner at the bottom. The chart area is empty apart from its axes."
|
|
555
585
|
</example>
|
|
556
586
|
`,
|
|
557
587
|
inputSchema: z.object({
|
|
558
588
|
request: z.string().describe('LLM-friendly description of the page contents to look for. 1-3 sentences. No more than 100 words.'),
|
|
559
589
|
}),
|
|
560
590
|
execute: async ({ request }) => {
|
|
561
|
-
if (visionDisabled) {
|
|
591
|
+
if (Stats.visionDisabled) {
|
|
562
592
|
return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
|
|
563
593
|
}
|
|
564
594
|
|
|
@@ -583,8 +613,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
583
613
|
} catch (error) {
|
|
584
614
|
throwIfFatalBrowserError(error);
|
|
585
615
|
const errorMessage = errorText(error);
|
|
586
|
-
|
|
587
|
-
tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
|
|
616
|
+
disableVision();
|
|
588
617
|
return failedToolResult('see', `See tool failed: ${errorMessage}`, {
|
|
589
618
|
suggestion: 'Vision is now disabled. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.',
|
|
590
619
|
});
|
|
@@ -676,6 +705,13 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
676
705
|
);
|
|
677
706
|
}
|
|
678
707
|
|
|
708
|
+
if (result.inexpressible) {
|
|
709
|
+
return failedToolResult('verify', `No assertion could express this claim: ${assertion}`, {
|
|
710
|
+
inexpressible: true,
|
|
711
|
+
suggestion: 'This is not evidence the page is wrong — the claim could not be turned into an assertion. Restate it in terms of what is visible or of a control state, or check it with see().',
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
679
715
|
return failedToolResult('verify', `Verification failed: ${assertion}`, {
|
|
680
716
|
suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
|
|
681
717
|
});
|
|
@@ -804,7 +840,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
804
840
|
context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
|
|
805
841
|
}),
|
|
806
842
|
execute: async ({ element, context }) => {
|
|
807
|
-
if (visionDisabled) {
|
|
843
|
+
if (Stats.visionDisabled) {
|
|
808
844
|
return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
|
|
809
845
|
}
|
|
810
846
|
|
|
@@ -860,8 +896,7 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
860
896
|
} catch (error) {
|
|
861
897
|
throwIfFatalBrowserError(error);
|
|
862
898
|
const errorMessage = errorText(error);
|
|
863
|
-
|
|
864
|
-
tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
|
|
899
|
+
disableVision();
|
|
865
900
|
return failedToolResult('visualClick', `visualClick tool failed: ${errorMessage}`, {
|
|
866
901
|
suggestion: 'Vision is now disabled. Use xpathCheck() to find the element, then click() with the discovered locator.',
|
|
867
902
|
});
|
|
@@ -1011,6 +1046,12 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
1011
1046
|
}),
|
|
1012
1047
|
};
|
|
1013
1048
|
|
|
1049
|
+
const disableVision = (): void => {
|
|
1050
|
+
Stats.visionDisabled = true;
|
|
1051
|
+
withdrawVisionTools(tools);
|
|
1052
|
+
tag('warning').log('⚠️ Vision model is not available. Visual tools are disabled for this session.');
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1014
1055
|
if (withExperience !== false) {
|
|
1015
1056
|
tools.learnExperience = createLearnExperienceTool({
|
|
1016
1057
|
getExperienceTracker: () => stateManager.getExperienceTracker(),
|
|
@@ -1063,6 +1104,8 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
1063
1104
|
});
|
|
1064
1105
|
}
|
|
1065
1106
|
|
|
1107
|
+
withdrawVisionTools(tools);
|
|
1108
|
+
|
|
1066
1109
|
return tools;
|
|
1067
1110
|
}
|
|
1068
1111
|
|
|
@@ -1112,14 +1155,33 @@ function errorText(error: unknown): string {
|
|
|
1112
1155
|
return 'Unknown error occurred';
|
|
1113
1156
|
}
|
|
1114
1157
|
|
|
1115
|
-
async function commitNote(activeNote: any, result: TestResult, toolResult: any, action: any): Promise<void> {
|
|
1158
|
+
export async function commitNote(activeNote: any, result: TestResult, toolResult: any, action: any): Promise<void> {
|
|
1116
1159
|
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
1117
1160
|
activeNote.screenshot = await action.saveScreenshot();
|
|
1118
1161
|
}
|
|
1119
1162
|
activeNote.commit(result);
|
|
1120
1163
|
}
|
|
1121
1164
|
|
|
1122
|
-
function
|
|
1165
|
+
async function describeRef(explorer: any, ref: string): Promise<{ role: string; text: string } | null> {
|
|
1166
|
+
return Promise.resolve(
|
|
1167
|
+
explorer?.withPage?.((page: any) =>
|
|
1168
|
+
page.locator(`aria-ref=${ref}`).evaluate((el: any) => {
|
|
1169
|
+
const tag = el.tagName.toLowerCase();
|
|
1170
|
+
const roles: Record<string, string> = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|
|
1171
|
+
const role = el.getAttribute('role') || roles[tag] || tag;
|
|
1172
|
+
const text = (el.getAttribute('aria-label') || el.innerText || el.value || '').trim().split('\n')[0];
|
|
1173
|
+
if (!text) return null;
|
|
1174
|
+
return { role, text };
|
|
1175
|
+
})
|
|
1176
|
+
)
|
|
1177
|
+
).catch(() => null);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async function hasFocusedElement(explorer: any): Promise<boolean> {
|
|
1181
|
+
return explorer.withPage((page: any) => page.evaluate(() => !!document.activeElement && document.activeElement !== document.body)).catch(() => true);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export function successToolResult(action: string, data?: Record<string, any>, source?: { playwrightGroupId?: string | null; assertionSteps?: any[] }) {
|
|
1123
1185
|
const result: Record<string, any> = { success: true, action, ...data };
|
|
1124
1186
|
if (source?.playwrightGroupId) {
|
|
1125
1187
|
result.playwrightGroupId = source.playwrightGroupId;
|
|
@@ -1155,7 +1217,7 @@ function hasObservablePageChange(data?: Record<string, any>): boolean {
|
|
|
1155
1217
|
return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
|
|
1156
1218
|
}
|
|
1157
1219
|
|
|
1158
|
-
async function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null) {
|
|
1220
|
+
export async function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null) {
|
|
1159
1221
|
const result: Record<string, any> = { success: false, action, message, ...data };
|
|
1160
1222
|
if (data?.pageDiff) {
|
|
1161
1223
|
result.suggestion = data.suggestion ? `${data.suggestion} ${PAGE_DIFF_SUGGESTION}` : PAGE_DIFF_SUGGESTION;
|
|
@@ -1192,6 +1254,40 @@ function getMultipleElementsSuggestion(): string {
|
|
|
1192
1254
|
`;
|
|
1193
1255
|
}
|
|
1194
1256
|
|
|
1257
|
+
export function withdrawVisionTools(tools: Record<string, any>): void {
|
|
1258
|
+
if (!Stats.visionDisabled) return;
|
|
1259
|
+
Reflect.deleteProperty(tools, 'see');
|
|
1260
|
+
Reflect.deleteProperty(tools, 'visualClick');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
export function clickFailureSuggestion(attempts: Array<{ error?: string }>): string {
|
|
1264
|
+
const errors = attempts.map((a) => a.error || '');
|
|
1265
|
+
|
|
1266
|
+
if (errors.some((e) => e.includes('not enabled'))) {
|
|
1267
|
+
return 'Element exists but is DISABLED — clicking it again cannot work. A precondition is unmet: a required field is empty, nothing is selected, or a dialog is blocking. Satisfy it, then retry.';
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (errors.some((e) => e.includes('intercepts pointer events'))) {
|
|
1271
|
+
return 'Element exists but another element covers it. Close the overlapping panel or dialog, then retry.';
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
if (errors.some((e) => e.includes('is not visible'))) {
|
|
1275
|
+
return 'Element is in the DOM but not visible. Reveal it first — scroll to it, expand its section, or open the panel holding it.';
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const notFound = errors.filter((e) => e.includes('was not found'));
|
|
1279
|
+
|
|
1280
|
+
if (notFound.length && notFound.every((e) => e.includes('was not found inside element'))) {
|
|
1281
|
+
return 'Element was not found inside that container — the container is wrong or stale, and the element may exist elsewhere on the page. Retry the same locator WITHOUT a container, or verify the container with xpathCheck().';
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (notFound.length) {
|
|
1285
|
+
return 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return "Try xpathCheck() to find the element's actual position, see() for visual analysis, or visualClick() to click by visual appearance.";
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1195
1291
|
const MAX_DISAMBIGUATE_ELEMENTS = 10;
|
|
1196
1292
|
const MULTIPLE_ELEMENTS_PATTERN = 'multiple elements';
|
|
1197
1293
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const APPLICATION_SPEC_FORMAT = 'explorbot-application-spec';
|
|
4
|
+
export const APPLICATION_SPEC_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
export const APPLICATION_SPEC_PAGE_SCHEMA = z.object({
|
|
7
|
+
format: z.literal(APPLICATION_SPEC_FORMAT),
|
|
8
|
+
version: z.literal(APPLICATION_SPEC_VERSION),
|
|
9
|
+
url: z.string().trim().min(1),
|
|
10
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { ActionResult } from './action-result.ts';
|
|
5
|
+
import { APPLICATION_SPEC_PAGE_SCHEMA } from './application-spec-contract.ts';
|
|
6
|
+
import { ConfigParser } from './config.ts';
|
|
7
|
+
import { tag } from './utils/logger.ts';
|
|
8
|
+
import { loadMarkdownFiles } from './utils/markdown-files.ts';
|
|
9
|
+
|
|
10
|
+
export class ApplicationSpec {
|
|
11
|
+
private pages: ApplicationSpecPage[] = [];
|
|
12
|
+
readonly sourcePath: string;
|
|
13
|
+
|
|
14
|
+
constructor(sourcePath: string) {
|
|
15
|
+
this.sourcePath = this.resolveSourcePath(sourcePath);
|
|
16
|
+
this.load();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
renderFor(state: ActionResult): string {
|
|
20
|
+
const relevant = this.pages.filter((page) => state.isMatchedBy({ url: page.url }));
|
|
21
|
+
if (relevant.length === 0) return '';
|
|
22
|
+
|
|
23
|
+
tag('operation').log(`Found application specification for ${state.url}`);
|
|
24
|
+
return dedent`
|
|
25
|
+
<application_spec>
|
|
26
|
+
This is previously collected application documentation. Treat User Can and observed state transitions as supporting context, not as a replacement for the current page state. Treat User Might as unverified possibilities that must be confirmed before use.
|
|
27
|
+
|
|
28
|
+
${relevant.map((page) => page.content).join('\n\n')}
|
|
29
|
+
</application_spec>
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get pageCount(): number {
|
|
34
|
+
return this.pages.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private load(): void {
|
|
38
|
+
if (!existsSync(this.sourcePath)) {
|
|
39
|
+
throw new Error(`Application spec not found: ${this.sourcePath}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isDirectory = statSync(this.sourcePath).isDirectory();
|
|
43
|
+
if (!isDirectory && path.basename(this.sourcePath).toLowerCase() !== 'index.md') {
|
|
44
|
+
throw new Error(`Application spec file must be index.md: ${this.sourcePath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const bundlePath = isDirectory ? this.sourcePath : path.dirname(this.sourcePath);
|
|
48
|
+
const indexPath = path.join(bundlePath, 'index.md');
|
|
49
|
+
if (!existsSync(indexPath)) {
|
|
50
|
+
throw new Error(`Application spec index not found: ${indexPath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pagesPath = path.join(bundlePath, 'pages');
|
|
54
|
+
if (!existsSync(pagesPath)) {
|
|
55
|
+
throw new Error(`Application spec pages directory not found: ${pagesPath}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const file of loadMarkdownFiles(pagesPath, { recursive: true })) {
|
|
59
|
+
const parsed = APPLICATION_SPEC_PAGE_SCHEMA.safeParse(file.data);
|
|
60
|
+
if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'format')) {
|
|
61
|
+
throw new Error(`Invalid application spec format in ${file.filePath}`);
|
|
62
|
+
}
|
|
63
|
+
if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'version')) {
|
|
64
|
+
throw new Error(`Unsupported application spec version in ${file.filePath}: ${String(file.data.version)}`);
|
|
65
|
+
}
|
|
66
|
+
if (!parsed.success) {
|
|
67
|
+
throw new Error(`Application spec page URL is missing in ${file.filePath}`);
|
|
68
|
+
}
|
|
69
|
+
this.pages.push({ url: parsed.data.url, content: file.content.trim() });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.pages.length === 0) {
|
|
73
|
+
throw new Error(`Application spec contains no documented pages: ${pagesPath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private resolveSourcePath(sourcePath: string): string {
|
|
78
|
+
if (path.isAbsolute(sourcePath)) return path.resolve(sourcePath);
|
|
79
|
+
const configParser = ConfigParser.getInstance();
|
|
80
|
+
return path.resolve(configParser.resolveProjectDir(sourcePath));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ApplicationSpecPage {
|
|
85
|
+
url: string;
|
|
86
|
+
content: string;
|
|
87
|
+
}
|