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
package/dist/src/action.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { faker } from '@faker-js/faker';
|
|
4
3
|
import { context, trace } from '@opentelemetry/api';
|
|
5
4
|
import { container, recorder } from 'codeceptjs';
|
|
6
5
|
import * as codeceptjs from 'codeceptjs';
|
|
7
|
-
import { hopeThat, retryTo, tryTo, within } from 'codeceptjs/lib/effects';
|
|
8
|
-
import step from 'codeceptjs/steps';
|
|
9
6
|
import { ActionResult } from './action-result.js';
|
|
10
7
|
import { clearActivity, setActivity } from "./activity.js";
|
|
11
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
12
9
|
import { Observability } from "./observability.js";
|
|
13
|
-
import { isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
14
|
-
import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
10
|
+
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
11
|
+
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
15
12
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
16
|
-
import { waitForPageReadiness } from "./utils/page-readiness.js";
|
|
13
|
+
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
14
|
+
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
17
15
|
import { safeFilename } from "./utils/strings.js";
|
|
18
16
|
const debugLog = createDebug('explorbot:action');
|
|
19
17
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
@@ -24,7 +22,6 @@ class Action {
|
|
|
24
22
|
config;
|
|
25
23
|
// action info
|
|
26
24
|
action = null;
|
|
27
|
-
expectation = null;
|
|
28
25
|
lastError = null;
|
|
29
26
|
playwrightHelper;
|
|
30
27
|
playwrightGroupId = null;
|
|
@@ -38,9 +35,6 @@ class Action {
|
|
|
38
35
|
this.playwrightHelper = container.helpers('Playwright');
|
|
39
36
|
this.recorder = recorder;
|
|
40
37
|
}
|
|
41
|
-
async caputrePageWithScreenshot() {
|
|
42
|
-
return this.capturePageState({ includeScreenshot: true });
|
|
43
|
-
}
|
|
44
38
|
async saveScreenshot() {
|
|
45
39
|
const currentState = this.stateManager.getCurrentState();
|
|
46
40
|
if (currentState?.screenshotFile)
|
|
@@ -58,7 +52,7 @@ class Action {
|
|
|
58
52
|
return undefined;
|
|
59
53
|
}
|
|
60
54
|
}
|
|
61
|
-
async capturePageState({ includeScreenshot = false } = {}) {
|
|
55
|
+
async capturePageState({ includeScreenshot = false, codeBlock } = {}) {
|
|
62
56
|
try {
|
|
63
57
|
const currentState = this.stateManager.getCurrentState();
|
|
64
58
|
const stateHash = currentState?.hash || 'screenshot';
|
|
@@ -145,11 +139,11 @@ class Action {
|
|
|
145
139
|
ariaSnapshotFile,
|
|
146
140
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
147
141
|
});
|
|
148
|
-
this.stateManager.updateState(result);
|
|
142
|
+
this.stateManager.updateState(result, codeBlock);
|
|
149
143
|
return result;
|
|
150
144
|
}
|
|
151
145
|
catch (err) {
|
|
152
|
-
const msg =
|
|
146
|
+
const msg = browserErrorMessage(err);
|
|
153
147
|
if (isFatalBrowserError(err))
|
|
154
148
|
throw err;
|
|
155
149
|
debugLog('capturePageState failed with non-fatal error:', msg);
|
|
@@ -272,21 +266,18 @@ class Action {
|
|
|
272
266
|
}
|
|
273
267
|
if (isPlaywright) {
|
|
274
268
|
const page = this.playwrightHelper.page;
|
|
275
|
-
|
|
276
|
-
await asyncFn(page);
|
|
269
|
+
await playwrightSandbox(page, sanitizedCode);
|
|
277
270
|
await sleep(this.config.action?.delay || 500);
|
|
278
271
|
}
|
|
279
272
|
else {
|
|
280
|
-
|
|
281
|
-
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
273
|
+
codeceptJSSandbox(this.actor, sanitizedCode);
|
|
282
274
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
283
275
|
await recorder.promise();
|
|
284
276
|
}
|
|
285
|
-
const pageState = await this.capturePageState();
|
|
286
277
|
if (executedSteps.length > 0) {
|
|
287
278
|
codeString = executedSteps.join('\n');
|
|
288
279
|
}
|
|
289
|
-
this.
|
|
280
|
+
const pageState = await this.capturePageState({ codeBlock: codeString });
|
|
290
281
|
this.actionResult = pageState;
|
|
291
282
|
this.assertionSteps = assertionSteps;
|
|
292
283
|
}
|
|
@@ -313,79 +304,15 @@ class Action {
|
|
|
313
304
|
}
|
|
314
305
|
return this;
|
|
315
306
|
}
|
|
316
|
-
async
|
|
317
|
-
const codeString = typeof codeOrFunction === 'string' ? codeOrFunction : codeOrFunction.toString();
|
|
318
|
-
this.expectation = codeString.toString();
|
|
319
|
-
const expectationPreview = sanitizeCodeBlock(codeString)
|
|
320
|
-
.split('\n')
|
|
321
|
-
.map((line) => line.trim())
|
|
322
|
-
.filter(Boolean)
|
|
323
|
-
.slice(0, 2)
|
|
324
|
-
.join(' ');
|
|
325
|
-
tag('step').log(`Expecting: ${expectationPreview || 'assertion'}`);
|
|
326
|
-
try {
|
|
327
|
-
debugLog('Executing expectation:', codeString);
|
|
328
|
-
let codeFunction;
|
|
329
|
-
if (typeof codeOrFunction === 'function') {
|
|
330
|
-
codeFunction = codeOrFunction;
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
const sanitizedCode = sanitizeCodeBlock(codeString);
|
|
334
|
-
if (!sanitizedCode) {
|
|
335
|
-
throw new Error('No valid I.* commands found in code block');
|
|
336
|
-
}
|
|
337
|
-
codeFunction = createSandboxedFunction(['I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step'], sanitizedCode);
|
|
338
|
-
}
|
|
339
|
-
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
|
|
340
|
-
await recorder.promise();
|
|
341
|
-
debugLog('Expectation executed successfully');
|
|
342
|
-
// Get current state from state manager
|
|
343
|
-
const currentState = this.stateManager.getCurrentState();
|
|
344
|
-
if (currentState) {
|
|
345
|
-
// Create ActionResult from current state for compatibility
|
|
346
|
-
this.actionResult = new ActionResult({
|
|
347
|
-
url: currentState.fullUrl || '',
|
|
348
|
-
title: currentState.title,
|
|
349
|
-
timestamp: currentState.timestamp,
|
|
350
|
-
html: '', // Empty HTML for expectation state
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
return this;
|
|
354
|
-
}
|
|
355
|
-
catch (err) {
|
|
356
|
-
tag('error').log('Expectation failed:', errorToString(err));
|
|
357
|
-
this.lastError = err;
|
|
358
|
-
await recorder.reset();
|
|
359
|
-
await recorder.start();
|
|
360
|
-
debugLog('Expectation failed:', errorToString(err));
|
|
361
|
-
}
|
|
362
|
-
finally {
|
|
363
|
-
clearActivity();
|
|
364
|
-
}
|
|
365
|
-
return this;
|
|
366
|
-
}
|
|
367
|
-
async waitForInteraction() {
|
|
368
|
-
// start with basic approach
|
|
369
|
-
await this.actor.wait(0.5);
|
|
370
|
-
return this;
|
|
371
|
-
}
|
|
372
|
-
async attempt(codeBlock, originalMessage, experience = true) {
|
|
307
|
+
async attempt(codeBlock, originalMessage) {
|
|
373
308
|
try {
|
|
374
309
|
debugLog('Resolution attempt...');
|
|
375
310
|
setActivity('🦾 Acting in browser...', 'action');
|
|
376
311
|
if (!this.actionResult) {
|
|
377
312
|
this.actionResult = ActionResult.fromState(this.stateManager.getCurrentState());
|
|
378
313
|
}
|
|
379
|
-
const prevActionResult = this.actionResult;
|
|
380
314
|
this.lastError = null;
|
|
381
315
|
await this.execute(codeBlock);
|
|
382
|
-
if (!this.expectation && originalMessage) {
|
|
383
|
-
this.expectation = originalMessage;
|
|
384
|
-
}
|
|
385
|
-
if (!this.expectation) {
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
debugLog('Resolved Expectation:', this.expectation);
|
|
389
316
|
return true;
|
|
390
317
|
}
|
|
391
318
|
catch (error) {
|
|
@@ -399,12 +326,6 @@ class Action {
|
|
|
399
326
|
getActor() {
|
|
400
327
|
return this.actor;
|
|
401
328
|
}
|
|
402
|
-
setActor(actor) {
|
|
403
|
-
this.actor = actor;
|
|
404
|
-
}
|
|
405
|
-
getCurrentState() {
|
|
406
|
-
return this.actionResult;
|
|
407
|
-
}
|
|
408
329
|
getActionResult() {
|
|
409
330
|
return this.actionResult;
|
|
410
331
|
}
|
|
@@ -423,6 +344,13 @@ function errorToString(error) {
|
|
|
423
344
|
return error.message || error.toString();
|
|
424
345
|
}
|
|
425
346
|
async function captureHtml(page, frame, actor) {
|
|
347
|
+
for (const scope of [frame, page]) {
|
|
348
|
+
if (!scope?.evaluate)
|
|
349
|
+
continue;
|
|
350
|
+
const html = await scope.evaluate(captureHtmlForSnapshot).catch(() => null);
|
|
351
|
+
if (typeof html === 'string')
|
|
352
|
+
return html;
|
|
353
|
+
}
|
|
426
354
|
if (frame?.content)
|
|
427
355
|
return frame.content();
|
|
428
356
|
if (page?.content)
|
|
@@ -438,27 +366,6 @@ async function captureTitle(page, actor) {
|
|
|
438
366
|
return actor.grabTitle();
|
|
439
367
|
return '';
|
|
440
368
|
}
|
|
441
|
-
const SHADOWED_GLOBALS = ['process', 'global', 'globalThis', 'fetch', 'Bun', 'require', 'module', 'exports'];
|
|
442
|
-
export function sanitizeCodeBlock(code) {
|
|
443
|
-
return code
|
|
444
|
-
.split('\n')
|
|
445
|
-
.map((line) => line.trim())
|
|
446
|
-
.filter((line) => line.startsWith('I.') || line.startsWith('page.') || line.startsWith('await page.') || line.startsWith('await I.'))
|
|
447
|
-
.join('\n');
|
|
448
|
-
}
|
|
449
|
-
export function createSandboxedFunction(argNames, body) {
|
|
450
|
-
const fn = new Function(...argNames, ...SHADOWED_GLOBALS, `'use strict';\n${body}`);
|
|
451
|
-
return (...args) => fn(...args);
|
|
452
|
-
}
|
|
453
|
-
function hasPlaywrightCommands(code) {
|
|
454
|
-
return code.split('\n').some((line) => {
|
|
455
|
-
const trimmed = line.trim();
|
|
456
|
-
return trimmed.startsWith('page.') || trimmed.startsWith('await page.');
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
function sleep(ms) {
|
|
460
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
461
|
-
}
|
|
462
369
|
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
463
370
|
const attachStepLogger = (target, assertionsTarget) => {
|
|
464
371
|
const listener = (step, error) => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { ActionResult } from "../../action-result.js";
|
|
5
4
|
import { actionRule, locatorRule, sectionContextRule } from "../rules.js";
|
|
6
5
|
import { createAgentTools, createCodeceptJSTools } from "../tools.js";
|
|
7
6
|
import { debugLog } from "./mixin.js";
|
|
@@ -9,17 +8,11 @@ export function WithWebMode(Base) {
|
|
|
9
8
|
return class extends Base {
|
|
10
9
|
webModeTools(ctx) {
|
|
11
10
|
const explorer = ctx.explorBot.getExplorer();
|
|
12
|
-
const stateManager = explorer.getStateManager();
|
|
13
11
|
const codeceptTools = createCodeceptJSTools(explorer, ctx.task);
|
|
14
12
|
const agentTools = createAgentTools({
|
|
15
13
|
explorer,
|
|
16
14
|
researcher: ctx.explorBot.agentResearcher(),
|
|
17
15
|
navigator: ctx.explorBot.agentNavigator(),
|
|
18
|
-
experienceTracker: stateManager.getExperienceTracker(),
|
|
19
|
-
getState: () => {
|
|
20
|
-
const state = stateManager.getCurrentState();
|
|
21
|
-
return state ? ActionResult.fromState(state) : null;
|
|
22
|
-
},
|
|
23
16
|
});
|
|
24
17
|
const { see, context, visualClick, learnExperience } = agentTools;
|
|
25
18
|
return {
|
package/dist/src/ai/captain.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExperienceTracker } from '../experience-tracker.js';
|
|
1
|
+
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
2
2
|
import type { ExplorBot } from '../explorbot.js';
|
|
3
3
|
import type { WebPageState } from '../state-manager.js';
|
|
4
4
|
import { Task, Test } from '../test-plan.js';
|
|
@@ -55,8 +55,6 @@ export declare class Captain extends CaptainBase implements Agent {
|
|
|
55
55
|
systemPrompt(): string;
|
|
56
56
|
resetConversation(): Conversation;
|
|
57
57
|
ensureConversation(): Conversation;
|
|
58
|
-
getConversation(): Conversation | null;
|
|
59
|
-
cleanConversation(): void;
|
|
60
58
|
getPageContext(): Promise<string>;
|
|
61
59
|
planSummary(): string;
|
|
62
60
|
reinjectContextIfNeeded(conversation: Conversation, currentState: WebPageState): Promise<void>;
|
package/dist/src/ai/captain.js
CHANGED
|
@@ -2,8 +2,8 @@ import { tool } from 'ai';
|
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from '../action-result.js';
|
|
5
|
-
import { ExperienceTracker } from '../experience-tracker.js';
|
|
6
5
|
import { Task } from "../test-plan.js";
|
|
6
|
+
import { formatHeadings } from "../utils/context-formatter.js";
|
|
7
7
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
8
8
|
import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
|
|
9
9
|
import { loop } from '../utils/loop.js';
|
|
@@ -28,7 +28,7 @@ export class Captain extends CaptainBase {
|
|
|
28
28
|
constructor(explorBot) {
|
|
29
29
|
super();
|
|
30
30
|
this.explorBot = explorBot;
|
|
31
|
-
this.experienceTracker =
|
|
31
|
+
this.experienceTracker = explorBot.experienceTracker();
|
|
32
32
|
}
|
|
33
33
|
setCommandExecutor(fn, descriptions) {
|
|
34
34
|
this.commandExecutor = fn;
|
|
@@ -48,7 +48,7 @@ export class Captain extends CaptainBase {
|
|
|
48
48
|
return this.experienceTracker;
|
|
49
49
|
}
|
|
50
50
|
getKnowledgeTracker() {
|
|
51
|
-
return this.explorBot.
|
|
51
|
+
return this.explorBot.knowledgeTracker();
|
|
52
52
|
}
|
|
53
53
|
getProvider() {
|
|
54
54
|
return this.explorBot.getProvider();
|
|
@@ -126,13 +126,6 @@ export class Captain extends CaptainBase {
|
|
|
126
126
|
}
|
|
127
127
|
return this.conversation;
|
|
128
128
|
}
|
|
129
|
-
getConversation() {
|
|
130
|
-
return this.conversation;
|
|
131
|
-
}
|
|
132
|
-
cleanConversation() {
|
|
133
|
-
this.conversation = null;
|
|
134
|
-
tag('info').log('Conversation cleaned');
|
|
135
|
-
}
|
|
136
129
|
async getPageContext() {
|
|
137
130
|
const state = this.explorBot.getExplorer().getStateManager().getCurrentState();
|
|
138
131
|
if (!state) {
|
|
@@ -141,15 +134,7 @@ export class Captain extends CaptainBase {
|
|
|
141
134
|
const actionResult = ActionResult.fromState(state);
|
|
142
135
|
const knowledge = this.getKnowledge(actionResult);
|
|
143
136
|
const experience = this.getExperience(actionResult);
|
|
144
|
-
const headingLines =
|
|
145
|
-
if (state.h1)
|
|
146
|
-
headingLines.push(`H1: ${state.h1}`);
|
|
147
|
-
if (state.h2)
|
|
148
|
-
headingLines.push(`H2: ${state.h2}`);
|
|
149
|
-
if (state.h3)
|
|
150
|
-
headingLines.push(`H3: ${state.h3}`);
|
|
151
|
-
if (state.h4)
|
|
152
|
-
headingLines.push(`H4: ${state.h4}`);
|
|
137
|
+
const headingLines = formatHeadings(state);
|
|
153
138
|
const headingsBlock = headingLines.join('\n');
|
|
154
139
|
let pageSummary = '';
|
|
155
140
|
const cachedResearch = Researcher.getCachedResearch(state);
|
|
@@ -5,18 +5,16 @@ export interface ToolExecution {
|
|
|
5
5
|
output: any;
|
|
6
6
|
wasSuccessful: boolean;
|
|
7
7
|
}
|
|
8
|
+
export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
|
|
8
9
|
export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
|
|
9
10
|
export declare class Conversation {
|
|
10
|
-
id: string;
|
|
11
11
|
messages: ModelMessage[];
|
|
12
12
|
model: any;
|
|
13
|
-
telemetryFunctionId?: string;
|
|
14
13
|
protectedPrefixCount: number;
|
|
15
14
|
autoTrimRules: Map<string, number>;
|
|
16
|
-
constructor(messages?: ModelMessage[], model?: any
|
|
15
|
+
constructor(messages?: ModelMessage[], model?: any);
|
|
17
16
|
protectPrefix(count: number): void;
|
|
18
17
|
addUserText(text: string): void;
|
|
19
|
-
addUserImage(image: string): void;
|
|
20
18
|
addAssistantText(text: string): void;
|
|
21
19
|
getLastMessage(): string;
|
|
22
20
|
clone(): Conversation;
|
|
@@ -26,6 +24,5 @@ export declare class Conversation {
|
|
|
26
24
|
markLastMessageCacheable(): void;
|
|
27
25
|
hasTag(tagName: string, lastN?: number): boolean;
|
|
28
26
|
applyAutoTrim(text: string): string;
|
|
29
|
-
generateId(): string;
|
|
30
27
|
getToolExecutions(): ToolExecution[];
|
|
31
28
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
+
export function toToolExecution(toolName, input, rawOutput) {
|
|
2
|
+
let output = rawOutput;
|
|
3
|
+
if (rawOutput?.type === 'json' && rawOutput?.value)
|
|
4
|
+
output = rawOutput.value;
|
|
5
|
+
return { toolName, input, output, wasSuccessful: output?.success !== false };
|
|
6
|
+
}
|
|
1
7
|
export function toolExecutionLabel(input) {
|
|
2
8
|
return input?.explanation || input?.assertion || input?.reason || input?.request || '';
|
|
3
9
|
}
|
|
4
10
|
const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
|
|
5
11
|
const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
|
|
6
12
|
export class Conversation {
|
|
7
|
-
id;
|
|
8
13
|
messages;
|
|
9
14
|
model;
|
|
10
|
-
telemetryFunctionId;
|
|
11
15
|
protectedPrefixCount = 0;
|
|
12
16
|
autoTrimRules;
|
|
13
|
-
constructor(messages = [], model
|
|
14
|
-
this.id = this.generateId();
|
|
17
|
+
constructor(messages = [], model) {
|
|
15
18
|
this.messages = messages;
|
|
16
19
|
this.model = model || '';
|
|
17
|
-
this.telemetryFunctionId = telemetryFunctionId;
|
|
18
20
|
this.autoTrimRules = new Map();
|
|
19
21
|
}
|
|
20
22
|
protectPrefix(count) {
|
|
@@ -26,17 +28,6 @@ export class Conversation {
|
|
|
26
28
|
content: this.applyAutoTrim(text),
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
|
-
addUserImage(image) {
|
|
30
|
-
if (!image || image.trim() === '') {
|
|
31
|
-
console.warn('Warning: Attempting to add empty image to conversation');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const imageData = image.startsWith('data:') ? image : `data:image/png;base64,${image}`;
|
|
35
|
-
this.messages.push({
|
|
36
|
-
role: 'user',
|
|
37
|
-
content: [{ type: 'file', mediaType: 'image/png', data: imageData }],
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
31
|
addAssistantText(text) {
|
|
41
32
|
// Skip empty or whitespace-only messages
|
|
42
33
|
if (!text || text.trim() === '') {
|
|
@@ -61,7 +52,7 @@ export class Conversation {
|
|
|
61
52
|
return '';
|
|
62
53
|
}
|
|
63
54
|
clone() {
|
|
64
|
-
return new Conversation([...this.messages], this.model
|
|
55
|
+
return new Conversation([...this.messages], this.model);
|
|
65
56
|
}
|
|
66
57
|
cleanupTag(tagName, replacement, keepLast = 0) {
|
|
67
58
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -190,9 +181,6 @@ export class Conversation {
|
|
|
190
181
|
}
|
|
191
182
|
return result;
|
|
192
183
|
}
|
|
193
|
-
generateId() {
|
|
194
|
-
return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
195
|
-
}
|
|
196
184
|
getToolExecutions() {
|
|
197
185
|
const toolCalls = new Map();
|
|
198
186
|
for (const message of this.messages) {
|
|
@@ -215,14 +203,7 @@ export class Conversation {
|
|
|
215
203
|
for (const part of message.content) {
|
|
216
204
|
if (part.type !== 'tool-result')
|
|
217
205
|
continue;
|
|
218
|
-
|
|
219
|
-
const output = rawOutput?.type === 'json' && rawOutput?.value ? rawOutput.value : rawOutput;
|
|
220
|
-
executions.push({
|
|
221
|
-
toolName: part.toolName,
|
|
222
|
-
input: toolCalls.get(part.toolCallId) || {},
|
|
223
|
-
output,
|
|
224
|
-
wasSuccessful: output?.success !== false,
|
|
225
|
-
});
|
|
206
|
+
executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
|
|
226
207
|
}
|
|
227
208
|
}
|
|
228
209
|
return executions;
|
package/dist/src/ai/driller.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { Plan, Test } from '../test-plan.js';
|
|
|
6
6
|
import { HooksRunner } from '../utils/hooks-runner.js';
|
|
7
7
|
import { WebElement } from '../utils/web-element.js';
|
|
8
8
|
import type { Agent } from './agent.js';
|
|
9
|
-
import type { Conversation } from './conversation.js';
|
|
10
9
|
import type { Navigator } from './navigator.js';
|
|
11
10
|
import type { Provider } from './provider.js';
|
|
12
11
|
import { TaskAgent } from './task-agent.js';
|
|
@@ -54,7 +53,6 @@ export declare class Driller extends TaskAgent implements Agent {
|
|
|
54
53
|
navigator: Navigator;
|
|
55
54
|
hooksRunner: HooksRunner;
|
|
56
55
|
currentPlan?: Plan;
|
|
57
|
-
currentConversation: Conversation | null;
|
|
58
56
|
allResults: InteractionResult[];
|
|
59
57
|
verifiedAction: {
|
|
60
58
|
componentId: string;
|
|
@@ -153,8 +151,6 @@ export declare class Driller extends TaskAgent implements Agent {
|
|
|
153
151
|
saveToKnowledge(knowledgePath: string, state: ActionResult, results: InteractionResult[]): Promise<void>;
|
|
154
152
|
generateKnowledgeContent(state: ActionResult, interactions: InteractionResult[]): string;
|
|
155
153
|
logSummary(): void;
|
|
156
|
-
getCurrentPlan(): Plan | undefined;
|
|
157
|
-
getConversation(): Conversation | null;
|
|
158
154
|
}
|
|
159
155
|
export declare function buildCanonicalClickCode(component: ComponentInfo): string;
|
|
160
156
|
export declare function formatExperienceTitle(interaction: InteractionResult): string;
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -11,7 +11,7 @@ import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES
|
|
|
11
11
|
import { createDebug, tag } from "../utils/logger.js";
|
|
12
12
|
import { loop, pause } from "../utils/loop.js";
|
|
13
13
|
import { WebElement } from "../utils/web-element.js";
|
|
14
|
-
import {
|
|
14
|
+
import { drillLocatorRule } from "./rules.js";
|
|
15
15
|
import { TaskAgent, isInteractive } from "./task-agent.js";
|
|
16
16
|
import { createCodeceptJSTools } from "./tools.js";
|
|
17
17
|
const debugLog = createDebug('explorbot:driller');
|
|
@@ -23,7 +23,6 @@ export class Driller extends TaskAgent {
|
|
|
23
23
|
navigator;
|
|
24
24
|
hooksRunner;
|
|
25
25
|
currentPlan;
|
|
26
|
-
currentConversation = null;
|
|
27
26
|
allResults = [];
|
|
28
27
|
verifiedAction = null;
|
|
29
28
|
pendingNestedContext = null;
|
|
@@ -245,7 +244,6 @@ export class Driller extends TaskAgent {
|
|
|
245
244
|
this.verifiedAction = null;
|
|
246
245
|
this.pendingNestedContext = null;
|
|
247
246
|
const conversation = this.provider.startConversation(this.getSystemMessage(component), 'driller');
|
|
248
|
-
this.currentConversation = conversation;
|
|
249
247
|
conversation.addUserText(await this.buildComponentPrompt(originalState, component));
|
|
250
248
|
let finished = false;
|
|
251
249
|
const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.explorer, test), component);
|
|
@@ -506,10 +504,10 @@ export class Driller extends TaskAgent {
|
|
|
506
504
|
const targetUrl = originalState.fullUrl || originalState.url;
|
|
507
505
|
const action = this.explorer.createAction();
|
|
508
506
|
if (currentState?.url !== originalState.url) {
|
|
509
|
-
await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)
|
|
507
|
+
await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (restore URL)`);
|
|
510
508
|
return;
|
|
511
509
|
}
|
|
512
|
-
await action.attempt('I.pressKey("Escape")', `${reason} (restore state)
|
|
510
|
+
await action.attempt('I.pressKey("Escape")', `${reason} (restore state)`);
|
|
513
511
|
}
|
|
514
512
|
async saveToExperience(state, results) {
|
|
515
513
|
const experienceTracker = this.getExperienceTracker();
|
|
@@ -667,12 +665,6 @@ export class Driller extends TaskAgent {
|
|
|
667
665
|
tag('step').log(` ${status} ${componentTest.component?.name || test.scenario}`);
|
|
668
666
|
}
|
|
669
667
|
}
|
|
670
|
-
getCurrentPlan() {
|
|
671
|
-
return this.currentPlan;
|
|
672
|
-
}
|
|
673
|
-
getConversation() {
|
|
674
|
-
return this.currentConversation;
|
|
675
|
-
}
|
|
676
668
|
}
|
|
677
669
|
function formatAriaNode(node) {
|
|
678
670
|
const role = typeof node.role === 'string' ? node.role : 'unknown';
|
|
@@ -1105,4 +1097,3 @@ function isInteractiveElement(element) {
|
|
|
1105
1097
|
return true;
|
|
1106
1098
|
return false;
|
|
1107
1099
|
}
|
|
1108
|
-
const drillLocatorRule = locatorRule.replace(/<context_simplification>[\s\S]*?<\/context_simplification>/, '').trim();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExperienceFile, type ExperienceTracker } from '../experience-tracker.js';
|
|
2
2
|
import type { Agent } from './agent.js';
|
|
3
3
|
import type { Provider } from './provider.js';
|
|
4
|
-
export
|
|
4
|
+
export declare const COMPACT_MAX_LENGTH = 5000;
|
|
5
5
|
interface MergeGroup {
|
|
6
6
|
pattern: string;
|
|
7
7
|
files: ExperienceFile[];
|
|
@@ -10,7 +10,6 @@ export declare class ExperienceCompactor implements Agent {
|
|
|
10
10
|
emoji: string;
|
|
11
11
|
provider: Provider;
|
|
12
12
|
experienceTracker: ExperienceTracker;
|
|
13
|
-
MAX_LENGTH: number;
|
|
14
13
|
constructor(provider: Provider, experienceTracker: ExperienceTracker);
|
|
15
14
|
compactExperience(experience: string): Promise<string>;
|
|
16
15
|
stripNonUsefulEntries(content: string): string;
|
|
@@ -51,3 +50,4 @@ export declare class ExperienceCompactor implements Agent {
|
|
|
51
50
|
title?: string;
|
|
52
51
|
}): string;
|
|
53
52
|
}
|
|
53
|
+
export {};
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import { unlinkSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
2
3
|
import dedent from 'dedent';
|
|
3
|
-
import { marked } from 'marked';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { RECENT_WINDOW_DAYS } from '../experience-tracker.js';
|
|
6
6
|
import { Observability } from '../observability.js';
|
|
7
7
|
import { createDebug, log, tag } from '../utils/logger.js';
|
|
8
8
|
import { mdq } from '../utils/markdown-query.js';
|
|
9
|
+
import { isNonReusableCode } from '../utils/step-analyzer.js';
|
|
9
10
|
import { generalizeUrl, hasDynamicUrlSegment } from '../utils/url-matcher.js';
|
|
10
11
|
const debugLog = createDebug('explorbot:experience-compactor');
|
|
12
|
+
export const COMPACT_MAX_LENGTH = 5000;
|
|
11
13
|
export class ExperienceCompactor {
|
|
12
14
|
emoji = '🗜️';
|
|
13
15
|
provider;
|
|
14
16
|
experienceTracker;
|
|
15
|
-
MAX_LENGTH = 5000;
|
|
16
17
|
constructor(provider, experienceTracker) {
|
|
17
18
|
this.provider = provider;
|
|
18
19
|
this.experienceTracker = experienceTracker;
|
|
19
20
|
}
|
|
20
21
|
async compactExperience(experience) {
|
|
21
22
|
const stripped = this.stripNonUsefulEntries(experience);
|
|
22
|
-
if (stripped.length <
|
|
23
|
+
if (stripped.length < COMPACT_MAX_LENGTH) {
|
|
23
24
|
return stripped;
|
|
24
25
|
}
|
|
25
26
|
const prompt = this.buildCompactionPrompt(stripped);
|
|
@@ -101,7 +102,7 @@ export class ExperienceCompactor {
|
|
|
101
102
|
const generalized = generalizeUrl(url);
|
|
102
103
|
if (generalized === url)
|
|
103
104
|
continue;
|
|
104
|
-
const stateHash = file.filePath
|
|
105
|
+
const stateHash = basename(file.filePath, '.md');
|
|
105
106
|
const newData = { ...file.data, url: `~${generalized}~`, mergedFrom: [url] };
|
|
106
107
|
this.experienceTracker.writeExperienceFile(stateHash, file.content, newData);
|
|
107
108
|
tag('substep').log(`Generalized URL: ${url} → ~${generalized}~`);
|
|
@@ -110,7 +111,7 @@ export class ExperienceCompactor {
|
|
|
110
111
|
return count;
|
|
111
112
|
}
|
|
112
113
|
async compactFiles(files, options) {
|
|
113
|
-
const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >=
|
|
114
|
+
const workingSet = options?.skipSmall ? files.filter((f) => f.content.length >= COMPACT_MAX_LENGTH) : files;
|
|
114
115
|
let compactedCount = 0;
|
|
115
116
|
const total = workingSet.length;
|
|
116
117
|
const aiReviewCount = workingSet.filter((f) => this.isRecent(f)).length;
|
|
@@ -119,7 +120,7 @@ export class ExperienceCompactor {
|
|
|
119
120
|
}
|
|
120
121
|
for (let i = 0; i < workingSet.length; i++) {
|
|
121
122
|
const experience = workingSet[i];
|
|
122
|
-
const shortName = experience.filePath
|
|
123
|
+
const shortName = basename(experience.filePath);
|
|
123
124
|
const willReview = this.isRecent(experience);
|
|
124
125
|
if (total > 1 && willReview) {
|
|
125
126
|
tag('substep').log(`[${i + 1}/${total}] reviewing ${shortName}`);
|
|
@@ -128,9 +129,9 @@ export class ExperienceCompactor {
|
|
|
128
129
|
if (willReview) {
|
|
129
130
|
content = await this.reviewExperienceQuality(content, experience.data);
|
|
130
131
|
}
|
|
131
|
-
if (content.length >=
|
|
132
|
+
if (content.length >= COMPACT_MAX_LENGTH) {
|
|
132
133
|
if (total > 1)
|
|
133
|
-
tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${
|
|
134
|
+
tag('substep').log(`[${i + 1}/${total}] compacting ${shortName} (over ${COMPACT_MAX_LENGTH} chars)`);
|
|
134
135
|
const prompt = this.buildCompactionPrompt(content);
|
|
135
136
|
const model = this.provider.getModelForAgent('experience-compactor');
|
|
136
137
|
const response = await this.provider.chat([
|
|
@@ -141,7 +142,7 @@ export class ExperienceCompactor {
|
|
|
141
142
|
}
|
|
142
143
|
if (content === experience.content)
|
|
143
144
|
continue;
|
|
144
|
-
const stateHash = experience.filePath
|
|
145
|
+
const stateHash = basename(experience.filePath, '.md');
|
|
145
146
|
this.experienceTracker.writeExperienceFile(stateHash, content, experience.data);
|
|
146
147
|
debugLog('Experience file compacted:', experience.filePath);
|
|
147
148
|
compactedCount++;
|
|
@@ -250,7 +251,7 @@ export class ExperienceCompactor {
|
|
|
250
251
|
async mergeExperienceGroup(group) {
|
|
251
252
|
const [targetFile, ...sourceFiles] = group.files;
|
|
252
253
|
const combinedContent = group.files.map((f) => f.content).join('\n\n---\n\n');
|
|
253
|
-
const targetStateHash = targetFile.filePath
|
|
254
|
+
const targetStateHash = basename(targetFile.filePath, '.md');
|
|
254
255
|
const newFrontmatter = {
|
|
255
256
|
...targetFile.data,
|
|
256
257
|
url: group.pattern,
|
|
@@ -283,7 +284,7 @@ export class ExperienceCompactor {
|
|
|
283
284
|
<rules>
|
|
284
285
|
- Use markdown h2 headers only (##) - NO XML tags or wrappers in output
|
|
285
286
|
- Merge similar flows to remove duplicates
|
|
286
|
-
- Keep output under ${
|
|
287
|
+
- Keep output under ${COMPACT_MAX_LENGTH} characters
|
|
287
288
|
- Be explicit and short - no proposals or explanations
|
|
288
289
|
|
|
289
290
|
KEEP only:
|
|
@@ -368,27 +369,9 @@ export class ExperienceCompactor {
|
|
|
368
369
|
}
|
|
369
370
|
}
|
|
370
371
|
function listSections(content) {
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
let currentRaw = '';
|
|
375
|
-
const flush = () => {
|
|
376
|
-
if (currentHeading !== null)
|
|
377
|
-
sections.push({ title: currentHeading, raw: currentRaw });
|
|
378
|
-
};
|
|
379
|
-
for (const token of tokens) {
|
|
380
|
-
const raw = token.raw || '';
|
|
381
|
-
if (token.type === 'heading' && token.depth === 2) {
|
|
382
|
-
flush();
|
|
383
|
-
currentHeading = token.text.trim();
|
|
384
|
-
currentRaw = raw;
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
if (currentHeading !== null)
|
|
388
|
-
currentRaw += raw;
|
|
389
|
-
}
|
|
390
|
-
flush();
|
|
391
|
-
return sections;
|
|
372
|
+
const sections = mdq(content).query('section2').each();
|
|
373
|
+
const metas = mdq(content).query('section2').meta();
|
|
374
|
+
return sections.map((q, i) => ({ title: metas[i].text.trim(), raw: q.text() }));
|
|
392
375
|
}
|
|
393
376
|
function dropEmptySections(content) {
|
|
394
377
|
let result = content;
|
|
@@ -408,7 +391,7 @@ function dropNonReusableSections(content) {
|
|
|
408
391
|
const sections = [...mdq(result).query('section2(~"FLOW:")').each(), ...mdq(result).query('section2(~"ACTION:")').each()];
|
|
409
392
|
for (const section of sections) {
|
|
410
393
|
const raw = section.text();
|
|
411
|
-
if (
|
|
394
|
+
if (!isNonReusableCode(raw))
|
|
412
395
|
continue;
|
|
413
396
|
result = result.replace(raw, '');
|
|
414
397
|
}
|