explorbot 0.3.5 → 0.4.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/bin/explorbot-cli.ts +23 -16
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +12 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/cli.ts +6 -1
- package/boat/doc-collector/src/docbot.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +23 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +24 -16
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +12 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/cli.js +6 -1
- package/dist/boat/doc-collector/src/docbot.js +4 -2
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +22 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +5 -3
- package/dist/src/ai/rules.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +1 -1
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +39 -3
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +24 -37
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/reference/commands.md +32 -5
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +12 -2
- package/docs/workflow/ci.md +1 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +2 -2
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/captain.ts +1 -1
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +1 -1
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +37 -3
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +21 -37
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { remote } from '../../remote.js';
|
|
2
|
+
import { BaseOption } from './base-option.js';
|
|
3
|
+
export class WsOption extends BaseOption {
|
|
4
|
+
flags = '--ws <url>';
|
|
5
|
+
description = 'Stream this run to a remote UI over WebSocket';
|
|
6
|
+
apply(options, command) {
|
|
7
|
+
const url = options.ws || process.env.EXPLORBOT_WS_URL;
|
|
8
|
+
if (!url)
|
|
9
|
+
return;
|
|
10
|
+
remote.attach(String(url), commandPath(command));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function commandPath(command) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
let node = command;
|
|
16
|
+
while (node) {
|
|
17
|
+
parts.unshift(node.name());
|
|
18
|
+
node = node.parent;
|
|
19
|
+
}
|
|
20
|
+
return parts.slice(1).join(' ') || parts.join(' ');
|
|
21
|
+
}
|
|
@@ -70,14 +70,14 @@ export class PlansCommand extends BaseCommand {
|
|
|
70
70
|
throw new Error(`Plan #${target} not found. Available: 1-${files.length}`);
|
|
71
71
|
return file;
|
|
72
72
|
}
|
|
73
|
-
const
|
|
74
|
-
if (!
|
|
75
|
-
throw new Error(`Plan file not found: ${
|
|
73
|
+
const plan = Plan.loadFromFile(target, this.explorBot.getPlansDir());
|
|
74
|
+
if (!plan?.filePath) {
|
|
75
|
+
throw new Error(`Plan file not found: ${target}`);
|
|
76
76
|
}
|
|
77
77
|
return {
|
|
78
|
-
name: path.basename(
|
|
79
|
-
path:
|
|
80
|
-
modifiedAt: statSync(
|
|
78
|
+
name: path.basename(plan.filePath),
|
|
79
|
+
path: plan.filePath,
|
|
80
|
+
modifiedAt: statSync(plan.filePath).mtimeMs,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -255,6 +255,7 @@ export declare class ConfigParser {
|
|
|
255
255
|
static getTestDirectories(): string[];
|
|
256
256
|
static cleanupAllTestDirectories(): void;
|
|
257
257
|
enterGlobalMode(config: ExplorbotConfig, target: string | null): void;
|
|
258
|
+
applyEnvSpec(config: ExplorbotConfig): void;
|
|
258
259
|
buildEnvConfig(baseUrl: string | undefined, outputRoot: string): Promise<ExplorbotConfig>;
|
|
259
260
|
findConfigFile(): string | null;
|
|
260
261
|
loadConfigModule(configPath: string): Promise<any>;
|
package/dist/src/config.js
CHANGED
|
@@ -46,8 +46,10 @@ export const EXPLORBOT_ENV_VARS = [
|
|
|
46
46
|
{ name: 'EXPLORBOT_EPHEMERAL', description: 'Keep no state between runs — output goes to a fresh temp directory instead of the site dir' },
|
|
47
47
|
{ name: 'EXPLORBOT_KNOWLEDGE', description: 'Inline knowledge text, applied to every page' },
|
|
48
48
|
{ name: 'EXPLORBOT_KNOWLEDGE_FILE', description: 'Path to a knowledge markdown file' },
|
|
49
|
+
{ name: 'EXPLORBOT_SPEC', description: 'Docbot application spec directory or index.md, used as page knowledge' },
|
|
49
50
|
{ name: 'EXPLORBOT_API_SPEC', description: 'OpenAPI spec path for the API boat' },
|
|
50
51
|
{ name: 'EXPLORBOT_NO_BANNER', description: 'Suppress the startup banner, for machine-readable output' },
|
|
52
|
+
{ name: 'EXPLORBOT_MAX_DURATION', description: 'Wall-clock budget in minutes for an explore run; same as --max-duration' },
|
|
51
53
|
];
|
|
52
54
|
export class ConfigParser {
|
|
53
55
|
static instance;
|
|
@@ -128,6 +130,7 @@ export class ConfigParser {
|
|
|
128
130
|
if (resolvedPath && isGlobalConfigPath(resolvedPath)) {
|
|
129
131
|
this.enterGlobalMode(this.config, target);
|
|
130
132
|
}
|
|
133
|
+
this.applyEnvSpec(this.config);
|
|
131
134
|
// Restore original directory after successful config load
|
|
132
135
|
if (options?.path && originalCwd !== process.cwd()) {
|
|
133
136
|
process.chdir(originalCwd);
|
|
@@ -268,8 +271,17 @@ export class ConfigParser {
|
|
|
268
271
|
this.siteStartPath = site.path;
|
|
269
272
|
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
270
273
|
config.playwright = { ...config.playwright, browser: config.playwright?.browser || 'chromium', url: site.baseUrl };
|
|
274
|
+
materializeKnowledge(this.site.dir);
|
|
271
275
|
log(`Global mode: ${site.baseUrl} stored in ${this.site.dir}`);
|
|
272
276
|
}
|
|
277
|
+
applyEnvSpec(config) {
|
|
278
|
+
const spec = process.env.EXPLORBOT_SPEC;
|
|
279
|
+
if (!spec)
|
|
280
|
+
return;
|
|
281
|
+
if (!config.dirs)
|
|
282
|
+
config.dirs = { knowledge: 'knowledge', experience: 'experience', output: 'output' };
|
|
283
|
+
config.dirs.spec = spec;
|
|
284
|
+
}
|
|
273
285
|
async buildEnvConfig(baseUrl, outputRoot) {
|
|
274
286
|
const provider = process.env.EXPLORBOT_AI_PROVIDER;
|
|
275
287
|
const modelSpec = process.env.EXPLORBOT_AI_MODEL;
|
|
@@ -105,6 +105,11 @@ export class ExperienceTracker {
|
|
|
105
105
|
url: state.url ? extractStatePath(state.url) : '',
|
|
106
106
|
title: state.title,
|
|
107
107
|
};
|
|
108
|
+
if (state.overlay.present && state.overlay.name) {
|
|
109
|
+
frontmatter.region = state.overlay.name;
|
|
110
|
+
if (state.overlay.root)
|
|
111
|
+
frontmatter.root = state.overlay.root;
|
|
112
|
+
}
|
|
108
113
|
this.writeExperienceFile(stateHash, '', frontmatter);
|
|
109
114
|
}
|
|
110
115
|
return filePath;
|
package/dist/src/explorbot.d.ts
CHANGED
|
@@ -114,7 +114,6 @@ export declare class ExplorBot {
|
|
|
114
114
|
savePlan(filename?: string): string | null;
|
|
115
115
|
savePlans(plans: Plan[], filename?: string): string | null;
|
|
116
116
|
generatePlanFilename(feature?: string): string;
|
|
117
|
-
resolvePlanPath(filename: string): string;
|
|
118
117
|
loadPlan(filename: string): Plan;
|
|
119
118
|
loadPlans(filename: string): Plan[];
|
|
120
119
|
setCurrentPlan(plan?: Plan): void;
|
package/dist/src/explorbot.js
CHANGED
|
@@ -132,7 +132,7 @@ export class ExplorBot {
|
|
|
132
132
|
return this.explorer;
|
|
133
133
|
}
|
|
134
134
|
knowledgeTracker() {
|
|
135
|
-
return (this._knowledgeTracker ||= new KnowledgeTracker(this.options.applicationSpec));
|
|
135
|
+
return (this._knowledgeTracker ||= new KnowledgeTracker({ applicationSpec: this.options.applicationSpec }));
|
|
136
136
|
}
|
|
137
137
|
experienceTracker() {
|
|
138
138
|
return (this._experienceTracker ||= new ExperienceTracker(this.knowledgeTracker()));
|
|
@@ -290,14 +290,22 @@ export class ExplorBot {
|
|
|
290
290
|
return null;
|
|
291
291
|
}
|
|
292
292
|
};
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
const browserHeaderProvider = async () => {
|
|
294
|
+
const session = await this.explorer
|
|
295
|
+
.withPage(async (page) => ({
|
|
296
|
+
cookies: await page.context().cookies(baseEndpoint),
|
|
297
|
+
csrf: await page.evaluate(() => document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '').catch(() => ''),
|
|
298
|
+
}))
|
|
299
|
+
.catch(() => ({ cookies: [], csrf: '' }));
|
|
300
|
+
const headers = {};
|
|
301
|
+
if (session.cookies.length)
|
|
302
|
+
headers.Cookie = session.cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
|
303
|
+
if (session.csrf)
|
|
304
|
+
headers['x-csrf-token'] = session.csrf;
|
|
305
|
+
return headers;
|
|
298
306
|
};
|
|
299
307
|
this.agents.fisherman = this.createAgent(({ ai }) => {
|
|
300
|
-
return new Fisherman(ai, apiClient, requestStore, specLoader, baseEndpoint,
|
|
308
|
+
return new Fisherman(ai, apiClient, requestStore, specLoader, baseEndpoint, browserHeaderProvider, configHeaders, hasApiConfig);
|
|
301
309
|
});
|
|
302
310
|
}
|
|
303
311
|
return this.agents.fisherman;
|
|
@@ -383,39 +391,18 @@ export class ExplorBot {
|
|
|
383
391
|
return urlPart.slice(0, 256 - suffix.length) + suffix;
|
|
384
392
|
return urlPart + featurePart.slice(0, maxFeatureLen) + suffix;
|
|
385
393
|
}
|
|
386
|
-
resolvePlanPath(filename) {
|
|
387
|
-
let planPath = filename;
|
|
388
|
-
if (path.isAbsolute(filename)) {
|
|
389
|
-
if (!existsSync(planPath) && !filename.endsWith('.md')) {
|
|
390
|
-
planPath = `${filename}.md`;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
else if (existsSync(filename) || existsSync(`${filename}.md`)) {
|
|
394
|
-
planPath = existsSync(filename) ? filename : `${filename}.md`;
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
const plansDir = this.getPlansDir();
|
|
398
|
-
planPath = path.join(plansDir, filename);
|
|
399
|
-
if (!existsSync(planPath) && !filename.endsWith('.md')) {
|
|
400
|
-
planPath = path.join(plansDir, `${filename}.md`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return planPath;
|
|
404
|
-
}
|
|
405
394
|
loadPlan(filename) {
|
|
406
|
-
const
|
|
407
|
-
if (!
|
|
408
|
-
throw new Error(`Plan file not found: ${
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return this.currentPlan;
|
|
395
|
+
const plan = Plan.loadFromFile(filename, this.getPlansDir());
|
|
396
|
+
if (!plan)
|
|
397
|
+
throw new Error(`Plan file not found: ${filename}`);
|
|
398
|
+
this.setCurrentPlan(plan);
|
|
399
|
+
return plan;
|
|
412
400
|
}
|
|
413
401
|
loadPlans(filename) {
|
|
414
|
-
const
|
|
415
|
-
if (!
|
|
416
|
-
throw new Error(`Plan file not found: ${
|
|
417
|
-
|
|
418
|
-
return parsePlansFromMarkdown(planPath);
|
|
402
|
+
const plan = Plan.loadFromFile(filename, this.getPlansDir());
|
|
403
|
+
if (!plan?.filePath)
|
|
404
|
+
throw new Error(`Plan file not found: ${filename}`);
|
|
405
|
+
return parsePlansFromMarkdown(plan.filePath);
|
|
419
406
|
}
|
|
420
407
|
setCurrentPlan(plan) {
|
|
421
408
|
this.currentPlan = plan;
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { ActionResult } from './action-result.js';
|
|
2
2
|
import { ApplicationSpec } from './application-spec.js';
|
|
3
|
-
export interface Knowledge {
|
|
4
|
-
filePath: string;
|
|
5
|
-
url: string;
|
|
6
|
-
content: string;
|
|
7
|
-
[key: string]: any;
|
|
8
|
-
}
|
|
9
3
|
export declare class KnowledgeTracker {
|
|
10
4
|
knowledgeDir: string;
|
|
11
5
|
knowledgeFiles: Knowledge[];
|
|
6
|
+
sessionKnowledge: Knowledge[];
|
|
12
7
|
isLoaded: boolean;
|
|
13
8
|
applicationSpec?: ApplicationSpec;
|
|
14
|
-
|
|
9
|
+
static appendSessionKnowledge(text: string): void;
|
|
10
|
+
static resetSessionKnowledge(): void;
|
|
11
|
+
constructor(options?: KnowledgeTrackerOptions);
|
|
15
12
|
loadKnowledgeFiles(): void;
|
|
16
13
|
getRelevantKnowledge(state: ActionResult): Knowledge[];
|
|
14
|
+
getEndpointKnowledge(endpoint: string): Knowledge[];
|
|
17
15
|
renderRelevantKnowledge(state: ActionResult): string;
|
|
16
|
+
renderEndpointKnowledge(endpoint: string): string;
|
|
18
17
|
renderRelevantContext(state: ActionResult): string;
|
|
19
18
|
renderApplicationSpec(state: ActionResult): string;
|
|
20
19
|
addKnowledge(urlPattern: string, description: string, opts?: {
|
|
@@ -36,4 +35,18 @@ export declare class KnowledgeTracker {
|
|
|
36
35
|
getMatchingKnowledge(url: string): Knowledge[];
|
|
37
36
|
normalizeUrl(url: string): string;
|
|
38
37
|
getStateParameters(state: ActionResult, keys: string[]): Record<string, any>;
|
|
38
|
+
allKnowledge(): Knowledge[];
|
|
39
|
+
toKnowledge(filePath: string, data: Record<string, any>, content: string): Knowledge;
|
|
40
|
+
renderKnowledge(knowledgeFiles: Knowledge[], scope: string): string;
|
|
41
|
+
}
|
|
42
|
+
export interface Knowledge {
|
|
43
|
+
filePath: string;
|
|
44
|
+
url?: string;
|
|
45
|
+
endpoint?: string;
|
|
46
|
+
content: string;
|
|
47
|
+
[key: string]: any;
|
|
48
|
+
}
|
|
49
|
+
export interface KnowledgeTrackerOptions {
|
|
50
|
+
applicationSpec?: string;
|
|
51
|
+
knowledgeDir?: string;
|
|
39
52
|
}
|
|
@@ -11,61 +11,67 @@ import { loadMarkdownFiles } from './utils/markdown-files.js';
|
|
|
11
11
|
import { mdq } from './utils/markdown-query.js';
|
|
12
12
|
import { isSecretName, registerSecret } from './utils/secrets.js';
|
|
13
13
|
import { slugify } from './utils/strings.js';
|
|
14
|
+
import { extractStatePath, matchesUrl } from './utils/url-matcher.js';
|
|
14
15
|
const debugLog = createDebug('explorbot:knowledge-tracker');
|
|
16
|
+
const sessionEntries = [];
|
|
15
17
|
export class KnowledgeTracker {
|
|
16
18
|
knowledgeDir;
|
|
17
19
|
knowledgeFiles = [];
|
|
20
|
+
sessionKnowledge = [];
|
|
18
21
|
isLoaded = false;
|
|
19
22
|
applicationSpec;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
static appendSessionKnowledge(text) {
|
|
24
|
+
sessionEntries.push(text);
|
|
25
|
+
}
|
|
26
|
+
static resetSessionKnowledge() {
|
|
27
|
+
sessionEntries.length = 0;
|
|
28
|
+
}
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
let knowledgeDir = options.knowledgeDir;
|
|
31
|
+
let specPath = options.applicationSpec;
|
|
32
|
+
if (!knowledgeDir) {
|
|
33
|
+
const configParser = ConfigParser.getInstance();
|
|
34
|
+
const config = configParser.getConfig();
|
|
35
|
+
knowledgeDir = configParser.resolveProjectDir(config.dirs?.knowledge || 'knowledge');
|
|
36
|
+
specPath ||= config.dirs?.spec;
|
|
37
|
+
}
|
|
38
|
+
this.knowledgeDir = knowledgeDir;
|
|
24
39
|
if (!existsSync(this.knowledgeDir)) {
|
|
25
40
|
mkdirSync(this.knowledgeDir, { recursive: true });
|
|
26
41
|
}
|
|
27
|
-
const specPath = applicationSpecPath || config.dirs?.spec;
|
|
28
42
|
if (specPath) {
|
|
29
43
|
this.applicationSpec = new ApplicationSpec(specPath);
|
|
30
44
|
tag('info').log(`Loaded application spec with ${this.applicationSpec.pageCount} documented pages`);
|
|
31
45
|
}
|
|
46
|
+
this.sessionKnowledge = sessionEntries.map((entry, index) => {
|
|
47
|
+
const parsed = matter(entry);
|
|
48
|
+
debugLog(`Session knowledge #${index + 1}`);
|
|
49
|
+
return this.toKnowledge(`--knowledge #${index + 1}`, parsed.data, parsed.content.trim());
|
|
50
|
+
});
|
|
32
51
|
}
|
|
33
52
|
loadKnowledgeFiles() {
|
|
34
53
|
if (this.isLoaded)
|
|
35
54
|
return;
|
|
36
55
|
this.knowledgeFiles = [];
|
|
37
56
|
for (const entry of loadMarkdownFiles(this.knowledgeDir, { recursive: true })) {
|
|
38
|
-
this.knowledgeFiles.push(
|
|
39
|
-
filePath: entry.filePath,
|
|
40
|
-
url: entry.data.url || entry.data.path || '*',
|
|
41
|
-
content: this.interpolateVars(entry.content),
|
|
42
|
-
...entry.data,
|
|
43
|
-
});
|
|
57
|
+
this.knowledgeFiles.push(this.toKnowledge(entry.filePath, entry.data, entry.content));
|
|
44
58
|
}
|
|
45
59
|
this.isLoaded = true;
|
|
46
60
|
}
|
|
47
61
|
getRelevantKnowledge(state) {
|
|
48
62
|
this.loadKnowledgeFiles();
|
|
49
|
-
return this.
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
return this.allKnowledge().filter((knowledge) => knowledge.url && state.isMatchedBy(knowledge));
|
|
64
|
+
}
|
|
65
|
+
getEndpointKnowledge(endpoint) {
|
|
66
|
+
this.loadKnowledgeFiles();
|
|
67
|
+
const path = extractStatePath(endpoint);
|
|
68
|
+
return this.allKnowledge().filter((knowledge) => knowledge.endpoint && matchesUrl(knowledge.endpoint, path));
|
|
52
69
|
}
|
|
53
70
|
renderRelevantKnowledge(state) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.map((k) => k.content)
|
|
59
|
-
.filter((k) => !!k)
|
|
60
|
-
.join('\n\n');
|
|
61
|
-
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
|
|
62
|
-
return dedent `
|
|
63
|
-
<knowledge>
|
|
64
|
-
Here is relevant knowledge for this page:
|
|
65
|
-
|
|
66
|
-
${knowledgeContent}
|
|
67
|
-
</knowledge>
|
|
68
|
-
`;
|
|
71
|
+
return this.renderKnowledge(this.getRelevantKnowledge(state), 'page');
|
|
72
|
+
}
|
|
73
|
+
renderEndpointKnowledge(endpoint) {
|
|
74
|
+
return this.renderKnowledge(this.getEndpointKnowledge(endpoint), 'endpoint');
|
|
69
75
|
}
|
|
70
76
|
renderRelevantContext(state) {
|
|
71
77
|
return [this.renderRelevantKnowledge(state), this.renderApplicationSpec(state)].filter(Boolean).join('\n\n');
|
|
@@ -151,7 +157,7 @@ export class KnowledgeTracker {
|
|
|
151
157
|
}
|
|
152
158
|
getExistingUrls() {
|
|
153
159
|
this.loadKnowledgeFiles();
|
|
154
|
-
return this.knowledgeFiles.map((knowledge) => knowledge.url).filter((url) => url && url !== '*');
|
|
160
|
+
return this.knowledgeFiles.map((knowledge) => knowledge.url || '').filter((url) => url && url !== '*');
|
|
155
161
|
}
|
|
156
162
|
getKnowledgeForUrl(urlPattern) {
|
|
157
163
|
this.loadKnowledgeFiles();
|
|
@@ -164,7 +170,7 @@ export class KnowledgeTracker {
|
|
|
164
170
|
const content = knowledge.content.trim();
|
|
165
171
|
const firstLine = mdq(content).meta()[0]?.text.split('\n')[0]?.trim() || '';
|
|
166
172
|
return {
|
|
167
|
-
url: knowledge.url,
|
|
173
|
+
url: knowledge.url || knowledge.endpoint || '',
|
|
168
174
|
firstLine,
|
|
169
175
|
filePath: knowledge.filePath,
|
|
170
176
|
};
|
|
@@ -194,4 +200,36 @@ export class KnowledgeTracker {
|
|
|
194
200
|
}
|
|
195
201
|
return result;
|
|
196
202
|
}
|
|
203
|
+
allKnowledge() {
|
|
204
|
+
return [...this.knowledgeFiles, ...this.sessionKnowledge];
|
|
205
|
+
}
|
|
206
|
+
toKnowledge(filePath, data, content) {
|
|
207
|
+
const knowledge = {
|
|
208
|
+
...data,
|
|
209
|
+
filePath,
|
|
210
|
+
url: data.url || data.path,
|
|
211
|
+
content: this.interpolateVars(content),
|
|
212
|
+
};
|
|
213
|
+
if (!data.url && !data.path && !data.endpoint) {
|
|
214
|
+
knowledge.url = '*';
|
|
215
|
+
knowledge.endpoint = '*';
|
|
216
|
+
}
|
|
217
|
+
return knowledge;
|
|
218
|
+
}
|
|
219
|
+
renderKnowledge(knowledgeFiles, scope) {
|
|
220
|
+
if (knowledgeFiles.length === 0)
|
|
221
|
+
return '';
|
|
222
|
+
const knowledgeContent = knowledgeFiles
|
|
223
|
+
.map((k) => k.content)
|
|
224
|
+
.filter((k) => !!k)
|
|
225
|
+
.join('\n\n');
|
|
226
|
+
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
|
|
227
|
+
return dedent `
|
|
228
|
+
<knowledge>
|
|
229
|
+
Here is relevant knowledge for this ${scope}:
|
|
230
|
+
|
|
231
|
+
${knowledgeContent}
|
|
232
|
+
</knowledge>
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
197
235
|
}
|
package/dist/src/remote.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Command } from 'commander';
|
|
2
1
|
import { type ActivityEntry } from './activity.js';
|
|
3
2
|
import { type LogDestination, type TaggedLogEntry } from './utils/logger.js';
|
|
4
3
|
/**
|
|
@@ -19,7 +18,6 @@ export declare class Remote implements LogDestination {
|
|
|
19
18
|
asks: Map<string, (value: string | null) => void>;
|
|
20
19
|
askCounter: number;
|
|
21
20
|
lastActivity: string | null;
|
|
22
|
-
registerOption(program: Command): void;
|
|
23
21
|
attach(url: string, command: string): void;
|
|
24
22
|
isAttached(): boolean;
|
|
25
23
|
send(type: string, data?: Record<string, unknown>): void;
|
|
@@ -42,7 +40,6 @@ export declare class Remote implements LogDestination {
|
|
|
42
40
|
* nothing — so skip repeats, the priming null included. */
|
|
43
41
|
reportActivity(activity: ActivityEntry | null): void;
|
|
44
42
|
errorOf(args: any[] | undefined): string | undefined;
|
|
45
|
-
commandPath(command: Command): string;
|
|
46
43
|
}
|
|
47
44
|
export declare const remote: Remote;
|
|
48
45
|
/** Whatever the run wants to say. Not a schema — the UI renders what it knows
|
package/dist/src/remote.js
CHANGED
|
@@ -26,15 +26,6 @@ export class Remote {
|
|
|
26
26
|
asks = new Map();
|
|
27
27
|
askCounter = 0;
|
|
28
28
|
lastActivity = null;
|
|
29
|
-
registerOption(program) {
|
|
30
|
-
program.option('--ws <url>', 'Stream this run to a remote UI over WebSocket');
|
|
31
|
-
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
32
|
-
const url = actionCommand.optsWithGlobals().ws || process.env.EXPLORBOT_WS_URL;
|
|
33
|
-
if (!url)
|
|
34
|
-
return;
|
|
35
|
-
this.attach(String(url), this.commandPath(actionCommand));
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
29
|
attach(url, command) {
|
|
39
30
|
if (this.url)
|
|
40
31
|
return;
|
|
@@ -222,14 +213,5 @@ export class Remote {
|
|
|
222
213
|
return failure.message;
|
|
223
214
|
return undefined;
|
|
224
215
|
}
|
|
225
|
-
commandPath(command) {
|
|
226
|
-
const parts = [];
|
|
227
|
-
let node = command;
|
|
228
|
-
while (node) {
|
|
229
|
-
parts.unshift(node.name());
|
|
230
|
-
node = node.parent;
|
|
231
|
-
}
|
|
232
|
-
return parts.slice(1).join(' ') || parts.join(' ');
|
|
233
|
-
}
|
|
234
216
|
}
|
|
235
217
|
export const remote = new Remote();
|
|
@@ -42,6 +42,10 @@ export interface WebPageState {
|
|
|
42
42
|
links?: Link[];
|
|
43
43
|
verifications?: Record<string, boolean>;
|
|
44
44
|
overlay?: Overlay;
|
|
45
|
+
/** Region name, the persisted scalar form of overlay.name used in experience frontmatter */
|
|
46
|
+
region?: string;
|
|
47
|
+
/** Region root selector, the persisted scalar form of overlay.root used in experience frontmatter */
|
|
48
|
+
root?: string;
|
|
45
49
|
}
|
|
46
50
|
export interface StateTransition {
|
|
47
51
|
/** Previous state (null if this is the first state) */
|
|
@@ -91,7 +95,7 @@ export declare class StateManager {
|
|
|
91
95
|
* Update state from basic data (for navigation events)
|
|
92
96
|
*/
|
|
93
97
|
updateStateFromBasic(url: string, title?: string, trigger?: 'manual' | 'navigation' | 'automatic'): WebPageState;
|
|
94
|
-
|
|
98
|
+
hasRegionAppeared(previousState: WebPageState | null, newState: WebPageState): boolean;
|
|
95
99
|
/**
|
|
96
100
|
* Generate a basic hash for state comparison
|
|
97
101
|
*/
|
|
@@ -41,7 +41,10 @@ export class StateManager {
|
|
|
41
41
|
*/
|
|
42
42
|
emitStateChange(event) {
|
|
43
43
|
const state = event.toState;
|
|
44
|
-
|
|
44
|
+
const payload = { url: state.fullUrl || state.url, path: state.url, title: state.title, h1: state.h1 };
|
|
45
|
+
if (state.overlay?.present)
|
|
46
|
+
payload.region = state.overlay.name || state.overlay.type;
|
|
47
|
+
tag('data').log('state', payload);
|
|
45
48
|
this.stateChangeListeners.forEach((listener) => {
|
|
46
49
|
try {
|
|
47
50
|
listener(event);
|
|
@@ -63,8 +66,8 @@ export class StateManager {
|
|
|
63
66
|
if (newState.url)
|
|
64
67
|
this.allVisitedUrls.add(normalizeUrl(newState.url));
|
|
65
68
|
const hashChanged = actionResult.hash !== previousHash;
|
|
66
|
-
const
|
|
67
|
-
if (hashChanged ||
|
|
69
|
+
const regionAppeared = !hashChanged && this.hasRegionAppeared(previousState, newState);
|
|
70
|
+
if (hashChanged || regionAppeared) {
|
|
68
71
|
const transition = {
|
|
69
72
|
fromState: previousState,
|
|
70
73
|
toState: newState,
|
|
@@ -74,8 +77,8 @@ export class StateManager {
|
|
|
74
77
|
};
|
|
75
78
|
this.stateHistory.push(transition);
|
|
76
79
|
this.emitStateChange(transition);
|
|
77
|
-
if (
|
|
78
|
-
debugLog('State change detected:
|
|
80
|
+
if (regionAppeared) {
|
|
81
|
+
debugLog('State change detected: region of interest appeared');
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
debugLog(`State updated: ${this.currentState.url} (${this.currentState.hash})`);
|
|
@@ -113,10 +116,10 @@ export class StateManager {
|
|
|
113
116
|
debugLog(`State updated from navigation: ${this.currentState.url} (${this.currentState.hash})`);
|
|
114
117
|
return newState;
|
|
115
118
|
}
|
|
116
|
-
|
|
119
|
+
hasRegionAppeared(previousState, newState) {
|
|
117
120
|
const prevFocus = previousState?.overlay ?? Overlay.fromAria(previousState?.ariaSnapshot ?? null);
|
|
118
121
|
const newFocus = newState.overlay ?? Overlay.fromAria(newState.ariaSnapshot ?? null);
|
|
119
|
-
return !prevFocus.
|
|
122
|
+
return !prevFocus.present && newFocus.present;
|
|
120
123
|
}
|
|
121
124
|
/**
|
|
122
125
|
* Generate a basic hash for state comparison
|
package/dist/src/test-plan.d.ts
CHANGED
|
@@ -129,6 +129,7 @@ export declare class Plan {
|
|
|
129
129
|
title: string;
|
|
130
130
|
tests: Test[];
|
|
131
131
|
url?: string;
|
|
132
|
+
filePath?: string;
|
|
132
133
|
iteration: number;
|
|
133
134
|
parentPlan?: Plan;
|
|
134
135
|
changeListeners: PlanChangeListener[];
|
|
@@ -141,10 +142,12 @@ export declare class Plan {
|
|
|
141
142
|
getAllTests(): Test[];
|
|
142
143
|
listTests(): Test[];
|
|
143
144
|
getPendingTests(): Test[];
|
|
145
|
+
get startUrl(): string | undefined;
|
|
144
146
|
get isComplete(): boolean;
|
|
145
147
|
get allSuccessful(): boolean;
|
|
146
148
|
get allFailed(): boolean;
|
|
147
149
|
updateStatus(): void;
|
|
150
|
+
static loadFromFile(file: string, plansDir?: string): Plan | null;
|
|
148
151
|
static fromMarkdown(filePath: string): Plan;
|
|
149
152
|
saveToMarkdown(filePath: string): void;
|
|
150
153
|
static saveMultipleToMarkdown(plans: Plan[], filePath: string): void;
|
package/dist/src/test-plan.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { listSites } from "./global-config.js";
|
|
2
5
|
import { tag } from "./utils/logger.js";
|
|
3
6
|
import { parsePlanFromMarkdown, planToAiContext, savePlanToMarkdown, savePlansToMarkdown } from "./utils/test-plan-markdown.js";
|
|
4
7
|
import { uniqSessionName } from "./utils/unique-names.js";
|
|
@@ -320,10 +323,12 @@ export class Test extends Task {
|
|
|
320
323
|
.sort((a, b) => a.timestamp - b.timestamp);
|
|
321
324
|
}
|
|
322
325
|
}
|
|
326
|
+
const SITE_PLANS_DIR = ['output', 'plans'];
|
|
323
327
|
export class Plan {
|
|
324
328
|
title;
|
|
325
329
|
tests = [];
|
|
326
330
|
url;
|
|
331
|
+
filePath;
|
|
327
332
|
iteration = 0;
|
|
328
333
|
parentPlan;
|
|
329
334
|
changeListeners = [];
|
|
@@ -380,6 +385,9 @@ export class Plan {
|
|
|
380
385
|
getPendingTests() {
|
|
381
386
|
return this.tests.filter((test) => test.status === 'pending' && test.enabled);
|
|
382
387
|
}
|
|
388
|
+
get startUrl() {
|
|
389
|
+
return this.url || this.tests[0]?.startUrl;
|
|
390
|
+
}
|
|
383
391
|
get isComplete() {
|
|
384
392
|
return this.tests.length > 0 && this.tests.every((test) => test.hasFinished);
|
|
385
393
|
}
|
|
@@ -390,6 +398,25 @@ export class Plan {
|
|
|
390
398
|
return this.tests.length > 0 && this.tests.every((test) => test.hasFailed);
|
|
391
399
|
}
|
|
392
400
|
updateStatus() { }
|
|
401
|
+
static loadFromFile(file, plansDir) {
|
|
402
|
+
const names = [file];
|
|
403
|
+
if (!file.endsWith('.md'))
|
|
404
|
+
names.push(`${file}.md`);
|
|
405
|
+
const dirs = [process.cwd()];
|
|
406
|
+
if (plansDir)
|
|
407
|
+
dirs.push(plansDir);
|
|
408
|
+
if (!plansDir)
|
|
409
|
+
dirs.push(...listSites().map((site) => path.join(site.dir, ...SITE_PLANS_DIR)));
|
|
410
|
+
for (const dir of dirs) {
|
|
411
|
+
const filePath = names.map((name) => path.resolve(dir, name)).find(existsSync);
|
|
412
|
+
if (!filePath)
|
|
413
|
+
continue;
|
|
414
|
+
const loaded = parsePlanFromMarkdown(filePath);
|
|
415
|
+
loaded.filePath = filePath;
|
|
416
|
+
return loaded;
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
393
420
|
static fromMarkdown(filePath) {
|
|
394
421
|
return parsePlanFromMarkdown(filePath);
|
|
395
422
|
}
|