@lasso-ai/cli 1.1.2 → 1.1.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.
@@ -45,7 +45,8 @@ export type AgentConfig = {
45
45
  baseUrl?: string;
46
46
  };
47
47
  export type LocalAgent = "claude-code" | "codex" | "opencode";
48
- export type AgentProgress = (message: string, detail?: string) => void;
48
+ export type AgentProgressLevel = "working" | "error";
49
+ export type AgentProgress = (message: string, detail?: string, level?: AgentProgressLevel) => void;
49
50
  export type AgentPrompt = {
50
51
  message: string;
51
52
  kind: "permission" | "input";
package/dist/cli/agent.js CHANGED
@@ -36,8 +36,9 @@ async function detectLocalAgents() {
36
36
  }
37
37
  const ignored = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo"]);
38
38
  const sourceExtensions = /\.(tsx?|jsx?|vue|svelte|css|scss|html)$/i;
39
+ const layoutFiles = /^(layout|_layout|_app|app|_document|document|template|_template)\.(tsx?|jsx?)$/i;
39
40
  const sourceFileCache = new Map();
40
- async function sourceFiles(directory, budget = { remaining: 40 }) {
41
+ async function sourceFiles(directory, budget = { remaining: 25 }) {
41
42
  if (budget.remaining <= 0)
42
43
  return [];
43
44
  const cached = sourceFileCache.get(directory);
@@ -48,6 +49,8 @@ async function sourceFiles(directory, budget = { remaining: 40 }) {
48
49
  }
49
50
  const entries = await promises_1.default.readdir(directory, { withFileTypes: true });
50
51
  const files = [];
52
+ const layoutFilesList = [];
53
+ const otherFiles = [];
51
54
  for (const entry of entries) {
52
55
  if (ignored.has(entry.name) || entry.name.startsWith("."))
53
56
  continue;
@@ -59,11 +62,18 @@ async function sourceFiles(directory, budget = { remaining: 40 }) {
59
62
  files.push(...nested);
60
63
  }
61
64
  else if (sourceExtensions.test(entry.name)) {
62
- files.push(fullPath);
63
- budget.remaining -= 1;
65
+ if (layoutFiles.test(entry.name)) {
66
+ layoutFilesList.push(fullPath);
67
+ }
68
+ else {
69
+ otherFiles.push(fullPath);
70
+ }
64
71
  }
65
72
  }
66
- sourceFileCache.set(directory, { expiresAt: Date.now() + 5000, files });
73
+ // Prioritize layout files first, then other files
74
+ files.push(...layoutFilesList, ...otherFiles);
75
+ budget.remaining -= files.length;
76
+ sourceFileCache.set(directory, { expiresAt: Date.now() + 30000, files });
67
77
  return files;
68
78
  }
69
79
  async function contextFor(cwd, element) {
@@ -95,7 +105,7 @@ async function contextFor(cwd, element) {
95
105
  try {
96
106
  const content = await promises_1.default.readFile(file, "utf8");
97
107
  if (!needle || (sourceFile && file.endsWith(sourceFile)) || content.includes(needle) || content.includes(element.label)) {
98
- return `FILE: ${node_path_1.default.relative(cwd, file)}\n${content.slice(0, 8000)}`;
108
+ return `FILE: ${node_path_1.default.relative(cwd, file)}\n${content.slice(0, 6000)}`;
99
109
  }
100
110
  }
101
111
  catch {
@@ -104,7 +114,7 @@ async function contextFor(cwd, element) {
104
114
  return null;
105
115
  }));
106
116
  for (const result of results) {
107
- if (result && snippets.length < 6)
117
+ if (result && snippets.length < 4)
108
118
  snippets.push(result);
109
119
  }
110
120
  return snippets.join("\n\n---\n\n");
@@ -560,9 +570,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
560
570
  else if (stream === "stderr" && line.trim()) {
561
571
  const hint = agentFailureHint(line);
562
572
  if (hint)
563
- onProgress?.(`${label} · ${hint}`);
573
+ onProgress?.(`${label} · ${hint}`, undefined, "error");
564
574
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
565
- onProgress?.("OpenCode · provider rejected the request; stopping this task…");
575
+ onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
566
576
  child.kill("SIGTERM");
567
577
  }
568
578
  }
@@ -611,9 +621,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
611
621
  }
612
622
  const hint = agentFailureHint(stderrTail);
613
623
  if (hint)
614
- onProgress?.(`${label} · ${hint}`);
624
+ onProgress?.(`${label} · ${hint}`, undefined, "error");
615
625
  if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
616
- onProgress?.("OpenCode · provider rejected the request; stopping this task…");
626
+ onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
617
627
  }
