explorbot 0.1.32-beta.1 → 0.2.0
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 +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
|
@@ -1,40 +1,27 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
1
|
import { ActionResult } from './action-result.js';
|
|
5
|
-
import { ConfigParser, outputPath } from './config.js';
|
|
6
|
-
import { ExperienceTracker } from './experience-tracker.js';
|
|
7
2
|
import { detectFocusArea } from './utils/aria.js';
|
|
8
3
|
import { createDebug } from './utils/logger.js';
|
|
4
|
+
import { slugify } from './utils/strings.js';
|
|
9
5
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
10
6
|
const debugLog = createDebug('explorbot:state');
|
|
11
7
|
export class StateManager {
|
|
12
8
|
currentState = null;
|
|
13
9
|
stateHistory = [];
|
|
14
10
|
allVisitedUrls = new Set();
|
|
15
|
-
knowledgeCache = [];
|
|
16
|
-
lastKnowledgeScan = null;
|
|
17
11
|
stateChangeListeners = [];
|
|
18
12
|
experienceTracker;
|
|
19
|
-
|
|
13
|
+
knowledgeTracker;
|
|
20
14
|
nextStateId = 1;
|
|
21
|
-
constructor() {
|
|
22
|
-
this.experienceTracker =
|
|
23
|
-
|
|
24
|
-
const config = configParser.getConfig();
|
|
25
|
-
const configPath = configParser.getConfigPath();
|
|
26
|
-
// Resolve knowledge directory relative to the config file location (project root)
|
|
27
|
-
if (configPath) {
|
|
28
|
-
const projectRoot = dirname(configPath);
|
|
29
|
-
this.knowledgeDir = join(projectRoot, config.dirs?.knowledge || 'knowledge');
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
this.knowledgeDir = config.dirs?.knowledge || 'knowledge';
|
|
33
|
-
}
|
|
15
|
+
constructor(experienceTracker, knowledgeTracker) {
|
|
16
|
+
this.experienceTracker = experienceTracker;
|
|
17
|
+
this.knowledgeTracker = knowledgeTracker;
|
|
34
18
|
}
|
|
35
19
|
getExperienceTracker() {
|
|
36
20
|
return this.experienceTracker;
|
|
37
21
|
}
|
|
22
|
+
getKnowledgeTracker() {
|
|
23
|
+
return this.knowledgeTracker;
|
|
24
|
+
}
|
|
38
25
|
/**
|
|
39
26
|
* Subscribe to state change events
|
|
40
27
|
*/
|
|
@@ -61,11 +48,6 @@ export class StateManager {
|
|
|
61
48
|
}
|
|
62
49
|
});
|
|
63
50
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Extract state path from full URL
|
|
66
|
-
* Removes domain, port, protocol
|
|
67
|
-
* Keeps path, query, and hash: /path/to/page?tab=users#section
|
|
68
|
-
*/
|
|
69
51
|
/**
|
|
70
52
|
* Update current state from ActionResult and record transition if state changed
|
|
71
53
|
*/
|
|
@@ -141,13 +123,7 @@ export class StateManager {
|
|
|
141
123
|
if (title) {
|
|
142
124
|
parts.push(`title_${title}`);
|
|
143
125
|
}
|
|
144
|
-
return parts
|
|
145
|
-
.join('_')
|
|
146
|
-
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
147
|
-
.replace(/_+/g, '_')
|
|
148
|
-
.replace(/^_|_$/g, '')
|
|
149
|
-
.toLowerCase()
|
|
150
|
-
.substring(0, 200);
|
|
126
|
+
return slugify(parts.join('_')).substring(0, 200);
|
|
151
127
|
}
|
|
152
128
|
/**
|
|
153
129
|
* Get current state
|
|
@@ -226,57 +202,14 @@ export class StateManager {
|
|
|
226
202
|
getRecentTransitions(count = 5) {
|
|
227
203
|
return this.stateHistory.slice(-count);
|
|
228
204
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Scan knowledge directory for .md files and cache them
|
|
231
|
-
*/
|
|
232
|
-
scanKnowledgeFiles() {
|
|
233
|
-
const now = new Date();
|
|
234
|
-
// Only rescan every 30 seconds to avoid excessive file I/O
|
|
235
|
-
if (this.lastKnowledgeScan && now.getTime() - this.lastKnowledgeScan.getTime() < 30000) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
this.knowledgeCache = [];
|
|
239
|
-
if (!existsSync(this.knowledgeDir)) {
|
|
240
|
-
debugLog(`Knowledge directory not found: ${this.knowledgeDir}`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
const files = readdirSync(this.knowledgeDir, { recursive: true })
|
|
245
|
-
.filter((file) => typeof file === 'string' && file.endsWith('.md'))
|
|
246
|
-
.map((file) => join(this.knowledgeDir, file));
|
|
247
|
-
for (const filePath of files) {
|
|
248
|
-
try {
|
|
249
|
-
const fileContent = readFileSync(filePath, 'utf8');
|
|
250
|
-
const parsed = matter(fileContent);
|
|
251
|
-
const urlPattern = parsed.data.url || parsed.data.path || '*';
|
|
252
|
-
this.knowledgeCache.push({
|
|
253
|
-
filePath,
|
|
254
|
-
url: urlPattern,
|
|
255
|
-
...parsed.data,
|
|
256
|
-
content: parsed.content,
|
|
257
|
-
});
|
|
258
|
-
debugLog(`Loaded knowledge file: ${filePath} (pattern: ${urlPattern})`);
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
debugLog(`Failed to load knowledge file ${filePath}:`, error);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
this.lastKnowledgeScan = now;
|
|
265
|
-
debugLog(`Scanned ${this.knowledgeCache.length} knowledge files`);
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
debugLog('Failed to scan knowledge directory:', error);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
205
|
/**
|
|
272
206
|
* Get relevant knowledge files for current state
|
|
273
207
|
*/
|
|
274
208
|
getRelevantKnowledge() {
|
|
275
209
|
if (!this.currentState)
|
|
276
210
|
return [];
|
|
277
|
-
this.scanKnowledgeFiles();
|
|
278
211
|
const actionResult = ActionResult.fromState(this.currentState);
|
|
279
|
-
return this.
|
|
212
|
+
return this.knowledgeTracker.getRelevantKnowledge(actionResult);
|
|
280
213
|
}
|
|
281
214
|
/**
|
|
282
215
|
* Get relevant experience files for current state
|
|
@@ -291,17 +224,6 @@ export class StateManager {
|
|
|
291
224
|
/**
|
|
292
225
|
* Get all context for current state (knowledge + experience)
|
|
293
226
|
*/
|
|
294
|
-
getCurrentContext() {
|
|
295
|
-
if (!this.currentState) {
|
|
296
|
-
throw new Error('No current state available');
|
|
297
|
-
}
|
|
298
|
-
return {
|
|
299
|
-
state: this.currentState,
|
|
300
|
-
knowledge: this.getRelevantKnowledge(),
|
|
301
|
-
experience: this.getRelevantExperience(),
|
|
302
|
-
recentTransitions: this.getRecentTransitions(),
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
227
|
/**
|
|
306
228
|
* Check if we've been in this state before
|
|
307
229
|
*/
|
|
@@ -333,41 +255,6 @@ export class StateManager {
|
|
|
333
255
|
* If the last transition changed URL, returns the fromState (for URL change detection).
|
|
334
256
|
* Otherwise returns the most recent toState with content (for diffing).
|
|
335
257
|
*/
|
|
336
|
-
getPreviousState() {
|
|
337
|
-
if (this.stateHistory.length === 0)
|
|
338
|
-
return null;
|
|
339
|
-
const lastTransition = this.stateHistory[this.stateHistory.length - 1];
|
|
340
|
-
if (lastTransition.fromState?.url !== lastTransition.toState?.url) {
|
|
341
|
-
return lastTransition.fromState;
|
|
342
|
-
}
|
|
343
|
-
for (let i = this.stateHistory.length - 1; i >= 0; i--) {
|
|
344
|
-
const toState = this.stateHistory[i].toState;
|
|
345
|
-
if (!toState)
|
|
346
|
-
continue;
|
|
347
|
-
if (toState.id === this.currentState?.id)
|
|
348
|
-
continue;
|
|
349
|
-
if (toState.html || toState.ariaSnapshot) {
|
|
350
|
-
return toState;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Load HTML content from file
|
|
357
|
-
*/
|
|
358
|
-
loadHtmlFromFile(htmlFile) {
|
|
359
|
-
try {
|
|
360
|
-
const filePath = outputPath('states', htmlFile);
|
|
361
|
-
if (existsSync(filePath)) {
|
|
362
|
-
return readFileSync(filePath, 'utf8');
|
|
363
|
-
}
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
debugLog('Failed to load HTML from file:', error);
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
258
|
/**
|
|
372
259
|
* Clear state history (useful for testing or reset)
|
|
373
260
|
* Note: This preserves the current state, only clears navigation history
|
|
@@ -398,13 +285,7 @@ export class StateManager {
|
|
|
398
285
|
this.stateHistory = [];
|
|
399
286
|
this.allVisitedUrls.clear();
|
|
400
287
|
this.stateChangeListeners = [];
|
|
401
|
-
this.knowledgeCache = [];
|
|
402
|
-
this.lastKnowledgeScan = null;
|
|
403
288
|
this.nextStateId = 1;
|
|
404
|
-
// Clean up experience tracker if it has cleanup method
|
|
405
|
-
if (this.experienceTracker && typeof this.experienceTracker.cleanup === 'function') {
|
|
406
|
-
this.experienceTracker.cleanup();
|
|
407
|
-
}
|
|
408
289
|
debugLog('StateManager cleanup completed');
|
|
409
290
|
}
|
|
410
291
|
}
|
package/dist/src/utils/aria.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface FocusAreaResult {
|
|
|
4
4
|
name: string | null;
|
|
5
5
|
}
|
|
6
6
|
export declare const compactAriaSnapshot: (snapshot: string | null, keepNamed?: boolean) => string;
|
|
7
|
-
export declare const diffAriaSnapshots: (previous: string | null, current: string | null) =>
|
|
7
|
+
export declare const diffAriaSnapshots: (previous: string | null, current: string | null) => AriaDiff;
|
|
8
8
|
export declare const detectFocusArea: (snapshot: string | null) => FocusAreaResult;
|
|
9
9
|
export declare const collectInteractiveNodes: (snapshot: string | null) => Array<Record<string, unknown>>;
|
|
10
10
|
export interface FocusedElementInfo {
|
|
@@ -18,3 +18,8 @@ export declare function parseAriaLocator(ariaStr: string): {
|
|
|
18
18
|
role: string;
|
|
19
19
|
text: string;
|
|
20
20
|
} | null;
|
|
21
|
+
export declare const LARGE_ARIA_CHANGE_THRESHOLD = 50;
|
|
22
|
+
export interface AriaDiff {
|
|
23
|
+
text: string | null;
|
|
24
|
+
count: number;
|
|
25
|
+
}
|
package/dist/src/utils/aria.js
CHANGED
|
@@ -490,7 +490,9 @@ export const diffAriaSnapshots = (previous, current) => {
|
|
|
490
490
|
const currTotals = countBy(curr.map((e) => e.summary));
|
|
491
491
|
const byCount = diffByCount(prevTotals, currTotals);
|
|
492
492
|
const renames = detectRenames(prev, curr, prevTotals, currTotals);
|
|
493
|
-
|
|
493
|
+
const added = [...byCount.added, ...renames.added];
|
|
494
|
+
const removed = [...byCount.removed, ...renames.removed];
|
|
495
|
+
return { text: formatDiff(added, removed, toggled), count: added.length + removed.length + toggled.length };
|
|
494
496
|
};
|
|
495
497
|
export const detectFocusArea = (snapshot) => {
|
|
496
498
|
let tree = parseSnapshot(snapshot);
|
|
@@ -543,3 +545,4 @@ export function parseAriaLocator(ariaStr) {
|
|
|
543
545
|
return null;
|
|
544
546
|
return { role: match[1], text: match[2] };
|
|
545
547
|
}
|
|
548
|
+
export const LARGE_ARIA_CHANGE_THRESHOLD = 50;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class TTLCache<T> {
|
|
2
|
+
store: Map<string, CacheEntry<T>>;
|
|
3
|
+
ttlMs: number;
|
|
4
|
+
constructor(ttlMs?: number);
|
|
5
|
+
get(key: string): T | undefined;
|
|
6
|
+
set(key: string, value: T): void;
|
|
7
|
+
fetch(key: string, compute: () => Promise<T>): Promise<T>;
|
|
8
|
+
delete(key: string): void;
|
|
9
|
+
clear(): void;
|
|
10
|
+
}
|
|
11
|
+
type CacheEntry<T> = {
|
|
12
|
+
value: T;
|
|
13
|
+
at: number;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class TTLCache {
|
|
2
|
+
store = new Map();
|
|
3
|
+
ttlMs;
|
|
4
|
+
constructor(ttlMs = Number.POSITIVE_INFINITY) {
|
|
5
|
+
this.ttlMs = ttlMs;
|
|
6
|
+
}
|
|
7
|
+
get(key) {
|
|
8
|
+
const entry = this.store.get(key);
|
|
9
|
+
if (!entry)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (Date.now() - entry.at > this.ttlMs) {
|
|
12
|
+
this.store.delete(key);
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return entry.value;
|
|
16
|
+
}
|
|
17
|
+
set(key, value) {
|
|
18
|
+
this.store.set(key, { value, at: Date.now() });
|
|
19
|
+
}
|
|
20
|
+
async fetch(key, compute) {
|
|
21
|
+
const cached = this.get(key);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const value = await compute();
|
|
25
|
+
this.set(key, value);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
delete(key) {
|
|
29
|
+
this.store.delete(key);
|
|
30
|
+
}
|
|
31
|
+
clear() {
|
|
32
|
+
this.store.clear();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -21,5 +21,11 @@ export interface ContextData {
|
|
|
21
21
|
combinedHtml?: string;
|
|
22
22
|
research?: string;
|
|
23
23
|
}
|
|
24
|
+
export declare function formatHeadings(headings: {
|
|
25
|
+
h1?: string;
|
|
26
|
+
h2?: string;
|
|
27
|
+
h3?: string;
|
|
28
|
+
h4?: string;
|
|
29
|
+
}): string[];
|
|
24
30
|
export type ContextMode = 'attached' | 'compact' | 'full';
|
|
25
31
|
export declare function formatContextSummary(data: ContextData, mode?: ContextMode): string;
|
|
@@ -10,6 +10,18 @@ function indent(text) {
|
|
|
10
10
|
.map((line) => chalk.gray(` ${line}`))
|
|
11
11
|
.join('\n');
|
|
12
12
|
}
|
|
13
|
+
export function formatHeadings(headings) {
|
|
14
|
+
const lines = [];
|
|
15
|
+
if (headings.h1)
|
|
16
|
+
lines.push(`H1: ${headings.h1}`);
|
|
17
|
+
if (headings.h2)
|
|
18
|
+
lines.push(`H2: ${headings.h2}`);
|
|
19
|
+
if (headings.h3)
|
|
20
|
+
lines.push(`H3: ${headings.h3}`);
|
|
21
|
+
if (headings.h4)
|
|
22
|
+
lines.push(`H4: ${headings.h4}`);
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
|
13
25
|
export function formatContextSummary(data, mode = 'compact') {
|
|
14
26
|
if (mode === 'full') {
|
|
15
27
|
return formatContextFull(data);
|
|
@@ -27,15 +39,7 @@ function formatContextAttached(data) {
|
|
|
27
39
|
lines.push(indent(data.title));
|
|
28
40
|
}
|
|
29
41
|
lines.push('');
|
|
30
|
-
const headingLines =
|
|
31
|
-
if (data.headings.h1)
|
|
32
|
-
headingLines.push(`H1: ${data.headings.h1}`);
|
|
33
|
-
if (data.headings.h2)
|
|
34
|
-
headingLines.push(`H2: ${data.headings.h2}`);
|
|
35
|
-
if (data.headings.h3)
|
|
36
|
-
headingLines.push(`H3: ${data.headings.h3}`);
|
|
37
|
-
if (data.headings.h4)
|
|
38
|
-
headingLines.push(`H4: ${data.headings.h4}`);
|
|
42
|
+
const headingLines = formatHeadings(data.headings);
|
|
39
43
|
if (headingLines.length > 0) {
|
|
40
44
|
lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
|
|
41
45
|
lines.push(indent(headingLines.join('\n')));
|
|
@@ -88,15 +92,7 @@ function formatContextCompact(data) {
|
|
|
88
92
|
lines.push(indent(data.title));
|
|
89
93
|
}
|
|
90
94
|
lines.push('');
|
|
91
|
-
const headingLines =
|
|
92
|
-
if (data.headings.h1)
|
|
93
|
-
headingLines.push(`H1: ${data.headings.h1}`);
|
|
94
|
-
if (data.headings.h2)
|
|
95
|
-
headingLines.push(`H2: ${data.headings.h2}`);
|
|
96
|
-
if (data.headings.h3)
|
|
97
|
-
headingLines.push(`H3: ${data.headings.h3}`);
|
|
98
|
-
if (data.headings.h4)
|
|
99
|
-
headingLines.push(`H4: ${data.headings.h4}`);
|
|
95
|
+
const headingLines = formatHeadings(data.headings);
|
|
100
96
|
if (headingLines.length > 0) {
|
|
101
97
|
lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
|
|
102
98
|
lines.push(indent(headingLines.join('\n')));
|
|
@@ -144,15 +140,7 @@ function formatContextFull(data) {
|
|
|
144
140
|
lines.push(indent(data.title));
|
|
145
141
|
}
|
|
146
142
|
lines.push('');
|
|
147
|
-
const headingLines =
|
|
148
|
-
if (data.headings.h1)
|
|
149
|
-
headingLines.push(`H1: ${data.headings.h1}`);
|
|
150
|
-
if (data.headings.h2)
|
|
151
|
-
headingLines.push(`H2: ${data.headings.h2}`);
|
|
152
|
-
if (data.headings.h3)
|
|
153
|
-
headingLines.push(`H3: ${data.headings.h3}`);
|
|
154
|
-
if (data.headings.h4)
|
|
155
|
-
headingLines.push(`H4: ${data.headings.h4}`);
|
|
143
|
+
const headingLines = formatHeadings(data.headings);
|
|
156
144
|
if (headingLines.length > 0) {
|
|
157
145
|
lines.push(section('HEADINGS') + chalk.gray(' (Auto-attached)'));
|
|
158
146
|
lines.push(indent(headingLines.join('\n')));
|
package/dist/src/utils/html.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type * as parse5TreeAdapter from 'parse5/lib/tree-adapters/default';
|
|
|
2
2
|
import type { HtmlConfig } from '../config.js';
|
|
3
3
|
export declare const EXPLORBOT_ATTRS: {
|
|
4
4
|
readonly area: "data-explorbot-area";
|
|
5
|
+
readonly coveredBy: "data-explorbot-covered-by";
|
|
5
6
|
readonly context: "data-explorbot-context";
|
|
6
7
|
readonly eidx: "data-explorbot-eidx";
|
|
8
|
+
readonly hit: "data-explorbot-hit";
|
|
7
9
|
readonly variant: "data-explorbot-variant";
|
|
8
10
|
};
|
|
9
11
|
export declare const HTML_SELECTORS: {
|
|
@@ -41,8 +43,10 @@ export declare function inferHtmlRole(data: {
|
|
|
41
43
|
export declare const ELEMENT_EXTRACTION_CONFIG: {
|
|
42
44
|
readonly attrs: {
|
|
43
45
|
readonly area: "data-explorbot-area";
|
|
46
|
+
readonly coveredBy: "data-explorbot-covered-by";
|
|
44
47
|
readonly context: "data-explorbot-context";
|
|
45
48
|
readonly eidx: "data-explorbot-eidx";
|
|
49
|
+
readonly hit: "data-explorbot-hit";
|
|
46
50
|
readonly variant: "data-explorbot-variant";
|
|
47
51
|
};
|
|
48
52
|
readonly codeEditorMarkers: readonly ["monaco", "codemirror", "ace", "ace_editor", "code"];
|
|
@@ -70,6 +74,7 @@ export type ComponentScopeExtractionConfig = {
|
|
|
70
74
|
interactiveControlSelector: string;
|
|
71
75
|
limits: typeof HTML_EXTRACTION_LIMITS;
|
|
72
76
|
};
|
|
77
|
+
export declare function captureHtmlForSnapshot(): string;
|
|
73
78
|
export declare function extractElementData(el: Element, config?: ElementExtractionConfig): {
|
|
74
79
|
tag: string;
|
|
75
80
|
text: string;
|
package/dist/src/utils/html.js
CHANGED
|
@@ -67,8 +67,10 @@ const INTERACTIVE_EVENT_ATTRIBUTES = new Set(['onclick', 'onchange', 'onblur', '
|
|
|
67
67
|
const HIDDEN_CLASSES = new Set(['hidden', 'invisible', 'd-none', 'hide', 'dn', 'u-hidden', 'is-hidden', 'visually-hidden', 'sr-only', 'screen-reader-only', 'visuallyhidden', 'opacity-0']);
|
|
68
68
|
export const EXPLORBOT_ATTRS = {
|
|
69
69
|
area: 'data-explorbot-area',
|
|
70
|
+
coveredBy: 'data-explorbot-covered-by',
|
|
70
71
|
context: 'data-explorbot-context',
|
|
71
72
|
eidx: 'data-explorbot-eidx',
|
|
73
|
+
hit: 'data-explorbot-hit',
|
|
72
74
|
variant: 'data-explorbot-variant',
|
|
73
75
|
};
|
|
74
76
|
export const HTML_SELECTORS = {
|
|
@@ -139,13 +141,41 @@ export const ELEMENT_EXTRACTION_CONFIG = {
|
|
|
139
141
|
semanticContextContainer: HTML_SELECTORS.semanticContextContainer,
|
|
140
142
|
},
|
|
141
143
|
};
|
|
144
|
+
export function captureHtmlForSnapshot() {
|
|
145
|
+
const clone = document.documentElement.cloneNode(true);
|
|
146
|
+
const liveControls = Array.from(document.querySelectorAll('input, textarea, select'));
|
|
147
|
+
const clonedControls = Array.from(clone.querySelectorAll('input, textarea, select'));
|
|
148
|
+
for (let i = 0; i < liveControls.length; i++) {
|
|
149
|
+
const source = liveControls[i];
|
|
150
|
+
const target = clonedControls[i];
|
|
151
|
+
if (!target)
|
|
152
|
+
continue;
|
|
153
|
+
if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {
|
|
154
|
+
target.setAttribute('data-explorbot-value', source.value);
|
|
155
|
+
}
|
|
156
|
+
if (source instanceof HTMLInputElement && (source.type === 'checkbox' || source.type === 'radio')) {
|
|
157
|
+
let checked = 'false';
|
|
158
|
+
if (source.checked)
|
|
159
|
+
checked = 'true';
|
|
160
|
+
target.setAttribute('data-explorbot-checked', checked);
|
|
161
|
+
}
|
|
162
|
+
if (source instanceof HTMLSelectElement) {
|
|
163
|
+
target.setAttribute('data-explorbot-value', Array.from(source.selectedOptions)
|
|
164
|
+
.map((option) => option.value)
|
|
165
|
+
.join(','));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return clone.outerHTML;
|
|
169
|
+
}
|
|
142
170
|
export function extractElementData(el, config) {
|
|
143
171
|
const cfg = config ||
|
|
144
172
|
{
|
|
145
173
|
attrs: {
|
|
146
174
|
area: 'data-explorbot-area',
|
|
175
|
+
coveredBy: 'data-explorbot-covered-by',
|
|
147
176
|
context: 'data-explorbot-context',
|
|
148
177
|
eidx: 'data-explorbot-eidx',
|
|
178
|
+
hit: 'data-explorbot-hit',
|
|
149
179
|
variant: 'data-explorbot-variant',
|
|
150
180
|
},
|
|
151
181
|
codeEditorMarkers: ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'],
|
|
@@ -223,6 +253,43 @@ export function extractElementData(el, config) {
|
|
|
223
253
|
tokens.add('navigates');
|
|
224
254
|
return Array.from(tokens).slice(0, 8);
|
|
225
255
|
}
|
|
256
|
+
function describeOverlappingElement(target) {
|
|
257
|
+
const parts = [target.tagName.toLowerCase()];
|
|
258
|
+
const role = target.getAttribute('role');
|
|
259
|
+
const ariaLabel = target.getAttribute('aria-label');
|
|
260
|
+
const id = target.getAttribute('id');
|
|
261
|
+
const className = (target.getAttribute('class') || '').split(/\s+/).filter(Boolean).slice(0, 3).join('.');
|
|
262
|
+
if (id)
|
|
263
|
+
parts.push(`#${id.slice(0, 40)}`);
|
|
264
|
+
if (className)
|
|
265
|
+
parts.push(`.${className.slice(0, 80)}`);
|
|
266
|
+
if (role)
|
|
267
|
+
parts.push(`[role="${role.slice(0, 40)}"]`);
|
|
268
|
+
if (ariaLabel)
|
|
269
|
+
parts.push(`[aria-label="${ariaLabel.slice(0, 80)}"]`);
|
|
270
|
+
return parts.join('');
|
|
271
|
+
}
|
|
272
|
+
function inspectHitTarget(target, rect) {
|
|
273
|
+
const viewportPoints = [
|
|
274
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
275
|
+
{ x: rect.left + Math.min(rect.width - 1, Math.max(1, rect.width * 0.25)), y: rect.top + Math.min(rect.height - 1, Math.max(1, rect.height * 0.25)) },
|
|
276
|
+
{ x: rect.left + Math.min(rect.width - 1, Math.max(1, rect.width * 0.75)), y: rect.top + Math.min(rect.height - 1, Math.max(1, rect.height * 0.75)) },
|
|
277
|
+
].filter((point) => point.x >= 0 && point.y >= 0 && point.x <= window.innerWidth && point.y <= window.innerHeight);
|
|
278
|
+
if (viewportPoints.length === 0) {
|
|
279
|
+
return { hit: 'offscreen' };
|
|
280
|
+
}
|
|
281
|
+
for (const point of viewportPoints) {
|
|
282
|
+
const top = document.elementFromPoint(point.x, point.y);
|
|
283
|
+
if (!top)
|
|
284
|
+
continue;
|
|
285
|
+
if (top === target || target.contains(top)) {
|
|
286
|
+
return { hit: 'target' };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const centerPoint = viewportPoints[0];
|
|
290
|
+
const top = document.elementFromPoint(centerPoint.x, centerPoint.y);
|
|
291
|
+
return { hit: 'covered', coveredBy: top ? describeOverlappingElement(top) : undefined };
|
|
292
|
+
}
|
|
226
293
|
function isEmbeddedCodeEditorFrame(target) {
|
|
227
294
|
const src = (target.getAttribute('src') || '').toLowerCase();
|
|
228
295
|
const markerSelector = cfg.codeEditorMarkers.map((marker) => `[class*="${marker}"]`).join(', ');
|
|
@@ -329,6 +396,10 @@ export function extractElementData(el, config) {
|
|
|
329
396
|
allAttrs[cfg.attrs.area] = areaHints.join('|');
|
|
330
397
|
allAttrs[cfg.attrs.context] = findContextLabel(el);
|
|
331
398
|
allAttrs[cfg.attrs.variant] = collectVariantHints(el).join('|');
|
|
399
|
+
const hitTarget = inspectHitTarget(el, rect);
|
|
400
|
+
allAttrs[cfg.attrs.hit] = hitTarget.hit;
|
|
401
|
+
if (hitTarget.coveredBy)
|
|
402
|
+
allAttrs[cfg.attrs.coveredBy] = hitTarget.coveredBy;
|
|
332
403
|
return {
|
|
333
404
|
tag: el.tagName.toLowerCase(),
|
|
334
405
|
text: normalizeText(el.textContent || '').slice(0, cfg.maxTextLength),
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function loadMarkdownFiles(dir: string, options?: {
|
|
2
|
+
recursive?: boolean;
|
|
3
|
+
onError?: (filePath: string, error: unknown) => void;
|
|
4
|
+
}): MarkdownFile[];
|
|
5
|
+
export interface MarkdownFile {
|
|
6
|
+
filePath: string;
|
|
7
|
+
content: string;
|
|
8
|
+
data: Record<string, any>;
|
|
9
|
+
mtime: Date;
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
export function loadMarkdownFiles(dir, options = {}) {
|
|
5
|
+
if (!existsSync(dir))
|
|
6
|
+
return [];
|
|
7
|
+
const results = [];
|
|
8
|
+
const files = readdirSync(dir, { recursive: options.recursive === true })
|
|
9
|
+
.filter((file) => typeof file === 'string' && file.endsWith('.md'))
|
|
10
|
+
.map((file) => join(dir, file));
|
|
11
|
+
for (const filePath of files) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = matter(readFileSync(filePath, 'utf8'));
|
|
14
|
+
results.push({ filePath, content: parsed.content, data: parsed.data, mtime: statSync(filePath).mtime });
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
options.onError?.(filePath, error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
@@ -30,11 +30,17 @@ export declare class MarkdownQuery {
|
|
|
30
30
|
get(): string;
|
|
31
31
|
toJson(): Record<string, string>[];
|
|
32
32
|
replace(content: string): string;
|
|
33
|
+
replaceEach(replacer: (match: MarkdownQuery, index: number) => string): string;
|
|
33
34
|
count(): number;
|
|
34
35
|
first(): MarkdownQuery;
|
|
35
36
|
last(): MarkdownQuery;
|
|
36
37
|
before(): MarkdownQuery;
|
|
37
38
|
after(): MarkdownQuery;
|
|
38
39
|
each(): MarkdownQuery[];
|
|
40
|
+
meta(): Array<{
|
|
41
|
+
type: string;
|
|
42
|
+
depth: number | null;
|
|
43
|
+
text: string;
|
|
44
|
+
}>;
|
|
39
45
|
}
|
|
40
46
|
export declare function mdq(source: string): MarkdownQuery;
|
|
@@ -341,6 +341,9 @@ export class MarkdownQuery {
|
|
|
341
341
|
return results;
|
|
342
342
|
}
|
|
343
343
|
replace(content) {
|
|
344
|
+
return this.replaceEach(() => content);
|
|
345
|
+
}
|
|
346
|
+
replaceEach(replacer) {
|
|
344
347
|
const sorted = [...this.matches].sort((a, b) => a.start - b.start);
|
|
345
348
|
const kept = [];
|
|
346
349
|
let lastEnd = -1;
|
|
@@ -350,10 +353,11 @@ export class MarkdownQuery {
|
|
|
350
353
|
kept.push(range);
|
|
351
354
|
lastEnd = range.start + range.length;
|
|
352
355
|
}
|
|
356
|
+
const replacements = kept.map((range, index) => replacer(new MarkdownQuery(this.source, [range]), index));
|
|
353
357
|
let result = this.source;
|
|
354
358
|
for (let i = kept.length - 1; i >= 0; i--) {
|
|
355
359
|
const range = kept[i];
|
|
356
|
-
result = result.slice(0, range.start) +
|
|
360
|
+
result = result.slice(0, range.start) + replacements[i] + result.slice(range.start + range.length);
|
|
357
361
|
}
|
|
358
362
|
return result;
|
|
359
363
|
}
|
|
@@ -386,6 +390,15 @@ export class MarkdownQuery {
|
|
|
386
390
|
each() {
|
|
387
391
|
return this.matches.map((m) => new MarkdownQuery(this.source, [m]));
|
|
388
392
|
}
|
|
393
|
+
meta() {
|
|
394
|
+
return this.matches.map((range) => {
|
|
395
|
+
const token = range.token;
|
|
396
|
+
let depth = null;
|
|
397
|
+
if (token.type === 'heading')
|
|
398
|
+
depth = token.depth;
|
|
399
|
+
return { type: token.type, depth, text: getTokenText(range.token) };
|
|
400
|
+
});
|
|
401
|
+
}
|
|
389
402
|
}
|
|
390
403
|
export function mdq(source) {
|
|
391
404
|
return new MarkdownQuery(source);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const MIN_SECRET_LENGTH = 4;
|
|
2
|
+
const SECRET_NAME_TOKENS = ['password', 'passwd', 'secret', 'token', 'apikey', 'api_key', 'credential', 'private_key', 'privatekey', 'access_key', 'jwt', 'auth', 'bearer', 'authorization', 'cert', 'certificate'];
|
|
2
3
|
const secretValues = new Set();
|
|
3
4
|
export function registerSecret(value) {
|
|
4
5
|
if (!value)
|
|
@@ -11,7 +12,7 @@ export function redactSecrets(text) {
|
|
|
11
12
|
if (!text)
|
|
12
13
|
return text;
|
|
13
14
|
let result = text;
|
|
14
|
-
for (const value of secretValues) {
|
|
15
|
+
for (const value of [...secretValues].sort((a, b) => b.length - a.length)) {
|
|
15
16
|
if (!result.includes(value))
|
|
16
17
|
continue;
|
|
17
18
|
result = result.split(value).join('***REDACTED***');
|
|
@@ -21,7 +22,6 @@ export function redactSecrets(text) {
|
|
|
21
22
|
export function clearRegisteredSecrets() {
|
|
22
23
|
secretValues.clear();
|
|
23
24
|
}
|
|
24
|
-
const SECRET_NAME_TOKENS = ['password', 'passwd', 'secret', 'token', 'apikey', 'api_key', 'credential', 'private_key', 'privatekey', 'access_key'];
|
|
25
25
|
export function isSecretName(name) {
|
|
26
26
|
const lower = name.toLowerCase();
|
|
27
27
|
return SECRET_NAME_TOKENS.some((token) => lower.includes(token));
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare function truncateJson(input: any): string;
|
|
2
|
+
export declare function slugify(text: string): string;
|
|
3
|
+
export declare function normalizeInlineText(text: string): string;
|
|
2
4
|
export declare function sanitizeFilename(name: string): string;
|
|
3
5
|
export declare function safeFilename(name: string, ext?: string, maxBytes?: number): string;
|
|
@@ -5,6 +5,16 @@ export function truncateJson(input) {
|
|
|
5
5
|
const str = JSON.stringify(input);
|
|
6
6
|
return str.length <= 80 ? str : `${str.slice(0, 77)}...`;
|
|
7
7
|
}
|
|
8
|
+
export function slugify(text) {
|
|
9
|
+
return text
|
|
10
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
11
|
+
.replace(/_+/g, '_')
|
|
12
|
+
.replace(/^_|_$/g, '')
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
export function normalizeInlineText(text) {
|
|
16
|
+
return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
17
|
+
}
|
|
8
18
|
export function sanitizeFilename(name) {
|
|
9
19
|
return name
|
|
10
20
|
.toLowerCase()
|