jerob 1.0.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.
Files changed (69) hide show
  1. package/CLI/cli.ts +42 -0
  2. package/README.md +137 -0
  3. package/SETUP.md +584 -0
  4. package/agent/action-tracker.ts +45 -0
  5. package/agent/agent-tools.ts +111 -0
  6. package/agent/approval.ts +137 -0
  7. package/agent/diff-view.ts +26 -0
  8. package/agent/orchestrator.ts +186 -0
  9. package/agent/tool-executor.ts +463 -0
  10. package/agent/types.ts +69 -0
  11. package/ask/orchestrator.ts +244 -0
  12. package/auth/auth.ts +567 -0
  13. package/auth/config-store.ts +77 -0
  14. package/auth/crypto.ts +51 -0
  15. package/auth/env-writer.ts +82 -0
  16. package/bin/jerob.js +28 -0
  17. package/config/ai.config.ts +163 -0
  18. package/email_ops/email-tools.ts +178 -0
  19. package/email_ops/email_functions.ts +443 -0
  20. package/email_ops/email_init.ts +92 -0
  21. package/email_ops/email_pass_store.ts +61 -0
  22. package/email_ops/email_server.ts +29 -0
  23. package/email_ops/types.ts +88 -0
  24. package/index.ts +176 -0
  25. package/package.json +88 -0
  26. package/plan/browser-agent/README.md +118 -0
  27. package/plan/browser-agent/USAGE.md +308 -0
  28. package/plan/browser-agent/evaluator.ts +353 -0
  29. package/plan/browser-agent/executor.ts +372 -0
  30. package/plan/browser-agent/index.ts +13 -0
  31. package/plan/browser-agent/orchestrator.ts +323 -0
  32. package/plan/browser-agent/planner.ts +200 -0
  33. package/plan/browser-agent/types.ts +62 -0
  34. package/plan/browser-tool.ts +128 -0
  35. package/plan/index.ts +12 -0
  36. package/plan/orchestrator.ts +214 -0
  37. package/plan/planner.ts +183 -0
  38. package/plan/selection.ts +50 -0
  39. package/plan/types.ts +13 -0
  40. package/plan/web-tools.ts +119 -0
  41. package/scheduler/ARCHITECTURE.md +263 -0
  42. package/scheduler/README.md +200 -0
  43. package/scheduler/SETUP-READY.sql +84 -0
  44. package/scheduler/check-status.sql +124 -0
  45. package/scheduler/config-sync.ts +91 -0
  46. package/scheduler/db-migrate.ts +271 -0
  47. package/scheduler/db.ts +162 -0
  48. package/scheduler/debug.ts +184 -0
  49. package/scheduler/orchestrator.ts +438 -0
  50. package/scheduler/planner.ts +170 -0
  51. package/scheduler/update-task-email.ts +70 -0
  52. package/supabase/.temp/cli-latest +1 -0
  53. package/supabase/.temp/gotrue-version +1 -0
  54. package/supabase/.temp/linked-project.json +1 -0
  55. package/supabase/.temp/pooler-url +1 -0
  56. package/supabase/.temp/postgres-version +1 -0
  57. package/supabase/.temp/project-ref +1 -0
  58. package/supabase/.temp/rest-version +1 -0
  59. package/supabase/.temp/storage-migration +1 -0
  60. package/supabase/.temp/storage-version +1 -0
  61. package/supabase/deploy.ps1 +50 -0
  62. package/supabase/functions/scheduler-tick/index.ts +496 -0
  63. package/supabase/supabase/.temp/linked-project.json +1 -0
  64. package/tsconfig.json +33 -0
  65. package/tui/spinner.ts +33 -0
  66. package/tui/spinup.ts +67 -0
  67. package/tui/terminal-render.ts +16 -0
  68. package/utils/llm-error.ts +185 -0
  69. package/utils/model-validator.ts +247 -0
