@lasso-ai/cli 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/agent.js CHANGED
@@ -14,13 +14,57 @@ exports.runLocalAgentPrompt = runLocalAgentPrompt;
14
14
  exports.proposeChanges = proposeChanges;
15
15
  exports.answerQuestion = answerQuestion;
16
16
  exports.generateCommitMessage = generateCommitMessage;
17
- const promises_1 = __importDefault(require("node:fs/promises"));
17
+ const node_fs_1 = __importDefault(require("node:fs"));
18
18
  const node_path_1 = __importDefault(require("node:path"));
19
19
  const node_child_process_1 = require("node:child_process");
20
20
  const node_util_1 = require("node:util");
21
21
  const auth_1 = require("./auth");
22
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
23
23
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
24
+ function detectProjectStructure(cwd) {
25
+ const pkgPath = node_path_1.default.join(cwd, "package.json");
26
+ let framework = "unknown";
27
+ const directories = [];
28
+ const appDirs = [];
29
+ if (node_fs_1.default.existsSync(pkgPath)) {
30
+ try {
31
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
32
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
33
+ if (deps.next)
34
+ framework = "Next.js";
35
+ else if (deps.vite)
36
+ framework = "Vite";
37
+ else if (deps.react)
38
+ framework = "React";
39
+ }
40
+ catch {
41
+ // Ignore errors
42
+ }
43
+ }
44
+ // Scan for important directories
45
+ const scanDirs = ["src", "app", "pages", "web", "lib", "components"];
46
+ for (const dir of scanDirs) {
47
+ if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, dir))) {
48
+ directories.push(dir);
49
+ if (dir === "app")
50
+ appDirs.push(dir);
51
+ // Check for src/app structure
52
+ if (dir === "src" && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "src", "app"))) {
53
+ directories.push("src/app");
54
+ appDirs.push("src/app");
55
+ }
56
+ }
57
+ }
58
+ // Determine source root
59
+ let sourceRoot = ".";
60
+ if (directories.includes("src/app"))
61
+ sourceRoot = "src";
62
+ else if (directories.includes("app"))
63
+ sourceRoot = ".";
64
+ else if (directories.includes("src"))
65
+ sourceRoot = "src";
66
+ return { framework, sourceRoot, directories, multipleAppDirs: appDirs };
67
+ }
24
68
  async function installPackages(cwd, packages, onProgress) {
25
69
  const validPkgs = [...new Set(packages
26
70
  .map((p) => p.trim().replace(/^['"`]|['"`]$/g, ""))
@@ -31,7 +75,7 @@ async function installPackages(cwd, packages, onProgress) {
31
75
  let existingDeps = {};
32
76
  try {
33
77
  const pkgJsonPath = node_path_1.default.join(cwd, "package.json");
34
- const raw = await promises_1.default.readFile(pkgJsonPath, "utf8");
78
+ const raw = node_fs_1.default.readFileSync(pkgJsonPath, "utf8");
35
79
  const parsed = JSON.parse(raw);
36
80
  existingDeps = { ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) };
37
81
  }
@@ -42,7 +86,7 @@ async function installPackages(cwd, packages, onProgress) {
42
86
  // Detect package manager
43
87
  let cmd = "npm install";
44
88
  try {
45
- const rootFiles = await promises_1.default.readdir(cwd);
89
+ const rootFiles = node_fs_1.default.readdirSync(cwd);
46
90
  if (rootFiles.includes("bun.lockb") || rootFiles.includes("bun.lock"))
47
91
  cmd = "bun add";
48
92
  else if (rootFiles.includes("pnpm-lock.yaml"))
@@ -165,7 +209,7 @@ const ignored = new Set(["node_modules", ".git", ".next", "dist", "build", ".tur
165
209
  const sourceExtensions = /\.(tsx?|jsx?|vue|svelte|css|scss|html)$/i;
166
210
  const layoutFiles = /^(layout|_layout|_app|app|_document|document|template|_template)\.(tsx?|jsx?)$/i;
167
211
  const sourceFileCache = new Map();
168
- async function sourceFiles(directory, budget = { remaining: 25 }) {
212
+ function sourceFiles(directory, budget = { remaining: 25 }) {
169
213
  if (budget.remaining <= 0)
170
214
  return [];
171
215
  const cached = sourceFileCache.get(directory);
@@ -174,7 +218,7 @@ async function sourceFiles(directory, budget = { remaining: 25 }) {
174
218
  budget.remaining -= files.length;
175
219
  return files;
176
220
  }
177
- const entries = await promises_1.default.readdir(directory, { withFileTypes: true });
221
+ const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
178
222
  const files = [];
179
223
  const layoutFilesList = [];
180
224
  const otherFiles = [];
@@ -185,7 +229,7 @@ async function sourceFiles(directory, budget = { remaining: 25 }) {
185
229
  if (budget.remaining <= 0)
186
230
  break;
187
231
  if (entry.isDirectory()) {
188
- const nested = await sourceFiles(fullPath, budget);
232
+ const nested = sourceFiles(fullPath, budget);
189
233
  files.push(...nested);
190
234
  }
191
235
  else if (sourceExtensions.test(entry.name)) {
@@ -203,10 +247,21 @@ async function sourceFiles(directory, budget = { remaining: 25 }) {
203
247
  sourceFileCache.set(directory, { expiresAt: Date.now() + 30000, files });
204
248
  return files;
205
249
  }
206
- async function contextFor(cwd, element) {
250
+ function contextFor(cwd, element) {
207
251
  const needle = element.label.replace(/^[^.#]+[.#]?/, "");
208
252
  const hintedPath = element.sourceHint?.split(":")[0];
209
253
  const sourceFile = hintedPath ? node_path_1.default.basename(hintedPath) : "";
254
+ // Add project structure information at the top
255
+ const structure = detectProjectStructure(cwd);
256
+ let projectStructureInfo = `PROJECT STRUCTURE:
257
+ - Framework: ${structure.framework}
258
+ - Source root: ${structure.sourceRoot}
259
+ - Directories found: ${structure.directories.join(", ") || "root"}
260
+ - Multiple app directories: ${structure.multipleAppDirs.length > 0 ? structure.multipleAppDirs.join(", ") : "none"}
261
+ - IMPORTANT: When creating files, use the existing directory structure. Do not create duplicate directories (e.g., if "src/app/" exists, use it instead of creating a new "app/").
262
+ - Path aliases: Check tsconfig.json or jsconfig.json for path aliases like "@/components"
263
+
264
+ `;
210
265
  // Extract unique class names and attributes from the element for better matching
211
266
  const classMatches = element.html?.match(/class="([^"]+)"/);
212
267
  const classNames = classMatches ? classMatches[1].split(/\s+/).filter((c) => c.length > 3) : [];
@@ -223,20 +278,20 @@ async function contextFor(cwd, element) {
223
278
  candidates.push(node_path_1.default.join(cwd, normalizedHint.slice(srcIndex + 1)));
224
279
  for (const candidate of candidates) {
225
280
  try {
226
- const content = await promises_1.default.readFile(candidate, "utf8");
227
- return `FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
281
+ const content = node_fs_1.default.readFileSync(candidate, "utf8");
282
+ return `${projectStructureInfo}FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
228
283
  }
229
284
  catch {
230
285
  // The runtime source hint can point to a different checkout.
231
286
  }
232
287
  }
233
288
  }
234
- const files = await sourceFiles(cwd);
289
+ const files = sourceFiles(cwd);
235
290
  const snippets = [];
236
291
  const scoredFiles = [];
237
- const results = await Promise.all(files.map(async (file) => {
292
+ const results = files.map((file) => {
238
293
  try {
239
- const content = await promises_1.default.readFile(file, "utf8");
294
+ const content = node_fs_1.default.readFileSync(file, "utf8");
240
295
  let score = 0;
241
296
  // Score files based on relevance
242
297
  if (sourceFile && file.endsWith(sourceFile))
@@ -264,7 +319,7 @@ async function contextFor(cwd, element) {
264
319
  // A file can disappear while a dev server is rebuilding; skip it.
265
320
  }
266
321
  return null;
267
- }));
322
+ }).filter((r) => r !== null);
268
323
  // Sort by score and take top files
269
324
  const sortedResults = results.filter((r) => r !== null).sort((a, b) => b.score - a.score);
270
325
  for (const result of sortedResults) {
@@ -272,7 +327,7 @@ async function contextFor(cwd, element) {
272
327
  snippets.push(`FILE: ${node_path_1.default.relative(cwd, result.file)}\n${result.content.slice(0, 6000)}`);
273
328
  }
274
329
  }
275
- return snippets.join("\n\n---\n\n");
330
+ return projectStructureInfo + snippets.join("\n\n---\n\n");
276
331
  }
277
332
  function jsonObjectCandidates(text) {
278
333
  const candidates = [];
@@ -934,7 +989,7 @@ async function runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, task
934
989
  return extractLocalAgentText(result.stdout, provider);
935
990
  }
936
991
  async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
937
- const context = await contextFor(cwd, input.element);
992
+ const context = contextFor(cwd, input.element);
938
993
  const runtimeErrors = input.context?.runtimeErrors || [];
939
994
  const visualContext = input.context ? { ...input.context, screenshots: undefined, runtimeErrors: undefined } : undefined;
940
995
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
@@ -945,6 +1000,8 @@ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt)
945
1000
  const runtimeErrorHint = runtimeErrors.length
946
1001
  ? `\n\nLIVE CONSOLE/RUNTIME ERRORS (reported by the user's browser — the page is currently broken by these):\n${runtimeErrors.map((error) => `! ${error}`).join("\n")}`
947
1002
  : "";
1003
+ // Detect project structure for instruction
1004
+ const structure = detectProjectStructure(cwd);
948
1005
  const instruction = `Selection context:
949
1006
  ${JSON.stringify(input.element, null, 2)}
950
1007
  ${JSON.stringify(visualContext, null, 2)}
@@ -962,6 +1019,8 @@ Relevant source context:
962
1019
  ${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}
963
1020
 
964
1021
  CRITICAL INSTRUCTIONS:
1022
+ 0. PROJECT STRUCTURE AWARENESS: This project has specific directory structure. Before creating or moving files, verify the actual structure. Do not assume standard locations.
1023
+ ${structure.multipleAppDirs.length > 0 ? ` WARNING: Multiple app directories detected: ${structure.multipleAppDirs.join(", ")}. Use the correct one for this project.\n` : ""}
965
1024
  1. You are an autonomous agent with the ability to install npm packages and edit source code.
966
1025
  2. If the user's request requires or asks for third-party libraries, icon packs, animation tools, or utility packages (e.g. icon libraries, motion, charts, UI primitives, etc.), determine the best npm package for this project and declare them in the "packages" array (e.g. ["@iconify/react", "@hugeicons/react"]).
967
1026
  3. In the "changes" array, propose concrete, minimal edits to the source code to implement the request. You may freely import and use the packages you specified in "packages".
@@ -1256,7 +1315,7 @@ RULES:
1256
1315
  return finalProposal;
1257
1316
  }
1258
1317
  async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
1259
- const context = await contextFor(cwd, input.element);
1318
+ const context = contextFor(cwd, input.element);
1260
1319
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
1261
1320
  const prompt = `Answer the user's question conversationally and directly. Do not propose file changes and do not return JSON. If the question is about the selected UI, use the selection and source context below.\n\nUser question:\n${input.question}\n\nSelected element:\n${JSON.stringify(input.element, null, 2)}\n\nVisual context:\n${JSON.stringify({ ...input.context, screenshots: undefined }, null, 2)}\n\nConversation:\n${history}\n\nRelevant source context:\n${context || "No matching source context was found."}`;
1262
1321
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
@@ -219,6 +219,11 @@ export type ServerBridgeMessage = {
219
219
  type: "applied";
220
220
  taskId?: string;
221
221
  message: string;
222
+ validation?: {
223
+ passed: boolean;
224
+ errors: string[];
225
+ command: string;
226
+ };
222
227
  } | {
223
228
  type: "undone";
224
229
  taskId?: string;
@@ -147,6 +147,249 @@ function proposalMatchesCurrentSource(cwd, changes) {
147
147
  return false;
148
148
  }
149
149
  }
150
+ async function triggerBuildFix(cwd, taskId, buildErrors, buildCommand, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext) {
151
+ const MAX_RETRIES = 3;
152
+ const context = buildErrorContext.get(taskId);
153
+ if (!context)
154
+ return;
155
+ context.retryCount += 1;
156
+ if (context.retryCount > MAX_RETRIES) {
157
+ // Max retries reached - revert changes and show error
158
+ for (const snapshot of taskSnapshots.get(taskId) || []) {
159
+ if (snapshot.content === null) {
160
+ if (node_fs_1.default.existsSync(snapshot.filePath))
161
+ node_fs_1.default.unlinkSync(snapshot.filePath);
162
+ }
163
+ else {
164
+ node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
165
+ }
166
+ }
167
+ taskSnapshots.delete(taskId);
168
+ buildErrorContext.delete(taskId);
169
+ socket.send(JSON.stringify({
170
+ type: "agent_status",
171
+ taskId,
172
+ status: "error",
173
+ message: `⚠️ Build validation failed after ${MAX_RETRIES} attempts. The change was reverted. Errors:\n${buildErrors.slice(0, 5).join("\n")}${buildErrors.length > 5 ? `\n... and ${buildErrors.length - 5} more errors` : ""}`
174
+ }));
175
+ return;
176
+ }
177
+ // Re-apply the original changes
178
+ try {
179
+ const planned = new Map();
180
+ for (const change of originalChanges) {
181
+ const isNew = !change.oldString;
182
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
183
+ const exists = node_fs_1.default.existsSync(filePath);
184
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
185
+ const prepared = (!exists || isNew)
186
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
187
+ : prepareChange(content, change);
188
+ const fileChanges = planned.get(filePath) || [];
189
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
190
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
191
+ }
192
+ fileChanges.push({ filePath, content, exists, ...prepared });
193
+ planned.set(filePath, fileChanges);
194
+ }
195
+ for (const [filePath, changes] of planned) {
196
+ const exists = changes[0].exists;
197
+ const content = changes[0].content;
198
+ const nextContent = exists
199
+ ? [...changes]
200
+ .sort((a, b) => b.start - a.start)
201
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
202
+ : changes[0].newString;
203
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
204
+ node_fs_1.default.writeFileSync(filePath, nextContent);
205
+ }
206
+ }
207
+ catch (applyError) {
208
+ socket.send(JSON.stringify({
209
+ type: "agent_status",
210
+ taskId,
211
+ status: "error",
212
+ message: `Failed to re-apply original changes: ${applyError instanceof Error ? applyError.message : "Unknown error"}`
213
+ }));
214
+ return;
215
+ }
216
+ // Run agent to fix build errors
217
+ const { runOneShotAgent } = await import("./oneshot.js");
218
+ const fixPrompt = `The previous change caused build errors. Please fix them. Build command: ${buildCommand}\n\nBuild errors:\n${buildErrors.slice(0, 10).join("\n")}${buildErrors.length > 10 ? `\n... and ${buildErrors.length - 10} more errors` : ""}\n\nFix these errors by making minimal changes to the files. Attempt ${context.retryCount} of ${MAX_RETRIES}.`;
219
+ const controller = new AbortController();
220
+ taskControllers.set(taskId, controller);
221
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: `Fixing build errors (attempt ${context.retryCount}/${MAX_RETRIES})...` }));
222
+ try {
223
+ const result = await runOneShotAgent(cwd, fixPrompt, "project", { provider: "google", model: "gemini-2.5-flash", lassoKey: resolvedLassoKey, serverUrl: "https://api.lasso.byorello.space" }, controller.signal, (status, message, detail) => {
224
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
225
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
226
+ }
227
+ }, (prompt) => {
228
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
229
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId, prompt }));
230
+ }
231
+ }, undefined, "https://api.lasso.byorello.space", resolvedLassoKey || agentConfig?.apiKey || collabConfig?.apiKey, taskId, undefined);
232
+ if (result.ok && result.changes && result.changes.length > 0) {
233
+ // Apply the fix
234
+ const snapshots = [];
235
+ const planned = new Map();
236
+ for (const change of result.changes) {
237
+ const isNew = !change.oldString;
238
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
239
+ const exists = node_fs_1.default.existsSync(filePath);
240
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
241
+ const prepared = (!exists || isNew)
242
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
243
+ : prepareChange(content, change);
244
+ const fileChanges = planned.get(filePath) || [];
245
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
246
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
247
+ }
248
+ fileChanges.push({ filePath, content, exists, ...prepared });
249
+ planned.set(filePath, fileChanges);
250
+ }
251
+ taskSnapshots.set(taskId, snapshots);
252
+ for (const [filePath, changes] of planned) {
253
+ const exists = changes[0].exists;
254
+ const content = changes[0].content;
255
+ snapshots.push({ filePath, content: exists ? content : null });
256
+ const nextContent = exists
257
+ ? [...changes]
258
+ .sort((a, b) => b.start - a.start)
259
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
260
+ : changes[0].newString;
261
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
262
+ node_fs_1.default.writeFileSync(filePath, nextContent);
263
+ }
264
+ // Validate the fix
265
+ const validation = await validateBuild(cwd);
266
+ if (validation.passed) {
267
+ // Build fixed!
268
+ buildErrorContext.delete(taskId);
269
+ socket.send(JSON.stringify({
270
+ type: "applied",
271
+ taskId,
272
+ message: `Build errors fixed after ${context.retryCount} attempt${context.retryCount > 1 ? "s" : ""}. ${result.changes.length} file${result.changes.length === 1 ? "" : "s"} updated.`,
273
+ validation
274
+ }));
275
+ }
276
+ else {
277
+ // Still failing - trigger another retry
278
+ socket.send(JSON.stringify({
279
+ type: "agent_status",
280
+ taskId,
281
+ status: "working",
282
+ message: `⚠️ Build still failing. Retrying...`,
283
+ detail: `Build command: ${validation.command}\nErrors:\n${validation.errors.slice(0, 5).join("\n")}${validation.errors.length > 5 ? `\n... and ${validation.errors.length - 5} more errors` : ""}`
284
+ }));
285
+ // Update context with new errors
286
+ buildErrorContext.set(taskId, {
287
+ errors: validation.errors,
288
+ command: validation.command,
289
+ originalChanges: originalChanges,
290
+ retryCount: context.retryCount
291
+ });
292
+ await triggerBuildFix(cwd, taskId, validation.errors, validation.command, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
293
+ }
294
+ }
295
+ else {
296
+ // Agent failed to produce a fix
297
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: result.error || "Failed to fix build errors" }));
298
+ }
299
+ }
300
+ catch (error) {
301
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "Build fix encountered an error" }));
302
+ }
303
+ finally {
304
+ if (taskControllers.get(taskId) === controller)
305
+ taskControllers.delete(taskId);
306
+ }
307
+ }
308
+ async function validateBuild(cwd) {
309
+ const pkgPath = node_path_1.default.join(cwd, "package.json");
310
+ if (!node_fs_1.default.existsSync(pkgPath)) {
311
+ return { passed: true, errors: [], command: "none" };
312
+ }
313
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
314
+ const scripts = pkg.scripts || {};
315
+ // Determine which build command to run
316
+ let buildCommand = "";
317
+ if (scripts.build) {
318
+ buildCommand = "build";
319
+ }
320
+ else if (scripts["build:cli"] || scripts["build:overlay"]) {
321
+ // Monorepo with separate build scripts
322
+ buildCommand = scripts["build:cli"] ? "build:cli" : "build:overlay";
323
+ }
324
+ else if (scripts.compile) {
325
+ buildCommand = "compile";
326
+ }
327
+ else if (scripts.check) {
328
+ buildCommand = "check";
329
+ }
330
+ else if (scripts.lint) {
331
+ buildCommand = "lint";
332
+ }
333
+ else if (scripts.typecheck) {
334
+ buildCommand = "typecheck";
335
+ }
336
+ else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"))) {
337
+ buildCommand = "tsc"; // Run TypeScript compiler directly
338
+ }
339
+ else {
340
+ // No suitable build command found
341
+ return { passed: true, errors: [], command: "none" };
342
+ }
343
+ try {
344
+ const result = await execFileAsync(buildCommand === "tsc" ? "tsc" : "npm", buildCommand === "tsc" ? [] : ["run", buildCommand], { cwd, maxBuffer: 1024 * 1024 * 10, timeout: 60000 });
345
+ const output = result.stdout + result.stderr;
346
+ const errors = [];
347
+ // Parse TypeScript errors
348
+ if (output.includes("error TS")) {
349
+ const tsErrors = output.match(/error TS\d+.*$/gm) || [];
350
+ errors.push(...tsErrors);
351
+ }
352
+ // Parse general build errors
353
+ if (output.includes("Error:") || output.includes("error:")) {
354
+ const lines = output.split("\n");
355
+ for (const line of lines) {
356
+ if (line.includes("Error:") || line.includes("error:")) {
357
+ errors.push(line.trim());
358
+ }
359
+ }
360
+ }
361
+ return {
362
+ passed: errors.length === 0,
363
+ errors,
364
+ command: buildCommand === "tsc" ? "tsc" : `npm run ${buildCommand}`
365
+ };
366
+ }
367
+ catch (error) {
368
+ const stderr = error.stderr || "";
369
+ const errors = [];
370
+ if (stderr.includes("error TS")) {
371
+ const tsErrors = stderr.match(/error TS\d+.*$/gm) || [];
372
+ errors.push(...tsErrors);
373
+ }
374
+ if (stderr.includes("Error:") || stderr.includes("error:")) {
375
+ const lines = stderr.split("\n");
376
+ for (const line of lines) {
377
+ if (line.includes("Error:") || line.includes("error:")) {
378
+ errors.push(line.trim());
379
+ }
380
+ }
381
+ }
382
+ // If no specific errors found, include the full stderr
383
+ if (errors.length === 0 && stderr) {
384
+ errors.push(stderr);
385
+ }
386
+ return {
387
+ passed: false,
388
+ errors,
389
+ command: buildCommand === "tsc" ? "tsc" : `npm run ${buildCommand}`
390
+ };
391
+ }
392
+ }
150
393
  function readEnvFile(cwd, filename) {
151
394
  try {
152
395
  return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
@@ -172,6 +415,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
172
415
  const editRequests = new Map();
173
416
  const editConfigs = new Map();
174
417
  const reviewRefreshAttempts = new Map();
418
+ const buildErrorContext = new Map();
175
419
  const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
176
420
  const fileEnv = envRoots.reduce((values, root) => ({
177
421
  ...values,
@@ -349,38 +593,45 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
349
593
  ...dynamicModels.nvidia.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
350
594
  // Fallback hardcoded models (only if dynamic fetch failed)
351
595
  ...(dynamicModels.anthropic.length === 0 ? [
596
+ { id: "claude-opus-4-5-20251101", label: "Claude Opus 4.5", provider: "anthropic" },
352
597
  { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
353
- { id: "claude-opus-4-1-20250805", label: "Claude Opus 4.1", provider: "anthropic" },
598
+ { id: "claude-haiku-4-5-20251015", label: "Claude Haiku 4.5", provider: "anthropic" },
354
599
  ] : []),
355
600
  ...(dynamicModels.openai.length === 0 ? [
356
- { id: "gpt-4.1", label: "GPT-4.1", provider: "openai" },
357
- { id: "gpt-4.1-mini", label: "GPT-4.1 mini", provider: "openai" },
601
+ { id: "gpt-5.1", label: "GPT-5.1", provider: "openai" },
602
+ { id: "gpt-5.1-codex", label: "GPT-5.1 Codex", provider: "openai" },
603
+ { id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini", provider: "openai" },
604
+ { id: "o3", label: "OpenAI o3", provider: "openai" },
605
+ { id: "o3-mini", label: "OpenAI o3-mini", provider: "openai" },
358
606
  ] : []),
359
607
  ...(dynamicModels.google.length === 0 ? [
360
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google" },
361
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "google" },
608
+ { id: "gemini-3-pro-preview", label: "Gemini 3 Pro", provider: "google" },
609
+ { id: "gemini-3-flash-preview", label: "Gemini 3 Flash", provider: "google" },
610
+ { id: "gemini-2.5-flash-preview-09-2025", label: "Gemini 2.5 Flash", provider: "google" },
611
+ { id: "gemini-2.5-flash-lite-preview-09-2025", label: "Gemini 2.5 Flash Lite", provider: "google" },
362
612
  ] : []),
363
613
  ...(dynamicModels.nvidia.length === 0 ? [
364
- { id: "meta/llama-3.3-70b-instruct", label: "Llama 3.3 70B Instruct", provider: "nvidia" },
365
- { id: "meta/llama-3.2-11b-vision-instruct", label: "Llama 3.2 11B Vision", provider: "nvidia" },
366
- { id: "deepseek-ai/deepseek-v4-flash-0731", label: "DeepSeek V4 Flash", provider: "nvidia" },
367
- { id: "google/gemma-4-31b-it", label: "Gemma 4 31B", provider: "nvidia" },
614
+ { id: "nvidia/llama-3.3-nemotron-super-49b-v1", label: "Llama 3.3 Nemotron Super 49B", provider: "nvidia" },
615
+ { id: "deepseek-ai/deepseek-r1-distill-llama-70b", label: "DeepSeek R1 Distill Llama 70B", provider: "nvidia" },
616
+ { id: "deepseek-ai/deepseek-r1-distill-llama-8b", label: "DeepSeek R1 Distill Llama 8B", provider: "nvidia" },
617
+ { id: "google/gemma-3-1b-it", label: "Gemma 3 1B Instruct", provider: "nvidia" },
618
+ { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B", provider: "nvidia" },
368
619
  ] : []),
369
620
  // Local CLI agents (always included)
370
621
  ...(localAgents.has("claude-code") ? [
371
- { id: "claude-code:sonnet", label: "Claude Code · Sonnet", provider: "cli" },
372
- { id: "claude-code:opus", label: "Claude Code · Opus", provider: "cli" },
373
- { id: "claude-code:haiku", label: "Claude Code · Haiku", provider: "cli" },
622
+ { id: "claude-code:sonnet", label: "Claude Code · Sonnet 4.5", provider: "cli" },
623
+ { id: "claude-code:opus", label: "Claude Code · Opus 4.5", provider: "cli" },
624
+ { id: "claude-code:haiku", label: "Claude Code · Haiku 4.5", provider: "cli" },
374
625
  ] : []),
375
626
  ...(localAgents.has("codex") ? [
376
- { id: "codex:gpt-5", label: "Codex · GPT-5", provider: "cli" },
377
- { id: "codex:gpt-5-codex", label: "Codex · GPT-5-Codex", provider: "cli" },
627
+ { id: "codex:gpt-5.1", label: "Codex · GPT-5.1", provider: "cli" },
628
+ { id: "codex:gpt-5.1-codex", label: "Codex · GPT-5.1-Codex", provider: "cli" },
378
629
  ] : []),
379
630
  ...(localAgents.has("opencode") ? [
380
631
  { id: "opencode:opencode/big-pickle", label: "OpenCode · Big Pickle", provider: "cli" },
381
632
  { id: "opencode:deepseek/deepseek-chat", label: "OpenCode · DeepSeek Chat", provider: "cli" },
382
- { id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet", provider: "cli" },
383
- { id: "opencode:openai/gpt-5", label: "OpenCode · GPT-5", provider: "cli" },
633
+ { id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet 4.5", provider: "cli" },
634
+ { id: "opencode:openai/gpt-5.1", label: "OpenCode · GPT-5.1", provider: "cli" },
384
635
  ] : []),
385
636
  ...(localAgents.has("cursor") ? [
386
637
  { id: "cursor:claude-sonnet-4.5", label: "Cursor · Claude Sonnet 4.5", provider: "cli" },
@@ -902,7 +1153,35 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
902
1153
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
903
1154
  node_fs_1.default.writeFileSync(filePath, nextContent);
904
1155
  }
905
- socket.send(JSON.stringify({ type: "applied", taskId: msg.taskId, message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.` }));
1156
+ // Run build validation after applying changes
1157
+ const validation = await validateBuild(cwd);
1158
+ if (!validation.passed) {
1159
+ // Build failed - automatically trigger retry with build error context
1160
+ const errorContext = validation.errors.slice(0, 10).join("\n");
1161
+ socket.send(JSON.stringify({
1162
+ type: "agent_status",
1163
+ taskId: msg.taskId,
1164
+ status: "working",
1165
+ message: `⚠️ Build validation failed. Attempting to fix build errors automatically...`,
1166
+ detail: `Build command: ${validation.command}\nErrors:\n${errorContext}${validation.errors.length > 10 ? `\n... and ${validation.errors.length - 10} more errors` : ""}`
1167
+ }));
1168
+ // Store context for retry
1169
+ buildErrorContext.set(msg.taskId, {
1170
+ errors: validation.errors,
1171
+ command: validation.command,
1172
+ originalChanges: msg.changes,
1173
+ retryCount: 0
1174
+ });
1175
+ // Trigger automatic build fix
1176
+ await triggerBuildFix(cwd, msg.taskId, validation.errors, validation.command, msg.changes, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
1177
+ return;
1178
+ }
1179
+ socket.send(JSON.stringify({
1180
+ type: "applied",
1181
+ taskId: msg.taskId,
1182
+ message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.`,
1183
+ validation
1184
+ }));
906
1185
  }
907
1186
  catch (error) {
908
1187
  // Validation happens before writes, but restore this task's snapshot
@@ -10,6 +10,7 @@ const node_child_process_1 = require("node:child_process");
10
10
  const node_util_1 = require("node:util");
11
11
  const agent_1 = require("./agent");
12
12
  const auth_1 = require("./auth");
13
+ const framework_1 = require("./utils/framework");
13
14
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
14
15
  const CLI_PROVIDERS = new Set(["claude-code", "codex", "opencode", "cursor"]);
15
16
  function isCliProvider(provider) {
@@ -225,7 +226,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
225
226
  }
226
227
  // Skip project structure analysis for simple conversational queries
227
228
  const projectStructure = isConversational ? null : await analyzeProject(cwd);
228
- const framework = isConversational ? null : detectFramework(cwd);
229
+ const framework = isConversational ? null : (0, framework_1.detectFramework)(cwd);
229
230
  if (!isConversational && !pageContext) {
230
231
  onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
231
232
  }
@@ -314,6 +315,11 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
314
315
  dependencies: projectStructure?.dependencies,
315
316
  devDependencies: projectStructure?.devDependencies,
316
317
  files: projectStructure?.files.slice(0, 100) || [],
318
+ directories: projectStructure?.directories.slice(0, 50) || [],
319
+ sourceRoot: projectStructure?.sourceRoot,
320
+ appRouter: projectStructure?.appRouter,
321
+ pagesRouter: projectStructure?.pagesRouter,
322
+ multipleAppDirs: projectStructure?.multipleAppDirs,
317
323
  sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined,
318
324
  currentRoute: pageContext?.route || "/",
319
325
  pageContext,
@@ -498,7 +504,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
498
504
  }
499
505
  // Phase 2: Local agent generation fallback for code changes
500
506
  onProgress("thinking", "Generating implementation plan...", `Planning components for ${prompt}`);
501
- const plan = await generatePlan(cwd, prompt, scope, projectStructure || { framework: "", hasTypeScript: false, hasTailwind: false, hasReact: false, hasNext: false, hasVite: false, dependencies: [], devDependencies: [], files: [] }, framework || "", config, signal, messages, pageContext);
507
+ const plan = await generatePlan(cwd, prompt, scope, projectStructure || { framework: "", hasTypeScript: false, hasTailwind: false, hasReact: false, hasNext: false, hasVite: false, dependencies: [], devDependencies: [], files: [], directories: [], sourceRoot: ".", appRouter: false, pagesRouter: false, multipleAppDirs: [] }, framework || "", config, signal, messages, pageContext);
502
508
  if (signal.aborted) {
503
509
  return { ok: false, error: "Operation was cancelled during planning" };
504
510
  }
@@ -593,24 +599,33 @@ async function analyzeProject(cwd) {
593
599
  const hasReact = dependencies.includes("react") || devDependencies.includes("react");
594
600
  const hasNext = dependencies.includes("next") || devDependencies.includes("next");
595
601
  const hasVite = dependencies.includes("vite") || devDependencies.includes("vite");
596
- // Scan src / app directory
602
+ // Scan directory structure
597
603
  const files = [];
598
- const roots = ["src", "app", "pages", "."];
604
+ const directories = [];
605
+ const appDirs = [];
606
+ const pagesDirs = [];
607
+ const roots = ["src", "app", "pages", ".", "web", "lib", "components"];
599
608
  for (const root of roots) {
600
609
  const rootPath = node_path_1.default.join(cwd, root);
601
610
  if (!node_fs_1.default.existsSync(rootPath))
602
611
  continue;
603
612
  const scanDir = (dir, base = "", depth = 0) => {
604
- if (depth > 4)
613
+ if (depth > 5)
605
614
  return;
606
615
  try {
607
616
  const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
608
617
  for (const entry of entries) {
609
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
618
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build" || entry.name === ".next") {
610
619
  continue;
611
620
  }
612
621
  const rel = base ? node_path_1.default.join(base, entry.name) : entry.name;
613
622
  if (entry.isDirectory()) {
623
+ directories.push(rel);
624
+ // Track app and pages directories
625
+ if (entry.name === "app")
626
+ appDirs.push(rel);
627
+ if (entry.name === "pages")
628
+ pagesDirs.push(rel);
614
629
  scanDir(node_path_1.default.join(dir, entry.name), rel, depth + 1);
615
630
  }
616
631
  else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
@@ -623,11 +638,25 @@ async function analyzeProject(cwd) {
623
638
  }
624
639
  };
625
640
  scanDir(rootPath);
626
- if (files.length > 60)
641
+ if (files.length > 100)
627
642
  break;
628
643
  }
644
+ // Determine source root
645
+ let sourceRoot = ".";
646
+ if (directories.includes("src/app"))
647
+ sourceRoot = "src";
648
+ else if (directories.includes("app"))
649
+ sourceRoot = ".";
650
+ else if (directories.includes("src"))
651
+ sourceRoot = "src";
652
+ // Detect App Router vs Pages Router
653
+ const appRouter = appDirs.length > 0 && appDirs.some(dir => {
654
+ const appPath = node_path_1.default.join(cwd, dir);
655
+ return node_fs_1.default.existsSync(node_path_1.default.join(appPath, "layout.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(appPath, "page.tsx"));
656
+ });
657
+ const pagesRouter = pagesDirs.length > 0 && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_app.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_document.tsx"));
629
658
  return {
630
- framework: detectFramework(cwd),
659
+ framework: (0, framework_1.detectFramework)(cwd),
631
660
  hasTypeScript,
632
661
  hasTailwind,
633
662
  hasReact,
@@ -636,31 +665,13 @@ async function analyzeProject(cwd) {
636
665
  dependencies: dependencies,
637
666
  devDependencies: devDependencies,
638
667
  files,
668
+ directories,
669
+ sourceRoot,
670
+ appRouter,
671
+ pagesRouter,
672
+ multipleAppDirs: appDirs,
639
673
  };
640
674
  }
641
- function detectFramework(cwd) {
642
- const pkgPath = node_path_1.default.join(cwd, "package.json");
643
- if (node_fs_1.default.existsSync(pkgPath)) {
644
- try {
645
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
646
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
647
- if (deps.next)
648
- return "next";
649
- if (deps.vite)
650
- return "vite";
651
- if (deps.react)
652
- return "react";
653
- if (deps.vue)
654
- return "vue";
655
- if (deps.svelte)
656
- return "svelte";
657
- }
658
- catch {
659
- // fallback
660
- }
661
- }
662
- return "unknown";
663
- }
664
675
  async function generatePlan(cwd, prompt, _scope, structure, framework, config, signal, _messages, pageContext) {
665
676
  const steps = [];
666
677
  // Identify relevant files to supply as context.
package/dist/overlay.js CHANGED
@@ -7848,6 +7848,8 @@
7848
7848
  return `${Math.floor(seconds / 86400)}d`;
7849
7849
  }
7850
7850
  var modelSessionKey = "lasso:selected-model";
7851
+ var chatHistoryKey = "lasso:chat-history";
7852
+ var changesHistoryKey = "lasso:changes-history";
7851
7853
  function storedModelId() {
7852
7854
  try {
7853
7855
  return window.sessionStorage.getItem(modelSessionKey);
@@ -7861,6 +7863,22 @@
7861
7863
  } catch {
7862
7864
  }
7863
7865
  }
7866
+ function loadChatHistory() {
7867
+ try {
7868
+ const chatHistory = JSON.parse(localStorage.getItem(chatHistoryKey) || "[]");
7869
+ const changesHistory = JSON.parse(localStorage.getItem(changesHistoryKey) || "[]");
7870
+ return { chatHistory, changesHistory };
7871
+ } catch {
7872
+ return { chatHistory: [], changesHistory: [] };
7873
+ }
7874
+ }
7875
+ function saveChatHistory(chatHistory, changesHistory) {
7876
+ try {
7877
+ localStorage.setItem(chatHistoryKey, JSON.stringify(chatHistory));
7878
+ localStorage.setItem(changesHistoryKey, JSON.stringify(changesHistory));
7879
+ } catch {
7880
+ }
7881
+ }
7864
7882
  var OverlayState = class {
7865
7883
  // Mode & Selection
7866
7884
  selectMode = false;
@@ -21611,7 +21629,24 @@
21611
21629
  recordTaskActivity(task.id, message.message || "Done.");
21612
21630
  }
21613
21631
  const isPromptTask = !taskId2 || taskId2 === state.promptTaskId;
21614
- const result = message.message || "Done.";
21632
+ let result = message.message || "Done.";
21633
+ if (message.type === "applied" && message.validation) {
21634
+ const { passed, errors, command } = message.validation;
21635
+ if (!passed && errors.length > 0) {
21636
+ result = `${message.message}
21637
+
21638
+ \u26A0\uFE0F Build validation failed after running \`${command}\`:
21639
+
21640
+ ${errors.slice(0, 5).join("\n")}${errors.length > 5 ? `
21641
+ ...and ${errors.length - 5} more errors` : ""}
21642
+
21643
+ The agent is automatically attempting to fix the build errors...`;
21644
+ } else if (passed) {
21645
+ result = `${message.message}
21646
+
21647
+ \u2705 Build validation passed.`;
21648
+ }
21649
+ }
21615
21650
  void notifyAgent(message.type === "applied" ? "Changes applied" : "Change undone", result);
21616
21651
  appendChat("assistant", result, taskId2);
21617
21652
  if (task?.element) releaseHeldLock(elementKey(task.element));
@@ -21743,6 +21778,8 @@
21743
21778
  updateAgentTask(id, {
21744
21779
  changesHistory: [...task.changesHistory, { summary, changes: changes.map((change) => ({ ...change })), createdAt: (/* @__PURE__ */ new Date()).toISOString() }]
21745
21780
  });
21781
+ state.changesHistory = task.changesHistory;
21782
+ saveChatHistory(state.chatHistory, state.changesHistory);
21746
21783
  }
21747
21784
  function appendTaskMessage(id, role, text) {
21748
21785
  const task = getAgentTask(id);
@@ -21757,6 +21794,8 @@
21757
21794
  contextId: task.selectionId || void 0
21758
21795
  };
21759
21796
  updateAgentTask(task.id, { messages: [...task.messages, message] });
21797
+ state.chatHistory = task.messages;
21798
+ saveChatHistory(state.chatHistory, state.changesHistory);
21760
21799
  return message;
21761
21800
  }
21762
21801
  function selectAgentTask(id) {
@@ -21916,6 +21955,7 @@
21916
21955
  state.selectionId = selectionId;
21917
21956
  state.chatHistory = conversation.messages;
21918
21957
  state.changesHistory = conversation.changesHistory;
21958
+ saveChatHistory(state.chatHistory, state.changesHistory);
21919
21959
  }
21920
21960
  function buildPrompt() {
21921
21961
  const dom = getDOM();
@@ -22101,6 +22141,12 @@
22101
22141
  if (task) restoreTaskPrompt(task);
22102
22142
  else resetPromptSession();
22103
22143
  });
22144
+ const { chatHistory, changesHistory } = loadChatHistory();
22145
+ if (chatHistory.length > 0) {
22146
+ state.chatHistory = chatHistory;
22147
+ state.changesHistory = changesHistory;
22148
+ renderChatThread();
22149
+ }
22104
22150
  onAgentTaskReviewRequested((task) => {
22105
22151
  state.promptTaskId = task.id;
22106
22152
  state.pendingChanges = [...task.pendingChanges.length ? task.pendingChanges : task.changes || []];
@@ -22610,6 +22656,7 @@
22610
22656
  contextId: state.selectionId || void 0
22611
22657
  });
22612
22658
  }
22659
+ saveChatHistory(state.chatHistory, state.changesHistory);
22613
22660
  renderChatThread();
22614
22661
  }
22615
22662
  function renderChatText(container, text) {
@@ -22696,6 +22743,7 @@
22696
22743
  state.chatHistory = [];
22697
22744
  state.changesHistory = [];
22698
22745
  state.selectionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
22746
+ saveChatHistory(state.chatHistory, state.changesHistory);
22699
22747
  }
22700
22748
  state.screenshotPromise = Promise.resolve({});
22701
22749
  hideReview();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",