618
628
  }
619
629
  resolve({ stdout, stderr, exitCode: code ?? 1 });
@@ -118,7 +118,20 @@ function proposalMatchesCurrentSource(cwd, changes) {
118
118
  try {
119
119
  for (const change of changes) {
120
120
  const filePath = resolveProposedFile(cwd, change.filePath);
121
- prepareChange(node_fs_1.default.readFileSync(filePath, "utf8"), change);
121
+ const content = node_fs_1.default.readFileSync(filePath, "utf8");
122
+ // Try exact match first
123
+ try {
124
+ prepareChange(content, change);
125
+ }
126
+ catch {
127
+ // If exact match fails, try with normalized whitespace
128
+ const normalizedContent = content.replace(/\s+/g, " ").trim();
129
+ const normalizedOldString = change.oldString.replace(/\s+/g, " ").trim();
130
+ const normalizedNewString = change.newString.replace(/\s+/g, " ").trim();
131
+ if (!normalizedContent.includes(normalizedOldString)) {
132
+ throw new Error("Source content does not match proposed change (even with normalized whitespace)");
133
+ }
134
+ }
122
135
  }
123
136
  return true;
124
137
  }
@@ -308,9 +321,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
308
321
  if (socket.readyState === socket.OPEN) {
309
322
  socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage || "Working…" }));
310
323
  }
311
- void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail) => {
324
+ void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail, level) => {
312
325
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
313
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message, detail }));
326
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
314
327
  }
315
328
  }, (prompt) => {
316
329
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
@@ -322,21 +335,30 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
322
335
  return;
323
336
  if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
324
337
  const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
325
- if (refreshAttempts < 1) {
338
+ const MAX_REFRESH_ATTEMPTS = 3;
339
+ if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
326
340
  reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
327
- runEditReview(request, config, "The source changed while the proposal was being prepared. Refreshing the review…");
341
+ setTimeout(() => {
342
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
343
+ runEditReview(request, config, `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})`);
344
+ }
345
+ }, 500 * (refreshAttempts + 1));
328
346
  }
329
347
  else {
330
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing. Stop the dev-server edit or try the request again." }));
348
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: "The source is still changing after multiple refresh attempts. Stop the dev-server edit or try the request again." }));
331
349
  }
332
350
  return;
333
351
  }
334
352
  socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
335
353
  })
336
354
  .catch((error) => {
337
- if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
338
- socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
355
+ if (socket.readyState !== socket.OPEN)
356
+ return;
357
+ if (controller.signal.aborted) {
358
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
359
+ return;
339
360
  }
361
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
340
362
  })
341
363
  .finally(() => {
342
364
  if (taskControllers.get(request.taskId) === controller)
@@ -432,9 +454,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
432
454
  ? { provider: cliProvider, model: msg.model }
433
455
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
434
456
  socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message: "Working…" }));
435
- void (0, agent_1.answerQuestion)(cwd, { taskId: msg.taskId, question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
457
+ void (0, agent_1.answerQuestion)(cwd, { taskId: msg.taskId, question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail, level) => {
436
458
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
437
- socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message, detail }));
459
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: level === "error" ? "error" : "working", message, detail }));
438
460
  }, (prompt) => {
439
461
  if (!controller.signal.aborted && socket.readyState === socket.OPEN)
440
462
  socket.send(JSON.stringify({ type: "agent_prompt", taskId: msg.taskId, prompt }));