@@ -0,0 +1,372 @@
1
+ import { Stagehand } from "@browserbasehq/stagehand";
2
+ import { existsSync, mkdirSync } from "fs";
3
+ import path, { resolve } from "path";
4
+ import type { BrowserPlan, ExecutionResult } from "./types";
5
+
6
+ const DOWNLOADS_PATH = resolve("C:/Users/SWEETY/Downloads");
7
+
8
+ let globalStagehand: InstanceType<typeof Stagehand> | null = null;
9
+
10
+ function getStagehandClient(): InstanceType<typeof Stagehand> {
11
+ if (!globalStagehand) {
12
+ globalStagehand = new Stagehand({
13
+ env: "LOCAL",
14
+ localBrowserLaunchOptions: {
15
+ headless: false, // Show browser window
16
+ // devtools: true, // Open developer tools
17
+ port: 9222, // Fixed CDP debugging port
18
+ args: [
19
+ '--no-sandbox',
20
+ '--disable-setuid-sandbox',
21
+ '--disable-web-security',
22
+ '--allow-running-insecure-content',
23
+ ],
24
+ chromiumSandbox: false, // Disable sandbox (adds --no-sandbox)
25
+ ignoreHTTPSErrors: true, // Ignore certificate errors
26
+ locale: 'en-US', // Set browser language
27
+ deviceScaleFactor: 1.0, // Display scaling
28
+ downloadsPath: './downloads', // Download directory
29
+ acceptDownloads: true, // Allow downloads
30
+ connectTimeoutMs: 30000, // Connection timeout
31
+ executablePath:"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
32
+ },
33
+ model: {
34
+ modelName: "google/gemini-3.1-flash-lite-preview",
35
+ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
36
+ },
37
+ });
38
+ }
39
+ return globalStagehand;
40
+ }
41
+
42
+ // Tools whose output is giant DOM/accessibility blobs — skip entirely
43
+ const SKIP_TOOL_OUTPUT = new Set(["ariaTree", "screenshot", "cdpSnapshot"]);
44
+
45
+ /**
46
+ * Parses stagehand agent messages into a compact, evaluator-friendly transcript.
47
+ *
48
+ * Stagehand message shapes (confirmed from live logs):
49
+ *
50
+ * assistant: { role:"assistant", content: [
51
+ * { type:"tool-call", toolName, input, ... } <- agent action intent
52
+ * { type:"text", text: "..." } <- agent's SUMMARY / reasoning text
53
+ * ]}
54
+ *
55
+ * tool: { role:"tool", content: [
56
+ * { type:"tool-result", toolName, output: { type:"json"|"content", value:... } }
57
+ * ]}
58
+ *
59
+ * Strategy:
60
+ * - assistant[type=text] → the agent's written summary/reasoning — MOST VALUABLE
61
+ * - done[input.reasoning] → agent's stated reason for finishing
62
+ * - extract[output] → structured data
63
+ * - navigate/act/wait/goto → one-line confirmation (success + brief detail)
64
+ * - ariaTree/screenshot → SKIPPED (too large, no signal for evaluator)
65
+ */
66
+ function parseAgentMessages(messages: unknown[]): {
67
+ transcript: string;
68
+ extractedData: unknown;
69
+ } {
70
+ if (!Array.isArray(messages)) return { transcript: "", extractedData: null };
71
+
72
+ const summaryTexts: string[] = []; // agent's written text — highest priority
73
+ const actionLines: string[] = []; // tool confirmations — supporting context
74
+ const allExtracts: unknown[] = [];
75
+
76
+ for (const msg of messages) {
77
+ if (typeof msg !== "object" || msg === null) continue;
78
+ const role: string = (msg as any).role ?? "";
79
+ const content: unknown = (msg as any).content;
80
+ const items: unknown[] = Array.isArray(content) ? content : [];
81
+
82
+ // ── assistant messages ────────────────────────────────────────────
83
+ if (role === "assistant") {
84
+ for (const item of items) {
85
+ if (typeof item !== "object" || item === null) continue;
86
+ const type: string = (item as any).type ?? "";
87
+
88
+ // {type:"text"} — this is the agent's written summary/answer
89
+ if (type === "text") {
90
+ const text = String((item as any).text ?? "").trim();
91
+ if (text) summaryTexts.push(text);
92
+ }
93
+
94
+ // {type:"tool-call", toolName:"done"} — capture reasoning from input
95
+ if (type === "tool-call" && (item as any).toolName === "done") {
96
+ const reasoning = (item as any).input?.reasoning;
97
+ if (typeof reasoning === "string" && reasoning.trim()) {
98
+ summaryTexts.push(`[DONE REASON] ${reasoning.trim()}`);
99
+ }
100
+ }
101
+ }
102
+ continue;
103
+ }
104
+
105
+ // ── tool result messages ──────────────────────────────────────────
106
+ if (role === "tool") {
107
+ for (const item of items) {
108
+ if (typeof item !== "object" || item === null) continue;
109
+ const toolName: string = (item as any).toolName ?? (item as any).name ?? "tool";
110
+ const output: unknown = (item as any).output;
111
+
112
+ // Skip large DOM blobs — useless to evaluator
113
+ if (SKIP_TOOL_OUTPUT.has(toolName)) continue;
114
+
115
+ // extract — capture structured data
116
+ if (toolName === "extract") {
117
+ const structured = extractStructured(output);
118
+ if (structured != null) allExtracts.push(structured);
119
+ actionLines.push(`[EXTRACT] ${JSON.stringify(structured ?? output)}`);
120
+ continue;
121
+ }
122
+
123
+ // done tool result — just note success
124
+ if (toolName === "done") {
125
+ const val = getOutputValue(output);
126
+ const ok = (val as any)?.success ?? (val as any)?.taskComplete ?? true;
127
+ actionLines.push(`[DONE] taskComplete=${ok}`);
128
+ continue;
129
+ }
130
+
131
+ // navigate / goto / act / wait / scroll / click / type
132
+ // Pull only the minimal success+detail line — not the whole DOM
133
+ const val = getOutputValue(output);
134
+ if (val != null) {
135
+ const success = (val as any)?.success;
136
+ const url = (val as any)?.url;
137
+ const action = (val as any)?.action;
138
+ const waited = (val as any)?.waited;
139
+ const detail = url ?? action ?? waited ?? "";
140
+ const line = [
141
+ `success=${success ?? "?"}`,
142
+ detail ? String(detail) : "",
143
+ ]
144
+ .filter(Boolean)
145
+ .join(" ");
146
+ actionLines.push(`[${toolName.toUpperCase()}] ${line}`);
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ const extractedData =
153
+ allExtracts.length === 1
154
+ ? allExtracts[0]
155
+ : allExtracts.length > 1
156
+ ? allExtracts
157
+ : null;
158
+
159
+ // Build compact transcript: summaries first (most signal), then action log
160
+ const parts: string[] = [];
161
+ if (summaryTexts.length > 0) {
162
+ parts.push("=== AGENT OUTPUT ===");
163
+ parts.push(summaryTexts.join("\n\n"));
164
+ }
165
+ if (actionLines.length > 0) {
166
+ parts.push("=== ACTION LOG ===");
167
+ parts.push(actionLines.join("\n"));
168
+ }
169
+
170
+ return { transcript: parts.join("\n"), extractedData };
171
+ }
172
+
173
+ function getOutputValue(output: unknown): unknown {
174
+ if (typeof output !== "object" || output === null) return output;
175
+ const o = output as Record<string, unknown>;
176
+ // { type: "json", value: {...} } or { type: "content", value: [...] }
177
+ if ("value" in o) {
178
+ const v = o.value;
179
+ // content arrays: [{type:"text", text:"..."}]
180
+ if (Array.isArray(v)) {
181
+ const texts = v
182
+ .filter((c: any) => c?.type === "text" && typeof c?.text === "string")
183
+ .map((c: any) => c.text)
184
+ .join(" ");
185
+ return texts || v;
186
+ }
187
+ return v;
188
+ }
189
+ return o;
190
+ }
191
+
192
+ /** Pull structured data out of an extract tool output */
193
+ function extractStructured(output: unknown): unknown {
194
+ if (output == null) return null;
195
+ const val = getOutputValue(output);
196
+ if (typeof val !== "object" || val === null) return val;
197
+ const v = val as Record<string, unknown>;
198
+ if (v.success && v.result != null) {
199
+ const r = v.result as Record<string, unknown>;
200
+ return r.extraction ?? r;
201
+ }
202
+ if (v.extraction != null) return v.extraction;
203
+ if (v.result != null) return v.result;
204
+ return val;
205
+ }
206
+
207
+ /**
208
+ * Executes a browser task using Stagehand's agent() function.
209
+ * The agent autonomously navigates, clicks, types, and extracts data
210
+ * based on the plan's goal — no manual step-by-step execution.
211
+ */
212
+ export async function executeBrowserPlan(
213
+ plan: BrowserPlan,
214
+ previousFeedback?: string
215
+ ): Promise<ExecutionResult[]> {
216
+ let stagehand: InstanceType<typeof Stagehand>;
217
+ try {
218
+ stagehand = getStagehandClient();
219
+ await stagehand.init();
220
+ } catch (err) {
221
+ const msg = err instanceof Error ? err.message : String(err);
222
+ const hint = msg.toLowerCase().includes("executable")
223
+ ? " Make sure Brave browser is installed at the configured path in executor.ts."
224
+ : msg.toLowerCase().includes("connect") || msg.toLowerCase().includes("timeout")
225
+ ? " Check that no other browser instance is blocking port 9222."
226
+ : "";
227
+ return [{
228
+ success: false,
229
+ error: `Browser failed to start: ${msg}${hint}`,
230
+ message: "Browser initialization failed",
231
+ stepNumber: 1,
232
+ action: "agent",
233
+ }];
234
+ }
235
+
236
+ // Build the instruction from the plan goal + feedback context
237
+ const instruction = previousFeedback
238
+ ? `${plan.goal}\n\nPrevious attempt feedback to address: ${previousFeedback}`
239
+ : plan.goal;
240
+
241
+ const systemPrompt = `You are a browser automation agent.
242
+
243
+ Rules:
244
+ - Execute the task efficiently and completely.
245
+ - Avoid unnecessary navigation.
246
+ - Extract all requested structured data.
247
+ - Stop only when the task is fully complete.
248
+ - Extract details in deep provide complete comprehensive details about the topic then call it done
249
+
250
+ Before calling done:
251
+ 1. Re-read the original user request.
252
+ 2. Verify every requested piece of information is collected.
253
+ 3. If anything is missing, continue browsing.
254
+ 4. Only call done when all requirements are satisfied.
255
+
256
+ User asks:
257
+ "Get top 5 jobs and detailed descriptions."
258
+
259
+ Required:
260
+ ✓ 5 jobs found
261
+ ✓ description for job 1
262
+ ✓ description for job 2
263
+ ✓ description for job 3
264
+ ✓ description for job 4
265
+ ✓ description for job 5
266
+
267
+ Only call done when all 6 checks pass.
268
+ `;
269
+
270
+ // Ensure downloads folder exists
271
+ if (!existsSync(DOWNLOADS_PATH)) mkdirSync(DOWNLOADS_PATH, { recursive: true });
272
+
273
+ // Use CDP to intercept downloads — may not be available on all setups
274
+ const conn = (stagehand.context as any)?.conn;
275
+ const downloadedFiles: string[] = [];
276
+
277
+ if (conn && typeof conn.send === "function") {
278
+ try {
279
+ await conn.send("Browser.setDownloadBehavior", {
280
+ behavior: "allow",
281
+ downloadPath: DOWNLOADS_PATH,
282
+ eventsEnabled: true,
283
+ });
284
+
285
+ const pendingDownloads = new Map<string, string>();
286
+ conn.on("Browser.downloadWillBegin", (params: any) => {
287
+ const filename = params.suggestedFilename || `download-${params.guid}`;
288
+ const savePath = resolve(DOWNLOADS_PATH, filename);
289
+ pendingDownloads.set(params.guid, savePath);
290
+ console.log(`[download] starting → ${savePath}`);
291
+ });
292
+
293
+ conn.on("Browser.downloadProgress", (params: any) => {
294
+ if (params.state === "completed") {
295
+ const savePath = pendingDownloads.get(params.guid);
296
+ if (savePath) {
297
+ downloadedFiles.push(savePath);
298
+ pendingDownloads.delete(params.guid);
299
+ console.log(`[download] saved → ${savePath}`);
300
+ }
301
+ } else if (params.state === "canceled") {
302
+ pendingDownloads.delete(params.guid);
303
+ }
304
+ });
305
+ } catch {
306
+ // CDP download tracking unavailable — continue without it
307
+ }
308
+ }
309
+
310
+ try {
311
+ const agent = stagehand.agent({
312
+ mode: "dom",
313
+ systemPrompt,
314
+ });
315
+
316
+ const res = await agent.execute({
317
+ instruction,
318
+ maxSteps: 10,
319
+ highlightCursor: true,
320
+ });
321
+
322
+ const messages = res.messages ?? [];
323
+ const { transcript, extractedData } = parseAgentMessages(messages);
324
+ const completed = res.completed ?? false;
325
+
326
+ // Capture any top-level text stagehand puts on the response object
327
+ const topLevelText = [
328
+ (res as any).output,
329
+ (res as any).result,
330
+ (res as any).message,
331
+ (res as any).text,
332
+ ]
333
+ .filter((v) => typeof v === "string" && v.trim().length > 0)
334
+ .join("\n");
335
+
336
+ const agentOutput = transcript || topLevelText;
337
+
338
+ return [
339
+ {
340
+ success: completed,
341
+ message: completed
342
+ ? "Agent completed the task successfully"
343
+ : "Agent did not complete the task",
344
+ stepNumber: 1,
345
+ action: "agent",
346
+ agentOutput,
347
+ agentMessages: messages,
348
+ data:
349
+ extractedData ??
350
+ (topLevelText ? { output: topLevelText } : undefined),
351
+ ...(downloadedFiles.length > 0 && { downloadedFiles }),
352
+ },
353
+ ];
354
+ } catch (error) {
355
+ return [
356
+ {
357
+ success: false,
358
+ error: error instanceof Error ? error.message : String(error),
359
+ message: "Agent execution failed",
360
+ stepNumber: 1,
361
+ action: "agent",
362
+ },
363
+ ];
364
+ }
365
+ }
366
+
367
+ export async function closeStagehand(): Promise<void> {
368
+ if (globalStagehand) {
369
+ await globalStagehand.close();
370
+ globalStagehand = null;
371
+ }
372
+ }
@@ -0,0 +1,13 @@
1
+ export { runBrowserAgentMode } from './orchestrator';
2
+ export type {
3
+ BrowserPlan,
4
+ BrowserStep,
5
+ ExecutionResult,
6
+ IterationResult,
7
+ EvaluationResult,
8
+ BrowserAgentConfig,
9
+ BrowserAgentResult,
10
+ } from './types';
11
+ export { generateBrowserPlan } from './planner';
12
+ export { executeBrowserPlan, closeStagehand } from './executor';
13
+ export { evaluateExecutionResults, shouldContinueIterating } from './evaluator';
@@ -0,0 +1,323 @@
1
+ import chalk from "chalk";
2
+ import { text, confirm } from "@clack/prompts";
3
+ import { withSpinner } from "../../tui/spinner";
4
+ import { renderHTMLMarkdown } from "../../tui/terminal-render.ts";
5
+ import { generateBrowserPlan } from "./planner";
6
+ import { executeBrowserPlan, closeStagehand } from "./executor";
7
+ import {
8
+ evaluateExecutionResults,
9
+ extractFeedbackForNextIteration,
10
+ } from "./evaluator";
11
+ import { sendMail } from "../../email_ops/email_functions";
12
+ import type {
13
+ BrowserAgentResult,
14
+ IterationResult,
15
+ BrowserAgentConfig,
16
+ } from "./types";
17
+ import { printLLMError } from "../../utils/llm-error";
18
+
19
+ const DEFAULT_CONFIG: BrowserAgentConfig = {
20
+ maxIterations: 5,
21
+ timeout: 120000,
22
+ model: "google/gemini-3.1-flash-lite-preview",
23
+ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY || "",
24
+ evaluationThreshold: 80,
25
+ };
26
+
27
+ export async function runBrowserAgentMode(): Promise<void> {
28
+ console.log(chalk.bold("\n🌐 Browser Agent Mode\n"));
29
+
30
+ const queryInput = await text({
31
+ message: "What would you like the browser agent to do?",
32
+ placeholder: "e.g., Find top 5 AI jobs and get their descriptions...",
33
+ });
34
+
35
+ const query = typeof queryInput === "string" ? queryInput : "";
36
+
37
+ if (!query || query.trim() === "") {
38
+ console.log(chalk.yellow("\nOperation cancelled.\n"));
39
+ return;
40
+ }
41
+
42
+ const liveOutput = await confirm({
43
+ message: "Show live iteration output?",
44
+ initialValue: false,
45
+ });
46
+
47
+ const maybeLog = (...args: any[]) => {
48
+ if (liveOutput) console.log(...args);
49
+ };
50
+
51
+ const config = { ...DEFAULT_CONFIG };
52
+ const iterations: IterationResult[] = [];
53
+ let iteration = 0;
54
+ let previousFeedback: string | undefined;
55
+ let finalData: unknown = null;
56
+ let lastEvaluation = null;
57
+
58
+ try {
59
+ while (iteration < config.maxIterations) {
60
+ iteration++;
61
+ maybeLog(chalk.bold(`\n📋 Iteration ${iteration}/${config.maxIterations}`));
62
+
63
+ // PLAN PHASE
64
+ let plan;
65
+ try {
66
+ plan = await withSpinner(
67
+ `[${iteration}/${config.maxIterations}] Planning...`,
68
+ () => generateBrowserPlan(query, previousFeedback)
69
+ );
70
+ } catch (error) {
71
+ printLLMError(error, "Browser planner");
72
+ break;
73
+ }
74
+
75
+ maybeLog(chalk.dim(`Goal: ${plan.goal}`));
76
+ maybeLog(chalk.dim(`Reasoning: ${plan.reasoning}`));
77
+
78
+ // EXECUTE PHASE — Stagehand agent() runs the task autonomously
79
+ let execution;
80
+ try {
81
+ execution = await withSpinner(
82
+ `[${iteration}/${config.maxIterations}] Browser agent running...`,
83
+ () => executeBrowserPlan(plan, previousFeedback)
84
+ );
85
+ } catch (error) {
86
+ console.log(chalk.red(`✖ Browser execution failed: ${error instanceof Error ? error.message : String(error)}`));
87
+ break;
88
+ }
89
+
90
+ // Collect extracted data from agent
91
+ for (const result of execution) {
92
+ if (result.data != null) {
93
+ finalData = result.data;
94
+ }
95
+ }
96
+
97
+ const agentResult = execution[0];
98
+ maybeLog(
99
+ agentResult?.success
100
+ ? chalk.green(`✓ Agent completed task`)
101
+ : chalk.red(`✗ Agent did not complete task`)
102
+ );
103
+ if (agentResult?.agentOutput) {
104
+ maybeLog(chalk.dim(`Output: ${agentResult.agentOutput}`));
105
+ }
106
+
107
+ // EVALUATE PHASE
108
+ let evaluation;
109
+ try {
110
+ evaluation = await withSpinner(
111
+ `[${iteration}/${config.maxIterations}] Evaluating...`,
112
+ () => evaluateExecutionResults(query, plan, execution, config.evaluationThreshold)
113
+ );
114
+ } catch (error) {
115
+ printLLMError(error, "Evaluator");
116
+ break;
117
+ }
118
+
119
+ maybeLog(chalk.cyan(`Score: ${evaluation.score}/100 | Completeness: ${evaluation.completeness}% | Accuracy: ${evaluation.accuracy}%`));
120
+ maybeLog(chalk.dim(`Feedback: ${evaluation.feedback}`));
121
+
122
+ lastEvaluation = evaluation;
123
+
124
+ iterations.push({
125
+ iteration,
126
+ plan,
127
+ execution,
128
+ evaluation,
129
+ shouldContinue: !evaluation.satisfied && iteration < config.maxIterations,
130
+ });
131
+
132
+ if (evaluation.satisfied) {
133
+ maybeLog(chalk.green.bold(`\n✓ Task satisfied!\n`));
134
+ break;
135
+ }
136
+
137
+ if (iteration >= config.maxIterations) {
138
+ maybeLog(chalk.yellow(`\n⚠ Max iterations reached.\n`));
139
+ break;
140
+ }
141
+
142
+ previousFeedback = extractFeedbackForNextIteration(evaluation);
143
+ maybeLog(chalk.yellow(`Retrying with feedback...`));
144
+ }
145
+
146
+ // BUILD CONSOLIDATED RESULT
147
+ const result: BrowserAgentResult = {
148
+ success: lastEvaluation?.satisfied || false,
149
+ query,
150
+ finalData,
151
+ iterations,
152
+ totalIterations: iteration,
153
+ completedAt: new Date().toISOString(),
154
+ error: lastEvaluation?.satisfied ? undefined : "Max iterations reached without full satisfaction",
155
+ };
156
+
157
+ // CONSOLIDATED OUTPUT
158
+ console.log(chalk.bold("\n═══════════════════════════════════════════"));
159
+ console.log(chalk.bold("📊 Browser Agent Result"));
160
+ console.log(chalk.bold("═══════════════════════════════════════════\n"));
161
+
162
+ console.log(`Query: ${chalk.cyan(query)}`);
163
+ console.log(`Status: ${result.success ? chalk.green("✓ Succeeded") : chalk.yellow("⚠ Partial")}`);
164
+ console.log(`Iterations: ${iteration}/${config.maxIterations}`);
165
+
166
+ if (lastEvaluation) {
167
+ console.log(`Score: ${chalk.cyan(`${lastEvaluation.score}/100`)}`);
168
+ console.log(`Complete: ${chalk.cyan(`${lastEvaluation.completeness}%`)}`);
169
+ }
170
+
171
+ // Show final agent output if available
172
+ const lastExecution = iterations[iterations.length - 1]?.execution[0];
173
+ if (lastExecution?.agentOutput) {
174
+ console.log(chalk.bold("\n🤖 Agent Output:"));
175
+ console.log(chalk.white(lastExecution.agentOutput));
176
+ }
177
+
178
+ if (finalData) {
179
+ console.log(chalk.bold("\n📦 Extracted Data:"));
180
+ console.log(chalk.dim(JSON.stringify(finalData, null, 2)));
181
+ }
182
+
183
+ if (lastEvaluation?.feedback) {
184
+ console.log(chalk.bold("\n💬 Final Feedback:"));
185
+ console.log(chalk.dim(lastEvaluation.feedback));
186
+ }
187
+
188
+ console.log(chalk.bold("\n═══════════════════════════════════════════\n"));
189
+
190
+ // Render markdown report
191
+ const reportMarkdown = buildBrowserAgentMarkdownReport(result);
192
+ console.log(chalk.bold("📄 Report\n"));
193
+ console.log(renderHTMLMarkdown(reportMarkdown));
194
+ console.log(chalk.bold("\n═══════════════════════════════════════════\n"));
195
+
196
+ // Save options
197
+ const shouldSaveJson = await confirm({
198
+ message: "Save results to JSON file?",
199
+ initialValue: false,
200
+ });
201
+
202
+ if (shouldSaveJson) {
203
+ const fs = await import("fs");
204
+ const path = await import("path");
205
+ const filename = `browser-agent-result-${Date.now()}.json`;
206
+ const filepath = path.resolve(process.cwd(), filename);
207
+ fs.writeFileSync(filepath, JSON.stringify(result, null, 2), "utf8");
208
+ console.log(chalk.green(`✓ Saved to ${filename}\n`));
209
+ }
210
+
211
+ const shouldSaveMd = await confirm({
212
+ message: "Save Markdown report to file?",
213
+ initialValue: false,
214
+ });
215
+
216
+ if (shouldSaveMd) {
217
+ const fs = await import("fs");
218
+ const path = await import("path");
219
+ const filename = `browser-agent-report-${Date.now()}.md`;
220
+ const filepath = path.resolve(process.cwd(), filename);
221
+ fs.writeFileSync(filepath, reportMarkdown, "utf8");
222
+ console.log(chalk.green(`✓ Saved to ${filename}\n`));
223
+ }
224
+
225
+ // ── Email option ──────────────────────────────────────────────────
226
+ const shouldEmail = await confirm({
227
+ message: "Send a summary to your email?",
228
+ initialValue: false,
229
+ });
230
+
231
+ if (shouldEmail) {
232
+ const emailTo = await text({ message: "Send to (email address)?" });
233
+ if (typeof emailTo === "string" && emailTo.trim()) {
234
+ // Build a concise email body from the agent output + score
235
+ const agentOut = lastExecution?.agentOutput ?? "";
236
+ const scoreInfo = lastEvaluation
237
+ ? `Score: ${lastEvaluation.score}/100 | Completeness: ${lastEvaluation.completeness}%`
238
+ : "";
239
+ const emailBody = [
240
+ `Query: ${query}`,
241
+ scoreInfo,
242
+ "",
243
+ agentOut || JSON.stringify(finalData, null, 2) || "(no output)",
244
+ ]
245
+ .filter(Boolean)
246
+ .join("\n");
247
+
248
+ await withSpinner("Sending email…", () =>
249
+ sendMail({
250
+ to: emailTo.trim(),
251
+ subject: `Browser Agent: ${query.slice(0, 60)}`,
252
+ body: emailBody,
253
+ })
254
+ );
255
+ console.log(chalk.green("✓ Email sent\n"));
256
+ }
257
+ }
258
+ } catch (error) {
259
+ printLLMError(error, "Browser Agent");
260
+ } finally {
261
+ try {
262
+ await closeStagehand();
263
+ } catch (err) {
264
+ console.error("Error closing browser:", err);
265
+ }
266
+ }
267
+ }
268
+
269
+ function buildBrowserAgentMarkdownReport(result: BrowserAgentResult): string {
270
+ const lines: string[] = [];
271
+
272
+ lines.push("# Browser Agent Report\n");
273
+ lines.push(`**Query:** ${result.query}`);
274
+ lines.push(`**Status:** ${result.success ? "✅ Succeeded" : "⚠️ Partial"}`);
275
+ lines.push(`**Total Iterations:** ${result.totalIterations}`);
276
+ lines.push(`**Completed At:** ${result.completedAt}`);
277
+ if (result.error) {
278
+ lines.push(`**Note:** ${result.error}`);
279
+ }
280
+ lines.push("");
281
+
282
+ // Only show the last (best) iteration's detail in consolidated mode
283
+ const lastIteration = result.iterations[result.iterations.length - 1];
284
+ if (lastIteration) {
285
+ lines.push("## Final Iteration Result\n");
286
+
287
+ const agentResult = lastIteration.execution[0];
288
+ if (agentResult?.agentOutput) {
289
+ lines.push("### Agent Output");
290
+ lines.push(agentResult.agentOutput);
291
+ lines.push("");
292
+ }
293
+
294
+ lines.push("### Evaluation");
295
+ lines.push(`- **Score:** ${lastIteration.evaluation.score}/100`);
296
+ lines.push(`- **Completeness:** ${lastIteration.evaluation.completeness}%`);
297
+ lines.push(`- **Accuracy:** ${lastIteration.evaluation.accuracy}%`);
298
+ lines.push(`- **Feedback:** ${lastIteration.evaluation.feedback}`);
299
+ if (lastIteration.evaluation.issues.length > 0) {
300
+ lines.push("- **Issues:**");
301
+ lastIteration.evaluation.issues.forEach((issue) => {
302
+ lines.push(` - ${issue}`);
303
+ });
304
+ }
305
+ lines.push("");
306
+ }
307
+
308
+ lines.push("## Extracted Data\n");
309
+ lines.push("```json");
310
+ lines.push(JSON.stringify(result.finalData ?? {}, null, 2));
311
+ lines.push("```\n");
312
+
313
+ if (result.iterations.length > 1) {
314
+ lines.push("## Iteration Summary\n");
315
+ result.iterations.forEach((it) => {
316
+ lines.push(`- Iteration ${it.iteration}: Score ${it.evaluation.score}/100 — ${it.evaluation.satisfied ? "✅ Satisfied" : "🔄 Continued"}`);
317
+ });
318
+ lines.push("");
319
+ }
320
+
321
+ lines.push("*Generated by Browser Agent*");
322
+ return lines.join("\n");
323
+ }