explorbot 0.1.5 → 0.1.7
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/dist/rules/planner/styles/curious.md +18 -5
- package/dist/rules/planner/styles/normal.md +4 -4
- package/dist/rules/planner/styles/psycho.md +14 -11
- package/dist/src/ai/captain/web-mode.js +9 -1
- package/dist/src/ai/historian.js +6 -0
- package/dist/src/ai/pilot.js +23 -2
- package/dist/src/ai/researcher/deep-analysis.js +65 -9
- package/dist/src/ai/researcher/sections.js +103 -0
- package/dist/src/ai/researcher.js +9 -46
- package/dist/src/ai/rules.js +9 -1
- package/dist/src/ai/task-agent.js +7 -27
- package/dist/src/ai/tester.js +41 -5
- package/dist/src/ai/tools.js +27 -2
- package/dist/src/commands/explore-command.js +4 -9
- package/dist/src/experience-tracker.js +126 -1
- package/dist/src/explorbot.js +9 -2
- package/dist/src/utils/aria.js +39 -2
- package/package.json +1 -1
- package/rules/planner/styles/curious.md +18 -5
- package/rules/planner/styles/normal.md +4 -4
- package/rules/planner/styles/psycho.md +14 -11
- package/src/ai/captain/web-mode.ts +9 -1
- package/src/ai/historian.ts +7 -0
- package/src/ai/pilot.ts +23 -3
- package/src/ai/researcher/deep-analysis.ts +74 -9
- package/src/ai/researcher/sections.ts +122 -0
- package/src/ai/researcher.ts +9 -47
- package/src/ai/rules.ts +9 -1
- package/src/ai/task-agent.ts +7 -31
- package/src/ai/tester.ts +44 -6
- package/src/ai/tools.ts +33 -1
- package/src/commands/explore-command.ts +4 -9
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +136 -1
- package/src/explorbot.ts +9 -2
- package/src/utils/aria.ts +40 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
|
+
import { marked, type Tokens } from 'marked';
|
|
4
5
|
import type { ActionResult } from './action-result.js';
|
|
5
6
|
import { ConfigParser } from './config.js';
|
|
6
7
|
import { KnowledgeTracker } from './knowledge-tracker.js';
|
|
@@ -332,6 +333,133 @@ ${filteredCode}
|
|
|
332
333
|
|
|
333
334
|
return results;
|
|
334
335
|
}
|
|
336
|
+
|
|
337
|
+
getExperienceTableOfContents(state: ActionResult, options?: { includeDescendantExperience?: boolean }): ExperienceTocEntry[] {
|
|
338
|
+
const records = this.getRelevantExperience(state, options);
|
|
339
|
+
if (records.length === 0) return [];
|
|
340
|
+
|
|
341
|
+
const sorted = [...records].sort((a, b) => {
|
|
342
|
+
const aHash = basename(a.filePath, '.md');
|
|
343
|
+
const bHash = basename(b.filePath, '.md');
|
|
344
|
+
return aHash.localeCompare(bHash);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const toc: ExperienceTocEntry[] = [];
|
|
348
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
349
|
+
const record = sorted[i];
|
|
350
|
+
const fileHash = basename(record.filePath, '.md');
|
|
351
|
+
const url = (record.data as WebPageState)?.url || '';
|
|
352
|
+
const sections = listTocHeadings(record.content);
|
|
353
|
+
if (sections.length === 0) continue;
|
|
354
|
+
toc.push({
|
|
355
|
+
fileTag: indexToLetters(i),
|
|
356
|
+
fileHash,
|
|
357
|
+
url,
|
|
358
|
+
sections,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return toc;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getExperienceSection(fileTag: string, sectionIndex: number, state: ActionResult, options?: { includeDescendantExperience?: boolean }): { title: string; url: string; content: string } | null {
|
|
365
|
+
const toc = this.getExperienceTableOfContents(state, options);
|
|
366
|
+
const entry = toc.find((e) => e.fileTag === fileTag);
|
|
367
|
+
if (!entry) return null;
|
|
368
|
+
|
|
369
|
+
const filePath = this.findExperienceFileByHash(entry.fileHash);
|
|
370
|
+
if (!filePath) return null;
|
|
371
|
+
|
|
372
|
+
const { content } = this.readExperienceFile(entry.fileHash);
|
|
373
|
+
const extracted = extractHeadingSection(content, sectionIndex);
|
|
374
|
+
if (!extracted) return null;
|
|
375
|
+
|
|
376
|
+
return { title: extracted.title, url: entry.url, content: extracted.body };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private findExperienceFileByHash(fileHash: string): string | null {
|
|
380
|
+
for (const dir of this.getExperienceDirectories()) {
|
|
381
|
+
const candidate = join(dir, `${fileHash}.md`);
|
|
382
|
+
if (existsSync(candidate)) return candidate;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function listTocHeadings(content: string): { index: number; level: 2 | 3; title: string }[] {
|
|
389
|
+
const tokens = marked.lexer(content);
|
|
390
|
+
const result: { index: number; level: 2 | 3; title: string }[] = [];
|
|
391
|
+
let index = 0;
|
|
392
|
+
for (const token of tokens) {
|
|
393
|
+
if (token.type !== 'heading') continue;
|
|
394
|
+
const heading = token as Tokens.Heading;
|
|
395
|
+
if (heading.depth !== 2 && heading.depth !== 3) continue;
|
|
396
|
+
index++;
|
|
397
|
+
result.push({ index, level: heading.depth as 2 | 3, title: heading.text });
|
|
398
|
+
}
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function extractHeadingSection(content: string, sectionIndex: number): { title: string; body: string } | null {
|
|
403
|
+
const tokens = marked.lexer(content);
|
|
404
|
+
const matching: { tokenIdx: number; depth: number; text: string }[] = [];
|
|
405
|
+
|
|
406
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
407
|
+
const token = tokens[i];
|
|
408
|
+
if (token.type !== 'heading') continue;
|
|
409
|
+
const heading = token as Tokens.Heading;
|
|
410
|
+
if (heading.depth !== 2 && heading.depth !== 3) continue;
|
|
411
|
+
matching.push({ tokenIdx: i, depth: heading.depth, text: heading.text });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (sectionIndex < 1 || sectionIndex > matching.length) return null;
|
|
415
|
+
|
|
416
|
+
const target = matching[sectionIndex - 1];
|
|
417
|
+
let endTokenIdx = tokens.length;
|
|
418
|
+
for (let j = target.tokenIdx + 1; j < tokens.length; j++) {
|
|
419
|
+
const token = tokens[j];
|
|
420
|
+
if (token.type !== 'heading') continue;
|
|
421
|
+
if ((token as Tokens.Heading).depth <= target.depth) {
|
|
422
|
+
endTokenIdx = j;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const body = tokens
|
|
428
|
+
.slice(target.tokenIdx, endTokenIdx)
|
|
429
|
+
.map((t) => (t as any).raw || '')
|
|
430
|
+
.join('');
|
|
431
|
+
return { title: target.text, body };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function indexToLetters(index: number): string {
|
|
435
|
+
let n = index;
|
|
436
|
+
let result = '';
|
|
437
|
+
while (true) {
|
|
438
|
+
result = String.fromCharCode(65 + (n % 26)) + result;
|
|
439
|
+
n = Math.floor(n / 26);
|
|
440
|
+
if (n === 0) break;
|
|
441
|
+
n -= 1;
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function renderExperienceToc(toc: ExperienceTocEntry[]): string {
|
|
447
|
+
if (toc.length === 0) return '';
|
|
448
|
+
|
|
449
|
+
const lines: string[] = [];
|
|
450
|
+
lines.push('<experience>');
|
|
451
|
+
lines.push('Past experience for this page. Call learn_experience({ fileTag, sectionIndex }) to read a section.');
|
|
452
|
+
lines.push('');
|
|
453
|
+
for (const entry of toc) {
|
|
454
|
+
lines.push(`File ${entry.fileTag} ${entry.url}:`);
|
|
455
|
+
for (const section of entry.sections) {
|
|
456
|
+
const prefix = '#'.repeat(section.level);
|
|
457
|
+
lines.push(` ${entry.fileTag}.${section.index} ${prefix} ${section.title}`);
|
|
458
|
+
}
|
|
459
|
+
lines.push('');
|
|
460
|
+
}
|
|
461
|
+
lines.push('</experience>');
|
|
462
|
+
return lines.join('\n');
|
|
335
463
|
}
|
|
336
464
|
|
|
337
465
|
export interface SessionStep {
|
|
@@ -348,3 +476,10 @@ export interface SessionExperienceEntry {
|
|
|
348
476
|
steps: SessionStep[];
|
|
349
477
|
relatedUrls?: string[];
|
|
350
478
|
}
|
|
479
|
+
|
|
480
|
+
export interface ExperienceTocEntry {
|
|
481
|
+
fileTag: string;
|
|
482
|
+
fileHash: string;
|
|
483
|
+
url: string;
|
|
484
|
+
sections: { index: number; level: 2 | 3; title: string }[];
|
|
485
|
+
}
|
package/src/explorbot.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { ActionResult } from './action-result.ts';
|
|
3
4
|
import { ApiClient } from './api/api-client.ts';
|
|
4
5
|
import { RequestStore } from './api/request-store.ts';
|
|
5
6
|
import { loadSpec } from './api/spec-reader.ts';
|
|
@@ -182,8 +183,14 @@ export class ExplorBot {
|
|
|
182
183
|
return (this.agents.pilot ||= this.createAgent(({ ai, explorer }) => {
|
|
183
184
|
const researcher = this.agentResearcher();
|
|
184
185
|
const navigator = this.agentNavigator();
|
|
185
|
-
const
|
|
186
|
-
|
|
186
|
+
const stateManager = explorer.getStateManager();
|
|
187
|
+
const experienceTracker = stateManager.getExperienceTracker();
|
|
188
|
+
const getState = () => {
|
|
189
|
+
const state = stateManager.getCurrentState();
|
|
190
|
+
return state ? ActionResult.fromState(state) : null;
|
|
191
|
+
};
|
|
192
|
+
const tools = createAgentTools({ explorer, researcher, navigator, experienceTracker, getState });
|
|
193
|
+
return new Pilot(ai, tools, researcher, explorer, experienceTracker);
|
|
187
194
|
}));
|
|
188
195
|
}
|
|
189
196
|
|
package/src/utils/aria.ts
CHANGED
|
@@ -355,8 +355,41 @@ export interface FocusAreaResult {
|
|
|
355
355
|
name: string | null;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
const CLOSE_OVERLAY_BUTTON_RE = /^close\s+(modal|dialog|popup|drawer|panel|sheet)\b/i;
|
|
359
|
+
|
|
360
|
+
const findOverlayByCloseButton = (nodeList: AriaNode[]): FocusAreaResult | null => {
|
|
361
|
+
const closeIdx = nodeList.findIndex((n) => n.role === 'button' && CLOSE_OVERLAY_BUTTON_RE.test(n.name || ''));
|
|
362
|
+
if (closeIdx !== -1) {
|
|
363
|
+
let heading: AriaNode | undefined;
|
|
364
|
+
for (let i = closeIdx - 1; i >= 0; i--) {
|
|
365
|
+
if (nodeList[i].role === 'heading' && nodeList[i].name) {
|
|
366
|
+
heading = nodeList[i];
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!heading) {
|
|
371
|
+
for (let i = closeIdx + 1; i < nodeList.length; i++) {
|
|
372
|
+
if (nodeList[i].role === 'heading' && nodeList[i].name) {
|
|
373
|
+
heading = nodeList[i];
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
detected: true,
|
|
380
|
+
type: 'dialog',
|
|
381
|
+
name: heading?.name || null,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
for (const node of nodeList) {
|
|
385
|
+
const inner = findOverlayByCloseButton(node.children);
|
|
386
|
+
if (inner) return inner;
|
|
387
|
+
}
|
|
388
|
+
return null;
|
|
389
|
+
};
|
|
390
|
+
|
|
358
391
|
export const detectFocusArea = (snapshot: string | null): FocusAreaResult => {
|
|
359
|
-
const nodes = parseAriaSnapshot(snapshot);
|
|
392
|
+
const nodes = parseAriaSnapshot(snapshot, true);
|
|
360
393
|
|
|
361
394
|
const findFocusArea = (nodeList: AriaNode[]): FocusAreaResult | null => {
|
|
362
395
|
for (const node of nodeList) {
|
|
@@ -385,7 +418,12 @@ export const detectFocusArea = (snapshot: string | null): FocusAreaResult => {
|
|
|
385
418
|
};
|
|
386
419
|
|
|
387
420
|
const result = findFocusArea(nodes);
|
|
388
|
-
|
|
421
|
+
if (result) return result;
|
|
422
|
+
|
|
423
|
+
const fallback = findOverlayByCloseButton(nodes);
|
|
424
|
+
if (fallback && fallback.name) return fallback;
|
|
425
|
+
|
|
426
|
+
return { detected: false, type: null, name: null };
|
|
389
427
|
};
|
|
390
428
|
|
|
391
429
|
export const collectInteractiveNodes = (snapshot: string | null): Array<Record<string, unknown>> => {
|