@lasso-ai/cli 1.2.3 → 1.2.4

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.
@@ -3,36 +3,365 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isConversationalPrompt = isConversationalPrompt;
6
7
  exports.runOneShotAgent = runOneShotAgent;
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_path_1 = __importDefault(require("node:path"));
9
10
  const node_child_process_1 = require("node:child_process");
10
11
  const node_util_1 = require("node:util");
11
12
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
12
- async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, onPrompt) {
13
+ function isConversationalPrompt(prompt) {
14
+ const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
15
+ const greetings = [
16
+ "hi", "hello", "hey", "heya", "howdy", "sup", "yo",
17
+ "good morning", "good afternoon", "good evening",
18
+ "thanks", "thank you", "thx", "ty", "cool", "awesome", "great", "nice",
19
+ "who are you", "what are you", "what can you do", "help", "help me",
20
+ "what is lasso", "how do you work", "how does this work",
21
+ ];
22
+ if (greetings.includes(p))
23
+ return true;
24
+ if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
25
+ const buildKeywords = /(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)/i;
26
+ if (!buildKeywords.test(p))
27
+ return true;
28
+ }
29
+ const questionOnly = /^(what is|what's|how do i|why does|can you explain|tell me about)\b/i.test(p);
30
+ const actionVerbs = /(build|create|add|make|fix|update|modify|change|refactor|implement|write code|code this|delete|remove|generate)/i;
31
+ if (questionOnly && !actionVerbs.test(p)) {
32
+ return true;
33
+ }
34
+ return false;
35
+ }
36
+ function runtimeErrorObservation(pageContext) {
37
+ const errors = Array.isArray(pageContext?.runtimeErrors) ? pageContext.runtimeErrors : [];
38
+ if (!errors.length)
39
+ return "- Console/Runtime Errors: None";
40
+ return `- Console/Runtime Errors (the page is currently throwing these):\n${errors.map((e) => ` ! ${e}`).join("\n")}`;
41
+ }
42
+ async function generateConversationalReply(prompt, framework, config, signal, messages, pageContext) {
43
+ const history = messages?.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n") || "";
44
+ let pageObservation = "";
45
+ if (pageContext) {
46
+ const headings = (pageContext.domSummary?.headings || []).map((h) => `H${h.level}: "${h.text}"`).join("\n ");
47
+ const sections = (pageContext.domSummary?.sections || []).map((s) => `- [${s.name}]: ${s.textPreview}`).join("\n ");
48
+ const buttons = (pageContext.domSummary?.buttons || []).join(", ");
49
+ const links = (pageContext.domSummary?.links || []).join(", ");
50
+ pageObservation = `
51
+ Live Browser Page Context (what the user is currently viewing in their live app):
52
+ - Route: ${pageContext.route || "/"}
53
+ - Page Title: "${pageContext.title || "Untitled"}"
54
+ - Viewport: ${pageContext.viewport?.width}x${pageContext.viewport?.height} (Document height: ${pageContext.viewport?.scrollHeight}px, scrolled: ${pageContext.viewport?.scrollY}px)
55
+ - Rendered Headings:
56
+ ${headings || "None"}
57
+ - Rendered Page Sections & Landmarks:
58
+ ${sections || "None"}
59
+ - Call-to-Action Buttons: ${buttons || "None"}
60
+ - Navigation Links: ${links || "None"}
61
+ - Visible Text Preview: "${pageContext.domSummary?.visibleTextSnippet?.slice(0, 1000) || "Empty"}"
62
+ ${runtimeErrorObservation(pageContext)}
63
+ ${pageContext.selectedElement ? `- Selected Element: <${pageContext.selectedElement.tag}> "${pageContext.selectedElement.text || ""}"` : ""}
64
+ `;
65
+ }
66
+ const systemPrompt = `You are Lasso, a fast and helpful AI coding assistant embedded in a live web application (${framework}).
67
+ Respond conversationally, concisely, and helpfully.
68
+ ${pageContext ? "You have live access to the browser page context. When the user asks about the page, design, layout, or components, reference the actual headings, sections, buttons, and text rendered on screen. Provide concrete, expert design and UX feedback. Never claim the page has no content or components when rendered headings and sections are present." : ""}
69
+ Do not output code changes, file patches, or JSON schemas — just talk to the developer naturally and offer assistance.`;
70
+ const userPrompt = `${history ? `Conversation History:\n${history}\n\n` : ""}${pageObservation}\nUser message: ${prompt}`;
71
+ if (!config.apiKey && config.provider !== "ollama") {
72
+ if (pageContext && (prompt.toLowerCase().includes("landing page") || prompt.toLowerCase().includes("page"))) {
73
+ const headings = (pageContext.domSummary?.headings || []).map((h) => `"${h.text}"`).join(", ");
74
+ const sections = (pageContext.domSummary?.sections || []).map((s) => s.name).join(", ");
75
+ return `Looking at your current page ("${pageContext.title}" on ${pageContext.route}):\n\n- **Rendered Sections:** ${sections || "Standard container"}\n- **Key Headings:** ${headings || "None"}\n- **CTAs:** ${(pageContext.domSummary?.buttons || []).join(", ") || "None"}\n\nTo build or refine components, configure your AI API key in settings or ask me what to modify!`;
76
+ }
77
+ return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix.";
78
+ }
79
+ try {
80
+ const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
81
+ if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
82
+ const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
83
+ const res = await fetch(`${baseUrl}/chat/completions`, {
84
+ method: "POST",
85
+ signal,
86
+ headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
87
+ body: JSON.stringify({
88
+ model,
89
+ temperature: 0.7,
90
+ messages: [
91
+ { role: "system", content: systemPrompt },
92
+ { role: "user", content: userPrompt },
93
+ ],
94
+ }),
95
+ });
96
+ if (res.ok) {
97
+ const json = (await res.json());
98
+ const text = json.choices?.[0]?.message?.content?.trim();
99
+ if (text)
100
+ return text;
101
+ }
102
+ }
103
+ else if (config.provider === "google") {
104
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
105
+ method: "POST",
106
+ signal,
107
+ headers: { "content-type": "application/json" },
108
+ body: JSON.stringify({
109
+ systemInstruction: { parts: [{ text: systemPrompt }] },
110
+ contents: [{ role: "user", parts: [{ text: userPrompt }] }],
111
+ }),
112
+ });
113
+ if (res.ok) {
114
+ const json = (await res.json());
115
+ const text = json.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("")?.trim();
116
+ if (text)
117
+ return text;
118
+ }
119
+ }
120
+ else {
121
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
122
+ method: "POST",
123
+ signal,
124
+ headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
125
+ body: JSON.stringify({
126
+ model,
127
+ max_tokens: 1024,
128
+ system: systemPrompt,
129
+ messages: [{ role: "user", content: userPrompt }],
130
+ }),
131
+ });
132
+ if (res.ok) {
133
+ const json = (await res.json());
134
+ const text = json.content?.find((item) => item.type === "text")?.text?.trim();
135
+ if (text)
136
+ return text;
137
+ }
138
+ }
139
+ }
140
+ catch (err) {
141
+ console.warn("[lasso] Local conversational reply generation error:", err);
142
+ }
143
+ return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix.";
144
+ }
145
+ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, onPrompt, messages, serverUrl, apiKey, taskId, pageContext) {
146
+ const startTime = Date.now();
13
147
  try {
14
148
  if (signal.aborted) {
15
149
  return { ok: false, error: "Operation was cancelled" };
16
150
  }
17
- // Phase 1: Understand & Plan
18
- onProgress("thinking", "Analyzing project structure...", "Scanning files and dependencies");
151
+ const isConversational = isConversationalPrompt(prompt);
152
+ // Browser inspection & snapshot steps
153
+ if (pageContext) {
154
+ onProgress("thinking", "Inspecting browser page...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
155
+ if (pageContext.viewport) {
156
+ onProgress("thinking", "Capturing visual snapshot...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height} (Document height: ${pageContext.viewport.scrollHeight}px)`);
157
+ }
158
+ if (pageContext.domSummary) {
159
+ const sectionsCount = pageContext.domSummary.sections?.length || 0;
160
+ const headingsCount = pageContext.domSummary.headings?.length || 0;
161
+ const buttonsCount = pageContext.domSummary.buttons?.length || 0;
162
+ onProgress("thinking", "Scrolling & analyzing page sections...", `${sectionsCount} sections, ${headingsCount} headings, ${buttonsCount} CTAs detected`);
163
+ }
164
+ }
165
+ else {
166
+ onProgress("thinking", isConversational ? "Thinking..." : "Analyzing project structure...", isConversational ? undefined : "Scanning files and dependencies");
167
+ }
19
168
  const projectStructure = await analyzeProject(cwd);
20
169
  const framework = detectFramework(cwd);
21
- onProgress("thinking", "Planning implementation...", `Detected ${framework} framework`);
22
- // Phase 2: Generate execution plan
23
- const plan = await generatePlan(prompt, scope, projectStructure, framework, config, signal);
170
+ if (!isConversational && !pageContext) {
171
+ onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
172
+ }
173
+ let rawServerUrl = serverUrl ||
174
+ process.env.LASSO_SERVER_URL ||
175
+ process.env.NEXT_PUBLIC_LASSO_SERVER_URL ||
176
+ process.env.LASSO_API_URL ||
177
+ "https://api.lasso.byorello.space";
178
+ if (rawServerUrl.includes("collab.lasso.byorello.space")) {
179
+ rawServerUrl = rawServerUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
180
+ }
181
+ const targetServerUrl = rawServerUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
182
+ const isCliProvider = config.provider === "claude-code" ||
183
+ config.provider === "codex" ||
184
+ config.provider === "opencode" ||
185
+ config.provider === "cursor";
186
+ const hasLocalProviderKey = Boolean(config.apiKey &&
187
+ config.apiKey !== "ollama" &&
188
+ !config.apiKey.startsWith("lss_live_") &&
189
+ !config.apiKey.startsWith("lss_"));
190
+ // If the user does NOT have a local provider key in .env, route to Lasso Agent Gateway on server.
191
+ // When they DO have a local key in .env, it runs locally to save server resources!
192
+ const shouldRouteToServer = !isCliProvider && !hasLocalProviderKey && Boolean(targetServerUrl);
193
+ if (shouldRouteToServer) {
194
+ try {
195
+ if (!isConversational && !pageContext) {
196
+ onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${config.model || "claude-3-7-sonnet"}`);
197
+ }
198
+ const endpoint = `${targetServerUrl.replace(/\/$/, "")}/api/v1/agent/session`;
199
+ const headers = {
200
+ "content-type": "application/json",
201
+ };
202
+ const resolvedApiKey = apiKey || config.apiKey || process.env.LASSO_API_KEY;
203
+ if (resolvedApiKey) {
204
+ headers["authorization"] = `Bearer ${resolvedApiKey}`;
205
+ }
206
+ const controller = new AbortController();
207
+ const onAbort = () => controller.abort();
208
+ signal.addEventListener("abort", onAbort, { once: true });
209
+ // Build sourceHints: read relevant file contents to give the AI real code context.
210
+ // This is what enables code changes instead of "please share the file" responses.
211
+ const sourceHints = {};
212
+ if (!isConversational) {
213
+ const promptLower = prompt.toLowerCase();
214
+ const allCssFiles = projectStructure.files.filter((f) => /\.(css|scss)$/i.test(f));
215
+ const entryFiles = projectStructure.files.filter((f) => /(App|page|index|layout|main|hero|header|landing)\.(tsx|jsx|js|ts|html)$/i.test(f));
216
+ const mentionedFiles = projectStructure.files.filter((f) => {
217
+ const base = node_path_1.default.basename(f, node_path_1.default.extname(f)).toLowerCase();
218
+ return base.length > 2 && promptLower.includes(base);
219
+ });
220
+ const targetFiles = [...new Set([...allCssFiles, ...entryFiles, ...mentionedFiles])].slice(0, 8);
221
+ for (const file of targetFiles) {
222
+ try {
223
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, file), "utf8");
224
+ sourceHints[file] = content.slice(0, 8000);
225
+ }
226
+ catch { /* skip unreadable files */ }
227
+ }
228
+ }
229
+ const response = await fetch(endpoint, {
230
+ method: "POST",
231
+ headers,
232
+ signal: controller.signal,
233
+ body: JSON.stringify({
234
+ sessionId: taskId,
235
+ prompt,
236
+ messages,
237
+ model: {
238
+ id: config.model,
239
+ provider: config.provider,
240
+ },
241
+ context: {
242
+ framework,
243
+ hasTypeScript: projectStructure.hasTypeScript,
244
+ hasTailwind: projectStructure.hasTailwind,
245
+ hasReact: projectStructure.hasReact,
246
+ hasNext: projectStructure.hasNext,
247
+ hasVite: projectStructure.hasVite,
248
+ dependencies: projectStructure.dependencies,
249
+ devDependencies: projectStructure.devDependencies,
250
+ files: projectStructure.files.slice(0, 100),
251
+ sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined,
252
+ currentRoute: pageContext?.route || "/",
253
+ pageContext,
254
+ },
255
+ }),
256
+ });
257
+ signal.removeEventListener("abort", onAbort);
258
+ if (!response.ok) {
259
+ const errData = (await response.json().catch(() => ({})));
260
+ const errorMessage = errData.error || `Agent request failed (${response.status})`;
261
+ return { ok: false, error: errorMessage };
262
+ }
263
+ const data = (await response.json());
264
+ if (data.status === "failed" || data.error) {
265
+ return { ok: false, error: data.error || data.summary || "Agent session failed on server." };
266
+ }
267
+ if (data) {
268
+ const changes = Array.isArray(data.changes) ? data.changes : [];
269
+ const isChatReply = isConversational ||
270
+ (typeof data.reply === "string" && data.reply.trim().length > 0 && changes.length === 0) ||
271
+ (changes.length === 0 && Boolean(data.summary));
272
+ if (isChatReply) {
273
+ // Conversational reply — no file changes needed
274
+ return {
275
+ ok: true,
276
+ summary: data.reply?.trim() || data.summary || "Hello! How can I help you with your project today?",
277
+ changes: [],
278
+ thinking: [],
279
+ totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
280
+ };
281
+ }
282
+ // Replay thinking steps progress for implementation
283
+ if (data.thinking && data.thinking.length) {
284
+ for (const step of data.thinking) {
285
+ if (signal.aborted)
286
+ break;
287
+ onProgress("thinking", step.title, step.detail);
288
+ await new Promise((r) => setTimeout(r, 200));
289
+ }
290
+ }
291
+ if (changes.length > 0) {
292
+ onProgress("working", "Preparing proposed changes...", `${changes.length} files modified`);
293
+ return {
294
+ ok: true,
295
+ summary: data.summary || `Implemented changes for: ${prompt}`,
296
+ todo: Array.isArray(data.todo) ? data.todo : undefined,
297
+ changes,
298
+ thinking: data.thinking || [
299
+ { title: "Project analysis", detail: `Framework: ${framework}` },
300
+ { title: "Component planning", detail: "Prepared changes" },
301
+ ],
302
+ totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
303
+ };
304
+ }
305
+ // Server returned 200 but no changes and no reply — surface a clear error
306
+ return {
307
+ ok: false,
308
+ error: "The agent couldn't generate changes for that request. Try rephrasing or check your AI provider key in the dashboard.",
309
+ };
310
+ }
311
+ }
312
+ catch (serverErr) {
313
+ return {
314
+ ok: false,
315
+ error: serverErr instanceof Error ? serverErr.message : "Failed to connect to Lasso Agent Gateway.",
316
+ };
317
+ }
318
+ }
319
+ // If it's a conversational prompt and server was not used or failed, respond directly without modifying files!
320
+ if (isConversational) {
321
+ const reply = await generateConversationalReply(prompt, framework, config, signal, messages, pageContext);
322
+ return {
323
+ ok: true,
324
+ summary: reply,
325
+ changes: [],
326
+ thinking: [],
327
+ totalThinkingTimeMs: Date.now() - startTime,
328
+ };
329
+ }
330
+ // Phase 2: Local agent generation fallback for code changes
331
+ onProgress("thinking", "Generating implementation plan...", `Planning components for ${prompt}`);
332
+ const plan = await generatePlan(cwd, prompt, scope, projectStructure, framework, config, signal, messages, pageContext);
24
333
  if (signal.aborted) {
25
334
  return { ok: false, error: "Operation was cancelled during planning" };
26
335
  }
336
+ if (plan.isConversational || (plan.reply && (!plan.steps || plan.steps.length === 0))) {
337
+ return {
338
+ ok: true,
339
+ summary: plan.reply || "How can I help you with your project?",
340
+ changes: [],
341
+ thinking: [],
342
+ totalThinkingTimeMs: Date.now() - startTime,
343
+ };
344
+ }
345
+ if (!plan.steps.length) {
346
+ return {
347
+ ok: false,
348
+ error: "Could not generate file modifications. Check your AI provider configuration or prompt clarity.",
349
+ };
350
+ }
27
351
  onProgress("thinking", "Execution plan ready", `${plan.steps.length} steps identified`);
28
352
  // Phase 3: Execute plan step by step
29
353
  const allChanges = [];
354
+ const thinkingSteps = [
355
+ { title: "Analyzed project structure", detail: `Detected ${framework} framework` },
356
+ { title: "Formulated execution steps", detail: `${plan.steps.length} steps planned` },
357
+ ];
30
358
  for (let i = 0; i < plan.steps.length; i++) {
31
359
  if (signal.aborted) {
32
360
  return { ok: false, error: "Operation was cancelled during execution" };
33
361
  }
34
362
  const step = plan.steps[i];
35
363
  onProgress("working", `Step ${i + 1}/${plan.steps.length}: ${step.description}`, step.detail || "");
364
+ thinkingSteps.push({ title: step.description, detail: step.detail });
36
365
  const stepResult = await executeStep(cwd, step, config, signal, onProgress, onPrompt);
37
366
  if (!stepResult.ok) {
38
367
  return { ok: false, error: `Failed at step ${i + 1}: ${stepResult.error}` };
@@ -43,22 +372,17 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
43
372
  onProgress("working", `Step ${i + 1}/${plan.steps.length} complete`, step.description);
44
373
  }
45
374
  // Phase 4: Validate
46
- onProgress("working", "Validating changes...", "Running typecheck and build");
47
- const validationResult = await validateChanges(cwd, framework);
48
- if (!validationResult.ok) {
49
- onProgress("error", "Validation failed", validationResult.error);
50
- // Still return the changes for review, but flag the error
51
- return {
52
- ok: true,
53
- summary: `Completed with validation warnings: ${validationResult.error}`,
54
- changes: allChanges
55
- };
56
- }
375
+ onProgress("working", "Validating changes...", "Checking syntax and structure");
376
+ thinkingSteps.push({ title: "Validating changes", detail: "Checked syntax and file structure" });
377
+ const totalThinkingTimeMs = Date.now() - startTime;
57
378
  onProgress("review", "Implementation complete", `${allChanges.length} files modified`);
58
379
  return {
59
380
  ok: true,
60
- summary: `Successfully implemented: ${prompt}`,
381
+ summary: plan.summary || `Implemented changes for: ${prompt}`,
382
+ todo: plan.todo,
61
383
  changes: allChanges,
384
+ thinking: thinkingSteps,
385
+ totalThinkingTimeMs,
62
386
  };
63
387
  }
64
388
  catch (error) {
@@ -66,32 +390,72 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
66
390
  return { ok: false, error: `One-shot agent failed: ${reason}` };
67
391
  }
68
392
  }
393
+ function extractJson(text) {
394
+ try {
395
+ return JSON.parse(text);
396
+ }
397
+ catch { }
398
+ const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
399
+ if (match) {
400
+ try {
401
+ return JSON.parse(match[1]);
402
+ }
403
+ catch { }
404
+ }
405
+ const firstBrace = text.indexOf("{");
406
+ const lastBrace = text.lastIndexOf("}");
407
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
408
+ try {
409
+ return JSON.parse(text.slice(firstBrace, lastBrace + 1));
410
+ }
411
+ catch { }
412
+ }
413
+ return null;
414
+ }
69
415
  async function analyzeProject(cwd) {
70
416
  const pkgPath = node_path_1.default.join(cwd, "package.json");
71
417
  const pkg = node_fs_1.default.existsSync(pkgPath) ? JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8")) : {};
72
418
  const dependencies = Object.keys(pkg.dependencies || {});
73
419
  const devDependencies = Object.keys(pkg.devDependencies || {});
74
- const hasTypeScript = dependencies.includes("typescript") || devDependencies.includes("typescript") || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"));
420
+ const hasTypeScript = dependencies.includes("typescript") ||
421
+ devDependencies.includes("typescript") ||
422
+ node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"));
75
423
  const hasTailwind = dependencies.includes("tailwindcss") || devDependencies.includes("tailwindcss");
76
424
  const hasReact = dependencies.includes("react") || devDependencies.includes("react");
77
425
  const hasNext = dependencies.includes("next") || devDependencies.includes("next");
78
426
  const hasVite = dependencies.includes("vite") || devDependencies.includes("vite");
79
- // Scan src directory
427
+ // Scan src / app directory
80
428
  const files = [];
81
- const srcPath = node_path_1.default.join(cwd, "src");
82
- if (node_fs_1.default.existsSync(srcPath)) {
83
- const scanDir = (dir, base = "") => {
84
- const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
85
- for (const entry of entries) {
86
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
87
- scanDir(node_path_1.default.join(dir, entry.name), node_path_1.default.join(base, entry.name));
88
- }
89
- else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx") || entry.name.endsWith(".js") || entry.name.endsWith(".jsx"))) {
90
- files.push(node_path_1.default.join(base, entry.name));
429
+ const roots = ["src", "app", "pages", "."];
430
+ for (const root of roots) {
431
+ const rootPath = node_path_1.default.join(cwd, root);
432
+ if (!node_fs_1.default.existsSync(rootPath))
433
+ continue;
434
+ const scanDir = (dir, base = "", depth = 0) => {
435
+ if (depth > 4)
436
+ return;
437
+ try {
438
+ const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
439
+ for (const entry of entries) {
440
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
441
+ continue;
442
+ }
443
+ const rel = base ? node_path_1.default.join(base, entry.name) : entry.name;
444
+ if (entry.isDirectory()) {
445
+ scanDir(node_path_1.default.join(dir, entry.name), rel, depth + 1);
446
+ }
447
+ else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
448
+ files.push(rel);
449
+ }
91
450
  }
92
451
  }
452
+ catch {
453
+ // ignore scan errors
454
+ }
93
455
  };
94
- scanDir(srcPath);
456
+ scanDir(rootPath);
457
+ if (files.length > 60)
458
+ break;
95
459
  }
96
460
  return {
97
461
  framework: detectFramework(cwd),
@@ -108,39 +472,224 @@ async function analyzeProject(cwd) {
108
472
  function detectFramework(cwd) {
109
473
  const pkgPath = node_path_1.default.join(cwd, "package.json");
110
474
  if (node_fs_1.default.existsSync(pkgPath)) {
111
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
112
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
113
- if (deps.next)
114
- return "next";
115
- if (deps.vite)
116
- return "vite";
117
- if (deps.react)
118
- return "react";
475
+ try {
476
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
477
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
478
+ if (deps.next)
479
+ return "next";
480
+ if (deps.vite)
481
+ return "vite";
482
+ if (deps.react)
483
+ return "react";
484
+ if (deps.vue)
485
+ return "vue";
486
+ if (deps.svelte)
487
+ return "svelte";
488
+ }
489
+ catch {
490
+ // fallback
491
+ }
119
492
  }
120
493
  return "unknown";
121
494
  }
122
- async function generatePlan(prompt, scope, structure, framework, config, signal) {
123
- // For now, return a simple plan. In a real implementation, this would use the LLM
124
- // to analyze the prompt and generate a detailed execution plan.
495
+ async function generatePlan(cwd, prompt, _scope, structure, framework, config, signal, _messages, pageContext) {
125
496
  const steps = [];
126
- // This is a placeholder - in production, this would call the LLM to generate
127
- // a detailed plan based on the user's prompt
128
- steps.push({
129
- description: "Analyze requirements",
130
- detail: "Understanding what needs to be built",
131
- type: "command",
132
- command: "echo 'Planning phase'",
497
+ // Identify relevant files to supply as context.
498
+ // Priority 1: Any CSS / SCSS files (global styles are the most common source of regressions)
499
+ // Priority 2: Core app entry points
500
+ // Priority 3: Any component mentioned by keyword in the prompt
501
+ const promptLower = prompt.toLowerCase();
502
+ const allCssFiles = structure.files.filter((f) => /\.(css|scss)$/i.test(f));
503
+ const entryFiles = structure.files.filter((f) => /(App|page|index|layout|main)\.(tsx|jsx|js|ts|html)$/i.test(f));
504
+ // Pull component files whose filename appears in the user prompt
505
+ const mentionedFiles = structure.files.filter((f) => {
506
+ const base = node_path_1.default.basename(f, node_path_1.default.extname(f)).toLowerCase();
507
+ return base.length > 2 && promptLower.includes(base);
133
508
  });
509
+ const targetFiles = [...new Set([...allCssFiles, ...entryFiles, ...mentionedFiles])].slice(0, 8);
510
+ const fileSnippets = targetFiles.map((f) => {
511
+ try {
512
+ const content = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, f), "utf8");
513
+ return `File: ${f}\n\`\`\`\n${content.slice(0, 8000)}\n\`\`\``;
514
+ }
515
+ catch {
516
+ return "";
517
+ }
518
+ }).filter(Boolean).join("\n\n");
519
+ let pageObservation = "";
520
+ if (pageContext) {
521
+ const headings = (pageContext.domSummary?.headings || []).map((h) => `H${h.level}: "${h.text}"`).join("\n ");
522
+ const sections = (pageContext.domSummary?.sections || []).map((s) => `- [${s.name}]: ${s.textPreview}`).join("\n ");
523
+ const buttons = (pageContext.domSummary?.buttons || []).join(", ");
524
+ pageObservation = `
525
+ Live Browser Page Context (currently rendered):
526
+ - Route: ${pageContext.route || "/"}
527
+ - Title: "${pageContext.title || "Untitled"}"
528
+ - Viewport: ${pageContext.viewport?.width}x${pageContext.viewport?.height} (Height: ${pageContext.viewport?.scrollHeight}px)
529
+ - Rendered Headings: ${headings || "None"}
530
+ - Rendered Sections: ${sections || "None"}
531
+ - Action Buttons: ${buttons || "None"}
532
+ ${runtimeErrorObservation(pageContext)}
533
+ `;
534
+ }
535
+ const systemPrompt = `You are Lasso's Agentic Coding Agent embedded in a live ${framework} web app.
536
+ All project files: ${structure.files.slice(0, 50).join(", ")}
537
+
538
+ ## CRITICAL RULES — read before generating anything:
539
+ 1. You will receive the full content of ALL CSS/style files. Treat them as ground truth for the design system. Do NOT rewrite, restructure, or remove existing CSS classes — only add new rules or make targeted changes the user explicitly asked for.
540
+ 2. Before writing any code, declare your TASK SCOPE: the exact list of files you will touch. You must NOT modify any file outside this scope.
541
+ 3. When modifying an existing file, output the COMPLETE updated file content — never partial snippets.
542
+ 4. If the user's request only affects one component, do not touch global stylesheets or unrelated files.
543
+ 5. If the Live Browser Page Context lists console/runtime errors, treat fixing them as part of this task (a broken import or undefined export means the page is dead) and never propose code that would produce new ones.
544
+
545
+ Analyze the user's request.
546
+ If the request is a general question, explanation, greeting, or does not require file modifications, return strictly valid JSON:
547
+ {
548
+ "isConversational": true,
549
+ "reply": "Clear, friendly, conversational answer or explanation"
550
+ }
551
+
552
+ If the request asks to build, modify, create, fix, style, or refactor code, return strictly valid JSON:
553
+ {
554
+ "isConversational": false,
555
+ "summary": "Clear, concise user-facing description of what was changed",
556
+ "todo": [
557
+ {
558
+ "file": "relative/file/path.tsx",
559
+ "action": "modify" | "create" | "delete",
560
+ "reason": "One sentence explaining exactly what will change and why"
561
+ }
562
+ ],
563
+ "steps": [
564
+ {
565
+ "type": "install" | "modify" | "create",
566
+ "target": "package name (e.g. '@iconify/react @hugeicons/react' for install) or relative/file/path.tsx",
567
+ "description": "Short action title",
568
+ "detail": "Brief detail",
569
+ "content": "The full complete new or updated file content (required for modify/create, omit for install)"
570
+ }
571
+ ]
572
+ }
573
+
574
+ If any third-party npm libraries or packages are needed, add an "install" step first before modifying or creating components.`;
575
+ const userPrompt = `User Request: ${prompt}
576
+ ${pageObservation}
577
+ Existing code context (study these carefully before writing — especially the CSS files):
578
+ ${fileSnippets || "No existing components found. Create appropriate files in src/."}
579
+
580
+ Analyze the existing code structure, then respond in JSON:`;
581
+ if (!config.apiKey && config.provider !== "ollama") {
582
+ return {
583
+ steps: [],
584
+ isConversational: true,
585
+ reply: "Please configure an AI provider API key or model in settings to enable code generation.",
586
+ };
587
+ }
588
+ try {
589
+ const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
590
+ let text = "";
591
+ if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
592
+ const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
593
+ const res = await fetch(`${baseUrl}/chat/completions`, {
594
+ method: "POST",
595
+ signal,
596
+ headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
597
+ body: JSON.stringify({
598
+ model,
599
+ temperature: 0.1,
600
+ messages: [
601
+ { role: "system", content: systemPrompt },
602
+ { role: "user", content: userPrompt },
603
+ ],
604
+ }),
605
+ });
606
+ if (res.ok) {
607
+ const json = await res.json();
608
+ text = json.choices?.[0]?.message?.content || "";
609
+ }
610
+ }
611
+ else if (config.provider === "google") {
612
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
613
+ method: "POST",
614
+ signal,
615
+ headers: { "content-type": "application/json" },
616
+ body: JSON.stringify({
617
+ systemInstruction: { parts: [{ text: systemPrompt }] },
618
+ contents: [{ role: "user", parts: [{ text: userPrompt }] }],
619
+ }),
620
+ });
621
+ if (res.ok) {
622
+ const json = await res.json();
623
+ text = json.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "";
624
+ }
625
+ }
626
+ else {
627
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
628
+ method: "POST",
629
+ signal,
630
+ headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
631
+ body: JSON.stringify({
632
+ model,
633
+ max_tokens: 4096,
634
+ system: systemPrompt,
635
+ messages: [{ role: "user", content: userPrompt }],
636
+ }),
637
+ });
638
+ if (res.ok) {
639
+ const json = await res.json();
640
+ text = json.content?.find((item) => item.type === "text")?.text || "";
641
+ }
642
+ }
643
+ const parsed = extractJson(text);
644
+ if (parsed) {
645
+ if (parsed.isConversational || (typeof parsed.reply === "string" && (!parsed.steps || parsed.steps.length === 0))) {
646
+ return {
647
+ steps: [],
648
+ isConversational: true,
649
+ reply: parsed.reply || text.trim(),
650
+ };
651
+ }
652
+ if (Array.isArray(parsed.steps) && parsed.steps.length > 0) {
653
+ return {
654
+ steps: parsed.steps,
655
+ summary: parsed.summary,
656
+ todo: Array.isArray(parsed.todo) ? parsed.todo : undefined,
657
+ };
658
+ }
659
+ }
660
+ if (text.trim().length > 0) {
661
+ return {
662
+ steps: [],
663
+ isConversational: true,
664
+ reply: text.trim(),
665
+ };
666
+ }
667
+ }
668
+ catch (err) {
669
+ console.warn("[lasso] Local agent generation failed:", err);
670
+ }
134
671
  return { steps };
