Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
start:
./bin/explorbot-cli.ts start

init:
./bin/explorbot-cli.ts init

63 changes: 48 additions & 15 deletions bin/explorbot-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,10 @@ addCommonOptions(program.command('plan <path> [feature]').description('Generate
}
}

await explorBot.plan(feature || undefined, { fresh: !options.append, style: options.style });
await explorBot.plan(feature || undefined, {
fresh: !options.append,
style: options.style,
});

const plan = explorBot.getCurrentPlan();
if (!plan?.tests.length) {
Expand Down Expand Up @@ -320,7 +323,13 @@ program
log(`Working in directory: ${resolvedPath}`);
}

const defaultConfig = `import { <your provider here> } from 'ai';
const defaultConfig = `import { '<your provider here>' } from '<your provider package here>';

// This example uses OpenRouter (one API key, many providers). Any Vercel AI SDK provider works; see
// https://github.com/testomatio/explorbot/blob/main/docs/providers.md
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});

const config = {
playwright: {
Expand All @@ -330,9 +339,9 @@ const config = {
},

ai: {
provider: <your provider here>,
model: '<your model here>',
apiKey: '<your api key here>',
visionModel: '<your vision model here>',
agenticModel: '<your agentic model here>',
},

reporter: {
Expand Down Expand Up @@ -470,7 +479,9 @@ program
.option('-p, --path <path>', 'Working directory path')
.action(async (url, description, options) => {
try {
await ConfigParser.getInstance().loadConfig({ path: options.path || process.cwd() });
await ConfigParser.getInstance().loadConfig({
path: options.path || process.cwd(),
});

if (url && description) {
const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
Expand Down Expand Up @@ -498,7 +509,9 @@ program
.option('-p, --path <path>', 'Working directory path')
.action(async (url, options) => {
try {
await ConfigParser.getInstance().loadConfig({ path: options.path || process.cwd() });
await ConfigParser.getInstance().loadConfig({
path: options.path || process.cwd(),
});
const { KnowsCommand } = await import('../src/commands/knows-command.js');
const explorBot = new ExplorBot({ path: options.path });
const command = new KnowsCommand(explorBot);
Expand Down Expand Up @@ -648,14 +661,20 @@ browserCmd
.option('-p, --path <path>', 'Working directory path')
.action(async (options) => {
const { launchServer, removeEndpointFile } = await import('../src/browser-server.js');
await ConfigParser.getInstance().loadConfig({ config: options.config, path: options.path });
await ConfigParser.getInstance().loadConfig({
config: options.config,
path: options.path,
});
const config = ConfigParser.getInstance().getConfig();

let show = config.playwright.show || false;
if (options.show !== undefined) show = true;
if (options.headless !== undefined) show = false;

const server = await launchServer({ browser: config.playwright.browser, show });
const server = await launchServer({
browser: config.playwright.browser,
show,
});

console.log('Browser server is running. Press Ctrl+C to stop.');

Expand All @@ -677,7 +696,10 @@ browserCmd
.option('-p, --path <path>', 'Working directory path')
.action(async (options) => {
const { getAliveEndpoint, removeEndpointFile } = await import('../src/browser-server.js');
await ConfigParser.getInstance().loadConfig({ config: options.config, path: options.path });
await ConfigParser.getInstance().loadConfig({
config: options.config,
path: options.path,
});

const endpoint = await getAliveEndpoint();
if (!endpoint) {
Expand All @@ -702,7 +724,10 @@ browserCmd
.option('-p, --path <path>', 'Working directory path')
.action(async (options) => {
const { getAliveEndpoint } = await import('../src/browser-server.js');
await ConfigParser.getInstance().loadConfig({ config: options.config, path: options.path });
await ConfigParser.getInstance().loadConfig({
config: options.config,
path: options.path,
});

const endpoint = await getAliveEndpoint();
if (endpoint) {
Expand Down Expand Up @@ -743,15 +768,23 @@ program

if (agent && name) {
const { AddRuleCommand } = await import('../src/commands/add-rule-command.js');
const result = AddRuleCommand.createRuleFile(agent, name, { urlPattern: options.url });
const result = AddRuleCommand.createRuleFile(agent, name, {
urlPattern: options.url,
});
process.exit(result ? 0 : 1);
}

const AddRule = (await import('../src/components/AddRule.js')).default;
render(React.createElement(AddRule, { initialAgent: agent || '', initialName: name || '' }), {
exitOnCtrlC: false,
patchConsole: false,
});
render(
React.createElement(AddRule, {
initialAgent: agent || '',
initialName: name || '',
}),
{
exitOnCtrlC: false,
patchConsole: false,
}
);
});

import { createApiCommands } from '../boat/api-tester/src/cli.ts';
Expand Down
6 changes: 5 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 18 additions & 6 deletions src/ai/pilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export class Pilot implements Agent {
const schema = z.object({
decision: z.enum(['pass', 'fail', 'continue', 'skipped']).describe('pass = test succeeded, fail = test failed, continue = tester should keep going, skipped = scenario is irrelevant OR systematic execution failures prevented testing'),
reason: z.string().describe('What happened and why (1-2 sentences). Do NOT repeat the decision status (e.g. "scenario goal achieved/not achieved") — just explain the evidence. For continue: explain why rejected and suggest alternatives.'),
guidance: z.string().nullish().describe('Required for "continue": specific actionable instruction for the tester — what exactly to verify, retry differently, or complete next. Be concrete.'),
guidance: z.string().nullable().describe('Required for "continue": specific actionable instruction for the tester — what exactly to verify, retry differently, or complete next. Be concrete.'),
});

const userContent = dedent`
Expand Down Expand Up @@ -117,12 +117,18 @@ export class Pilot implements Agent {
`;

const messages = [
{ role: 'system' as const, content: this.buildVerdictSystemPrompt(type, task) },
{
role: 'system' as const,
content: this.buildVerdictSystemPrompt(type, task),
},
{ role: 'user' as const, content: userContent },
];

try {
const response = await this.provider.generateObject(messages, schema, this.provider.getAgenticModel('pilot'), { agentName: 'pilot', experimental_telemetry: { functionId: 'pilot.reviewVerdict' } });
const response = await this.provider.generateObject(messages, schema, this.provider.getAgenticModel('pilot'), {
agentName: 'pilot',
experimental_telemetry: { functionId: 'pilot.reviewVerdict' },
});

const result = response?.object;
if (!result) {
Expand Down Expand Up @@ -222,7 +228,9 @@ export class Pilot implements Agent {
async planTest(task: Test, currentState: ActionResult): Promise<string> {
tag('substep').log('Pilot planning test...');

const pageSummary = await this.researcher.summary(currentState, { allowNewResearch: false });
const pageSummary = await this.researcher.summary(currentState, {
allowNewResearch: false,
});
const agenticModel = this.provider.getAgenticModel('pilot');
this.conversation = this.provider.startConversation(this.getSystemPrompt(task, currentState, pageSummary), 'pilot', agenticModel);

Expand Down Expand Up @@ -257,7 +265,9 @@ export class Pilot implements Agent {

tag('substep').log('Pilot reviewing new page...');

const pageSummary = await this.researcher.summary(currentState, { allowNewResearch: false });
const pageSummary = await this.researcher.summary(currentState, {
allowNewResearch: false,
});
if (!pageSummary) return '';

const stateContext = this.buildStateContext(currentState);
Expand Down Expand Up @@ -289,7 +299,9 @@ export class Pilot implements Agent {
tag('substep').log('Pilot analyzing progress...');

if (!this.conversation) {
const pageSummary = await this.researcher.summary(currentState, { allowNewResearch: false });
const pageSummary = await this.researcher.summary(currentState, {
allowNewResearch: false,
});
const agenticModel = this.provider.getAgenticModel('pilot');
this.conversation = this.provider.startConversation(this.getSystemPrompt(task, currentState, pageSummary), 'pilot', agenticModel);
}
Expand Down
16 changes: 12 additions & 4 deletions src/ai/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const TasksSchema = z.object({
z.object({
scenario: z.string().describe('A single sentence describing what to test'),
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
startUrl: z.string().optional().describe('Start URL for the test if different from plan URL (only for tests on visited subpages)'),
startUrl: z.string().nullable().describe('Start URL for the test if different from plan URL (only for tests on visited subpages)'),
steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "Click on Login button", "Enter username in email field", "Submit the form"). Keep steps atomic and actionable.'),
expectedOutcomes: z
.array(z.string())
Expand Down Expand Up @@ -325,13 +325,18 @@ export class Planner extends PlannerBase implements Agent {

conversation.addUserText(planningPrompt);
const currentState = this.stateManager.getCurrentState();
const research = await this.researcher.research(currentState || state, { deep: true });
const research = await this.researcher.research(currentState || state, {
deep: true,
});
let plannerResearch = mdq(research).query('code').replace('');
for (const table of mdq(plannerResearch).query('table').each()) {
const rawTable = table.text();
const rows = table.toJson();
if (rows.length === 0 || !rows[0].Element) continue;
const elementWithType = rows.map((r) => ({ Element: r.Element, Type: r.Type || '' }));
const elementWithType = rows.map((r) => ({
Element: r.Element,
Type: r.Type || '',
}));
plannerResearch = plannerResearch.replace(rawTable, jsonToTable(elementWithType, ['Element', 'Type']));
}

Expand All @@ -349,7 +354,10 @@ export class Planner extends PlannerBase implements Agent {
</page_research>
`);

const rawFlows = this.experienceTracker.getSuccessfulExperience(state, { includeDescendants: true, stripCode: true });
const rawFlows = this.experienceTracker.getSuccessfulExperience(state, {
includeDescendants: true,
stripCode: true,
});
const flows = rawFlows.map((f) => this.cleanExperienceFlows(f)).filter(Boolean) as string[];
if (flows.length > 0) {
conversation.addUserText(dedent`
Expand Down
Loading