explorbot 0.1.26 → 0.1.28
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/package.json +1 -1
- package/dist/src/action-result.js +2 -0
- package/dist/src/action.js +88 -7
- package/dist/src/ai/captain/file-tools.js +100 -0
- package/dist/src/ai/captain/idle-mode.js +70 -6
- package/dist/src/ai/captain/web-mode.js +36 -6
- package/dist/src/ai/captain.js +87 -19
- package/dist/src/ai/historian/screencast.js +11 -2
- package/dist/src/ai/navigator.js +5 -2
- package/dist/src/ai/pilot.js +33 -5
- package/dist/src/ai/researcher/cache.js +8 -0
- package/dist/src/ai/researcher/coordinates.js +2 -3
- package/dist/src/ai/researcher/deep-analysis.js +149 -65
- package/dist/src/ai/researcher/locators.js +1 -2
- package/dist/src/ai/researcher.js +17 -18
- package/dist/src/ai/task-agent.js +1 -0
- package/dist/src/ai/tester.js +91 -39
- package/dist/src/ai/tools.js +17 -7
- package/dist/src/commands/explore-command.js +6 -1
- package/dist/src/components/LogPane.js +4 -3
- package/dist/src/explorer.js +270 -35
- package/dist/src/utils/browser-errors.js +23 -0
- package/dist/src/utils/error-page.js +17 -2
- package/dist/src/utils/logger.js +2 -2
- package/package.json +1 -1
- package/src/action-result.ts +2 -0
- package/src/action.ts +83 -7
- package/src/ai/captain/file-tools.ts +126 -0
- package/src/ai/captain/idle-mode.ts +72 -6
- package/src/ai/captain/mixin.ts +1 -1
- package/src/ai/captain/web-mode.ts +40 -5
- package/src/ai/captain.ts +94 -20
- package/src/ai/historian/screencast.ts +11 -2
- package/src/ai/navigator.ts +6 -2
- package/src/ai/pilot.ts +34 -5
- package/src/ai/researcher/cache.ts +7 -0
- package/src/ai/researcher/coordinates.ts +2 -3
- package/src/ai/researcher/deep-analysis.ts +169 -72
- package/src/ai/researcher/locators.ts +1 -2
- package/src/ai/researcher.ts +17 -18
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +101 -41
- package/src/ai/tools.ts +17 -7
- package/src/commands/explore-command.ts +6 -1
- package/src/components/LogPane.tsx +4 -3
- package/src/explorer.ts +295 -38
- package/src/state-manager.ts +2 -0
- package/src/utils/browser-errors.ts +25 -0
- package/src/utils/error-page.ts +16 -3
- package/src/utils/logger.ts +3 -3
package/dist/package.json
CHANGED
|
@@ -10,6 +10,7 @@ const debugLog = createDebug('explorbot:state');
|
|
|
10
10
|
export class ActionResult {
|
|
11
11
|
id;
|
|
12
12
|
title = '';
|
|
13
|
+
httpStatus = undefined;
|
|
13
14
|
error = null;
|
|
14
15
|
timestamp = new Date();
|
|
15
16
|
h1 = undefined;
|
|
@@ -39,6 +40,7 @@ export class ActionResult {
|
|
|
39
40
|
this.url = data.url ?? '';
|
|
40
41
|
this.fullUrl = data.fullUrl;
|
|
41
42
|
this.title = data.title ?? '';
|
|
43
|
+
this.httpStatus = data.httpStatus;
|
|
42
44
|
this.error = data.error ?? null;
|
|
43
45
|
this.browserLogs = data.browserLogs ?? [];
|
|
44
46
|
this.iframeSnapshots = data.iframeSnapshots ?? [];
|
package/dist/src/action.js
CHANGED
|
@@ -13,8 +13,8 @@ import { Observability } from "./observability.js";
|
|
|
13
13
|
import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
14
14
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
15
15
|
import { safeFilename } from "./utils/strings.js";
|
|
16
|
+
import { isFatalBrowserError } from "./utils/browser-errors.js";
|
|
16
17
|
const debugLog = createDebug('explorbot:action');
|
|
17
|
-
const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
|
|
18
18
|
class Action {
|
|
19
19
|
actor;
|
|
20
20
|
stateManager;
|
|
@@ -28,6 +28,7 @@ class Action {
|
|
|
28
28
|
playwrightGroupId = null;
|
|
29
29
|
assertionSteps = [];
|
|
30
30
|
recorder;
|
|
31
|
+
mainDocumentStatus = undefined;
|
|
31
32
|
constructor(actor, stateManager, recorder) {
|
|
32
33
|
this.actor = actor;
|
|
33
34
|
this.stateManager = stateManager;
|
|
@@ -63,20 +64,25 @@ class Action {
|
|
|
63
64
|
const page = this.playwrightHelper.page;
|
|
64
65
|
const frame = this.playwrightHelper.frame;
|
|
65
66
|
await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => { });
|
|
66
|
-
|
|
67
|
+
await waitForUsablePageDom(page);
|
|
68
|
+
const grabAll = () => Promise.all([captureHtml(page, frame, this.actor), captureTitle(page, this.actor), this.captureBrowserLogs()]);
|
|
67
69
|
const [html, title, browserLogs] = await grabAll().catch(async (err) => {
|
|
68
70
|
const msg = err instanceof Error ? err.message : String(err);
|
|
69
71
|
if (!/navigating and changing the content/i.test(msg))
|
|
70
72
|
throw err;
|
|
71
73
|
await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => { });
|
|
74
|
+
await waitForUsablePageDom(page);
|
|
72
75
|
return grabAll();
|
|
73
76
|
});
|
|
74
77
|
const url = page?.url() || (await this.actor.grabCurrentUrl?.());
|
|
75
78
|
let screenshotFile = undefined;
|
|
79
|
+
const statesDir = outputPath('states');
|
|
80
|
+
fs.mkdirSync(statesDir, { recursive: true });
|
|
76
81
|
if (includeScreenshot) {
|
|
77
82
|
const filename = safeFilename(`${stateHash}_${timestamp}`, '.png');
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
const screenshotPath = join(statesDir, filename);
|
|
84
|
+
screenshotFile = await page
|
|
85
|
+
?.screenshot({ path: screenshotPath, fullPage: true })
|
|
80
86
|
.then(() => filename)
|
|
81
87
|
.catch((err) => {
|
|
82
88
|
debugLog('Screenshot failed, continuing without it:', err);
|
|
@@ -84,8 +90,6 @@ class Action {
|
|
|
84
90
|
});
|
|
85
91
|
}
|
|
86
92
|
// Save HTML to file
|
|
87
|
-
const statesDir = outputPath('states');
|
|
88
|
-
fs.mkdirSync(statesDir, { recursive: true });
|
|
89
93
|
const htmlFile = safeFilename(`${stateHash}_${timestamp}`, '.html');
|
|
90
94
|
const htmlPath = join(statesDir, htmlFile);
|
|
91
95
|
fs.writeFileSync(htmlPath, html, 'utf8');
|
|
@@ -121,6 +125,7 @@ class Action {
|
|
|
121
125
|
const result = new ActionResult({
|
|
122
126
|
html,
|
|
123
127
|
title,
|
|
128
|
+
httpStatus: await this.captureMainDocumentStatus(),
|
|
124
129
|
url,
|
|
125
130
|
browserLogs,
|
|
126
131
|
htmlFile,
|
|
@@ -136,13 +141,57 @@ class Action {
|
|
|
136
141
|
}
|
|
137
142
|
catch (err) {
|
|
138
143
|
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
-
if (
|
|
144
|
+
if (isFatalBrowserError(err))
|
|
140
145
|
throw err;
|
|
141
146
|
debugLog('capturePageState failed with non-fatal error:', msg);
|
|
142
147
|
const url = this.playwrightHelper.page?.url?.() || '';
|
|
143
148
|
return new ActionResult({ url, error: msg });
|
|
144
149
|
}
|
|
145
150
|
}
|
|
151
|
+
async captureMainDocumentStatus() {
|
|
152
|
+
if (this.mainDocumentStatus)
|
|
153
|
+
return this.mainDocumentStatus;
|
|
154
|
+
try {
|
|
155
|
+
const page = this.playwrightHelper.page;
|
|
156
|
+
const status = await page.evaluate(() => {
|
|
157
|
+
const navigation = performance.getEntriesByType('navigation').at(-1);
|
|
158
|
+
if (!navigation)
|
|
159
|
+
return undefined;
|
|
160
|
+
if (new URL(navigation.name).href !== window.location.href)
|
|
161
|
+
return undefined;
|
|
162
|
+
return navigation.responseStatus;
|
|
163
|
+
});
|
|
164
|
+
if (typeof status !== 'number')
|
|
165
|
+
return undefined;
|
|
166
|
+
if (status <= 0)
|
|
167
|
+
return undefined;
|
|
168
|
+
return status;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
captureMainDocumentResponse() {
|
|
175
|
+
const page = this.playwrightHelper.page;
|
|
176
|
+
if (!page?.on || !page?.off)
|
|
177
|
+
return () => { };
|
|
178
|
+
this.mainDocumentStatus = undefined;
|
|
179
|
+
const handler = (response) => {
|
|
180
|
+
const request = response.request();
|
|
181
|
+
if (request.resourceType() !== 'document')
|
|
182
|
+
return;
|
|
183
|
+
if (response.frame() !== page.mainFrame())
|
|
184
|
+
return;
|
|
185
|
+
const status = response.status();
|
|
186
|
+
if (typeof status !== 'number')
|
|
187
|
+
return;
|
|
188
|
+
if (status <= 0)
|
|
189
|
+
return;
|
|
190
|
+
this.mainDocumentStatus = status;
|
|
191
|
+
};
|
|
192
|
+
page.on('response', handler);
|
|
193
|
+
return () => page.off('response', handler);
|
|
194
|
+
}
|
|
146
195
|
/**
|
|
147
196
|
* Capture HTML snapshots of all iframes on the page
|
|
148
197
|
*/
|
|
@@ -200,6 +249,7 @@ class Action {
|
|
|
200
249
|
const stepListener = attachStepLogger(executedSteps, assertionSteps);
|
|
201
250
|
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
202
251
|
this.playwrightGroupId = groupId;
|
|
252
|
+
const detachMainDocumentResponse = this.captureMainDocumentResponse();
|
|
203
253
|
const activeSpan = Observability.getSpan();
|
|
204
254
|
const tracer = trace.getTracer('ai');
|
|
205
255
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
@@ -242,6 +292,7 @@ class Action {
|
|
|
242
292
|
throw err;
|
|
243
293
|
}
|
|
244
294
|
finally {
|
|
295
|
+
detachMainDocumentResponse();
|
|
245
296
|
if (groupId)
|
|
246
297
|
await this.recorder.endAction();
|
|
247
298
|
detachStepLogger(stepListener);
|
|
@@ -330,6 +381,8 @@ class Action {
|
|
|
330
381
|
}
|
|
331
382
|
catch (error) {
|
|
332
383
|
this.lastError = error;
|
|
384
|
+
if (isFatalBrowserError(error))
|
|
385
|
+
throw error;
|
|
333
386
|
debugLog(`Attempt failed: ${codeBlock}: ${errorToString(error) || this.lastError?.toString()}`);
|
|
334
387
|
return false;
|
|
335
388
|
}
|
|
@@ -354,6 +407,34 @@ function errorToString(error) {
|
|
|
354
407
|
}
|
|
355
408
|
return error.message || error.toString();
|
|
356
409
|
}
|
|
410
|
+
async function waitForUsablePageDom(page) {
|
|
411
|
+
if (!page?.waitForFunction)
|
|
412
|
+
return;
|
|
413
|
+
await page
|
|
414
|
+
.waitForFunction(() => {
|
|
415
|
+
const body = document.body;
|
|
416
|
+
if (!body)
|
|
417
|
+
return false;
|
|
418
|
+
return body.children.length > 0 || body.textContent?.trim().length > 0;
|
|
419
|
+
}, undefined, { timeout: 5000 })
|
|
420
|
+
.catch(() => { });
|
|
421
|
+
}
|
|
422
|
+
async function captureHtml(page, frame, actor) {
|
|
423
|
+
if (frame?.content)
|
|
424
|
+
return frame.content();
|
|
425
|
+
if (page?.content)
|
|
426
|
+
return page.content();
|
|
427
|
+
if (actor?.grabSource)
|
|
428
|
+
return actor.grabSource();
|
|
429
|
+
throw new Error('Playwright page is unavailable for HTML capture');
|
|
430
|
+
}
|
|
431
|
+
async function captureTitle(page, actor) {
|
|
432
|
+
if (page?.title)
|
|
433
|
+
return page.title();
|
|
434
|
+
if (actor?.grabTitle)
|
|
435
|
+
return actor.grabTitle();
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
357
438
|
function sanitizeCodeBlock(code) {
|
|
358
439
|
return code
|
|
359
440
|
.split('\n')
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
|
3
|
+
export const CAPTAIN_ARTIFACT_DIRS = ['reports', 'plans', 'tests', 'states'];
|
|
4
|
+
export const CAPTAIN_ALLOWED_READ_DIRS = ['output', 'knowledge', 'experience'];
|
|
5
|
+
export const CAPTAIN_ARTIFACT_SCAN_LIMIT = 200;
|
|
6
|
+
export const CAPTAIN_ARTIFACT_LIST_LIMIT = 20;
|
|
7
|
+
export const CAPTAIN_READ_FILE_DEFAULT_LIMIT = 12000;
|
|
8
|
+
export const CAPTAIN_READ_FILE_MAX_LIMIT = 50000;
|
|
9
|
+
export const CAPTAIN_READ_FILE_MIN_LIMIT = 1000;
|
|
10
|
+
export function listRecentArtifacts(outputDir) {
|
|
11
|
+
const artifacts = [];
|
|
12
|
+
for (const dir of CAPTAIN_ARTIFACT_DIRS) {
|
|
13
|
+
if (artifacts.length >= CAPTAIN_ARTIFACT_SCAN_LIMIT)
|
|
14
|
+
break;
|
|
15
|
+
const targetDir = join(outputDir, dir);
|
|
16
|
+
if (!existsSync(targetDir))
|
|
17
|
+
continue;
|
|
18
|
+
collectArtifacts(outputDir, targetDir, artifacts);
|
|
19
|
+
}
|
|
20
|
+
return artifacts
|
|
21
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
22
|
+
.slice(0, CAPTAIN_ARTIFACT_LIST_LIMIT)
|
|
23
|
+
.map(({ timestamp, ...artifact }) => artifact);
|
|
24
|
+
}
|
|
25
|
+
export function readCaptainFile(projectRoot, input, allowedDirs = CAPTAIN_ALLOWED_READ_DIRS) {
|
|
26
|
+
const resolved = resolveReadableFile(projectRoot, input.path, allowedDirs);
|
|
27
|
+
if (!resolved) {
|
|
28
|
+
return { success: false, message: 'File is outside allowed directories' };
|
|
29
|
+
}
|
|
30
|
+
if (!existsSync(resolved)) {
|
|
31
|
+
return { success: false, message: `File not found: ${input.path}` };
|
|
32
|
+
}
|
|
33
|
+
if (!statSync(resolved).isFile()) {
|
|
34
|
+
return { success: false, message: `Not a file: ${input.path}` };
|
|
35
|
+
}
|
|
36
|
+
const maxChars = normalizeMaxChars(input.maxChars);
|
|
37
|
+
const fullContent = readFileSync(resolved, 'utf8');
|
|
38
|
+
const content = selectContent(fullContent, input);
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
path: relative(projectRoot || process.cwd(), resolved),
|
|
42
|
+
truncated: content.length > maxChars,
|
|
43
|
+
content: content.slice(0, maxChars),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function collectArtifacts(outputDir, targetDir, artifacts) {
|
|
47
|
+
for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
|
|
48
|
+
if (artifacts.length >= CAPTAIN_ARTIFACT_SCAN_LIMIT)
|
|
49
|
+
return;
|
|
50
|
+
const entryPath = join(targetDir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
collectArtifacts(outputDir, entryPath, artifacts);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const stats = statSync(entryPath);
|
|
56
|
+
artifacts.push({
|
|
57
|
+
path: relative(outputDir, entryPath),
|
|
58
|
+
size: stats.size,
|
|
59
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
60
|
+
timestamp: stats.mtimeMs,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function resolveReadableFile(projectRoot, requestedPath, allowedDirs) {
|
|
65
|
+
if (!projectRoot)
|
|
66
|
+
return null;
|
|
67
|
+
let cleanPath = requestedPath.trim();
|
|
68
|
+
const projectName = basename(projectRoot);
|
|
69
|
+
if (cleanPath.startsWith(`${projectName}/`) || cleanPath.startsWith(`${projectName}\\`)) {
|
|
70
|
+
cleanPath = cleanPath.slice(projectName.length + 1);
|
|
71
|
+
}
|
|
72
|
+
const resolved = isAbsolute(cleanPath) ? resolve(cleanPath) : resolve(projectRoot, cleanPath);
|
|
73
|
+
const allowedRoots = allowedDirs.map((dir) => resolve(projectRoot, dir));
|
|
74
|
+
for (const root of allowedRoots) {
|
|
75
|
+
const rel = relative(root, resolved);
|
|
76
|
+
if (!rel || (!rel.startsWith('..') && !isAbsolute(rel)))
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function selectContent(content, input) {
|
|
82
|
+
if (!input.startLine && !input.endLine)
|
|
83
|
+
return content;
|
|
84
|
+
const lines = content.split(/\r?\n/);
|
|
85
|
+
const startIndex = resolveLineIndex(input.startLine, lines.length, 1);
|
|
86
|
+
const endIndex = resolveLineIndex(input.endLine, lines.length, lines.length);
|
|
87
|
+
if (endIndex < startIndex)
|
|
88
|
+
return '';
|
|
89
|
+
return lines.slice(startIndex - 1, endIndex).join('\n');
|
|
90
|
+
}
|
|
91
|
+
function resolveLineIndex(line, totalLines, fallback) {
|
|
92
|
+
if (!line)
|
|
93
|
+
return fallback;
|
|
94
|
+
if (line < 0)
|
|
95
|
+
return Math.max(1, totalLines + line + 1);
|
|
96
|
+
return Math.min(Math.max(1, line), totalLines);
|
|
97
|
+
}
|
|
98
|
+
function normalizeMaxChars(maxChars) {
|
|
99
|
+
return Math.max(CAPTAIN_READ_FILE_MIN_LIMIT, Math.min(maxChars || CAPTAIN_READ_FILE_DEFAULT_LIMIT, CAPTAIN_READ_FILE_MAX_LIMIT));
|
|
100
|
+
}
|
|
@@ -4,6 +4,7 @@ import dedent from 'dedent';
|
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ConfigParser } from "../../config.js";
|
|
6
6
|
import { Test } from "../../test-plan.js";
|
|
7
|
+
import { listRecentArtifacts, readCaptainFile } from "./file-tools.js";
|
|
7
8
|
import { resolveProjectRoot } from "./mixin.js";
|
|
8
9
|
let cachedBashTool = null;
|
|
9
10
|
export function WithIdleMode(Base) {
|
|
@@ -13,6 +14,8 @@ export function WithIdleMode(Base) {
|
|
|
13
14
|
const config = ConfigParser.getInstance().getConfig();
|
|
14
15
|
const knowledgeDir = config.dirs?.knowledge || 'knowledge';
|
|
15
16
|
const experienceDir = config.dirs?.experience || 'experience';
|
|
17
|
+
const outputDir = config.dirs?.output || 'output';
|
|
18
|
+
const readableDirs = [outputDir, knowledgeDir, experienceDir];
|
|
16
19
|
if (!cachedBashTool && projectRoot) {
|
|
17
20
|
cachedBashTool = await createBashTool({
|
|
18
21
|
destination: projectRoot,
|
|
@@ -69,6 +72,53 @@ export function WithIdleMode(Base) {
|
|
|
69
72
|
return { success: true, tests: plan.tests.length };
|
|
70
73
|
},
|
|
71
74
|
}),
|
|
75
|
+
project: tool({
|
|
76
|
+
description: dedent `
|
|
77
|
+
Inspect Explorbot project configuration and recent generated artifacts.
|
|
78
|
+
Use this before answering questions about setup, previous sessions, reports, saved plans, or output files.
|
|
79
|
+
`,
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
view: z.enum(['config', 'artifacts']).optional().describe('config shows setup summary; artifacts lists recent generated files'),
|
|
82
|
+
}),
|
|
83
|
+
execute: async ({ view }) => {
|
|
84
|
+
const parser = ConfigParser.getInstance();
|
|
85
|
+
const config = parser.getConfig();
|
|
86
|
+
const outputDir = parser.getOutputDir();
|
|
87
|
+
if (view === 'artifacts') {
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
outputDir,
|
|
91
|
+
artifacts: listRecentArtifacts(outputDir),
|
|
92
|
+
suggestion: 'Use readFile to inspect specific reports, plans, logs, generated tests, knowledge, or experience files.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
success: true,
|
|
97
|
+
configPath: parser.getConfigPath(),
|
|
98
|
+
baseUrl: config.playwright?.url,
|
|
99
|
+
browser: config.playwright?.browser,
|
|
100
|
+
headed: config.playwright?.show === true,
|
|
101
|
+
dirs: config.dirs,
|
|
102
|
+
agents: Object.fromEntries(Object.entries(config.ai?.agents || {}).map(([name, agentConfig]) => [name, { enabled: agentConfig?.enabled !== false, hasModelOverride: !!agentConfig?.model }])),
|
|
103
|
+
reporterEnabled: config.reporter?.enabled === true,
|
|
104
|
+
apiEnabled: !!config.api,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
readFile: tool({
|
|
109
|
+
description: dedent `
|
|
110
|
+
Read a specific Explorbot project file for analysis.
|
|
111
|
+
Use this for explicit user questions about reports, plans, logs, generated tests, knowledge, or experience files.
|
|
112
|
+
Prefer this over bash() for reading file contents after bash has found the file.
|
|
113
|
+
`,
|
|
114
|
+
inputSchema: z.object({
|
|
115
|
+
path: z.string().describe('Path inside output, knowledge, or experience directories'),
|
|
116
|
+
startLine: z.number().optional().describe('First line to read, 1-based. Negative values count from the end of the file'),
|
|
117
|
+
endLine: z.number().optional().describe('Last line to read, 1-based and inclusive. Negative values count from the end of the file'),
|
|
118
|
+
maxChars: z.number().optional().describe('Maximum characters to return, default 12000'),
|
|
119
|
+
}),
|
|
120
|
+
execute: async (input) => readCaptainFile(projectRoot, input, readableDirs),
|
|
121
|
+
}),
|
|
72
122
|
};
|
|
73
123
|
if (cachedBashTool) {
|
|
74
124
|
tools.bash = cachedBashTool.bash;
|
|
@@ -79,17 +129,31 @@ export function WithIdleMode(Base) {
|
|
|
79
129
|
const config = ConfigParser.getInstance().getConfig();
|
|
80
130
|
const knowledgeDir = config.dirs?.knowledge || 'knowledge';
|
|
81
131
|
const experienceDir = config.dirs?.experience || 'experience';
|
|
132
|
+
const outputDir = config.dirs?.output || 'output';
|
|
82
133
|
return dedent `
|
|
83
134
|
<idle_capabilities>
|
|
84
135
|
- Plan management: updatePlan() — replace or append tests in the current plan
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
- Use
|
|
90
|
-
- Use
|
|
136
|
+
- readFile() — read specific report, plan, log, generated test, knowledge, or experience file content
|
|
137
|
+
- bash() — discover files and inspect file metadata
|
|
138
|
+
- READ from: ${knowledgeDir}/, ${experienceDir}/, ${outputDir}/
|
|
139
|
+
- WRITE to: ${knowledgeDir}/, ${experienceDir}/ only (NOT ${outputDir}/)
|
|
140
|
+
- Use wc -l -c file.txt to inspect size
|
|
141
|
+
- Use file file.txt to inspect type
|
|
142
|
+
- Use find . -name "*.md" to discover files
|
|
143
|
+
- Use grep -n "keyword" file.txt to find matching lines
|
|
144
|
+
- Use ls -lh to list files
|
|
91
145
|
</idle_capabilities>
|
|
92
146
|
|
|
147
|
+
<file_reading>
|
|
148
|
+
Use bash() for file discovery and search. Once the needed file and line range are known,
|
|
149
|
+
use readFile() to read its contents. Do not use bash() to print file contents.
|
|
150
|
+
</file_reading>
|
|
151
|
+
|
|
152
|
+
<project_inspection>
|
|
153
|
+
Use project({ view: "config" }) before explaining Explorbot setup or suggesting config improvements.
|
|
154
|
+
Use project({ view: "artifacts" }) before answering questions about previous sessions, reports, plans, generated tests, or logs.
|
|
155
|
+
</project_inspection>
|
|
156
|
+
|
|
93
157
|
<knowledge_saving>
|
|
94
158
|
When user shares credentials, selectors, or important domain info during conversation,
|
|
95
159
|
suggest saving it to a knowledge file using bash tool.
|
|
@@ -45,19 +45,49 @@ export function WithWebMode(Base) {
|
|
|
45
45
|
description: dedent `
|
|
46
46
|
Direct browser access via Playwright. Use for diagnostics and browser management.
|
|
47
47
|
Actions:
|
|
48
|
+
- status: Inspect browser/page availability, URL, title, tab count
|
|
48
49
|
- evaluate: Run JavaScript in browser context (localStorage, cookies, DOM, console)
|
|
49
50
|
- closeTabs: Close all browser tabs except the current one
|
|
50
|
-
- screenshot: Take a screenshot of current page
|
|
51
51
|
- reload: Reload the current page
|
|
52
|
+
- screenshot: Take a screenshot of current page
|
|
53
|
+
- recover: Recover from a closed/crashed page using Explorer recovery
|
|
54
|
+
- restart: Restart the browser when page/context recovery is not enough
|
|
55
|
+
- openFreshTab: Open a fresh tab in the current browser context
|
|
52
56
|
`,
|
|
53
57
|
inputSchema: z.object({
|
|
54
|
-
action: z.enum(['evaluate', 'closeTabs', 'screenshot', '
|
|
58
|
+
action: z.enum(['status', 'evaluate', 'closeTabs', 'reload', 'screenshot', 'recover', 'restart', 'openFreshTab']).describe('Browser action to perform'),
|
|
55
59
|
code: z.string().optional().describe('JavaScript code for evaluate action'),
|
|
56
60
|
}),
|
|
57
61
|
execute: async ({ action, code }) => {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
|
|
62
|
+
const explorer = ctx.explorBot.getExplorer();
|
|
63
|
+
if (action === 'status') {
|
|
64
|
+
const page = explorer.playwrightHelper?.page;
|
|
65
|
+
const pages = page?.context?.().pages?.() || [];
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
hasPage: !!page,
|
|
69
|
+
isClosed: page?.isClosed?.() || false,
|
|
70
|
+
url: page && !page.isClosed?.() ? await page.url() : null,
|
|
71
|
+
title: page && !page.isClosed?.() ? await page.title().catch(() => null) : null,
|
|
72
|
+
tabs: pages.length,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (action === 'recover') {
|
|
76
|
+
const recovered = await explorer.recoverFromBrowserError();
|
|
77
|
+
return { success: recovered, message: recovered ? 'Browser page recovered' : 'Browser recovery failed' };
|
|
78
|
+
}
|
|
79
|
+
if (action === 'restart') {
|
|
80
|
+
const restarted = await explorer.restartBrowser();
|
|
81
|
+
return { success: restarted, message: restarted ? 'Browser restarted' : 'Browser restart failed' };
|
|
82
|
+
}
|
|
83
|
+
if (action === 'openFreshTab') {
|
|
84
|
+
await ctx.explorBot.openFreshTab();
|
|
85
|
+
const state = explorer.getStateManager().getCurrentState();
|
|
86
|
+
return { success: true, url: state?.url, title: state?.title };
|
|
87
|
+
}
|
|
88
|
+
const page = explorer.playwrightHelper?.page;
|
|
89
|
+
if (!page || page.isClosed?.())
|
|
90
|
+
return { success: false, message: 'No browser page available. Try browser({ action: "recover" }) first.' };
|
|
61
91
|
if (action === 'evaluate') {
|
|
62
92
|
if (!code)
|
|
63
93
|
return { success: false, message: 'Code required for evaluate action' };
|
|
@@ -106,7 +136,7 @@ export function WithWebMode(Base) {
|
|
|
106
136
|
<web_capabilities>
|
|
107
137
|
- Page actions: click, pressKey, form (CodeceptJS tools)
|
|
108
138
|
- Navigation: navigate() — AI-powered navigation to URLs or page descriptions
|
|
109
|
-
- Browser diagnostics: browser() — evaluate JS, close tabs, screenshot, reload
|
|
139
|
+
- Browser diagnostics: browser() — inspect status, evaluate JS, close tabs, screenshot, reload, recover closed/crashed pages, restart browser, open a fresh tab
|
|
110
140
|
- Visual analysis: see() — screenshot-based page verification
|
|
111
141
|
- Context refresh: context() — get fresh HTML/ARIA snapshot
|
|
112
142
|
- Visual fallback: visualClick() — coordinate-based click when locators fail
|