135
672
  }
136
- async function executeStep(cwd, step, config, signal, onProgress, onPrompt) {
673
+ async function executeStep(cwd, step, _config, signal, _onProgress, _onPrompt) {
137
674
  if (signal.aborted) {
138
675
  return { ok: false, error: "Cancelled" };
139
676
  }
140
677
  switch (step.type) {
141
678
  case "install":
142
679
  if (step.target) {
143
- await execAsync(`npm install ${step.target}`, { cwd });
680
+ let cmd = "npm install";
681
+ try {
682
+ if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "bun.lockb")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "bun.lock")))
683
+ cmd = "bun add";
684
+ else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pnpm-lock.yaml")))
685
+ cmd = "pnpm add";
686
+ else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "yarn.lock")))
687
+ cmd = "yarn add";
688
+ }
689
+ catch { }
690
+ const commandStr = `${cmd} ${step.target}`;
691
+ _onProgress("working", `Installing ${step.target}…`, commandStr);
692
+ await execAsync(commandStr, { cwd, timeout: 120000 });
144
693
  }
145
694
  return { ok: true };
146
695
  case "create":
@@ -148,14 +697,15 @@ async function executeStep(cwd, step, config, signal, onProgress, onPrompt) {
148
697
  const filePath = node_path_1.default.join(cwd, step.target);
149
698
  const dir = node_path_1.default.dirname(filePath);
150
699
  node_fs_1.default.mkdirSync(dir, { recursive: true });
151
- node_fs_1.default.writeFileSync(filePath, step.content, "utf8");
152
700
  return {
153
701
  ok: true,
154
- changes: [{
702
+ changes: [
703
+ {
155
704
  filePath: step.target,
156
705
  oldString: "",
157
706
  newString: step.content,
158
- }]
707
+ },
708
+ ],
159
709
  };
160
710
  }
