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';
|
|
@@ -65,11 +66,16 @@ export class FreesailCommand extends BaseCommand {
|
|
|
65
66
|
const suggestion = await navigator.freeSail({ strategy, scope, visitedUrls });
|
|
66
67
|
if (!suggestion) {
|
|
67
68
|
tag('info').log('No navigation suggestion available');
|
|
68
|
-
|
|
69
|
+
ctx.stop();
|
|
69
70
|
}
|
|
70
71
|
if (scope && !suggestion.target.startsWith(scope)) {
|
|
71
72
|
tag('warning').log(`Suggestion ${suggestion.target} is outside scope ${scope}, skipping`);
|
|
72
|
-
|
|
73
|
+
ctx.stop();
|
|
74
|
+
}
|
|
75
|
+
const currentUrl = stateManager.getCurrentState()?.url;
|
|
76
|
+
if (currentUrl && normalizeUrl(suggestion.target) === normalizeUrl(currentUrl)) {
|
|
77
|
+
tag('info').log(`No new navigation target available after ${currentUrl}; stopping exploration`);
|
|
78
|
+
ctx.stop();
|
|
73
79
|
}
|
|
74
80
|
tag('info').log(`Navigating to: ${suggestion.target} - ${suggestion.reason}`);
|
|
75
81
|
await this.explorBot.openTab();
|
|
@@ -145,7 +145,7 @@ export function runInitCommand(options) {
|
|
|
145
145
|
log('2. Set AI models config file');
|
|
146
146
|
log('3. Set web application URL in the config file');
|
|
147
147
|
log('4. Add initial knowledge (how to authorize to the application, etc.)');
|
|
148
|
-
tag('substep').log(chalk.yellow(`${getCliName()} learn * 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
148
|
+
tag('substep').log(chalk.yellow(`${getCliName()} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
149
149
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
150
150
|
log('5. Launch application on a relative URL');
|
|
151
151
|
tag('substep').log(chalk.yellow(`${getCliName()} start /dashboard`));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
1
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
2
2
|
export declare class NavigateCommand extends BaseCommand {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
5
|
suggestions: Suggestion[];
|
|
6
6
|
execute(args: string): Promise<void>;
|
|
7
|
+
completeArguments(): ArgumentCompletion[];
|
|
7
8
|
}
|
|
@@ -15,4 +15,10 @@ export class NavigateCommand extends BaseCommand {
|
|
|
15
15
|
await this.explorBot.visit(destination);
|
|
16
16
|
tag('success').log(`Navigation requested: ${destination}`);
|
|
17
17
|
}
|
|
18
|
+
completeArguments() {
|
|
19
|
+
return this.explorBot
|
|
20
|
+
.stateManager()
|
|
21
|
+
.getKnownUrls()
|
|
22
|
+
.map((url) => ({ value: url }));
|
|
23
|
+
}
|
|
18
24
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseOption } from './base-option.js';
|
|
2
|
+
export class DecisionModelOption extends BaseOption {
|
|
3
|
+
flags = '--decision-model <model>';
|
|
4
|
+
description = 'Turn on the decision model for this run, as provider/model-id (e.g. openrouter/typesafe/jev-1.13)';
|
|
5
|
+
apply(options) {
|
|
6
|
+
if (!options.decisionModel)
|
|
7
|
+
return;
|
|
8
|
+
process.env.EXPLORBOT_DECISION_MODEL = options.decisionModel;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
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
|
export { BaseOption } from './base-option.js';
|
|
5
|
+
export declare const decisionModelOption: DecisionModelOption;
|
|
4
6
|
export declare const knowledgeOption: KnowledgeOption;
|
|
5
7
|
export declare const wsOption: WsOption;
|
|
@@ -1,5 +1,7 @@
|
|
|
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
|
export { BaseOption } from './base-option.js';
|
|
5
|
+
export const decisionModelOption = new DecisionModelOption();
|
|
4
6
|
export const knowledgeOption = new KnowledgeOption();
|
|
5
7
|
export const wsOption = new WsOption();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
1
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
2
2
|
export declare class PlanLoadCommand extends BaseCommand {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
5
|
suggestions: Suggestion[];
|
|
6
6
|
execute(args: string): Promise<void>;
|
|
7
|
+
completeArguments(): ArgumentCompletion[];
|
|
7
8
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Plan } from '../test-plan.js';
|
|
1
2
|
import { tag } from '../utils/logger.js';
|
|
2
3
|
import { BaseCommand } from './base-command.js';
|
|
3
4
|
export class PlanLoadCommand extends BaseCommand {
|
|
@@ -15,4 +16,7 @@ export class PlanLoadCommand extends BaseCommand {
|
|
|
15
16
|
const plan = this.explorBot.loadPlan(filename);
|
|
16
17
|
tag('success').log(`Plan loaded: ${plan.title} with ${plan.tests.length} tests`);
|
|
17
18
|
}
|
|
19
|
+
completeArguments() {
|
|
20
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
21
|
+
}
|
|
18
22
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Plan } from '../test-plan.js';
|
|
2
|
-
import { BaseCommand } from './base-command.js';
|
|
1
|
+
import { Plan, type PlanFile } from '../test-plan.js';
|
|
2
|
+
import { type ArgumentCompletion, BaseCommand } from './base-command.js';
|
|
3
3
|
export declare class PlansCommand extends BaseCommand {
|
|
4
4
|
name: string;
|
|
5
5
|
description: string;
|
|
@@ -8,7 +8,7 @@ export declare class PlansCommand extends BaseCommand {
|
|
|
8
8
|
description: string;
|
|
9
9
|
}[];
|
|
10
10
|
execute(args: string): Promise<void>;
|
|
11
|
-
|
|
11
|
+
completeArguments(): ArgumentCompletion[];
|
|
12
12
|
printPlans(files: PlanFile[]): void;
|
|
13
13
|
printPlanDetails(plan: Plan, file: PlanFile): void;
|
|
14
14
|
resolvePlanFile(target: string, files: PlanFile[]): {
|
|
@@ -16,9 +16,3 @@ export declare class PlansCommand extends BaseCommand {
|
|
|
16
16
|
file: PlanFile;
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
interface PlanFile {
|
|
20
|
-
name: string;
|
|
21
|
-
path: string;
|
|
22
|
-
modifiedAt: number;
|
|
23
|
-
}
|
|
24
|
-
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { Plan } from '../test-plan.js';
|
|
4
4
|
import { getCliName } from '../utils/cli-name.js';
|
|
@@ -11,7 +11,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
11
11
|
options = [{ flags: '--from-plan <file>', description: 'Plan file to show' }];
|
|
12
12
|
async execute(args) {
|
|
13
13
|
const { opts, args: remaining } = this.parseArgs(args);
|
|
14
|
-
const files = this.
|
|
14
|
+
const files = Plan.listFiles(this.explorBot.getPlansDir());
|
|
15
15
|
const target = String(opts.fromPlan || remaining[0] || '').trim();
|
|
16
16
|
if (!target) {
|
|
17
17
|
this.printPlans(files);
|
|
@@ -20,22 +20,8 @@ export class PlansCommand extends BaseCommand {
|
|
|
20
20
|
const { plan, file } = this.resolvePlanFile(target, files);
|
|
21
21
|
this.printPlanDetails(plan, file);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!existsSync(plansDir))
|
|
26
|
-
return [];
|
|
27
|
-
return readdirSync(plansDir)
|
|
28
|
-
.filter((file) => file.endsWith('.md'))
|
|
29
|
-
.map((file) => {
|
|
30
|
-
const filePath = path.join(plansDir, file);
|
|
31
|
-
const stat = statSync(filePath);
|
|
32
|
-
return {
|
|
33
|
-
name: file,
|
|
34
|
-
path: filePath,
|
|
35
|
-
modifiedAt: stat.mtimeMs,
|
|
36
|
-
};
|
|
37
|
-
})
|
|
38
|
-
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
23
|
+
completeArguments() {
|
|
24
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
39
25
|
}
|
|
40
26
|
printPlans(files) {
|
|
41
27
|
if (files.length === 0) {
|
|
@@ -45,8 +31,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
45
31
|
tag('info').log('Saved plans:');
|
|
46
32
|
for (let i = 0; i < files.length; i++) {
|
|
47
33
|
const file = files[i];
|
|
48
|
-
|
|
49
|
-
tag('info').log(`${i + 1}. ${plan.title} (${plan.tests.length} tests) - ${file.name}`);
|
|
34
|
+
tag('info').log(`${i + 1}. ${file.title} (${file.testCount} tests) - ${file.name}`);
|
|
50
35
|
}
|
|
51
36
|
tag('info').log('');
|
|
52
37
|
tag('info').log(`View plan tests: ${getCliName()} plans <number>`);
|
|
@@ -73,10 +58,15 @@ export class PlansCommand extends BaseCommand {
|
|
|
73
58
|
if (!plan?.filePath) {
|
|
74
59
|
throw new Error(`Plan file not found: ${target}`);
|
|
75
60
|
}
|
|
61
|
+
const name = path.basename(plan.filePath);
|
|
76
62
|
const file = {
|
|
77
|
-
name
|
|
63
|
+
name,
|
|
78
64
|
path: plan.filePath,
|
|
79
65
|
modifiedAt: statSync(plan.filePath).mtimeMs,
|
|
66
|
+
title: plan.title,
|
|
67
|
+
url: plan.startUrl || '',
|
|
68
|
+
testCount: plan.tests.length,
|
|
69
|
+
label: name,
|
|
80
70
|
};
|
|
81
71
|
return { plan, file };
|
|
82
72
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { BaseCommand } from './base-command.js';
|
|
1
|
+
import { type ArgumentCompletion, BaseCommand } from './base-command.js';
|
|
2
2
|
export declare class RerunCommand extends BaseCommand {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
5
5
|
tuiEnabled: boolean;
|
|
6
6
|
execute(args: string): Promise<void>;
|
|
7
|
+
completeArguments(): ArgumentCompletion[];
|
|
7
8
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
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.js";
|
|
4
4
|
import { tag } from "../utils/logger.js";
|
|
5
|
+
import { listTestFiles } from "../utils/test-files.js";
|
|
5
6
|
import { BaseCommand } from './base-command.js';
|
|
6
7
|
export class RerunCommand extends BaseCommand {
|
|
7
8
|
name = 'rerun';
|
|
@@ -26,6 +27,9 @@ export class RerunCommand extends BaseCommand {
|
|
|
26
27
|
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
27
28
|
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
28
29
|
}
|
|
30
|
+
completeArguments() {
|
|
31
|
+
return listTestFiles(ConfigParser.getInstance().getTestsDir()).map((file) => ({ value: basename(file) }));
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
function parseTestIndices(input) {
|
|
31
35
|
if (input === '*' || input === 'all')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
1
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
2
2
|
export declare class ResearchCommand extends BaseCommand {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
@@ -8,4 +8,5 @@ export declare class ResearchCommand extends BaseCommand {
|
|
|
8
8
|
description: string;
|
|
9
9
|
}[];
|
|
10
10
|
execute(args: string): Promise<void>;
|
|
11
|
+
completeArguments(): ArgumentCompletion[];
|
|
11
12
|
}
|
|
@@ -43,4 +43,10 @@ export class ResearchCommand extends BaseCommand {
|
|
|
43
43
|
this.suggestions = [{ command: 'research <page> --deep', hint: 'analyze page for all expandable elements and interactions' }];
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
completeArguments() {
|
|
47
|
+
return this.explorBot
|
|
48
|
+
.stateManager()
|
|
49
|
+
.getKnownUrls()
|
|
50
|
+
.map((url) => ({ value: url }));
|
|
51
|
+
}
|
|
46
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
1
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
2
2
|
export declare class TestCommand extends BaseCommand {
|
|
3
3
|
name: string;
|
|
4
4
|
description: string;
|
|
@@ -8,4 +8,5 @@ export declare class TestCommand extends BaseCommand {
|
|
|
8
8
|
}[];
|
|
9
9
|
suggestions: Suggestion[];
|
|
10
10
|
execute(args: string): Promise<void>;
|
|
11
|
+
completeArguments(): ArgumentCompletion[];
|
|
11
12
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
import { BaseCommand } from './base-command.js';
|
|
5
5
|
export class TestCommand extends BaseCommand {
|
|
@@ -74,6 +74,9 @@ export class TestCommand extends BaseCommand {
|
|
|
74
74
|
}
|
|
75
75
|
tag('success').log('Test execution finished');
|
|
76
76
|
}
|
|
77
|
+
completeArguments() {
|
|
78
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: `--from-plan ${file.name}`, display: file.label }));
|
|
79
|
+
}
|
|
77
80
|
}
|
|
78
81
|
function parseTestIndices(input, total) {
|
|
79
82
|
const indices = new Set();
|
|
@@ -1,8 +1,12 @@
|
|
|
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';
|
|
5
|
+
const MAX_HEIGHT = 7;
|
|
6
|
+
const COLUMN_WIDTH = 24;
|
|
4
7
|
const Autocomplete = () => {
|
|
5
8
|
const { suggestions, selectedIndex, visible, argumentHint } = useAutocompleteState();
|
|
9
|
+
const { stdout } = useStdout();
|
|
6
10
|
if (!visible) {
|
|
7
11
|
if (!argumentHint) {
|
|
8
12
|
return null;
|
|
@@ -13,19 +17,32 @@ const Autocomplete = () => {
|
|
|
13
17
|
React.createElement(Text, { dimColor: true }, argumentHint)));
|
|
14
18
|
}
|
|
15
19
|
const effectiveSelectedIndex = Math.min(selectedIndex, suggestions.length - 1);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
20
|
+
const rowsPerColumn = Math.min(Math.max(1, MAX_HEIGHT - 2), suggestions.length);
|
|
21
|
+
const widest = Math.max(...suggestions.map((suggestion) => suggestion.display.length));
|
|
22
|
+
if (widest > COLUMN_WIDTH) {
|
|
23
|
+
const width = Math.max(COLUMN_WIDTH, (stdout?.columns || 80) - 4);
|
|
24
|
+
let firstRow = 0;
|
|
25
|
+
if (effectiveSelectedIndex >= rowsPerColumn) {
|
|
26
|
+
firstRow = effectiveSelectedIndex - rowsPerColumn + 1;
|
|
27
|
+
}
|
|
28
|
+
return (React.createElement(Box, { position: "absolute", top: 0, left: 0, width: "100%", maxHeight: MAX_HEIGHT, overflow: "hidden", paddingX: 1, paddingY: 1, backgroundColor: "#2a2a2a", flexDirection: "column" }, suggestions.slice(firstRow, firstRow + rowsPerColumn).map((suggestion, rowIndex) => {
|
|
29
|
+
const isSelected = firstRow + rowIndex === effectiveSelectedIndex;
|
|
30
|
+
let color = 'white';
|
|
31
|
+
let backgroundColor = '#2a2a2a';
|
|
32
|
+
if (isSelected) {
|
|
33
|
+
color = 'black';
|
|
34
|
+
backgroundColor = '#e6e6e6';
|
|
35
|
+
}
|
|
36
|
+
return (React.createElement(Text, { key: suggestion.value, color: color, backgroundColor: backgroundColor }, ` ${truncate(suggestion.display, width).padEnd(width)} `));
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
18
39
|
const columns = [];
|
|
19
40
|
for (let index = 0; index < suggestions.length; index += rowsPerColumn) {
|
|
20
41
|
columns.push(suggestions.slice(index, index + rowsPerColumn));
|
|
21
42
|
}
|
|
22
|
-
return (React.createElement(Box, { position: "absolute", top: 0, left: 0, width: "100%", maxHeight:
|
|
43
|
+
return (React.createElement(Box, { position: "absolute", top: 0, left: 0, width: "100%", maxHeight: MAX_HEIGHT, overflow: "hidden", paddingX: 1, paddingY: 1, backgroundColor: "#2a2a2a", flexDirection: "row", columnGap: 2 }, columns.map((column, columnIndex) => (React.createElement(Box, { key: columnIndex, flexDirection: "column" }, column.map((suggestion, rowIndex) => {
|
|
23
44
|
const index = columnIndex * rowsPerColumn + rowIndex;
|
|
24
45
|
const isSelected = index === effectiveSelectedIndex;
|
|
25
|
-
let display = suggestion.display;
|
|
26
|
-
if (display.length > 24) {
|
|
27
|
-
display = `${display.slice(0, 21)}...`;
|
|
28
|
-
}
|
|
29
46
|
let color = 'white';
|
|
30
47
|
let backgroundColor = '#2a2a2a';
|
|
31
48
|
if (isSelected) {
|
|
@@ -33,10 +50,7 @@ const Autocomplete = () => {
|
|
|
33
50
|
backgroundColor = '#e6e6e6';
|
|
34
51
|
}
|
|
35
52
|
return (React.createElement(Box, { key: suggestion.display, marginBottom: 1 },
|
|
36
|
-
React.createElement(Text, { color: color, backgroundColor: backgroundColor },
|
|
37
|
-
' ',
|
|
38
|
-
display,
|
|
39
|
-
' ')));
|
|
53
|
+
React.createElement(Text, { color: color, backgroundColor: backgroundColor }, ` ${suggestion.display} `)));
|
|
40
54
|
}))))));
|
|
41
55
|
};
|
|
42
56
|
export default Autocomplete;
|
|
@@ -17,6 +17,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
17
17
|
const cursorRef = useRef(cursorPosition);
|
|
18
18
|
const showAutocompleteRef = useRef(showAutocomplete);
|
|
19
19
|
const selectedIndexRef = useRef(selectedIndex);
|
|
20
|
+
const navigatedRef = useRef(false);
|
|
20
21
|
const historyRef = useRef([]);
|
|
21
22
|
const historyIndexRef = useRef(-1);
|
|
22
23
|
const historyDraftRef = useRef('');
|
|
@@ -55,6 +56,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
55
56
|
cursorRef.current = 0;
|
|
56
57
|
showAutocompleteRef.current = false;
|
|
57
58
|
selectedIndexRef.current = 0;
|
|
59
|
+
navigatedRef.current = false;
|
|
58
60
|
historyIndexRef.current = -1;
|
|
59
61
|
historyDraftRef.current = '';
|
|
60
62
|
setInputState({ value: '', cursor: 0, showAutocomplete: false, selectedIndex: 0 });
|
|
@@ -101,6 +103,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
101
103
|
cursorRef.current = safeCursor;
|
|
102
104
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
103
105
|
selectedIndexRef.current = 0;
|
|
106
|
+
navigatedRef.current = false;
|
|
104
107
|
historyIndexRef.current = -1;
|
|
105
108
|
historyDraftRef.current = '';
|
|
106
109
|
if (isControlled) {
|
|
@@ -173,6 +176,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
173
176
|
if (showAutocompleteRef.current) {
|
|
174
177
|
showAutocompleteRef.current = false;
|
|
175
178
|
selectedIndexRef.current = 0;
|
|
179
|
+
navigatedRef.current = false;
|
|
176
180
|
setInputState((prev) => ({ ...prev, showAutocomplete: false, selectedIndex: 0 }));
|
|
177
181
|
}
|
|
178
182
|
return;
|
|
@@ -184,7 +188,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
184
188
|
}
|
|
185
189
|
if (commandHandler && showAutocompleteRef.current) {
|
|
186
190
|
const autocomplete = getAutocomplete(inputRef.current, cursorRef.current);
|
|
187
|
-
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
191
|
+
if (autocomplete.visible && autocomplete.suggestions.length > 0 && (!autocomplete.completesArgument || navigatedRef.current)) {
|
|
188
192
|
const chosen = applyAutocomplete(selectedIndexRef.current);
|
|
189
193
|
if (chosen) {
|
|
190
194
|
handleSubmit(chosen);
|
|
@@ -321,6 +325,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
321
325
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
322
326
|
const nextIndex = selectedIndexRef.current > 0 ? selectedIndexRef.current - 1 : autocomplete.suggestions.length - 1;
|
|
323
327
|
selectedIndexRef.current = nextIndex;
|
|
328
|
+
navigatedRef.current = true;
|
|
324
329
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
325
330
|
return;
|
|
326
331
|
}
|
|
@@ -330,6 +335,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
330
335
|
if (autocomplete.visible && autocomplete.suggestions.length > 0) {
|
|
331
336
|
const nextIndex = selectedIndexRef.current < autocomplete.suggestions.length - 1 ? selectedIndexRef.current + 1 : 0;
|
|
332
337
|
selectedIndexRef.current = nextIndex;
|
|
338
|
+
navigatedRef.current = true;
|
|
333
339
|
setInputState((prev) => ({ ...prev, selectedIndex: nextIndex }));
|
|
334
340
|
return;
|
|
335
341
|
}
|
|
@@ -388,6 +394,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
388
394
|
inputRef.current = nextValue;
|
|
389
395
|
cursorRef.current = nextValue.length;
|
|
390
396
|
selectedIndexRef.current = 0;
|
|
397
|
+
navigatedRef.current = false;
|
|
391
398
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
392
399
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
393
400
|
return;
|
|
@@ -410,6 +417,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
410
417
|
inputRef.current = nextValue;
|
|
411
418
|
cursorRef.current = nextValue.length;
|
|
412
419
|
selectedIndexRef.current = 0;
|
|
420
|
+
navigatedRef.current = false;
|
|
413
421
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
414
422
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
415
423
|
return;
|
|
@@ -421,6 +429,7 @@ const InputReadline = React.memo(({ commandHandler, exitOnEmptyInput = false, on
|
|
|
421
429
|
inputRef.current = nextValue;
|
|
422
430
|
cursorRef.current = nextValue.length;
|
|
423
431
|
selectedIndexRef.current = 0;
|
|
432
|
+
navigatedRef.current = false;
|
|
424
433
|
showAutocompleteRef.current = nextShowAutocomplete;
|
|
425
434
|
setInputState({ value: nextValue, cursor: nextValue.length, selectedIndex: 0, showAutocomplete: nextShowAutocomplete });
|
|
426
435
|
return;
|
package/dist/src/config.d.ts
CHANGED
|
@@ -157,6 +157,12 @@ interface AIConfig {
|
|
|
157
157
|
vision?: boolean;
|
|
158
158
|
visionModel?: any;
|
|
159
159
|
agenticModel?: any;
|
|
160
|
+
decisionModel?: {
|
|
161
|
+
provider: string;
|
|
162
|
+
model: string;
|
|
163
|
+
tool?: boolean;
|
|
164
|
+
direct?: boolean;
|
|
165
|
+
};
|
|
160
166
|
retryAttempts?: number;
|
|
161
167
|
retryDelay?: number;
|
|
162
168
|
maxParallelRequests?: number;
|
|
@@ -264,6 +270,7 @@ export declare class ConfigParser {
|
|
|
264
270
|
static cleanupAllTestDirectories(): void;
|
|
265
271
|
enterGlobalMode(config: ExplorbotConfig, target: string | null): Promise<ExplorbotConfig>;
|
|
266
272
|
applyEnvSpec(config: ExplorbotConfig): void;
|
|
273
|
+
applyEnvDecisionModel(config: ExplorbotConfig): void;
|
|
267
274
|
buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig>;
|
|
268
275
|
findConfigFile(): string | null;
|
|
269
276
|
loadConfigModule(configPath: string): Promise<any>;
|
package/dist/src/config.js
CHANGED
|
@@ -45,6 +45,7 @@ export const EXPLORBOT_ENV_VARS = [
|
|
|
45
45
|
{ name: 'EXPLORBOT_URL', required: true, description: 'Base URL to test; the API boat reads it as the base endpoint' },
|
|
46
46
|
{ name: 'EXPLORBOT_VISION_MODEL', description: 'Screenshot analysis; overrides the provider recommendation' },
|
|
47
47
|
{ name: 'EXPLORBOT_AGENTIC_MODEL', description: 'Captain and Pilot decisions; overrides the provider recommendation' },
|
|
48
|
+
{ 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' },
|
|
48
49
|
{ name: 'EXPLORBOT_OUTPUT', description: 'Output root for states, plans, research, and reports. Defaults to the site dir under ~/.explorbot/sites' },
|
|
49
50
|
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
50
51
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
@@ -138,6 +139,7 @@ export class ConfigParser {
|
|
|
138
139
|
this.runtimeTarget = target;
|
|
139
140
|
this.configPath = sourcePath;
|
|
140
141
|
this.applyEnvSpec(this.config);
|
|
142
|
+
this.applyEnvDecisionModel(this.config);
|
|
141
143
|
// Restore original directory after successful config load
|
|
142
144
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
143
145
|
process.chdir(originalCwd);
|
|
@@ -300,6 +302,15 @@ export class ConfigParser {
|
|
|
300
302
|
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
301
303
|
config.dirs.spec = spec;
|
|
302
304
|
}
|
|
305
|
+
applyEnvDecisionModel(config) {
|
|
306
|
+
const spec = process.env.EXPLORBOT_DECISION_MODEL;
|
|
307
|
+
if (!spec)
|
|
308
|
+
return;
|
|
309
|
+
const separator = spec.indexOf('/');
|
|
310
|
+
if (separator < 1)
|
|
311
|
+
throw new Error('EXPLORBOT_DECISION_MODEL must be written as "provider/model-id", e.g. openrouter/typesafe/jev-1.13');
|
|
312
|
+
config.ai.decisionModel = { ...config.ai.decisionModel, provider: spec.slice(0, separator), model: spec.slice(separator + 1) };
|
|
313
|
+
}
|
|
303
314
|
async buildEnvConfig(baseUrl, outputRoot) {
|
|
304
315
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
305
316
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
@@ -224,7 +224,7 @@ export class ExperienceTracker {
|
|
|
224
224
|
const sections = mdq(content).query('section2').each();
|
|
225
225
|
if (sections.length <= 1)
|
|
226
226
|
break;
|
|
227
|
-
content = sections[sections.length - 1].replace('');
|
|
227
|
+
content = sections[sections.length - 1].replace('').toString();
|
|
228
228
|
}
|
|
229
229
|
return { ...experience, content };
|
|
230
230
|
});
|
|
@@ -244,7 +244,7 @@ export class ExperienceTracker {
|
|
|
244
244
|
continue;
|
|
245
245
|
combined = renderAsHowTo(combined);
|
|
246
246
|
if (options?.stripCode) {
|
|
247
|
-
combined = mdq(combined).query('code').replace('');
|
|
247
|
+
combined = mdq(combined).query('code').replace('').toString();
|
|
248
248
|
}
|
|
249
249
|
if (combined.trim())
|
|
250
250
|
results.push(combined.trim());
|
|
@@ -459,5 +459,6 @@ function renderAsHowTo(content) {
|
|
|
459
459
|
if (text.startsWith('ACTION:'))
|
|
460
460
|
return `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
461
461
|
return heading.text();
|
|
462
|
-
})
|
|
462
|
+
})
|
|
463
|
+
.toString();
|
|
463
464
|
}
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Driller } from './ai/driller.js';
|
|
|
5
5
|
import { ExperienceCompactor } from './ai/experience-compactor.js';
|
|
6
6
|
import { Fisherman } from './ai/fisherman.js';
|
|
7
7
|
import { Historian } from './ai/historian.js';
|
|
8
|
+
import { Judge } from './ai/judge.js';
|
|
8
9
|
import { Navigator } from './ai/navigator.js';
|
|
9
10
|
import { Pilot } from './ai/pilot.js';
|
|
10
11
|
import { Planner } from './ai/planner.js';
|
|
@@ -64,6 +65,7 @@ export declare class ExplorBot {
|
|
|
64
65
|
_reporter?: Reporter;
|
|
65
66
|
_requestStore?: RequestStore;
|
|
66
67
|
_playwrightRecorder?: PlaywrightRecorder;
|
|
68
|
+
judgeInstance: Judge | null | undefined;
|
|
67
69
|
constructor(options?: ExplorBotOptions);
|
|
68
70
|
get isExploring(): boolean;
|
|
69
71
|
setUserResolve(fn: UserResolveFunction): void;
|
|
@@ -83,6 +85,7 @@ export declare class ExplorBot {
|
|
|
83
85
|
reporter(): Reporter;
|
|
84
86
|
requestStore(): RequestStore;
|
|
85
87
|
playwrightRecorder(): PlaywrightRecorder;
|
|
88
|
+
judge(): Judge | null;
|
|
86
89
|
getConfig(): ExplorbotConfig;
|
|
87
90
|
getOptions(): ExplorBotOptions;
|
|
88
91
|
getProvider(): AIProvider;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Driller } from "./ai/driller.js";
|
|
|
5
5
|
import { ExperienceCompactor } from "./ai/experience-compactor.js";
|
|
6
6
|
import { Fisherman } from "./ai/fisherman.js";
|
|
7
7
|
import { Historian } from "./ai/historian.js";
|
|
8
|
+
import { Judge } from "./ai/judge.js";
|
|
8
9
|
import { Navigator } from "./ai/navigator.js";
|
|
9
10
|
import { Pilot } from "./ai/pilot.js";
|
|
10
11
|
import { Planner } from "./ai/planner.js";
|
|
@@ -56,6 +57,7 @@ export class ExplorBot {
|
|
|
56
57
|
_reporter;
|
|
57
58
|
_requestStore;
|
|
58
59
|
_playwrightRecorder;
|
|
60
|
+
judgeInstance;
|
|
59
61
|
constructor(options = {}) {
|
|
60
62
|
this.options = options;
|
|
61
63
|
this.configParser = ConfigParser.getInstance();
|
|
@@ -152,6 +154,11 @@ export class ExplorBot {
|
|
|
152
154
|
playwrightRecorder() {
|
|
153
155
|
return (this._playwrightRecorder ||= new PlaywrightRecorder());
|
|
154
156
|
}
|
|
157
|
+
judge() {
|
|
158
|
+
if (this.judgeInstance === undefined)
|
|
159
|
+
this.judgeInstance = Judge.fromConfig(this.config.ai?.decisionModel);
|
|
160
|
+
return this.judgeInstance;
|
|
161
|
+
}
|
|
155
162
|
getConfig() {
|
|
156
163
|
return this.config;
|
|
157
164
|
}
|
|
@@ -170,6 +177,7 @@ export class ExplorBot {
|
|
|
170
177
|
knowledgeTracker: this.knowledgeTracker(),
|
|
171
178
|
requestStore: this.requestStore(),
|
|
172
179
|
playwrightRecorder: this.playwrightRecorder(),
|
|
180
|
+
judge: this.judge() || undefined,
|
|
173
181
|
});
|
|
174
182
|
const agentName = agent.constructor.name.toLowerCase();
|
|
175
183
|
tag('debug').log(`Created ${agentName} agent`);
|
|
@@ -171,7 +171,7 @@ export class KnowledgeTracker {
|
|
|
171
171
|
this.loadKnowledgeFiles();
|
|
172
172
|
return this.knowledgeFiles.map((knowledge) => {
|
|
173
173
|
const content = knowledge.content.trim();
|
|
174
|
-
const firstLine = mdq(content).
|
|
174
|
+
const firstLine = mdq(content).nodes()[0]?.text.split('\n')[0]?.trim() || '';
|
|
175
175
|
return {
|
|
176
176
|
url: knowledge.url || knowledge.endpoint || '',
|
|
177
177
|
firstLine,
|
|
@@ -72,6 +72,7 @@ export declare class StateManager {
|
|
|
72
72
|
currentState: WebPageState | null;
|
|
73
73
|
stateHistory: StateTransition[];
|
|
74
74
|
allVisitedUrls: Set<string>;
|
|
75
|
+
experienceUrls?: string[];
|
|
75
76
|
stateChangeListeners: StateChangeListener[];
|
|
76
77
|
experienceTracker: ExperienceTracker;
|
|
77
78
|
knowledgeTracker: KnowledgeTracker;
|
|
@@ -137,6 +138,7 @@ export declare class StateManager {
|
|
|
137
138
|
*/
|
|
138
139
|
hasVisitedState(path: string): boolean;
|
|
139
140
|
getAllVisitedUrls(): Set<string>;
|
|
141
|
+
getKnownUrls(): string[];
|
|
140
142
|
/**
|
|
141
143
|
* Get how many times we've visited a specific path
|
|
142
144
|
*/
|
|
@@ -9,6 +9,7 @@ export class StateManager {
|
|
|
9
9
|
currentState = null;
|
|
10
10
|
stateHistory = [];
|
|
11
11
|
allVisitedUrls = new Set();
|
|
12
|
+
experienceUrls;
|
|
12
13
|
stateChangeListeners = [];
|
|
13
14
|
experienceTracker;
|
|
14
15
|
knowledgeTracker;
|
|
@@ -243,6 +244,21 @@ export class StateManager {
|
|
|
243
244
|
getAllVisitedUrls() {
|
|
244
245
|
return this.allVisitedUrls;
|
|
245
246
|
}
|
|
247
|
+
getKnownUrls() {
|
|
248
|
+
this.experienceUrls ||= this.experienceTracker
|
|
249
|
+
.getAllExperience()
|
|
250
|
+
.sort((left, right) => right.mtime.getTime() - left.mtime.getTime())
|
|
251
|
+
.flatMap((experience) => experience.data.url || []);
|
|
252
|
+
const sessionUrls = this.stateHistory.map((transition) => transition.toState.url).reverse();
|
|
253
|
+
const urls = new Map();
|
|
254
|
+
for (const url of [...sessionUrls, ...this.experienceUrls]) {
|
|
255
|
+
const key = normalizeUrl(url);
|
|
256
|
+
if (urls.has(key))
|
|
257
|
+
continue;
|
|
258
|
+
urls.set(key, url);
|
|
259
|
+
}
|
|
260
|
+
return [...urls.values()];
|
|
261
|
+
}
|
|
246
262
|
/**
|
|
247
263
|
* Get how many times we've visited a specific path
|
|
248
264
|
*/
|