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,200 @@
1
+ import { getAgentModel, getAgentModel2, getAgentModel2Fallback } from "../../config/ai.config";
2
+ import type { BrowserPlan, BrowserStep } from "./types";
3
+ import { generateText } from "ai";
4
+ import { z } from "zod";
5
+ import chalk from "chalk";
6
+ import { parseLLMError } from "../../utils/llm-error";
7
+
8
+ const optionalString = () =>
9
+ z.preprocess((value) => (value === null ? undefined : value), z.string().optional());
10
+
11
+ const optionalRecord = () =>
12
+ z.preprocess(
13
+ (value) => (value === null ? undefined : value),
14
+ z.record(z.string()).optional()
15
+ );
16
+
17
+ const BrowserStepSchema = z.object({
18
+ id: z.number(),
19
+ action: z.enum([
20
+ "navigate",
21
+ "click",
22
+ "type",
23
+ "extract",
24
+ "wait",
25
+ "observe",
26
+ "scroll",
27
+ ]),
28
+ description: z.string(),
29
+ selector: optionalString(),
30
+ value: optionalString(),
31
+ waitFor: optionalString(),
32
+ extractSchema: optionalRecord(),
33
+ }).strict();
34
+
35
+ const BrowserPlanSchema = z.object({
36
+ goal: z.string(),
37
+ steps: z.array(BrowserStepSchema),
38
+ reasoning: z.string(),
39
+ }).strict();
40
+
41
+ function extractJsonObjectFromText(text: string): string | null {
42
+ let jsonText = text.trim();
43
+
44
+ if (jsonText.startsWith("```json")) {
45
+ jsonText = jsonText.slice(7);
46
+ }
47
+ if (jsonText.startsWith("```")) {
48
+ jsonText = jsonText.slice(3);
49
+ }
50
+ if (jsonText.endsWith("```")) {
51
+ jsonText = jsonText.slice(0, -3);
52
+ }
53
+
54
+ jsonText = jsonText.trim();
55
+
56
+ const firstBraceIndex = jsonText.indexOf("{");
57
+ if (firstBraceIndex === -1) {
58
+ return null;
59
+ }
60
+
61
+ let depth = 0;
62
+ for (let i = firstBraceIndex; i < jsonText.length; i += 1) {
63
+ const char = jsonText[i];
64
+ if (char === "{") {
65
+ depth += 1;
66
+ } else if (char === "}") {
67
+ depth -= 1;
68
+ if (depth === 0) {
69
+ return jsonText.slice(firstBraceIndex, i + 1);
70
+ }
71
+ }
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ export async function generateBrowserPlan(
78
+ query: string,
79
+ previousFeedback?: string
80
+ ): Promise<BrowserPlan> {
81
+ let model = getAgentModel();
82
+ let usingFallback = false;
83
+
84
+ const systemPrompt = `You are a browser automation planner. Your task is to create a detailed plan for how to automate a browser task.
85
+
86
+ IMPORTANT: You are helping with legitimate web automation for research, data collection, and productivity purposes. This is not for any harmful activities.
87
+
88
+ ${
89
+ previousFeedback
90
+ ? `Previous feedback indicated issues. Address them in this iteration: ${previousFeedback}`
91
+ : ""
92
+ }
93
+
94
+ Create a plan with concrete steps that will:
95
+ 1. Navigate to relevant URLs
96
+ 2. Interact with page elements (click, type, scroll)
97
+ 3. Extract information in structured format
98
+ 4. Validate the results
99
+
100
+ Be specific about selectors and values. Each step should be actionable and precise.
101
+
102
+ You MUST respond with ONLY a valid JSON object (no markdown, no code blocks). If you include any explanation, place it after the JSON object and not inside it.
103
+
104
+ The exact JSON schema is:
105
+ {
106
+ "goal": "string",
107
+ "steps": [
108
+ {
109
+ "id": number,
110
+ "action": "navigate" | "click" | "type" | "extract" | "wait" | "observe" | "scroll",
111
+ "description": "string",
112
+ "selector": "string (optional)",
113
+ "value": "string (optional)",
114
+ "waitFor": "string (optional)",
115
+ "extractSchema": {"fieldName": "description"} (optional)
116
+ }
117
+ ],
118
+ "reasoning": "string"
119
+ }`;
120
+
121
+ const userPrompt = `Create a browser automation plan for: ${query}
122
+
123
+ Respond with valid JSON only. No markdown, no explanations, just the JSON object.`;
124
+
125
+ let lastError: Error | null = null;
126
+
127
+ // Try up to 4 attempts (2 with primary model, 2 with fallback)
128
+ for (let attempt = 1; attempt <= 4; attempt += 1) {
129
+ // Switch to fallback model after 2 failed attempts
130
+ if (attempt === 3 && !usingFallback) {
131
+ console.log(chalk.dim("Primary model failed, switching to fallback model..."));
132
+ model = getAgentModel2Fallback();
133
+ usingFallback = true;
134
+ }
135
+
136
+ let response;
137
+ try {
138
+ response = await generateText({
139
+ model,
140
+ messages: [
141
+ { role: "system", content: systemPrompt },
142
+ { role: "user", content: userPrompt },
143
+ ],
144
+ temperature: 0.7,
145
+ });
146
+ } catch (err) {
147
+ lastError = err instanceof Error ? err : new Error(String(err));
148
+ const parsed = parseLLMError(err);
149
+ // Don't retry on auth/quota errors
150
+ if (!parsed.retryable) {
151
+ throw new Error(`Browser planner: ${parsed.message}`);
152
+ }
153
+ continue;
154
+ }
155
+
156
+ const rawText = response.text ?? "";
157
+
158
+ // Check for refusal patterns
159
+ if (rawText.includes("I'm sorry") || rawText.includes("I can't help") || rawText.includes("cannot assist")) {
160
+ lastError = new Error(
161
+ `Model refused to generate plan. This may be due to content safety filters. Raw response: ${rawText}\n\nTry:\n1. Rephrasing your request more explicitly as legitimate research/automation\n2. Using a different model\n3. Being more specific about the legitimate use case`
162
+ );
163
+ continue;
164
+ }
165
+
166
+ const jsonText = extractJsonObjectFromText(rawText);
167
+
168
+ if (!jsonText) {
169
+ lastError = new Error(
170
+ `Unable to locate a valid JSON object in the model response. Raw response: ${rawText}`
171
+ );
172
+ continue;
173
+ }
174
+
175
+ try {
176
+ const parsed = JSON.parse(jsonText);
177
+
178
+ // Check if this is an error response instead of a valid plan
179
+ if (parsed.error || (!parsed.goal && !parsed.steps)) {
180
+ lastError = new Error(
181
+ `Model returned an error or invalid response instead of a plan: ${JSON.stringify(parsed)}\n\nThis typically means content safety filters were triggered.`
182
+ );
183
+ continue;
184
+ }
185
+
186
+ return BrowserPlanSchema.parse(parsed);
187
+ } catch (error) {
188
+ lastError = new Error(
189
+ `Unable to parse JSON plan from model response. Raw extracted JSON: ${jsonText}. Error: ${
190
+ error instanceof Error ? error.message : String(error)
191
+ }`
192
+ );
193
+ continue;
194
+ }
195
+ }
196
+
197
+ throw new Error(
198
+ `Failed to parse browser plan after trying both primary and fallback models: ${lastError?.message ?? "Unknown parser failure."}`
199
+ );
200
+ }
@@ -0,0 +1,62 @@
1
+ export interface BrowserPlan {
2
+ goal: string;
3
+ steps: BrowserStep[];
4
+ reasoning: string;
5
+ }
6
+
7
+ export interface BrowserStep {
8
+ id: number;
9
+ action: "navigate" | "click" | "type" | "extract" | "wait" | "observe" | "scroll";
10
+ description: string;
11
+ selector?: string;
12
+ value?: string;
13
+ waitFor?: string;
14
+ extractSchema?: Record<string, string>;
15
+ }
16
+
17
+ export interface ExecutionResult {
18
+ success: boolean;
19
+ data?: unknown;
20
+ error?: string;
21
+ message: string;
22
+ stepNumber: number;
23
+ action: string;
24
+ // Raw output from stagehand agent
25
+ agentOutput?: string;
26
+ agentMessages?: unknown[];
27
+ }
28
+
29
+ export interface IterationResult {
30
+ iteration: number;
31
+ plan: BrowserPlan;
32
+ execution: ExecutionResult[];
33
+ evaluation: EvaluationResult;
34
+ shouldContinue: boolean;
35
+ }
36
+
37
+ export interface EvaluationResult {
38
+ satisfied: boolean;
39
+ score: number; // 0-100
40
+ feedback: string;
41
+ completeness: number; // 0-100
42
+ accuracy: number; // 0-100
43
+ issues: string[];
44
+ }
45
+
46
+ export interface BrowserAgentConfig {
47
+ maxIterations: number;
48
+ timeout: number;
49
+ model: string;
50
+ apiKey: string;
51
+ evaluationThreshold: number; // 0-100, default 80
52
+ }
53
+
54
+ export interface BrowserAgentResult {
55
+ success: boolean;
56
+ query: string;
57
+ finalData: unknown;
58
+ iterations: IterationResult[];
59
+ totalIterations: number;
60
+ completedAt: string;
61
+ error?: string;
62
+ }
@@ -0,0 +1,128 @@
1
+ import { Stagehand } from "@browserbasehq/stagehand";
2
+ import { config } from "dotenv";
3
+ config({ path: "../.env" });
4
+
5
+ let globalstageHand: InstanceType<typeof Stagehand> | null = null;
6
+
7
+ function getExtractResults(messages: any[]) {
8
+ const extracts = [];
9
+
10
+ for (const msg of messages) {
11
+ if (msg.role !== "tool") continue;
12
+
13
+ for (const item of msg.content ?? []) {
14
+ if (item.toolName === "extract" && item.output?.value?.success) {
15
+ extracts.push(item.output.value.result.extraction);
16
+ }
17
+ }
18
+ }
19
+
20
+ return extracts;
21
+ }
22
+
23
+ const getStagehandClient = () => {
24
+ if (!globalstageHand) {
25
+ globalstageHand = new Stagehand({
26
+ env: "LOCAL",
27
+ localBrowserLaunchOptions: {
28
+ headless: false, // Show browser window
29
+ devtools: false, // Open developer tools
30
+ port: 9222, // Fixed CDP debugging port
31
+ args: [
32
+ '--no-sandbox',
33
+ '--disable-setuid-sandbox',
34
+ '--disable-web-security',
35
+ '--allow-running-insecure-content',
36
+ ],
37
+ ignoreHTTPSErrors: true, // Ignore certificate errors
38
+ locale: 'en-US', // Set browser language
39
+ deviceScaleFactor: 1.0, // Display scaling
40
+ downloadsPath: './downloads', // Download directory
41
+ acceptDownloads: true, // Allow downloads
42
+ connectTimeoutMs: 30000, // Connection timeout
43
+ executablePath:"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
44
+ },
45
+ model: {
46
+ modelName: "google/gemini-3.1-flash-lite-preview",
47
+ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
48
+ },
49
+ });
50
+ }
51
+ return globalstageHand;
52
+ };
53
+ export const browserSearch = async (query: string) => {
54
+ // const stagehand = new Stagehand({
55
+ // env: "LOCAL",
56
+ // localBrowserLaunchOptions: {
57
+ // headless: false,
58
+ // devtools: false,
59
+ // },
60
+ // model: {
61
+ // modelName: "google/gemini-3.1-flash-lite-preview",
62
+ // apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
63
+ // },
64
+ // });
65
+ const stagehand = getStagehandClient();
66
+ await stagehand.init();
67
+
68
+ const agent = stagehand.agent({
69
+ mode: "dom",
70
+
71
+ systemPrompt: `
72
+ You are a browser automation agent.
73
+
74
+ Rules:
75
+ - Search efficiently.
76
+ - Avoid unnecessary navigation.
77
+ - Extract structured data.
78
+ - Stop when task is complete.
79
+
80
+
81
+ Before calling done:
82
+
83
+ 1. Re-read the original user request.
84
+
85
+ 2. Create a checklist.
86
+
87
+ 3. Verify every item is satisfied.
88
+
89
+ 4. If any requested information is missing,
90
+ DO NOT call done.
91
+
92
+ 5. Explicitly count collected results.
93
+
94
+ Example:
95
+
96
+ User asks:
97
+ "Get top 5 jobs and detailed descriptions."
98
+
99
+ Required:
100
+ āœ“ 5 jobs found
101
+ āœ“ description for job 1
102
+ āœ“ description for job 2
103
+ āœ“ description for job 3
104
+ āœ“ description for job 4
105
+ āœ“ description for job 5
106
+
107
+ Only call done when all 6 checks pass.
108
+ `,
109
+ });
110
+
111
+ const res = await agent.execute({
112
+ instruction: query,
113
+ maxSteps: 10,
114
+ highlightCursor: true,
115
+ });
116
+
117
+ res.messages?.forEach((e) => {
118
+ console.log(e.role);
119
+ console.log(JSON.stringify(e.content));
120
+ });
121
+ const output = getExtractResults(res.messages ?? []);
122
+ console.log("\n\n\n\n output " + output);
123
+ await stagehand.close();
124
+ };
125
+
126
+ browserSearch(
127
+ `go and find rag youtube video fo piyush garg get its link then go to youtube transcript site and get me its complete transcript`,
128
+ );
package/plan/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { runPlanMode } from './orchestrator';
2
+ export type { Plan, PlanStep } from './types';
3
+ export { runBrowserAgentMode } from './browser-agent';
4
+ export type {
5
+ BrowserPlan,
6
+ BrowserStep,
7
+ ExecutionResult,
8
+ IterationResult,
9
+ EvaluationResult,
10
+ BrowserAgentConfig,
11
+ BrowserAgentResult,
12
+ } from './browser-agent';
@@ -0,0 +1,214 @@
1
+ import chalk from 'chalk';
2
+ import { confirm, isCancel, text } from '@clack/prompts';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { ToolLoopAgent, stepCountIs } from 'ai';
6
+ import { ActionTracker } from '../agent/action-tracker.ts';
7
+ import { ToolExecutor } from '../agent/tool-executor.ts';
8
+ import { createAgentTools } from '../agent/agent-tools.ts';
9
+ import { defaultAgentConfig } from '../agent/types.ts';
10
+ import { runApprovalFlow } from '../agent/approval.ts';
11
+ import { generatePlan } from './planner';
12
+ import { printPlan, selectSteps } from './selection';
13
+ import { createWebTools } from './web-tools';
14
+ import type { PlanStep } from './types';
15
+ import { renderHTMLMarkdown } from '../tui/terminal-render.ts';
16
+ import { getAgentModel } from '../config/ai.config.ts';
17
+ import { withSpinner } from '../tui/spinner';
18
+ import { withLLMRetry, printLLMError } from '../utils/llm-error';
19
+
20
+ function stepPrompt(goal: string, step: PlanStep, projectDir?: string): string {
21
+ const parts = [`Overall Goal: ${goal}`, `Current Step: ${step.title}`, step.description];
22
+ if (projectDir) {
23
+ parts.push(`\nIMPORTANT: The project already exists at: ${projectDir}\nAll file operations MUST be inside this folder. Do NOT create a new project folder.`);
24
+ }
25
+ return parts.join('\n');
26
+ }
27
+
28
+ /** After each step, detect which project folder was created/modified */
29
+ function detectProjectDir(codebasePath: string, knownDir?: string): string | undefined {
30
+ if (knownDir) return knownDir;
31
+ // Look for folders that contain a package.json — likely the project root
32
+ try {
33
+ const entries = fs.readdirSync(codebasePath, { withFileTypes: true });
34
+ for (const e of entries) {
35
+ if (!e.isDirectory()) continue;
36
+ if (['node_modules', '.git', 'dist', 'build'].includes(e.name)) continue;
37
+ const pkgPath = path.join(codebasePath, e.name, 'package.json');
38
+ if (fs.existsSync(pkgPath)) return e.name;
39
+ }
40
+ } catch {}
41
+ return undefined;
42
+ }
43
+
44
+ export async function runPlanMode(): Promise<void> {
45
+ console.log(chalk.bold('\n🧭 Plan Mode\n'));
46
+
47
+ const goal = await text({ message: 'What is your goal?' });
48
+ if (isCancel(goal) || !goal.trim()) return;
49
+ const includeWorkspace = await confirm({
50
+ message: 'Include workspace research (scan repository)?',
51
+ initialValue: true,
52
+ });
53
+
54
+ const spinnerLabel = includeWorkspace ? 'Researching & drafting a plan…' : 'Drafting plan…';
55
+ let plan;
56
+ try {
57
+ plan = await withSpinner(spinnerLabel, async () =>
58
+ generatePlan(goal.trim(), { useWorkspace: includeWorkspace as boolean }),
59
+ );
60
+ } catch (err) {
61
+ printLLMError(err, "Plan");
62
+ return;
63
+ }
64
+ printPlan(plan);
65
+
66
+ const wantsSave = await confirm({
67
+ message: 'Save this plan to a .md file?',
68
+ initialValue: true,
69
+ });
70
+
71
+ let savedPath: string | undefined;
72
+ if (wantsSave) {
73
+ const filename = await text({ message: 'Filename', initialValue: 'plan.md', validate: (v) => {
74
+ const s = (v ?? '').trim();
75
+ if (!s) return 'Required';
76
+ if (s.includes('..') || s.includes('/') || s.includes('\\')) return 'No paths';
77
+ if (!s.toLowerCase().endsWith('.md')) return 'Must end with .md';
78
+ } });
79
+ if (!isCancel(filename)) {
80
+ const outPath = path.resolve(process.cwd(), filename.trim());
81
+ const md = [`# Plan: ${plan.goal}`, '', '```json', JSON.stringify({ goal: plan.goal, researchSummary: plan.researchSummary, steps: plan.steps }, null, 2), '```', ''].join('\n');
82
+ fs.writeFileSync(outPath, md, 'utf8');
83
+ savedPath = outPath;
84
+ console.log(chalk.green(`Saved plan to ${outPath}`));
85
+ }
86
+ }
87
+
88
+ const selected = await selectSteps(plan);
89
+ if (selected.length === 0) return;
90
+
91
+ const executeNow = await confirm({ message: `Execute ${selected.length} step(s) via Agent Mode?`, initialValue: false });
92
+ if (!isCancel(executeNow) && executeNow) {
93
+ // Ensure plan is stored in an .md file to drive execution
94
+ if (!savedPath) {
95
+ const mustSave = await confirm({ message: 'Execution requires a saved plan .md file. Save now?', initialValue: true });
96
+ if (isCancel(mustSave) || !mustSave) {
97
+ console.log(chalk.yellow('Execution cancelled (plan not saved).'));
98
+ return;
99
+ }
100
+ const filename = await text({ message: 'Filename', initialValue: 'plan.md', validate: (v) => {
101
+ const s = (v ?? '').trim();
102
+ if (!s) return 'Required';
103
+ if (s.includes('..') || s.includes('/') || s.includes('\\')) return 'No paths';
104
+ if (!s.toLowerCase().endsWith('.md')) return 'Must end with .md';
105
+ } });
106
+ if (isCancel(filename)) return;
107
+ const outPath = path.resolve(process.cwd(), filename.trim());
108
+ const md = [`# Plan: ${plan.goal}`, '', '```json', JSON.stringify({ goal: plan.goal, researchSummary: plan.researchSummary, steps: plan.steps }, null, 2), '```', ''].join('\n');
109
+ fs.writeFileSync(outPath, md, 'utf8');
110
+ savedPath = outPath;
111
+ console.log(chalk.green(`Saved plan to ${outPath}`));
112
+ }
113
+
114
+ // Read plan from savedPath and parse JSON block
115
+ let planObj: { goal: string; researchSummary?: string; steps: PlanStep[] } | null= null;
116
+ try {
117
+ const raw = fs.readFileSync(savedPath!, 'utf8');
118
+ const m = raw.match(/```json\s*([\s\S]*?)\s*```/i);
119
+ if (m && m[1]) {
120
+ planObj = JSON.parse(m[1]);
121
+ } else {
122
+ throw new Error('No JSON block found in plan file');
123
+ }
124
+ } catch (err) {
125
+ console.log(chalk.red('Failed to parse saved plan .md; aborting execution.'));
126
+ console.error(err);
127
+ return;
128
+ }
129
+
130
+ const config = defaultAgentConfig();
131
+ // Single tracker accumulates all step actions for the final review log
132
+ const tracker = new ActionTracker();
133
+ const executor = new ToolExecutor(tracker, config);
134
+
135
+ const tools = {
136
+ ...createAgentTools(executor),
137
+ ...(process.env.FIRECRAWL_API_KEY ? createWebTools(tracker) : {}),
138
+ };
139
+
140
+ let projectDir: string | undefined;
141
+ const totalSteps = planObj!.steps.length;
142
+ const allErrors: string[] = [];
143
+
144
+ for (let stepIdx = 0; stepIdx < planObj!.steps.length; stepIdx++) {
145
+ const step = planObj!.steps[stepIdx]!;
146
+ console.log(chalk.bold(`\nšŸ”§ [${stepIdx + 1}/${totalSteps}] ${step.title}\n`));
147
+
148
+ // Detect project folder from real disk (visible after previous step was applied)
149
+ projectDir = detectProjectDir(config.codebasePath, projectDir);
150
+
151
+ const agent = new ToolLoopAgent({
152
+ model: getAgentModel(),
153
+ stopWhen: stepCountIs(30),
154
+ tools,
155
+ instructions: `
156
+ WorkDir: ${config.codebasePath}
157
+ You are an AI coding agent executing ONE step of a multi-step plan.
158
+
159
+ CRITICAL RULES:
160
+ 1. This is step ${stepIdx + 1} of ${totalSteps}. Do ONLY what this step describes — no more, no less.
161
+ 2. ${projectDir
162
+ ? `The project already exists at "${projectDir}/" (relative to WorkDir). ALL files MUST go inside "${projectDir}/". NEVER create a new top-level folder.`
163
+ : `If this step scaffolds a new project, choose ONE folder name. Every subsequent step will use that same folder.`}
164
+ 3. ALWAYS call list_files first to see what already exists on disk. Use modify_file for existing files, create_file only for genuinely new ones.
165
+ 4. NEVER create a duplicate or alternate project folder. There is exactly ONE project folder.
166
+ 5. Use create_file / modify_file / create_folder tools only — never print code as plain text.
167
+ `.trim(),
168
+ });
169
+
170
+ let r;
171
+ try {
172
+ r = await withLLMRetry(
173
+ () => agent.generate({
174
+ prompt: stepPrompt(planObj!.goal, step, projectDir),
175
+ onStepFinish: ({ toolCalls }) => {
176
+ for (const tc of toolCalls) {
177
+ const preview = JSON.stringify(tc?.input).slice(0, 160);
178
+ console.log(chalk.green(' āœ“'), chalk.bold(String(tc?.toolName)), chalk.dim(preview + (preview.length >= 160 ? '...' : '')));
179
+ }
180
+ },
181
+ }),
182
+ { maxRetries: 3, context: `Step: ${step.title}` }
183
+ );
184
+ } catch (err) {
185
+ printLLMError(err, `Step: ${step.title}`);
186
+ continue;
187
+ }
188
+ if (r.text?.trim()) console.log(renderHTMLMarkdown(r.text));
189
+
190
+ // Auto-approve and apply this step's staged changes immediately to disk
191
+ // so the next step's list_files sees the real files
192
+ const pending = tracker.getPendingMutations();
193
+ for (const a of pending) tracker.updateStatus(a.id, 'approved', true);
194
+ const { errors } = executor.applyApprovedFromTracker();
195
+ executor.clearStaging();
196
+ if (errors.length) {
197
+ allErrors.push(...errors.map(e => `[${step.title}] ${e}`));
198
+ console.log(chalk.yellow(` ⚠ ${errors.length} error(s) applying step — continuing`));
199
+ } else {
200
+ console.log(chalk.green(` āœ“ Step ${stepIdx + 1} applied to disk\n`));
201
+ }
202
+
203
+ // Re-detect project dir now that files are on disk
204
+ projectDir = detectProjectDir(config.codebasePath, projectDir);
205
+ }
206
+
207
+ if (allErrors.length) {
208
+ console.log(chalk.red('\nErrors during execution:'));
209
+ for (const e of allErrors) console.log(chalk.red(` • ${e}`));
210
+ } else {
211
+ console.log(chalk.green('\nāœ“ All steps applied.\n'));
212
+ }
213
+ }
214
+ }