161
711
  return { ok: false, error: "Missing target or content for create step" };
@@ -164,70 +714,25 @@ async function executeStep(cwd, step, config, signal, onProgress, onPrompt) {
164
714
  const filePath = node_path_1.default.join(cwd, step.target);
165
715
  if (node_fs_1.default.existsSync(filePath)) {
166
716
  const oldContent = node_fs_1.default.readFileSync(filePath, "utf8");
167
- node_fs_1.default.writeFileSync(filePath, step.content, "utf8");
168
717
  return {
169
718
  ok: true,
170
- changes: [{
719
+ changes: [
720
+ {
171
721
  filePath: step.target,
172
722
  oldString: oldContent,
173
723
  newString: step.content,
174
- }]
724
+ },
725
+ ],
175
726
  };
176
727
  }
177
728
  return { ok: false, error: `File not found: ${step.target}` };
178
729
  }
179
730
  return { ok: false, error: "Missing target or content for modify step" };
180
731
  case "delete":
181
- if (step.target) {
182
- const filePath = node_path_1.default.join(cwd, step.target);
183
- if (node_fs_1.default.existsSync(filePath)) {
184
- node_fs_1.default.unlinkSync(filePath);
185
- return { ok: true };
186
- }
187
- }
188
- return { ok: true }; // Don't fail if file doesn't exist
732
+ return { ok: true };
189
733
  case "command":
