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.
- package/CLI/cli.ts +42 -0
- package/README.md +137 -0
- package/SETUP.md +584 -0
- package/agent/action-tracker.ts +45 -0
- package/agent/agent-tools.ts +111 -0
- package/agent/approval.ts +137 -0
- package/agent/diff-view.ts +26 -0
- package/agent/orchestrator.ts +186 -0
- package/agent/tool-executor.ts +463 -0
- package/agent/types.ts +69 -0
- package/ask/orchestrator.ts +244 -0
- package/auth/auth.ts +567 -0
- package/auth/config-store.ts +77 -0
- package/auth/crypto.ts +51 -0
- package/auth/env-writer.ts +82 -0
- package/bin/jerob.js +28 -0
- package/config/ai.config.ts +163 -0
- package/email_ops/email-tools.ts +178 -0
- package/email_ops/email_functions.ts +443 -0
- package/email_ops/email_init.ts +92 -0
- package/email_ops/email_pass_store.ts +61 -0
- package/email_ops/email_server.ts +29 -0
- package/email_ops/types.ts +88 -0
- package/index.ts +176 -0
- package/package.json +88 -0
- package/plan/browser-agent/README.md +118 -0
- package/plan/browser-agent/USAGE.md +308 -0
- package/plan/browser-agent/evaluator.ts +353 -0
- package/plan/browser-agent/executor.ts +372 -0
- package/plan/browser-agent/index.ts +13 -0
- package/plan/browser-agent/orchestrator.ts +323 -0
- package/plan/browser-agent/planner.ts +200 -0
- package/plan/browser-agent/types.ts +62 -0
- package/plan/browser-tool.ts +128 -0
- package/plan/index.ts +12 -0
- package/plan/orchestrator.ts +214 -0
- package/plan/planner.ts +183 -0
- package/plan/selection.ts +50 -0
- package/plan/types.ts +13 -0
- package/plan/web-tools.ts +119 -0
- package/scheduler/ARCHITECTURE.md +263 -0
- package/scheduler/README.md +200 -0
- package/scheduler/SETUP-READY.sql +84 -0
- package/scheduler/check-status.sql +124 -0
- package/scheduler/config-sync.ts +91 -0
- package/scheduler/db-migrate.ts +271 -0
- package/scheduler/db.ts +162 -0
- package/scheduler/debug.ts +184 -0
- package/scheduler/orchestrator.ts +438 -0
- package/scheduler/planner.ts +170 -0
- package/scheduler/update-task-email.ts +70 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/linked-project.json +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/deploy.ps1 +50 -0
- package/supabase/functions/scheduler-tick/index.ts +496 -0
- package/supabase/supabase/.temp/linked-project.json +1 -0
- package/tsconfig.json +33 -0
- package/tui/spinner.ts +33 -0
- package/tui/spinup.ts +67 -0
- package/tui/terminal-render.ts +16 -0
- package/utils/llm-error.ts +185 -0
- 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
|
+
}
|