explorbot 0.1.9 → 0.1.10
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 +70 -8
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +70 -7
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +1 -1
- package/dist/src/ai/bosun.js +5 -1
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian.js +13 -6
- package/dist/src/ai/navigator.js +62 -62
- package/dist/src/ai/pilot.js +22 -0
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +4 -4
- package/dist/src/ai/provider.js +1 -1
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +4 -11
- package/dist/src/ai/tools.js +5 -3
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +1 -1
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +1 -1
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +28 -5
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +2 -2
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +11 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +1 -1
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/experience-tracker.js +191 -56
- package/dist/src/explorbot.js +26 -14
- package/dist/src/explorer.js +3 -3
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/utils/error-page.js +10 -0
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +1 -1
- package/src/ai/bosun.ts +5 -1
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian.ts +12 -7
- package/src/ai/navigator.ts +68 -66
- package/src/ai/pilot.ts +22 -0
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +5 -5
- package/src/ai/provider.ts +1 -1
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +4 -11
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tools.ts +6 -4
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +2 -2
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +2 -2
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +30 -7
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +2 -2
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +12 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +2 -2
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/experience-tracker.ts +220 -71
- package/src/explorbot.ts +28 -15
- package/src/explorer.ts +3 -3
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/utils/error-page.ts +10 -0
- package/src/utils/logger.ts +1 -1
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
|
@@ -9,6 +9,22 @@ import { mdq } from './utils/markdown-query.js';
|
|
|
9
9
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
10
10
|
const debugLog = createDebug('explorbot:experience');
|
|
11
11
|
const DEFAULT_MAX_EXPERIENCE_LINES = 100;
|
|
12
|
+
export const RECENT_WINDOW_DAYS = 30;
|
|
13
|
+
/**
|
|
14
|
+
* Stores and reads per-page experience files (`./experience/<stateHash>.md`).
|
|
15
|
+
*
|
|
16
|
+
* Format rules (enforced by writeFlow/writeAction — the only supported writers):
|
|
17
|
+
*
|
|
18
|
+
* ## FLOW: <imperative title> multi-step, `*` bullets + optional ```js``` + `>` discovery, ends with `---`
|
|
19
|
+
* ## ACTION: <imperative title> single-step, optional `Solution:` line + one ```js``` code block
|
|
20
|
+
*
|
|
21
|
+
* - Always h2. Never h3 for FLOW/ACTION.
|
|
22
|
+
* - Title is an imperative verb phrase, lowercase-first, no trailing punctuation.
|
|
23
|
+
* Writers normalize automatically (strip own `FLOW:` / `ACTION:` prefix if the caller included it,
|
|
24
|
+
* lowercase first char, trim trailing `.!?,;:`).
|
|
25
|
+
* - On read (getSuccessfulExperience), headings are rendered as
|
|
26
|
+
* `## HOW to <title> (multi-step|single-step)` so prompts get natural phrasing.
|
|
27
|
+
*/
|
|
12
28
|
export class ExperienceTracker {
|
|
13
29
|
experienceDir;
|
|
14
30
|
disabled;
|
|
@@ -122,28 +138,50 @@ export class ExperienceTracker {
|
|
|
122
138
|
isWritingDisabled(state) {
|
|
123
139
|
return this.knowledgeTracker.getRelevantKnowledge(state).some((k) => k.noExperienceWriting === true || k.noExperienceWriting === 'true');
|
|
124
140
|
}
|
|
125
|
-
|
|
141
|
+
writeAction(state, action) {
|
|
126
142
|
if (this.disabled || this.isWritingDisabled(state))
|
|
127
143
|
return;
|
|
144
|
+
if (!action.code?.trim())
|
|
145
|
+
return;
|
|
128
146
|
this.ensureExperienceFile(state);
|
|
129
147
|
const stateHash = state.getStateHash();
|
|
130
148
|
const { content, data } = this.readExperienceFile(stateHash);
|
|
131
|
-
if (content.includes(code)) {
|
|
132
|
-
debugLog('Skipping duplicate
|
|
149
|
+
if (content.includes(action.code)) {
|
|
150
|
+
debugLog('Skipping duplicate action', action.code);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const title = normalizeTitle(action.title.split('\n')[0]);
|
|
154
|
+
if (!title)
|
|
155
|
+
return;
|
|
156
|
+
const filteredCode = action.code.replace(/I\.amOnPage\s*\([^)]*\)/gs, '');
|
|
157
|
+
const newEntry = generateActionContent(title, filteredCode, action.explanation);
|
|
158
|
+
const updatedContent = `${newEntry}\n\n${content}`;
|
|
159
|
+
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
160
|
+
tag('substep').log(` Added ACTION to: ${stateHash}.md`);
|
|
161
|
+
}
|
|
162
|
+
writeFlow(state, flow) {
|
|
163
|
+
if (this.disabled || this.isWritingDisabled(state))
|
|
164
|
+
return;
|
|
165
|
+
if (!flow.steps?.length)
|
|
133
166
|
return;
|
|
167
|
+
this.ensureExperienceFile(state);
|
|
168
|
+
const stateHash = state.getStateHash();
|
|
169
|
+
const { content, data } = this.readExperienceFile(stateHash);
|
|
170
|
+
if (flow.relatedUrls?.length) {
|
|
171
|
+
const currentPath = extractStatePath(state.url || '');
|
|
172
|
+
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
173
|
+
const allRelated = [...new Set([...existingRelated, ...flow.relatedUrls])];
|
|
174
|
+
data.related = allRelated.filter((url) => url !== currentPath);
|
|
134
175
|
}
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
${
|
|
142
|
-
\`\`\`
|
|
143
|
-
`;
|
|
144
|
-
const updatedContent = `${newEntryContent}\n\n${content}`;
|
|
176
|
+
const title = normalizeTitle(flow.scenario);
|
|
177
|
+
if (!title)
|
|
178
|
+
return;
|
|
179
|
+
const sessionContent = this.trimSessionContent(generateFlowContent(title, flow.steps));
|
|
180
|
+
if (!sessionContent)
|
|
181
|
+
return;
|
|
182
|
+
const updatedContent = `${sessionContent}\n${content}`;
|
|
145
183
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
146
|
-
tag('substep').log(`
|
|
184
|
+
tag('substep').log(`Added FLOW to: ${stateHash}.md`);
|
|
147
185
|
}
|
|
148
186
|
getAllExperience() {
|
|
149
187
|
const allFiles = [];
|
|
@@ -159,10 +197,12 @@ ${filteredCode}
|
|
|
159
197
|
try {
|
|
160
198
|
const content = readFileSync(file, 'utf8');
|
|
161
199
|
const parsed = matter(content);
|
|
200
|
+
const mtime = statSync(file).mtime;
|
|
162
201
|
allFiles.push({
|
|
163
202
|
filePath: file,
|
|
164
203
|
data: parsed.data,
|
|
165
204
|
content: parsed.content,
|
|
205
|
+
mtime,
|
|
166
206
|
});
|
|
167
207
|
}
|
|
168
208
|
catch (error) {
|
|
@@ -205,44 +245,6 @@ ${filteredCode}
|
|
|
205
245
|
// Clear any in-memory state if needed
|
|
206
246
|
// The actual files will be cleaned up by test cleanup
|
|
207
247
|
}
|
|
208
|
-
saveSessionExperience(state, entry) {
|
|
209
|
-
if (this.disabled || this.isWritingDisabled(state))
|
|
210
|
-
return;
|
|
211
|
-
this.ensureExperienceFile(state);
|
|
212
|
-
const stateHash = state.getStateHash();
|
|
213
|
-
const { content, data } = this.readExperienceFile(stateHash);
|
|
214
|
-
if (entry.relatedUrls?.length) {
|
|
215
|
-
const currentPath = extractStatePath(state.url || '');
|
|
216
|
-
const existingRelated = Array.isArray(data.related) ? data.related : [];
|
|
217
|
-
const allRelated = [...new Set([...existingRelated, ...entry.relatedUrls])];
|
|
218
|
-
data.related = allRelated.filter((url) => url !== currentPath);
|
|
219
|
-
}
|
|
220
|
-
const sessionContent = this.trimSessionContent(this.generateSessionContent(entry));
|
|
221
|
-
if (!sessionContent)
|
|
222
|
-
return;
|
|
223
|
-
const updatedContent = `${sessionContent}\n${content}`;
|
|
224
|
-
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
225
|
-
tag('substep').log(`Added session experience to: ${stateHash}.md`);
|
|
226
|
-
}
|
|
227
|
-
generateSessionContent(entry) {
|
|
228
|
-
let content = `## Successful Flow: ${entry.scenario}\n\n`;
|
|
229
|
-
for (const step of entry.steps) {
|
|
230
|
-
content += `* ${step.message}\n\n`;
|
|
231
|
-
if (step.code) {
|
|
232
|
-
content += '```js\n';
|
|
233
|
-
content += `${step.code}\n`;
|
|
234
|
-
content += '```\n\n';
|
|
235
|
-
}
|
|
236
|
-
if (step.discovery) {
|
|
237
|
-
const discoveries = step.discovery.split('\n').filter((d) => d.trim());
|
|
238
|
-
for (const discovery of discoveries) {
|
|
239
|
-
content += `> ${discovery.trim()}\n\n`;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
content += '---\n';
|
|
244
|
-
return content;
|
|
245
|
-
}
|
|
246
248
|
trimSessionContent(content) {
|
|
247
249
|
const q = mdq(content);
|
|
248
250
|
if (q.query('heading').count() === 0)
|
|
@@ -278,11 +280,12 @@ ${filteredCode}
|
|
|
278
280
|
for (const record of records) {
|
|
279
281
|
if (!record.content)
|
|
280
282
|
continue;
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
let combined = [
|
|
283
|
+
const flows = mdq(record.content).query('section(~"FLOW:")').text();
|
|
284
|
+
const actions = mdq(record.content).query('section(~"ACTION:")').text();
|
|
285
|
+
let combined = [flows, actions].filter(Boolean).join('\n\n');
|
|
284
286
|
if (!combined.trim())
|
|
285
287
|
continue;
|
|
288
|
+
combined = renderAsHowTo(combined);
|
|
286
289
|
if (options?.stripCode) {
|
|
287
290
|
combined = mdq(combined).query('code').replace('');
|
|
288
291
|
}
|
|
@@ -339,6 +342,69 @@ ${filteredCode}
|
|
|
339
342
|
}
|
|
340
343
|
return null;
|
|
341
344
|
}
|
|
345
|
+
listAllExperienceToc(filter, options) {
|
|
346
|
+
const records = this.getAllExperience();
|
|
347
|
+
if (records.length === 0)
|
|
348
|
+
return [];
|
|
349
|
+
const trimmed = filter?.trim();
|
|
350
|
+
let matching = records;
|
|
351
|
+
if (trimmed) {
|
|
352
|
+
if (trimmed.endsWith('.md')) {
|
|
353
|
+
const bare = trimmed.slice(0, -3);
|
|
354
|
+
const byFilename = records.find((record) => basename(record.filePath, '.md') === bare);
|
|
355
|
+
matching = byFilename ? [byFilename] : [];
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
const lower = trimmed.toLowerCase();
|
|
359
|
+
matching = records.filter((record) => {
|
|
360
|
+
const url = (record.data?.url || '').toLowerCase();
|
|
361
|
+
if (!url)
|
|
362
|
+
return false;
|
|
363
|
+
return url.includes(lower);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (options?.recency) {
|
|
368
|
+
const cutoff = Date.now() - RECENT_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
369
|
+
matching = matching.filter((record) => {
|
|
370
|
+
const isRecent = record.mtime.getTime() >= cutoff;
|
|
371
|
+
return options.recency === 'recent' ? isRecent : !isRecent;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
const sorted = matching.sort((a, b) => {
|
|
375
|
+
const aUrl = a.data?.url || '';
|
|
376
|
+
const bUrl = b.data?.url || '';
|
|
377
|
+
return aUrl.localeCompare(bUrl);
|
|
378
|
+
});
|
|
379
|
+
const toc = [];
|
|
380
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
381
|
+
const record = sorted[i];
|
|
382
|
+
const sections = listTocHeadings(record.content);
|
|
383
|
+
if (sections.length === 0)
|
|
384
|
+
continue;
|
|
385
|
+
toc.push({
|
|
386
|
+
fileTag: indexToLetters(toc.length),
|
|
387
|
+
fileHash: basename(record.filePath, '.md'),
|
|
388
|
+
url: record.data?.url || '',
|
|
389
|
+
sections,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return toc;
|
|
393
|
+
}
|
|
394
|
+
getExperienceSectionByTag(fileTag, sectionIndex, filter) {
|
|
395
|
+
const toc = this.listAllExperienceToc(filter);
|
|
396
|
+
const entry = toc.find((e) => e.fileTag === fileTag);
|
|
397
|
+
if (!entry)
|
|
398
|
+
return null;
|
|
399
|
+
const filePath = this.findExperienceFileByHash(entry.fileHash);
|
|
400
|
+
if (!filePath)
|
|
401
|
+
return null;
|
|
402
|
+
const { content } = this.readExperienceFile(entry.fileHash);
|
|
403
|
+
const extracted = extractHeadingSection(content, sectionIndex);
|
|
404
|
+
if (!extracted)
|
|
405
|
+
return null;
|
|
406
|
+
return { title: extracted.title, url: entry.url, content: extracted.body, fileHash: entry.fileHash };
|
|
407
|
+
}
|
|
342
408
|
}
|
|
343
409
|
function listTocHeadings(content) {
|
|
344
410
|
const tokens = marked.lexer(content);
|
|
@@ -403,7 +469,9 @@ export function renderExperienceToc(toc) {
|
|
|
403
469
|
return '';
|
|
404
470
|
const lines = [];
|
|
405
471
|
lines.push('<experience>');
|
|
406
|
-
lines.push('Past experience for this page
|
|
472
|
+
lines.push('Past experience for this page — reusable recipes recorded from prior successful runs.');
|
|
473
|
+
lines.push('FLOW: = multi-step recipe (bullets + code + discovery). ACTION: = single-step snippet (one code block).');
|
|
474
|
+
lines.push('Call learn_experience({ fileTag, sectionIndex }) to read a section when it looks relevant to the current step.');
|
|
407
475
|
lines.push('');
|
|
408
476
|
for (const entry of toc) {
|
|
409
477
|
lines.push(`File ${entry.fileTag} ${entry.url}:`);
|
|
@@ -416,3 +484,70 @@ export function renderExperienceToc(toc) {
|
|
|
416
484
|
lines.push('</experience>');
|
|
417
485
|
return lines.join('\n');
|
|
418
486
|
}
|
|
487
|
+
function normalizeTitle(raw) {
|
|
488
|
+
let t = (raw || '').trim();
|
|
489
|
+
for (const p of ['FLOW:', 'ACTION:']) {
|
|
490
|
+
if (t.toLowerCase().startsWith(p.toLowerCase())) {
|
|
491
|
+
t = t.slice(p.length).trim();
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
while (t.length > 0 && '.!?,;:'.includes(t[t.length - 1])) {
|
|
496
|
+
t = t.slice(0, -1);
|
|
497
|
+
}
|
|
498
|
+
if (t.length > 0)
|
|
499
|
+
t = t[0].toLowerCase() + t.slice(1);
|
|
500
|
+
return t;
|
|
501
|
+
}
|
|
502
|
+
function generateActionContent(title, code, explanation) {
|
|
503
|
+
const lines = [];
|
|
504
|
+
lines.push(`## ACTION: ${title}`);
|
|
505
|
+
lines.push('');
|
|
506
|
+
if (explanation) {
|
|
507
|
+
lines.push(`Solution: ${explanation}`);
|
|
508
|
+
lines.push('');
|
|
509
|
+
}
|
|
510
|
+
lines.push('```javascript');
|
|
511
|
+
lines.push(code);
|
|
512
|
+
lines.push('```');
|
|
513
|
+
lines.push('');
|
|
514
|
+
return lines.join('\n');
|
|
515
|
+
}
|
|
516
|
+
function generateFlowContent(title, steps) {
|
|
517
|
+
let content = `## FLOW: ${title}\n\n`;
|
|
518
|
+
for (const step of steps) {
|
|
519
|
+
content += `* ${step.message}\n\n`;
|
|
520
|
+
if (step.code) {
|
|
521
|
+
content += '```js\n';
|
|
522
|
+
content += `${step.code}\n`;
|
|
523
|
+
content += '```\n\n';
|
|
524
|
+
}
|
|
525
|
+
if (step.discovery) {
|
|
526
|
+
const discoveries = step.discovery.split('\n').filter((d) => d.trim());
|
|
527
|
+
for (const discovery of discoveries) {
|
|
528
|
+
content += `> ${discovery.trim()}\n\n`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
content += '---\n';
|
|
533
|
+
return content;
|
|
534
|
+
}
|
|
535
|
+
function renderAsHowTo(content) {
|
|
536
|
+
const tokens = marked.lexer(content);
|
|
537
|
+
let result = '';
|
|
538
|
+
for (const token of tokens) {
|
|
539
|
+
if (token.type === 'heading' && token.depth === 2) {
|
|
540
|
+
const text = token.text.trim();
|
|
541
|
+
if (text.startsWith('FLOW:')) {
|
|
542
|
+
result += `## HOW to ${text.slice(5).trim()} (multi-step)\n\n`;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (text.startsWith('ACTION:')) {
|
|
546
|
+
result += `## HOW to ${text.slice(7).trim()} (single-step)\n\n`;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
result += token.raw || '';
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
553
|
+
}
|
package/dist/src/explorbot.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ActionResult } from "./action-result.js";
|
|
4
|
-
import { ApiClient } from "./api/api-client.js";
|
|
5
|
-
import { RequestStore } from "./api/request-store.js";
|
|
6
|
-
import { loadSpec } from "./api/spec-reader.js";
|
|
7
4
|
import { Bosun } from "./ai/bosun.js";
|
|
8
5
|
import { Captain } from "./ai/captain.js";
|
|
9
6
|
import { ExperienceCompactor } from "./ai/experience-compactor.js";
|
|
@@ -14,11 +11,15 @@ import { Pilot } from "./ai/pilot.js";
|
|
|
14
11
|
import { Planner } from "./ai/planner.js";
|
|
15
12
|
import { AIProvider } from "./ai/provider.js";
|
|
16
13
|
import { Quartermaster } from "./ai/quartermaster.js";
|
|
17
|
-
import { Researcher } from "./ai/researcher.js";
|
|
18
14
|
import { Rerunner } from "./ai/rerunner.js";
|
|
15
|
+
import { Researcher } from "./ai/researcher.js";
|
|
19
16
|
import { Tester } from "./ai/tester.js";
|
|
20
17
|
import { createAgentTools } from "./ai/tools.js";
|
|
18
|
+
import { ApiClient } from "./api/api-client.js";
|
|
19
|
+
import { RequestStore } from "./api/request-store.js";
|
|
20
|
+
import { loadSpec } from "./api/spec-reader.js";
|
|
21
21
|
import { ConfigParser } from "./config.js";
|
|
22
|
+
import { ExperienceTracker } from "./experience-tracker.js";
|
|
22
23
|
import Explorer from "./explorer.js";
|
|
23
24
|
import { KnowledgeTracker } from "./knowledge-tracker.js";
|
|
24
25
|
import { Plan } from "./test-plan.js";
|
|
@@ -34,6 +35,7 @@ export class ExplorBot {
|
|
|
34
35
|
needsInput = false;
|
|
35
36
|
currentPlan;
|
|
36
37
|
planFeature;
|
|
38
|
+
lastPlanError = null;
|
|
37
39
|
agents = {};
|
|
38
40
|
constructor(options = {}) {
|
|
39
41
|
this.options = options;
|
|
@@ -54,13 +56,11 @@ export class ExplorBot {
|
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
56
58
|
try {
|
|
57
|
-
|
|
58
|
-
this.provider = new AIProvider(this.config.ai);
|
|
59
|
-
await this.provider.validateConnection();
|
|
59
|
+
await this.startProviderOnly();
|
|
60
60
|
this.explorer = new Explorer(this.config, this.provider, this.options);
|
|
61
61
|
await this.explorer.start();
|
|
62
62
|
if (!this.options.incognito) {
|
|
63
|
-
await this.agentExperienceCompactor().
|
|
63
|
+
await this.agentExperienceCompactor().autocompact();
|
|
64
64
|
}
|
|
65
65
|
if (this.userResolveFn)
|
|
66
66
|
this.explorer.setUserResolve(this.userResolveFn);
|
|
@@ -71,9 +71,16 @@ export class ExplorBot {
|
|
|
71
71
|
process.exit(1);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
async startProviderOnly() {
|
|
75
|
+
if (this.provider)
|
|
76
|
+
return;
|
|
77
|
+
this.config = await this.configParser.loadConfig(this.options);
|
|
78
|
+
this.provider = new AIProvider(this.config.ai);
|
|
79
|
+
await this.provider.validateConnection();
|
|
80
|
+
}
|
|
74
81
|
async stop() {
|
|
75
82
|
this.agents.quartermaster?.stop();
|
|
76
|
-
await this.explorer
|
|
83
|
+
await this.explorer?.stop();
|
|
77
84
|
}
|
|
78
85
|
async visitInitialState() {
|
|
79
86
|
const url = this.options.from || '/';
|
|
@@ -97,6 +104,12 @@ export class ExplorBot {
|
|
|
97
104
|
}
|
|
98
105
|
return new KnowledgeTracker();
|
|
99
106
|
}
|
|
107
|
+
getExperienceTracker() {
|
|
108
|
+
if (this.explorer) {
|
|
109
|
+
return this.explorer.getStateManager().getExperienceTracker();
|
|
110
|
+
}
|
|
111
|
+
return new ExperienceTracker();
|
|
112
|
+
}
|
|
100
113
|
getConfig() {
|
|
101
114
|
return this.config;
|
|
102
115
|
}
|
|
@@ -186,10 +199,7 @@ export class ExplorBot {
|
|
|
186
199
|
return this.agents.captain;
|
|
187
200
|
}
|
|
188
201
|
agentExperienceCompactor() {
|
|
189
|
-
return (this.agents.experienceCompactor ||= this.
|
|
190
|
-
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
191
|
-
return new ExperienceCompactor(ai, experienceTracker);
|
|
192
|
-
}));
|
|
202
|
+
return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.getExperienceTracker()));
|
|
193
203
|
}
|
|
194
204
|
agentQuartermaster() {
|
|
195
205
|
const config = this.config.ai?.agents?.quartermaster;
|
|
@@ -293,11 +303,13 @@ export class ExplorBot {
|
|
|
293
303
|
if (this.currentPlan) {
|
|
294
304
|
planner.setPlan(this.currentPlan);
|
|
295
305
|
}
|
|
306
|
+
this.lastPlanError = null;
|
|
296
307
|
try {
|
|
297
308
|
this.currentPlan = await planner.plan(feature, opts.style, opts.extend, opts.completedPlans);
|
|
298
309
|
}
|
|
299
310
|
catch (err) {
|
|
300
|
-
|
|
311
|
+
this.lastPlanError = err instanceof Error ? err : new Error(String(err));
|
|
312
|
+
tag('warning').log(`Planning failed: ${this.lastPlanError.message}`);
|
|
301
313
|
if (!this.currentPlan)
|
|
302
314
|
return undefined;
|
|
303
315
|
return this.currentPlan;
|
package/dist/src/explorer.js
CHANGED
|
@@ -8,12 +8,12 @@ import { createTest } from 'codeceptjs/lib/mocha/test';
|
|
|
8
8
|
import { ActionResult } from "./action-result.js";
|
|
9
9
|
import Action from './action.js';
|
|
10
10
|
import { visuallyAnnotateContainers } from "./ai/researcher/coordinates.js";
|
|
11
|
+
import { RequestStore } from "./api/request-store.js";
|
|
12
|
+
import { XhrCapture } from "./api/xhr-capture.js";
|
|
11
13
|
import { ConfigParser, outputPath } from './config.js';
|
|
12
14
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
13
15
|
import { Reporter } from "./reporter.js";
|
|
14
16
|
import { StateManager } from './state-manager.js';
|
|
15
|
-
import { RequestStore } from "./api/request-store.js";
|
|
16
|
-
import { XhrCapture } from "./api/xhr-capture.js";
|
|
17
17
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
18
18
|
import { WebElement, extractElementData } from "./utils/web-element.js";
|
|
19
19
|
const debugLog = createDebug('explorbot:explorer');
|
|
@@ -42,7 +42,7 @@ class Explorer {
|
|
|
42
42
|
this.initializeContainer();
|
|
43
43
|
this.stateManager = new StateManager({ incognito: this.options?.incognito });
|
|
44
44
|
this.knowledgeTracker = new KnowledgeTracker();
|
|
45
|
-
this.reporter = new Reporter(config.reporter);
|
|
45
|
+
this.reporter = new Reporter(config.reporter, this.stateManager);
|
|
46
46
|
}
|
|
47
47
|
initializeContainer() {
|
|
48
48
|
try {
|
package/dist/src/reporter.js
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import { Client } from '@testomatio/reporter';
|
|
2
2
|
import { outputPath } from './config.js';
|
|
3
|
+
import { Stats } from './stats.js';
|
|
3
4
|
import { createDebug } from './utils/logger.js';
|
|
4
5
|
const debugLog = createDebug('explorbot:reporter');
|
|
5
6
|
export class Reporter {
|
|
6
7
|
client;
|
|
7
8
|
isRunStarted = false;
|
|
8
9
|
reporterEnabled;
|
|
9
|
-
|
|
10
|
+
stateManager;
|
|
11
|
+
constructor(config, stateManager) {
|
|
10
12
|
this.reporterEnabled = Reporter.resolveEnabled(config);
|
|
13
|
+
this.stateManager = stateManager;
|
|
11
14
|
if (this.reporterEnabled && (!process.env.TESTOMATIO || config?.html)) {
|
|
12
15
|
this.configureHtmlPipe();
|
|
13
16
|
}
|
|
14
|
-
this.client = new Client({ apiKey: process.env.TESTOMATIO || '' });
|
|
15
17
|
const pipe = process.env.TESTOMATIO && config?.html ? 'both' : process.env.TESTOMATIO ? 'testomatio' : 'html';
|
|
16
18
|
debugLog('Reporter initialized', { enabled: this.reporterEnabled, pipe });
|
|
17
19
|
}
|
|
20
|
+
buildTitle() {
|
|
21
|
+
if (process.env.TESTOMATIO_TITLE)
|
|
22
|
+
return process.env.TESTOMATIO_TITLE;
|
|
23
|
+
const url = this.stateManager?.getCurrentState()?.url;
|
|
24
|
+
const parts = ['Explorbot session'];
|
|
25
|
+
if (url)
|
|
26
|
+
parts.push(url);
|
|
27
|
+
if (Stats.focus)
|
|
28
|
+
parts.push(`focus: "${Stats.focus}"`);
|
|
29
|
+
parts.push(`at ${new Date().toISOString().slice(0, 16)}`);
|
|
30
|
+
return parts.join(' ');
|
|
31
|
+
}
|
|
18
32
|
static resolveEnabled(config) {
|
|
19
33
|
if (config?.enabled === true)
|
|
20
34
|
return true;
|
|
@@ -35,6 +49,7 @@ export class Reporter {
|
|
|
35
49
|
return;
|
|
36
50
|
}
|
|
37
51
|
try {
|
|
52
|
+
this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
|
|
38
53
|
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
39
54
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
40
55
|
const result = await Promise.race([this.client.createRun().then(() => 'success'), timeoutPromise]);
|
package/dist/src/stats.js
CHANGED
package/dist/src/suite.js
CHANGED
|
@@ -3,8 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { Reflection } from '@codeceptjs/reflection';
|
|
4
4
|
import { ConfigParser } from "./config.js";
|
|
5
5
|
import { normalizeUrl } from "./state-manager.js";
|
|
6
|
-
import { parsePlanFromMarkdown } from "./utils/test-plan-markdown.js";
|
|
7
6
|
import { createDebug } from "./utils/logger.js";
|
|
7
|
+
import { parsePlanFromMarkdown } from "./utils/test-plan-markdown.js";
|
|
8
8
|
const debugLog = createDebug('explorbot:suite');
|
|
9
9
|
export class Suite {
|
|
10
10
|
url;
|
|
@@ -16,3 +16,13 @@ export function isErrorPage(actionResult) {
|
|
|
16
16
|
return true;
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
|
+
export class ErrorPageError extends Error {
|
|
20
|
+
url;
|
|
21
|
+
title;
|
|
22
|
+
constructor(url, title) {
|
|
23
|
+
super(`Error page detected at ${url}${title ? ` (${title})` : ''}`);
|
|
24
|
+
this.url = url;
|
|
25
|
+
this.title = title;
|
|
26
|
+
this.name = 'ErrorPageError';
|
|
27
|
+
}
|
|
28
|
+
}
|
package/dist/src/utils/logger.js
CHANGED
|
@@ -4,9 +4,9 @@ import { context, trace } from '@opentelemetry/api';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import debug from 'debug';
|
|
6
6
|
import dedent from 'dedent';
|
|
7
|
+
import stripAnsi from 'strip-ansi';
|
|
7
8
|
import { ConfigParser } from '../config.js';
|
|
8
9
|
import { Observability } from "../observability.js";
|
|
9
|
-
import stripAnsi from 'strip-ansi';
|
|
10
10
|
import { parseMarkdownToTerminal } from "./markdown-terminal.js";
|
|
11
11
|
class DebugFilter {
|
|
12
12
|
patterns = [];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { tag } from "./logger.js";
|
|
@@ -3,9 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { highlight } from 'cli-highlight';
|
|
5
5
|
import * as codeceptjs from 'codeceptjs';
|
|
6
|
-
import store from 'codeceptjs/lib/store';
|
|
7
6
|
import stepsListener from 'codeceptjs/lib/listener/steps';
|
|
8
7
|
import storeListener from 'codeceptjs/lib/listener/store';
|
|
8
|
+
import store from 'codeceptjs/lib/store';
|
|
9
9
|
import figureSet from 'figures';
|
|
10
10
|
import { ConfigParser } from "../config.js";
|
|
11
11
|
export function loadTestSuites(testsDir) {
|
|
@@ -1,4 +1,54 @@
|
|
|
1
1
|
import micromatch from 'micromatch';
|
|
2
|
+
import { ConfigParser } from '../config.js';
|
|
3
|
+
export function isDynamicSegment(segment) {
|
|
4
|
+
try {
|
|
5
|
+
const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
|
|
6
|
+
if (configRegex)
|
|
7
|
+
return new RegExp(configRegex, 'i').test(segment);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
/* config not loaded yet */
|
|
11
|
+
}
|
|
12
|
+
// numeric: /users/123
|
|
13
|
+
if (/^\d+$/.test(segment))
|
|
14
|
+
return true;
|
|
15
|
+
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
16
|
+
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment))
|
|
17
|
+
return true;
|
|
18
|
+
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
19
|
+
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment))
|
|
20
|
+
return true;
|
|
21
|
+
// hex ID (4+ chars): /suite/70dae98a
|
|
22
|
+
if (/^[a-f0-9]{4,}$/i.test(segment))
|
|
23
|
+
return true;
|
|
24
|
+
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
25
|
+
if (/^[a-f0-9]{8,}-/i.test(segment))
|
|
26
|
+
return true;
|
|
27
|
+
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|
|
28
|
+
if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment))
|
|
29
|
+
return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
export function hasDynamicUrlSegment(url) {
|
|
33
|
+
return url.split('/').some((seg) => seg.length > 0 && isDynamicSegment(seg));
|
|
34
|
+
}
|
|
35
|
+
export function generalizeSegment(segment) {
|
|
36
|
+
if (/^\d+$/.test(segment))
|
|
37
|
+
return '\\d+';
|
|
38
|
+
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment))
|
|
39
|
+
return '[a-f0-9-]+';
|
|
40
|
+
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment))
|
|
41
|
+
return '[0-9A-HJKMNP-TV-Z]+';
|
|
42
|
+
if (/^[a-f0-9]+$/i.test(segment))
|
|
43
|
+
return '[a-f0-9]+';
|
|
44
|
+
return '[^/]+';
|
|
45
|
+
}
|
|
46
|
+
export function generalizeUrl(url) {
|
|
47
|
+
return url
|
|
48
|
+
.split('/')
|
|
49
|
+
.map((seg) => (seg.length > 0 && isDynamicSegment(seg) ? generalizeSegment(seg) : seg))
|
|
50
|
+
.join('/');
|
|
51
|
+
}
|
|
2
52
|
export function matchesUrl(pattern, path) {
|
|
3
53
|
if (pattern === '*')
|
|
4
54
|
return true;
|
package/package.json
CHANGED
package/src/ai/bosun.ts
CHANGED
|
@@ -473,7 +473,11 @@ export class Bosun extends TaskAgent implements Agent {
|
|
|
473
473
|
const successfulInteractions = results.filter((r) => r.result === 'success' && r.code);
|
|
474
474
|
|
|
475
475
|
for (const interaction of successfulInteractions) {
|
|
476
|
-
|
|
476
|
+
experienceTracker.writeAction(actionResult, {
|
|
477
|
+
title: `Drill ${interaction.action}: ${interaction.component}`,
|
|
478
|
+
code: interaction.code!,
|
|
479
|
+
explanation: interaction.description,
|
|
480
|
+
});
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
if (successfulInteractions.length > 0) {
|