@lasso-ai/cli 1.1.2 → 1.1.3

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
@@ -37,7 +37,7 @@ async function detectLocalAgents() {
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
39
  const sourceFileCache = new Map();
40
- async function sourceFiles(directory, budget = { remaining: 40 }) {
40
+ async function sourceFiles(directory, budget = { remaining: 25 }) {
41
41
  if (budget.remaining <= 0)
42
42
  return [];
43
43
  const cached = sourceFileCache.get(directory);
@@ -63,7 +63,7 @@ async function sourceFiles(directory, budget = { remaining: 40 }) {
63
63
  budget.remaining -= 1;
64
64
  }
65
65
  }
66
- sourceFileCache.set(directory, { expiresAt: Date.now() + 5000, files });
66
+ sourceFileCache.set(directory, { expiresAt: Date.now() + 30000, files });
67
67
  return files;
68
68
  }
69
69
  async function contextFor(cwd, element) {
@@ -95,7 +95,7 @@ async function contextFor(cwd, element) {
95
95
  try {
96
96
  const content = await promises_1.default.readFile(file, "utf8");
97
97
  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)}`;
98
+ return `FILE: ${node_path_1.default.relative(cwd, file)}\n${content.slice(0, 6000)}`;
99
99
  }
100
100
  }
101
101
  catch {
@@ -104,7 +104,7 @@ async function contextFor(cwd, element) {
104
104
  return null;
105
105
  }));
106
106
  for (const result of results) {
107
- if (result && snippets.length < 6)
107
+ if (result && snippets.length < 4)
108
108
  snippets.push(result);
109
109
  }
110
110
  return snippets.join("\n\n---\n\n");
@@ -560,9 +560,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
560
560
  else if (stream === "stderr" && line.trim()) {
561
561
  const hint = agentFailureHint(line);
562
562
  if (hint)
563
- onProgress?.(`${label} · ${hint}`);
563
+ onProgress?.(`${label} · ${hint}`, undefined, "error");
564
564
  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…");
565
+ onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
566
566
  child.kill("SIGTERM");
567
567
  }
568
568
  }
@@ -611,9 +611,9 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
611
611
  }
612
612
  const hint = agentFailureHint(stderrTail);
613
613
  if (hint)
614
- onProgress?.(`${label} · ${hint}`);
614
+ onProgress?.(`${label} · ${hint}`, undefined, "error");
615
615
  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…");
616
+ onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
617
617
  }
618
618
  }
619
619
  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 }));