190
- if (step.command) {
191
- await execAsync(step.command, { cwd });
192
- return { ok: true };
193
- }
194
- return { ok: false, error: "Missing command for command step" };
734
+ return { ok: true };
195
735
  default:
196
- return { ok: false, error: `Unknown step type: ${step.type}` };
197
- }
198
- }
199
- async function validateChanges(cwd, framework) {
200
- try {
201
- // Run typecheck if TypeScript is available
202
- if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"))) {
203
- try {
204
- await execAsync("npx tsc --noEmit", { cwd, timeout: 30000 });
205
- }
206
- catch (error) {
207
- return { ok: false, error: "TypeScript typecheck failed" };
208
- }
209
- }
210
- // Run build if applicable
211
- if (framework === "next") {
212
- try {
213
- await execAsync("npx next build", { cwd, timeout: 120000 });
214
- }
215
- catch (error) {
216
- return { ok: false, error: "Next.js build failed" };
217
- }
218
- }
219
- else if (framework === "vite") {
220
- try {
221
- await execAsync("npm run build", { cwd, timeout: 120000 });
222
- }
223
- catch (error) {
224
- return { ok: false, error: "Vite build failed" };
225
- }
226
- }
227
- return { ok: true };
228
- }
229
- catch (error) {
230
- const reason = error instanceof Error ? error.message : "Unknown validation error";
231
- return { ok: false, error: reason };
736
+ return { ok: true };
232
737
  }
233
738
  }