@bbearai/ai-executor 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/dist/cli.js ADDED
@@ -0,0 +1,591 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_supabase_js = require("@supabase/supabase-js");
28
+
29
+ // src/runner.ts
30
+ var import_sdk = __toESM(require("@anthropic-ai/sdk"));
31
+ var import_zod = require("zod");
32
+
33
+ // src/browser.ts
34
+ var import_stagehand = require("@browserbasehq/stagehand");
35
+ var DEFAULT_MODEL = "anthropic/claude-sonnet-4-20250514";
36
+ async function createStagehandSession(config, anthropicApiKey) {
37
+ const modelName = config.model ?? DEFAULT_MODEL;
38
+ const viewport = config.viewport ?? { width: 1280, height: 720 };
39
+ const stagehand = new import_stagehand.Stagehand({
40
+ env: config.provider === "browserbase" ? "BROWSERBASE" : "LOCAL",
41
+ apiKey: config.provider === "browserbase" ? config.browserbaseApiKey : void 0,
42
+ projectId: config.provider === "browserbase" ? config.browserbaseProjectId : void 0,
43
+ model: {
44
+ modelName,
45
+ apiKey: anthropicApiKey
46
+ },
47
+ localBrowserLaunchOptions: config.provider === "local" ? {
48
+ headless: config.headless ?? true,
49
+ viewport
50
+ } : void 0,
51
+ browserbaseSessionCreateParams: config.provider === "browserbase" ? { projectId: config.browserbaseProjectId } : void 0
52
+ });
53
+ await stagehand.init();
54
+ const page = stagehand.context.activePage();
55
+ await page.setViewportSize(viewport.width, viewport.height);
56
+ let sessionId = `local-${Date.now()}`;
57
+ if (config.provider === "browserbase" && stagehand.browserbaseSessionID) {
58
+ sessionId = stagehand.browserbaseSessionID;
59
+ }
60
+ return {
61
+ stagehand,
62
+ page,
63
+ sessionId,
64
+ close: async () => {
65
+ await stagehand.close().catch(() => {
66
+ });
67
+ }
68
+ };
69
+ }
70
+ async function injectAuth(page, auth, stagehand) {
71
+ if (auth.type === "cookie") {
72
+ for (const c of auth.cookies) {
73
+ await page.sendCDP("Network.setCookie", {
74
+ name: c.name,
75
+ value: c.value,
76
+ domain: c.domain,
77
+ path: c.path ?? "/",
78
+ secure: c.secure ?? false,
79
+ httpOnly: c.httpOnly ?? false,
80
+ sameSite: c.sameSite ?? "Lax"
81
+ });
82
+ }
83
+ } else if (auth.type === "localStorage") {
84
+ const currentUrl = page.url();
85
+ if (currentUrl === "about:blank") {
86
+ return;
87
+ }
88
+ await page.evaluate((items) => {
89
+ for (const [key, value] of Object.entries(items)) {
90
+ localStorage.setItem(key, value);
91
+ }
92
+ }, auth.items);
93
+ } else if (auth.type === "form-login") {
94
+ await performFormLogin(page, auth, stagehand);
95
+ }
96
+ }
97
+ async function performFormLogin(page, auth, stagehand) {
98
+ await page.goto(auth.loginUrl, { waitUntil: "domcontentloaded" });
99
+ await page.waitForLoadState("networkidle", 15e3).catch(() => {
100
+ });
101
+ if (stagehand) {
102
+ await stagehand.act(
103
+ `Fill in the email/username field with "${auth.email}" and the password field with "${auth.password}", then click the login/sign-in button to submit the form.`
104
+ );
105
+ } else {
106
+ await manualFormLogin(page, auth);
107
+ }
108
+ await page.waitForLoadState("networkidle", 15e3).catch(() => {
109
+ });
110
+ }
111
+ async function manualFormLogin(page, auth) {
112
+ await page.waitForSelector(
113
+ 'input[type="email"], input[type="text"][name*="email"], input[name*="user"], input[type="text"]',
114
+ { timeout: 15e3 }
115
+ ).catch(() => {
116
+ });
117
+ const emailSelectors = [
118
+ 'input[type="email"]',
119
+ 'input[name="email"]',
120
+ 'input[name="username"]',
121
+ 'input[autocomplete="email"]',
122
+ 'input[autocomplete="username"]',
123
+ 'input[type="text"][name*="email"]',
124
+ 'input[type="text"][name*="user"]',
125
+ 'input[type="text"]'
126
+ ];
127
+ let emailFilled = false;
128
+ for (const sel of emailSelectors) {
129
+ const locator = page.locator(sel);
130
+ if (await locator.count() > 0 && await locator.isVisible()) {
131
+ await locator.fill(auth.email);
132
+ emailFilled = true;
133
+ break;
134
+ }
135
+ }
136
+ if (!emailFilled) {
137
+ throw new Error("Could not find email/username input on login page");
138
+ }
139
+ const passwordLocator = page.locator('input[type="password"]');
140
+ if (await passwordLocator.count() > 0 && await passwordLocator.isVisible()) {
141
+ await passwordLocator.fill(auth.password);
142
+ } else {
143
+ throw new Error("Could not find password input on login page");
144
+ }
145
+ const submitSelectors = [
146
+ 'button[type="submit"]',
147
+ 'input[type="submit"]'
148
+ ];
149
+ let submitted = false;
150
+ for (const sel of submitSelectors) {
151
+ const locator = page.locator(sel);
152
+ if (await locator.count() > 0 && await locator.isVisible()) {
153
+ await locator.click();
154
+ submitted = true;
155
+ break;
156
+ }
157
+ }
158
+ if (!submitted) {
159
+ await page.locator('input[type="password"]').type("\n");
160
+ }
161
+ }
162
+
163
+ // src/evaluator.ts
164
+ async function generateRunSummary(anthropic, testTitle, steps, model) {
165
+ const stepsText = steps.map(
166
+ (s) => `Step ${s.stepNumber}: ${s.action}
167
+ Expected: ${s.expectedResult}
168
+ Actual: ${s.actualResult}
169
+ Result: ${s.passed ? "PASS" : "FAIL"} (confidence: ${Math.round(s.confidence * 100)}%)${s.error ? `
170
+ Error: ${s.error}` : ""}`
171
+ ).join("\n\n");
172
+ const passCount = steps.filter((s) => s.passed).length;
173
+ const failCount = steps.filter((s) => !s.passed).length;
174
+ const response = await anthropic.messages.create({
175
+ model,
176
+ max_tokens: 512,
177
+ messages: [
178
+ {
179
+ role: "user",
180
+ content: `Summarize this AI test execution in 2-3 sentences. Focus on what was tested, what passed, and what failed (if anything). Be concise and factual.
181
+
182
+ Test: ${testTitle}
183
+ Results: ${passCount} passed, ${failCount} failed out of ${steps.length} steps
184
+
185
+ ${stepsText}`
186
+ }
187
+ ]
188
+ });
189
+ return response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
190
+ }
191
+
192
+ // src/runner.ts
193
+ async function runTest(config) {
194
+ const anthropic = new import_sdk.default({ apiKey: config.anthropicApiKey });
195
+ const startTime = Date.now();
196
+ const browserConfig = config.browser ?? {
197
+ provider: "local",
198
+ headless: true
199
+ };
200
+ config.onStatusChange?.("initializing");
201
+ const session = await createStagehandSession(browserConfig, config.anthropicApiKey);
202
+ const { stagehand, page } = session;
203
+ const stepResults = [];
204
+ let pendingConsoleLogs = [];
205
+ let pendingNetworkErrors = [];
206
+ let stepStartTime = Date.now();
207
+ const rawPage = page;
208
+ rawPage.on("console", (msg) => {
209
+ const level = msg.type?.() ?? msg.type ?? "log";
210
+ const mappedLevel = level === "error" ? "error" : level === "warn" || level === "warning" ? "warning" : level === "info" ? "info" : level === "debug" ? "debug" : "log";
211
+ pendingConsoleLogs.push({
212
+ level: mappedLevel,
213
+ text: (typeof msg.text === "function" ? msg.text() : String(msg.text ?? msg)).slice(0, 2e3),
214
+ source: typeof msg.location === "function" ? msg.location()?.url : void 0,
215
+ timestamp: Date.now() - stepStartTime
216
+ });
217
+ });
218
+ rawPage.on("requestfailed", (req) => {
219
+ const url = typeof req.url === "function" ? req.url() : String(req.url ?? "");
220
+ const method = typeof req.method === "function" ? req.method() : String(req.method ?? "GET");
221
+ const failure = typeof req.failure === "function" ? req.failure() : req.failure;
222
+ pendingNetworkErrors.push({
223
+ method,
224
+ url: url.slice(0, 500),
225
+ status: 0,
226
+ statusText: failure?.errorText ?? "Request failed",
227
+ timestamp: Date.now() - stepStartTime
228
+ });
229
+ });
230
+ rawPage.on("response", (res) => {
231
+ const status = typeof res.status === "function" ? res.status() : Number(res.status ?? 0);
232
+ if (status >= 400) {
233
+ const url = typeof res.url === "function" ? res.url() : String(res.url ?? "");
234
+ const statusText = typeof res.statusText === "function" ? res.statusText() : String(res.statusText ?? "");
235
+ const req = typeof res.request === "function" ? res.request() : res.request;
236
+ const method = req ? typeof req.method === "function" ? req.method() : String(req.method ?? "GET") : "GET";
237
+ pendingNetworkErrors.push({
238
+ method,
239
+ url: url.slice(0, 500),
240
+ status,
241
+ statusText,
242
+ timestamp: Date.now() - stepStartTime
243
+ });
244
+ }
245
+ });
246
+ try {
247
+ if (config.auth?.type === "form-login") {
248
+ config.onStatusChange?.("authenticating");
249
+ await injectAuth(page, config.auth, stagehand);
250
+ }
251
+ config.onStatusChange?.("navigating");
252
+ const targetUrl = config.testCase.targetRoute ? `${config.targetUrl.replace(/\/$/, "")}${config.testCase.targetRoute}` : config.targetUrl;
253
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeoutMs: 3e4 });
254
+ if (config.auth && config.auth.type !== "form-login") {
255
+ config.onStatusChange?.("authenticating");
256
+ await injectAuth(page, config.auth, stagehand);
257
+ if (config.auth.type === "localStorage") {
258
+ await page.evaluate((items) => {
259
+ for (const [key, value] of Object.entries(items)) {
260
+ localStorage.setItem(key, value);
261
+ }
262
+ }, config.auth.items);
263
+ await page.reload({ waitUntil: "domcontentloaded" });
264
+ }
265
+ }
266
+ await page.waitForLoadState("networkidle").catch(() => {
267
+ });
268
+ pendingConsoleLogs = [];
269
+ pendingNetworkErrors = [];
270
+ config.onStatusChange?.("executing");
271
+ const steps = config.testCase.steps;
272
+ for (let i = 0; i < steps.length; i++) {
273
+ const step = steps[i];
274
+ stepStartTime = Date.now();
275
+ pendingConsoleLogs = [];
276
+ pendingNetworkErrors = [];
277
+ const screenshotBefore = await page.screenshot({ type: "png" });
278
+ let error;
279
+ let screenshotAfter = screenshotBefore;
280
+ let actSucceeded = false;
281
+ try {
282
+ await stagehand.act(step.action);
283
+ actSucceeded = true;
284
+ await page.waitForLoadState("networkidle").catch(() => {
285
+ });
286
+ await page.waitForTimeout(500);
287
+ screenshotAfter = await page.screenshot({ type: "png" });
288
+ } catch (err) {
289
+ error = err instanceof Error ? err.message : String(err);
290
+ screenshotAfter = await page.screenshot({ type: "png" }).catch(() => screenshotBefore);
291
+ }
292
+ let evaluation = {
293
+ passed: false,
294
+ confidence: 0,
295
+ actualResult: error ?? "Action execution failed"
296
+ };
297
+ if (actSucceeded) {
298
+ try {
299
+ const verificationSchema = import_zod.z.object({
300
+ passed: import_zod.z.boolean().describe("Whether the expected result was achieved"),
301
+ confidence: import_zod.z.number().min(0).max(1).describe("Confidence in the assessment (0.9+ = very sure, 0.7-0.9 = likely, below 0.7 = uncertain)"),
302
+ actualResult: import_zod.z.string().describe("Description of what actually happened on the page")
303
+ });
304
+ const verification = await stagehand.extract(
305
+ `You are evaluating a QA test step. The action "${step.action}" was just performed. Check if this expected result was achieved: "${step.expectedResult}". Look at the current page state and describe what actually happened. Be precise and factual in your assessment.`,
306
+ verificationSchema
307
+ );
308
+ evaluation = {
309
+ passed: verification.passed,
310
+ confidence: verification.confidence,
311
+ actualResult: verification.actualResult
312
+ };
313
+ } catch (evalErr) {
314
+ evaluation = {
315
+ passed: false,
316
+ confidence: 0.2,
317
+ actualResult: `Verification error: ${evalErr instanceof Error ? evalErr.message : String(evalErr)}`
318
+ };
319
+ }
320
+ }
321
+ const consoleLogs = pendingConsoleLogs.slice(0, 50);
322
+ const networkErrors = pendingNetworkErrors.slice(0, 30);
323
+ const result = {
324
+ stepNumber: step.stepNumber,
325
+ action: step.action,
326
+ expectedResult: step.expectedResult,
327
+ actualResult: evaluation.actualResult,
328
+ passed: evaluation.passed,
329
+ confidence: evaluation.confidence,
330
+ screenshotBefore,
331
+ screenshotAfter,
332
+ actionsTaken: [],
333
+ // Stagehand handles actions internally
334
+ error,
335
+ durationMs: Date.now() - stepStartTime,
336
+ consoleLogs,
337
+ networkErrors
338
+ };
339
+ stepResults.push(result);
340
+ config.onStepComplete?.(result, i, steps.length);
341
+ }
342
+ config.onStatusChange?.("completed");
343
+ const model = config.model ?? "claude-sonnet-4-20250514";
344
+ const summary = await generateRunSummary(anthropic, config.testCase.title, stepResults, model);
345
+ const overallResult = determineOverallResult(stepResults);
346
+ return {
347
+ testCaseId: config.testCase.id,
348
+ testCaseTitle: config.testCase.title,
349
+ overallResult,
350
+ steps: stepResults,
351
+ totalDurationMs: Date.now() - startTime,
352
+ summary,
353
+ screenshotUrls: [],
354
+ tokenUsage: {
355
+ // Stagehand tracks tokens internally; these are approximate
356
+ inputTokens: steps.length * 3e3,
357
+ outputTokens: steps.length * 500
358
+ },
359
+ browserSessionId: session.sessionId
360
+ };
361
+ } catch (err) {
362
+ return {
363
+ testCaseId: config.testCase.id,
364
+ testCaseTitle: config.testCase.title,
365
+ overallResult: "error",
366
+ steps: stepResults,
367
+ totalDurationMs: Date.now() - startTime,
368
+ summary: `Test execution failed: ${err instanceof Error ? err.message : String(err)}`,
369
+ screenshotUrls: [],
370
+ tokenUsage: {
371
+ inputTokens: stepResults.length * 3e3,
372
+ outputTokens: stepResults.length * 500
373
+ },
374
+ browserSessionId: session.sessionId
375
+ };
376
+ } finally {
377
+ await session.close();
378
+ }
379
+ }
380
+ function determineOverallResult(steps) {
381
+ if (steps.length === 0) return "error";
382
+ const allPassed = steps.every((s) => s.passed);
383
+ const allFailed = steps.every((s) => !s.passed);
384
+ const hasErrors = steps.some((s) => s.error);
385
+ if (allPassed) return "passed";
386
+ if (allFailed || hasErrors) return "failed";
387
+ return "partial";
388
+ }
389
+
390
+ // src/cli.ts
391
+ function parseArgs() {
392
+ const args = process.argv.slice(2);
393
+ const parsed = {
394
+ url: "",
395
+ headless: true,
396
+ provider: "local"
397
+ };
398
+ for (let i = 0; i < args.length; i++) {
399
+ switch (args[i]) {
400
+ case "--url":
401
+ parsed.url = args[++i] || "";
402
+ break;
403
+ case "--test-case-id":
404
+ parsed.testCaseId = args[++i];
405
+ break;
406
+ case "--test-file":
407
+ parsed.testFile = args[++i];
408
+ break;
409
+ case "--project-id":
410
+ parsed.projectId = args[++i];
411
+ break;
412
+ case "--headless":
413
+ parsed.headless = args[++i] !== "false";
414
+ break;
415
+ case "--provider":
416
+ parsed.provider = args[++i];
417
+ break;
418
+ case "--cookies":
419
+ parsed.cookies = args[++i];
420
+ break;
421
+ case "--local-storage":
422
+ parsed.localStorage = args[++i];
423
+ break;
424
+ case "--help":
425
+ printHelp();
426
+ process.exit(0);
427
+ }
428
+ }
429
+ return parsed;
430
+ }
431
+ function printHelp() {
432
+ console.log(`
433
+ @bbearai/ai-executor - AI-powered QA test executor
434
+
435
+ Usage:
436
+ bbear-execute --url <target-url> --test-case-id <id> [options]
437
+ bbear-execute --url <target-url> --test-file <path> [options]
438
+
439
+ Required:
440
+ --url <url> Target application URL
441
+
442
+ Test Source (one required):
443
+ --test-case-id <id> Fetch test case from BugBear (requires SUPABASE_URL, SUPABASE_ANON_KEY)
444
+ --test-file <path> Path to a JSON test case file
445
+
446
+ Options:
447
+ --project-id <id> BugBear project ID (for fetching test cases)
448
+ --headless <true|false> Run browser headlessly (default: true)
449
+ --provider <provider> Browser provider: local or browserbase (default: local)
450
+ --cookies <json> JSON array of cookies for authentication
451
+ --local-storage <json> JSON object of localStorage items for authentication
452
+ --help Show this help message
453
+
454
+ Environment Variables:
455
+ ANTHROPIC_API_KEY Required. Claude API key for AI interpretation.
456
+ SUPABASE_URL Required when using --test-case-id. BugBear Supabase URL.
457
+ SUPABASE_ANON_KEY Required when using --test-case-id. BugBear Supabase anon key.
458
+ BROWSERBASE_API_KEY Required when --provider=browserbase.
459
+ BROWSERBASE_PROJECT_ID Required when --provider=browserbase.
460
+
461
+ Examples:
462
+ # Run with a local test file
463
+ bbear-execute --url https://staging.myapp.com --test-file ./login-test.json --headless false
464
+
465
+ # Run with a BugBear test case
466
+ bbear-execute --url https://staging.myapp.com --test-case-id abc-123 --project-id xyz-789
467
+
468
+ # Run with cookie auth
469
+ bbear-execute --url https://staging.myapp.com --test-file ./test.json \\
470
+ --cookies '[{"name":"session","value":"abc123","domain":".myapp.com"}]'
471
+ `);
472
+ }
473
+ async function fetchTestCase(testCaseId) {
474
+ const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
475
+ const supabaseKey = process.env.SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
476
+ if (!supabaseUrl || !supabaseKey) {
477
+ throw new Error("SUPABASE_URL and SUPABASE_ANON_KEY are required to fetch test cases");
478
+ }
479
+ const supabase = (0, import_supabase_js.createClient)(supabaseUrl, supabaseKey);
480
+ const { data, error } = await supabase.from("test_cases").select("id, title, description, steps, expected_result, preconditions, target_route, estimated_minutes").eq("id", testCaseId).single();
481
+ if (error || !data) {
482
+ throw new Error(`Failed to fetch test case ${testCaseId}: ${error?.message ?? "Not found"}`);
483
+ }
484
+ return {
485
+ id: data.id,
486
+ title: data.title,
487
+ description: data.description ?? void 0,
488
+ steps: data.steps,
489
+ preconditions: data.preconditions ?? void 0,
490
+ targetRoute: data.target_route ?? void 0,
491
+ estimatedMinutes: data.estimated_minutes ?? void 0
492
+ };
493
+ }
494
+ async function loadTestFile(path) {
495
+ const fs = await import("fs");
496
+ const content = fs.readFileSync(path, "utf-8");
497
+ return JSON.parse(content);
498
+ }
499
+ function parseAuth(args) {
500
+ if (args.cookies) {
501
+ try {
502
+ return { type: "cookie", cookies: JSON.parse(args.cookies) };
503
+ } catch {
504
+ console.error("Failed to parse --cookies JSON");
505
+ process.exit(1);
506
+ }
507
+ }
508
+ if (args.localStorage) {
509
+ try {
510
+ return { type: "localStorage", items: JSON.parse(args.localStorage) };
511
+ } catch {
512
+ console.error("Failed to parse --local-storage JSON");
513
+ process.exit(1);
514
+ }
515
+ }
516
+ return void 0;
517
+ }
518
+ async function main() {
519
+ const args = parseArgs();
520
+ if (!args.url) {
521
+ console.error("Error: --url is required");
522
+ printHelp();
523
+ process.exit(1);
524
+ }
525
+ if (!args.testCaseId && !args.testFile) {
526
+ console.error("Error: Either --test-case-id or --test-file is required");
527
+ printHelp();
528
+ process.exit(1);
529
+ }
530
+ const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
531
+ if (!anthropicApiKey) {
532
+ console.error("Error: ANTHROPIC_API_KEY environment variable is required");
533
+ process.exit(1);
534
+ }
535
+ console.log("Loading test case...");
536
+ const testCase = args.testCaseId ? await fetchTestCase(args.testCaseId) : await loadTestFile(args.testFile);
537
+ console.log(`Test: ${testCase.title}`);
538
+ console.log(`Steps: ${testCase.steps.length}`);
539
+ console.log(`Target: ${args.url}${testCase.targetRoute ?? ""}`);
540
+ console.log("");
541
+ const browser = {
542
+ provider: args.provider,
543
+ headless: args.headless,
544
+ browserbaseApiKey: process.env.BROWSERBASE_API_KEY,
545
+ browserbaseProjectId: process.env.BROWSERBASE_PROJECT_ID
546
+ };
547
+ const result = await runTest({
548
+ targetUrl: args.url,
549
+ testCase,
550
+ auth: parseAuth(args),
551
+ browser,
552
+ anthropicApiKey,
553
+ onStepComplete: (step, index, total) => {
554
+ const icon = step.passed ? "\u2705" : "\u274C";
555
+ const confidence = Math.round(step.confidence * 100);
556
+ console.log(
557
+ `${icon} Step ${index + 1}/${total}: ${step.action.slice(0, 60)}... [${step.passed ? "PASS" : "FAIL"} ${confidence}%] (${step.durationMs}ms)`
558
+ );
559
+ if (!step.passed) {
560
+ console.log(` Expected: ${step.expectedResult.slice(0, 80)}`);
561
+ console.log(` Actual: ${step.actualResult.slice(0, 80)}`);
562
+ }
563
+ if (step.error) {
564
+ console.log(` Error: ${step.error.slice(0, 80)}`);
565
+ }
566
+ },
567
+ onStatusChange: (status) => {
568
+ if (status !== "executing" && status !== "evaluating") {
569
+ console.log(`[${status}]`);
570
+ }
571
+ }
572
+ });
573
+ console.log("");
574
+ console.log("=".repeat(60));
575
+ console.log(`Result: ${result.overallResult.toUpperCase()}`);
576
+ console.log(`Duration: ${Math.round(result.totalDurationMs / 1e3)}s`);
577
+ console.log(
578
+ `Steps: ${result.steps.filter((s) => s.passed).length} passed, ${result.steps.filter((s) => !s.passed).length} failed`
579
+ );
580
+ console.log(`Tokens: ${result.tokenUsage.inputTokens} in / ${result.tokenUsage.outputTokens} out`);
581
+ console.log("");
582
+ console.log("Summary:");
583
+ console.log(result.summary);
584
+ console.log("=".repeat(60));
585
+ process.exit(result.overallResult === "passed" ? 0 : 1);
586
+ }
587
+ main().catch((err) => {
588
+ console.error("Fatal error:", err);
589
+ process.exit(1);
590
+ });
591
+ //# sourceMappingURL=cli.js.map