explorbot 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/explorbot-cli.ts +2 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/README.md +31 -4
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/explorbot-cli.js +2 -1
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +4 -3
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +53 -8
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/rules.js +13 -0
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/options/decision-model-option.d.ts +6 -0
- package/dist/src/commands/options/decision-model-option.js +10 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +7 -0
- package/dist/src/config.js +11 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +8 -2
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/docs/api-testing/planning.md +1 -1
- package/docs/basics/providers.md +37 -0
- package/docs/index.json +1 -0
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +2 -0
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/docs/web-testing/decisions.md +108 -0
- package/docs/workflow/agentic-usage.md +1 -0
- package/package.json +4 -3
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +52 -8
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/rules.ts +13 -0
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/options/decision-model-option.ts +11 -0
- package/src/commands/options/index.ts +2 -0
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +11 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +6 -1
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Planner } from '../ai/planner.js';
|
|
2
2
|
import { Researcher } from '../ai/researcher.js';
|
|
3
|
+
import { normalizeUrl } from '../state-manager.js';
|
|
3
4
|
import { Stats } from '../stats.js';
|
|
4
5
|
import { tag } from '../utils/logger.js';
|
|
5
6
|
import { loop } from '../utils/loop.js';
|
|
@@ -67,12 +68,18 @@ export class FreesailCommand extends BaseCommand {
|
|
|
67
68
|
const suggestion = await navigator.freeSail({ strategy, scope, visitedUrls });
|
|
68
69
|
if (!suggestion) {
|
|
69
70
|
tag('info').log('No navigation suggestion available');
|
|
70
|
-
|
|
71
|
+
ctx.stop();
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
if (scope && !suggestion.target.startsWith(scope)) {
|
|
74
75
|
tag('warning').log(`Suggestion ${suggestion.target} is outside scope ${scope}, skipping`);
|
|
75
|
-
|
|
76
|
+
ctx.stop();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const currentUrl = stateManager.getCurrentState()?.url;
|
|
80
|
+
if (currentUrl && normalizeUrl(suggestion.target) === normalizeUrl(currentUrl)) {
|
|
81
|
+
tag('info').log(`No new navigation target available after ${currentUrl}; stopping exploration`);
|
|
82
|
+
ctx.stop();
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
tag('info').log(`Navigating to: ${suggestion.target} - ${suggestion.reason}`);
|
|
@@ -163,7 +163,7 @@ export function runInitCommand(options: InitCommandOptions): void {
|
|
|
163
163
|
log('2. Set AI models config file');
|
|
164
164
|
log('3. Set web application URL in the config file');
|
|
165
165
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
166
|
-
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
166
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
167
167
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
168
168
|
|
|
169
169
|
log('5. Launch application on a relative URL');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tag } from '../utils/logger.js';
|
|
2
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
2
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
3
3
|
|
|
4
4
|
export class NavigateCommand extends BaseCommand {
|
|
5
5
|
name = 'navigate';
|
|
@@ -18,4 +18,11 @@ export class NavigateCommand extends BaseCommand {
|
|
|
18
18
|
await this.explorBot.visit(destination);
|
|
19
19
|
tag('success').log(`Navigation requested: ${destination}`);
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
completeArguments(): ArgumentCompletion[] {
|
|
23
|
+
return this.explorBot
|
|
24
|
+
.stateManager()
|
|
25
|
+
.getKnownUrls()
|
|
26
|
+
.map((url) => ({ value: url }));
|
|
27
|
+
}
|
|
21
28
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseOption } from './base-option.js';
|
|
2
|
+
|
|
3
|
+
export class DecisionModelOption extends BaseOption {
|
|
4
|
+
flags = '--decision-model <model>';
|
|
5
|
+
description = 'Turn on the decision model for this run, as provider/model-id (e.g. openrouter/typesafe/jev-1.13)';
|
|
6
|
+
|
|
7
|
+
protected apply(options: Record<string, any>): void {
|
|
8
|
+
if (!options.decisionModel) return;
|
|
9
|
+
process.env.EXPLORBOT_DECISION_MODEL = options.decisionModel;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { DecisionModelOption } from './decision-model-option.js';
|
|
1
2
|
import { KnowledgeOption } from './knowledge-option.js';
|
|
2
3
|
import { WsOption } from './ws-option.js';
|
|
3
4
|
|
|
4
5
|
export { BaseOption } from './base-option.js';
|
|
5
6
|
|
|
7
|
+
export const decisionModelOption = new DecisionModelOption();
|
|
6
8
|
export const knowledgeOption = new KnowledgeOption();
|
|
7
9
|
export const wsOption = new WsOption();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { Plan } from '../test-plan.js';
|
|
1
2
|
import { tag } from '../utils/logger.js';
|
|
2
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
3
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
3
4
|
|
|
4
5
|
export class PlanLoadCommand extends BaseCommand {
|
|
5
6
|
name = 'plan:load';
|
|
@@ -18,4 +19,8 @@ export class PlanLoadCommand extends BaseCommand {
|
|
|
18
19
|
const plan = this.explorBot.loadPlan(filename);
|
|
19
20
|
tag('success').log(`Plan loaded: ${plan.title} with ${plan.tests.length} tests`);
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
completeArguments(): ArgumentCompletion[] {
|
|
24
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
25
|
+
}
|
|
21
26
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { Plan } from '../test-plan.js';
|
|
3
|
+
import { Plan, type PlanFile } from '../test-plan.js';
|
|
4
4
|
import { getCliName } from '../utils/cli-name.js';
|
|
5
5
|
import { tag } from '../utils/logger.js';
|
|
6
6
|
import { relativeToCwd } from '../utils/next-steps.js';
|
|
7
|
-
import { BaseCommand } from './base-command.js';
|
|
7
|
+
import { type ArgumentCompletion, BaseCommand } from './base-command.js';
|
|
8
8
|
|
|
9
9
|
export class PlansCommand extends BaseCommand {
|
|
10
10
|
name = 'plans';
|
|
@@ -13,7 +13,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
13
13
|
|
|
14
14
|
async execute(args: string): Promise<void> {
|
|
15
15
|
const { opts, args: remaining } = this.parseArgs(args);
|
|
16
|
-
const files = this.
|
|
16
|
+
const files = Plan.listFiles(this.explorBot.getPlansDir());
|
|
17
17
|
const target = String(opts.fromPlan || remaining[0] || '').trim();
|
|
18
18
|
|
|
19
19
|
if (!target) {
|
|
@@ -25,22 +25,8 @@ export class PlansCommand extends BaseCommand {
|
|
|
25
25
|
this.printPlanDetails(plan, file);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (!existsSync(plansDir)) return [];
|
|
31
|
-
|
|
32
|
-
return readdirSync(plansDir)
|
|
33
|
-
.filter((file) => file.endsWith('.md'))
|
|
34
|
-
.map((file) => {
|
|
35
|
-
const filePath = path.join(plansDir, file);
|
|
36
|
-
const stat = statSync(filePath);
|
|
37
|
-
return {
|
|
38
|
-
name: file,
|
|
39
|
-
path: filePath,
|
|
40
|
-
modifiedAt: stat.mtimeMs,
|
|
41
|
-
};
|
|
42
|
-
})
|
|
43
|
-
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
28
|
+
completeArguments(): ArgumentCompletion[] {
|
|
29
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
44
30
|
}
|
|
45
31
|
|
|
46
32
|
private printPlans(files: PlanFile[]): void {
|
|
@@ -52,8 +38,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
52
38
|
tag('info').log('Saved plans:');
|
|
53
39
|
for (let i = 0; i < files.length; i++) {
|
|
54
40
|
const file = files[i];
|
|
55
|
-
|
|
56
|
-
tag('info').log(`${i + 1}. ${plan.title} (${plan.tests.length} tests) - ${file.name}`);
|
|
41
|
+
tag('info').log(`${i + 1}. ${file.title} (${file.testCount} tests) - ${file.name}`);
|
|
57
42
|
}
|
|
58
43
|
tag('info').log('');
|
|
59
44
|
tag('info').log(`View plan tests: ${getCliName()} plans <number>`);
|
|
@@ -83,17 +68,16 @@ export class PlansCommand extends BaseCommand {
|
|
|
83
68
|
throw new Error(`Plan file not found: ${target}`);
|
|
84
69
|
}
|
|
85
70
|
|
|
71
|
+
const name = path.basename(plan.filePath);
|
|
86
72
|
const file = {
|
|
87
|
-
name
|
|
73
|
+
name,
|
|
88
74
|
path: plan.filePath,
|
|
89
75
|
modifiedAt: statSync(plan.filePath).mtimeMs,
|
|
76
|
+
title: plan.title,
|
|
77
|
+
url: plan.startUrl || '',
|
|
78
|
+
testCount: plan.tests.length,
|
|
79
|
+
label: name,
|
|
90
80
|
};
|
|
91
81
|
return { plan, file };
|
|
92
82
|
}
|
|
93
83
|
}
|
|
94
|
-
|
|
95
|
-
interface PlanFile {
|
|
96
|
-
name: string;
|
|
97
|
-
path: string;
|
|
98
|
-
modifiedAt: number;
|
|
99
|
-
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
3
|
import { ConfigParser } from '../config.ts';
|
|
4
4
|
import { tag } from '../utils/logger.ts';
|
|
5
|
-
import {
|
|
5
|
+
import { listTestFiles } from '../utils/test-files.ts';
|
|
6
|
+
import { type ArgumentCompletion, BaseCommand } from './base-command.js';
|
|
6
7
|
|
|
7
8
|
export class RerunCommand extends BaseCommand {
|
|
8
9
|
name = 'rerun';
|
|
@@ -32,6 +33,10 @@ export class RerunCommand extends BaseCommand {
|
|
|
32
33
|
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
33
34
|
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
completeArguments(): ArgumentCompletion[] {
|
|
38
|
+
return listTestFiles(ConfigParser.getInstance().getTestsDir()).map((file) => ({ value: basename(file) }));
|
|
39
|
+
}
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function parseTestIndices(input: string): number[] {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ConfigParser } from '../config.ts';
|
|
3
3
|
import { tag } from '../utils/logger.ts';
|
|
4
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
4
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
5
5
|
|
|
6
6
|
export class ResearchCommand extends BaseCommand {
|
|
7
7
|
name = 'research';
|
|
@@ -51,4 +51,11 @@ export class ResearchCommand extends BaseCommand {
|
|
|
51
51
|
this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
completeArguments(): ArgumentCompletion[] {
|
|
56
|
+
return this.explorBot
|
|
57
|
+
.stateManager()
|
|
58
|
+
.getKnownUrls()
|
|
59
|
+
.map((url) => ({ value: url }));
|
|
60
|
+
}
|
|
54
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Stats } from '../stats.js';
|
|
2
|
-
import { Test } from '../test-plan.js';
|
|
2
|
+
import { Plan, Test } from '../test-plan.js';
|
|
3
3
|
import { tag } from '../utils/logger.js';
|
|
4
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
4
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
5
5
|
|
|
6
6
|
export class TestCommand extends BaseCommand {
|
|
7
7
|
name = 'test';
|
|
@@ -75,6 +75,10 @@ export class TestCommand extends BaseCommand {
|
|
|
75
75
|
}
|
|
76
76
|
tag('success').log('Test execution finished');
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
completeArguments(): ArgumentCompletion[] {
|
|
80
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: `--from-plan ${file.name}`, display: file.label }));
|
|
81
|
+
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function parseTestIndices(input: string, total: number): number[] {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
1
|
+
import { Box, Text, useStdout } from 'ink';
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import { truncate } from '../utils/strings.js';
|
|
3
4
|
import { useAutocompleteState } from './autocomplete-store.js';
|
|
4
5
|
|
|
6
|
+
const MAX_HEIGHT = 7;
|
|
7
|
+
const COLUMN_WIDTH = 24;
|
|
8
|
+
|
|
5
9
|
const Autocomplete: React.FC = () => {
|
|
6
10
|
const { suggestions, selectedIndex, visible, argumentHint } = useAutocompleteState();
|
|
11
|
+
const { stdout } = useStdout();
|
|
7
12
|
|
|
8
13
|
if (!visible) {
|
|
9
14
|
if (!argumentHint) {
|
|
@@ -20,24 +25,49 @@ const Autocomplete: React.FC = () => {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
const effectiveSelectedIndex = Math.min(selectedIndex, suggestions.length - 1);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
28
|
+
const rowsPerColumn = Math.min(Math.max(1, MAX_HEIGHT - 2), suggestions.length);
|
|
29
|
+
const widest = Math.max(...suggestions.map((suggestion) => suggestion.display.length));
|
|
30
|
+
|
|
31
|
+
if (widest > COLUMN_WIDTH) {
|
|
32
|
+
const width = Math.max(COLUMN_WIDTH, (stdout?.columns || 80) - 4);
|
|
33
|
+
let firstRow = 0;
|
|
34
|
+
if (effectiveSelectedIndex >= rowsPerColumn) {
|
|
35
|
+
firstRow = effectiveSelectedIndex - rowsPerColumn + 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box position="absolute" top={0} left={0} width="100%" maxHeight={MAX_HEIGHT} overflow="hidden" paddingX={1} paddingY={1} backgroundColor="#2a2a2a" flexDirection="column">
|
|
40
|
+
{suggestions.slice(firstRow, firstRow + rowsPerColumn).map((suggestion, rowIndex) => {
|
|
41
|
+
const isSelected = firstRow + rowIndex === effectiveSelectedIndex;
|
|
42
|
+
let color = 'white';
|
|
43
|
+
let backgroundColor = '#2a2a2a';
|
|
44
|
+
if (isSelected) {
|
|
45
|
+
color = 'black';
|
|
46
|
+
backgroundColor = '#e6e6e6';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Text key={suggestion.value} color={color} backgroundColor={backgroundColor}>
|
|
51
|
+
{` ${truncate(suggestion.display, width).padEnd(width)} `}
|
|
52
|
+
</Text>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
const columns: (typeof suggestions)[] = [];
|
|
26
60
|
for (let index = 0; index < suggestions.length; index += rowsPerColumn) {
|
|
27
61
|
columns.push(suggestions.slice(index, index + rowsPerColumn));
|
|
28
62
|
}
|
|
29
63
|
|
|
30
64
|
return (
|
|
31
|
-
<Box position="absolute" top={0} left={0} width="100%" maxHeight={
|
|
65
|
+
<Box position="absolute" top={0} left={0} width="100%" maxHeight={MAX_HEIGHT} overflow="hidden" paddingX={1} paddingY={1} backgroundColor="#2a2a2a" flexDirection="row" columnGap={2}>
|
|
32
66
|
{columns.map((column, columnIndex) => (
|
|
33
67
|
<Box key={columnIndex} flexDirection="column">
|
|
34
68
|
{column.map((suggestion, rowIndex) => {
|
|
35
69
|
const index = columnIndex * rowsPerColumn + rowIndex;
|
|
36
70
|
const isSelected = index === effectiveSelectedIndex;
|
|
37
|
-
let display = suggestion.display;
|
|
38
|
-
if (display.length > 24) {
|
|
39
|
-
display = `${display.slice(0, 21)}...`;
|
|
40
|
-
}
|
|
41
71
|
let color = 'white';
|
|
42
72
|
let backgroundColor = '#2a2a2a';
|
|
43
73
|
if (isSelected) {
|
|
@@ -48,8 +78,7 @@ const Autocomplete: React.FC = () => {
|
|
|
48
78
|
return (
|
|
49
79
|
<Box key={suggestion.display} marginBottom={1}>
|
|
50
80
|
<Text color={color} backgroundColor={backgroundColor}>
|
|
51
|
-
{
|
|
52
|
-
{display}{' '}
|
|
81
|
+
{` ${suggestion.display} `}
|
|
53
82
|
</Text>
|
|
54
83
|
</Box>
|
|
55
84
|
);
|
|
@@ -43,6 +43,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
43
43
|
const cursorRef = useRef(cursorPosition);
|
|
44
44
|
const showAutocompleteRef = useRef(showAutocomplete);
|
|
45
45
|
const selectedIndexRef = useRef(selectedIndex);
|
|
46
|
+
const navigatedRef = useRef(false);
|
|
46
47
|
const historyRef = useRef<string[]>([]);
|
|
47
48
|
const historyIndexRef = useRef(-1);
|
|
48
49
|
const historyDraftRef = useRef('');
|
|
@@ -85,6 +86,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
85
86
|
cursorRef.current = 0;
|
|
86
87
|
showAutocompleteRef.current = false;
|
|
87
88
|
selectedIndexRef.current = 0;
|
|
89
|
+
navigatedRef.current = false;
|
|
88
90
|
historyIndexRef.current = -1;
|
|
89
91
|
historyDraftRef.current = '';
|
|
90
92
|
setInputState({ value: '', cursor: 0, showAutocomplete: false, selectedIndex: 0 });
|
|
@@ -141,6 +143,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
141
143
|
cursorRef.current = safeCursor;
|
|
142
144
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
143
145
|
selectedIndexRef.current = 0;
|
|
146
|
+
navigatedRef.current = false;
|
|
144
147
|
historyIndexRef.current = -1;
|
|
145
148
|
historyDraftRef.current = '';
|
|
146
149
|
|
|
@@ -235,6 +238,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
235
238
|
if (showAutocompleteRef.current) {
|
|
236
239
|
showAutocompleteRef.current = false;
|
|
237
240
|
selectedIndexRef.current = 0;
|
|
241
|
+
navigatedRef.current = false;
|
|
238
242
|
setInputState((prev) => ({ ...prev, showAutocomplete: false, selectedIndex: 0 }));
|
|
239
243
|
}
|
|
240
244
|
return;
|
|
@@ -247,7 +251,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
247
251
|
}
|
|
248
252
|
if (commandHandler && showAutocompleteRef.current) {
|
|
249
253
|
const autocomplete = getAutocomplete(inputRef.current, cursorRef.current);
|
|
250
|
-
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
254
|
+
if (autocomplete.visible && autocomplete.suggestions.length > 0 && (!autocomplete.completesArgument || navigatedRef.current)) {
|
|
251
255
|
const chosen = applyAutocomplete(selectedIndexRef.current);
|
|
252
256
|
if (chosen) {
|
|
253
257
|
handleSubmit(chosen);
|
|
@@ -381,6 +385,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
381
385
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
382
386
|
const nextIndex = selectedIndexRef.current > 0 ? selectedIndexRef.current - 1 : autocomplete.suggestions.length - 1;
|
|
383
387
|
selectedIndexRef.current = nextIndex;
|
|
388
|
+
navigatedRef.current = true;
|
|
384
389
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
385
390
|
return;
|
|
386
391
|
}
|
|
@@ -391,6 +396,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
391
396
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
392
397
|
const nextIndex = selectedIndexRef.current < autocomplete.suggestions.length - 1 ? selectedIndexRef.current + 1 : 0;
|
|
393
398
|
selectedIndexRef.current = nextIndex;
|
|
399
|
+
navigatedRef.current = true;
|
|
394
400
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
395
401
|
return;
|
|
396
402
|
}
|
|
@@ -449,6 +455,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
449
455
|
inputRef.current = nextValue;
|
|
450
456
|
cursorRef.current = nextValue.length;
|
|
451
457
|
selectedIndexRef.current = 0;
|
|
458
|
+
navigatedRef.current = false;
|
|
452
459
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
453
460
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
454
461
|
return;
|
|
@@ -470,6 +477,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
470
477
|
inputRef.current = nextValue;
|
|
471
478
|
cursorRef.current = nextValue.length;
|
|
472
479
|
selectedIndexRef.current = 0;
|
|
480
|
+
navigatedRef.current = false;
|
|
473
481
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
474
482
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
475
483
|
return;
|
|
@@ -481,6 +489,7 @@ const InputReadline: React.FC<InputReadlineProps> = React.memo(({ commandHandler
|
|
|
481
489
|
inputRef.current = nextValue;
|
|
482
490
|
cursorRef.current = nextValue.length;
|
|
483
491
|
selectedIndexRef.current = 0;
|
|
492
|
+
navigatedRef.current = false;
|
|
484
493
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
485
494
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
486
495
|
return;
|
package/src/config.ts
CHANGED
|
@@ -185,6 +185,7 @@ interface AIConfig {
|
|
|
185
185
|
vision?: boolean;
|
|
186
186
|
visionModel?: any;
|
|
187
187
|
agenticModel?: any;
|
|
188
|
+
decisionModel?: { provider: string; model: string; tool?: boolean; direct?: boolean };
|
|
188
189
|
retryAttempts?: number;
|
|
189
190
|
retryDelay?: number;
|
|
190
191
|
maxParallelRequests?: number;
|
|
@@ -277,6 +278,7 @@ export const EXPLORBOT_ENV_VARS: EnvVar[] = [
|
|
|
277
278
|
{ name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
|
|
278
279
|
{ name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
|
|
279
280
|
{ name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
|
|
281
|
+
{ name: 'EXPLORBOT_DECISION_MODEL', description: 'Turns on the decision model, as provider/model-id (openrouter/typesafe/jev-1.13 or typesafe/jev-latest); overrides ai.decisionModel' },
|
|
280
282
|
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
|
|
281
283
|
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
282
284
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
@@ -422,6 +424,7 @@ export class ConfigParser {
|
|
|
422
424
|
this.runtimeTarget = target;
|
|
423
425
|
this.configPath = sourcePath;
|
|
424
426
|
this.applyEnvSpec(this.config);
|
|
427
|
+
this.applyEnvDecisionModel(this.config);
|
|
425
428
|
|
|
426
429
|
// Restore original directory after successful config load
|
|
427
430
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
@@ -599,6 +602,14 @@ export class ConfigParser {
|
|
|
599
602
|
config.dirs.spec = spec;
|
|
600
603
|
}
|
|
601
604
|
|
|
605
|
+
private applyEnvDecisionModel(config: ExplorbotConfig): void {
|
|
606
|
+
const spec = process.env.EXPLORBOT_DECISION_MODEL;
|
|
607
|
+
if (!spec) return;
|
|
608
|
+
const separator = spec.indexOf('/');
|
|
609
|
+
if (separator < 1) throw new Error('EXPLORBOT_DECISION_MODEL must be written as "provider/model-id", e.g. openrouter/typesafe/jev-1.13');
|
|
610
|
+
config.ai.decisionModel = { ...config.ai.decisionModel, provider: spec.slice(0, separator), model: spec.slice(separator + 1) };
|
|
611
|
+
}
|
|
612
|
+
|
|
602
613
|
private async buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig> {
|
|
603
614
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
604
615
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
@@ -262,7 +262,7 @@ export class ExperienceTracker {
|
|
|
262
262
|
while (content.split('\n').length > maxLines) {
|
|
263
263
|
const sections = mdq(content).query('section2').each();
|
|
264
264
|
if (sections.length <= 1) break;
|
|
265
|
-
content = sections[sections.length - 1].replace('');
|
|
265
|
+
content = sections[sections.length - 1].replace('').toString();
|
|
266
266
|
}
|
|
267
267
|
return { ...experience, content };
|
|
268
268
|
});
|
|
@@ -286,7 +286,7 @@ export class ExperienceTracker {
|
|
|
286
286
|
combined = renderAsHowTo(combined);
|
|
287
287
|
|
|
288
288
|
if (options?.stripCode) {
|
|
289
|
-
combined = mdq(combined).query('code').replace('');
|
|
289
|
+
combined = mdq(combined).query('code').replace('').toString();
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
if (combined.trim()) results.push(combined.trim());
|
|
@@ -515,7 +515,8 @@ function renderAsHowTo(content: string): string {
|
|
|
515
515
|
if (text.startsWith('FLOW:')) return `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
|
|
516
516
|
if (text.startsWith('ACTION:')) return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
517
517
|
return heading.text();
|
|
518
|
-
})
|
|
518
|
+
})
|
|
519
|
+
.toString();
|
|
519
520
|
}
|
|
520
521
|
|
|
521
522
|
export interface ExperienceFile {
|
package/src/explorbot.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Driller } from './ai/driller.ts';
|
|
|
7
7
|
import { ExperienceCompactor } from './ai/experience-compactor.ts';
|
|
8
8
|
import { Fisherman } from './ai/fisherman.ts';
|
|
9
9
|
import { Historian } from './ai/historian.ts';
|
|
10
|
+
import { Judge } from './ai/judge.ts';
|
|
10
11
|
import { Navigator } from './ai/navigator.ts';
|
|
11
12
|
import { Pilot } from './ai/pilot.ts';
|
|
12
13
|
import { Planner } from './ai/planner.ts';
|
|
@@ -80,6 +81,7 @@ export class ExplorBot {
|
|
|
80
81
|
private _reporter?: Reporter;
|
|
81
82
|
private _requestStore?: RequestStore;
|
|
82
83
|
private _playwrightRecorder?: PlaywrightRecorder;
|
|
84
|
+
private judgeInstance: Judge | null | undefined;
|
|
83
85
|
|
|
84
86
|
constructor(options: ExplorBotOptions = {}) {
|
|
85
87
|
this.options = options;
|
|
@@ -192,6 +194,11 @@ export class ExplorBot {
|
|
|
192
194
|
return (this._playwrightRecorder ||= new PlaywrightRecorder());
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
judge(): Judge | null {
|
|
198
|
+
if (this.judgeInstance === undefined) this.judgeInstance = Judge.fromConfig(this.config.ai?.decisionModel);
|
|
199
|
+
return this.judgeInstance;
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
getConfig(): ExplorbotConfig {
|
|
196
203
|
return this.config;
|
|
197
204
|
}
|
|
@@ -213,6 +220,7 @@ export class ExplorBot {
|
|
|
213
220
|
knowledgeTracker: this.knowledgeTracker(),
|
|
214
221
|
requestStore: this.requestStore(),
|
|
215
222
|
playwrightRecorder: this.playwrightRecorder(),
|
|
223
|
+
judge: this.judge() || undefined,
|
|
216
224
|
});
|
|
217
225
|
|
|
218
226
|
const agentName = (agent as any).constructor.name.toLowerCase();
|
package/src/knowledge-tracker.ts
CHANGED
|
@@ -214,7 +214,7 @@ export class KnowledgeTracker {
|
|
|
214
214
|
|
|
215
215
|
return this.knowledgeFiles.map((knowledge) => {
|
|
216
216
|
const content = knowledge.content.trim();
|
|
217
|
-
const firstLine = mdq(content).
|
|
217
|
+
const firstLine = mdq(content).nodes()[0]?.text.split('\n')[0]?.trim() || '';
|
|
218
218
|
return {
|
|
219
219
|
url: knowledge.url || knowledge.endpoint || '',
|
|
220
220
|
firstLine,
|
package/src/state-manager.ts
CHANGED
|
@@ -86,6 +86,7 @@ export class StateManager {
|
|
|
86
86
|
private currentState: WebPageState | null = null;
|
|
87
87
|
private stateHistory: StateTransition[] = [];
|
|
88
88
|
private allVisitedUrls: Set<string> = new Set();
|
|
89
|
+
private experienceUrls?: string[];
|
|
89
90
|
private stateChangeListeners: StateChangeListener[] = [];
|
|
90
91
|
private experienceTracker!: ExperienceTracker;
|
|
91
92
|
private knowledgeTracker: KnowledgeTracker;
|
|
@@ -359,6 +360,21 @@ export class StateManager {
|
|
|
359
360
|
return this.allVisitedUrls;
|
|
360
361
|
}
|
|
361
362
|
|
|
363
|
+
getKnownUrls(): string[] {
|
|
364
|
+
this.experienceUrls ||= this.experienceTracker
|
|
365
|
+
.getAllExperience()
|
|
366
|
+
.sort((left, right) => right.mtime.getTime() - left.mtime.getTime())
|
|
367
|
+
.flatMap((experience) => experience.data.url || []);
|
|
368
|
+
const sessionUrls = this.stateHistory.map((transition) => transition.toState.url).reverse();
|
|
369
|
+
const urls = new Map<string, string>();
|
|
370
|
+
for (const url of [...sessionUrls, ...this.experienceUrls]) {
|
|
371
|
+
const key = normalizeUrl(url);
|
|
372
|
+
if (urls.has(key)) continue;
|
|
373
|
+
urls.set(key, url);
|
|
374
|
+
}
|
|
375
|
+
return [...urls.values()];
|
|
376
|
+
}
|
|
377
|
+
|
|
362
378
|
/**
|
|
363
379
|
* Get how many times we've visited a specific path
|
|
364
380
|
*/
|
package/src/test-plan.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import figures from 'figures';
|
|
5
5
|
import type { ActionResult } from './action-result.ts';
|
|
6
6
|
import { listSitePlanDirs } from './global-config.ts';
|
|
7
7
|
import { WebPageState } from './state-manager.ts';
|
|
8
|
-
import { tag } from './utils/logger.ts';
|
|
8
|
+
import { createDebug, pluralize, tag } from './utils/logger.ts';
|
|
9
|
+
import { truncateMiddle } from './utils/strings.ts';
|
|
9
10
|
import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from './utils/test-plan-markdown.ts';
|
|
10
11
|
import { uniqSessionName } from './utils/unique-names.ts';
|
|
12
|
+
import { extractStatePath } from './utils/url-matcher.ts';
|
|
11
13
|
|
|
12
14
|
export const TestResult = {
|
|
13
15
|
PASSED: 'passed',
|
|
@@ -241,6 +243,7 @@ export class Test extends Task {
|
|
|
241
243
|
endTime?: number;
|
|
242
244
|
resetCount = 0;
|
|
243
245
|
appliedExperience: AppliedExperience[] = [];
|
|
246
|
+
preparedData: string[] = [];
|
|
244
247
|
|
|
245
248
|
constructor(scenario: string, priority: 'critical' | 'important' | 'high' | 'normal' | 'low', expectedOutcome: string | string[], startUrl: string, plannedSteps: string[] = []) {
|
|
246
249
|
super(scenario, startUrl);
|
|
@@ -477,6 +480,39 @@ export class Plan {
|
|
|
477
480
|
|
|
478
481
|
updateStatus(): void {}
|
|
479
482
|
|
|
483
|
+
static listFiles(plansDir: string): PlanFile[] {
|
|
484
|
+
if (!existsSync(plansDir)) return [];
|
|
485
|
+
|
|
486
|
+
const files = readdirSync(plansDir)
|
|
487
|
+
.filter((file) => file.endsWith('.md'))
|
|
488
|
+
.map((file) => {
|
|
489
|
+
const filePath = path.join(plansDir, file);
|
|
490
|
+
const modifiedAt = statSync(filePath).mtimeMs;
|
|
491
|
+
const plan = readPlanFile(filePath, modifiedAt);
|
|
492
|
+
return {
|
|
493
|
+
name: file,
|
|
494
|
+
path: filePath,
|
|
495
|
+
modifiedAt,
|
|
496
|
+
title: plan.title,
|
|
497
|
+
url: extractStatePath(plan.startUrl || ''),
|
|
498
|
+
testCount: plan.tests.length,
|
|
499
|
+
label: '',
|
|
500
|
+
};
|
|
501
|
+
})
|
|
502
|
+
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
503
|
+
|
|
504
|
+
if (files.length === 0) return files;
|
|
505
|
+
|
|
506
|
+
const nameWidth = Math.min(MAX_PLAN_NAME_WIDTH, Math.max(...files.map((file) => file.name.length)));
|
|
507
|
+
const urlWidth = Math.min(MAX_PLAN_URL_WIDTH, Math.max(...files.map((file) => file.url.length)));
|
|
508
|
+
for (const file of files) {
|
|
509
|
+
const name = truncateMiddle(file.name, nameWidth).padEnd(nameWidth);
|
|
510
|
+
const url = truncateMiddle(file.url, urlWidth).padEnd(urlWidth);
|
|
511
|
+
file.label = `${name} ${url} ${file.testCount} ${pluralize(file.testCount, 'test')}`;
|
|
512
|
+
}
|
|
513
|
+
return files;
|
|
514
|
+
}
|
|
515
|
+
|
|
480
516
|
static resolveFile(file: string, plansDir?: string): string | null {
|
|
481
517
|
const names = [file];
|
|
482
518
|
if (!file.endsWith('.md')) names.push(`${file}.md`);
|
|
@@ -579,3 +615,32 @@ interface AppliedExperience {
|
|
|
579
615
|
url: string;
|
|
580
616
|
content: string;
|
|
581
617
|
}
|
|
618
|
+
|
|
619
|
+
export interface PlanFile {
|
|
620
|
+
name: string;
|
|
621
|
+
path: string;
|
|
622
|
+
modifiedAt: number;
|
|
623
|
+
title: string;
|
|
624
|
+
url: string;
|
|
625
|
+
testCount: number;
|
|
626
|
+
label: string;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const debugLog = createDebug('explorbot:test-plan');
|
|
630
|
+
const MAX_PLAN_NAME_WIDTH = 38;
|
|
631
|
+
const MAX_PLAN_URL_WIDTH = 26;
|
|
632
|
+
const planFileCache = new Map<string, { modifiedAt: number; plan: Plan }>();
|
|
633
|
+
|
|
634
|
+
function readPlanFile(filePath: string, modifiedAt: number): Plan {
|
|
635
|
+
const cached = planFileCache.get(filePath);
|
|
636
|
+
if (cached?.modifiedAt === modifiedAt) return cached.plan;
|
|
637
|
+
|
|
638
|
+
let plan = new Plan(path.basename(filePath));
|
|
639
|
+
try {
|
|
640
|
+
plan = parsePlanFromMarkdown(filePath);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
debugLog(`Failed to read plan file ${filePath}:`, error);
|
|
643
|
+
}
|
|
644
|
+
planFileCache.set(filePath, { modifiedAt, plan });
|
|
645
|
+
return plan;
|
|
646
|
+
}
|
package/src/utils/aria-ref.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { WebElement } from './web-element.ts';
|
|
2
2
|
|
|
3
|
-
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=(e\d+)\]/;
|
|
3
|
+
const REF_LINE_PATTERN = /^(\s*)-\s+(\w+)\s*(?:"([^"]*)")?.*?\[ref=((?:f\d+)?e\d+)\]/;
|
|
4
4
|
const ARIA_REF_PATTERN = /^(f\d+)?e\d+$/i;
|
|
5
5
|
|
|
6
6
|
const REF_ROLES: Record<string, string> = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' };
|