@lasso-ai/cli 1.0.15 → 1.0.17

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
@@ -314,7 +314,7 @@ function localCommand(provider, model, prompt) {
314
314
  return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
315
315
  }
316
316
  async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
317
- const outputContract = `Return ONLY valid JSON in this exact shape: {"summary":"short explanation","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}. Every oldString must occur exactly once. Do not edit files, run write commands, commit, or produce markdown fences.`;
317
+ const outputContract = `Return ONLY valid JSON in this exact shape: {"summary":"short explanation","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}. Treat the supplied source context as read-only. Before returning, verify every oldString against that context. Use project-relative paths only. Do not edit files, run write commands, commit, or produce markdown fences.`;
318
318
  const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
319
319
  const local = localCommand(config.provider, config.model, prompt);
320
320
  const command = local.command;
@@ -114,6 +114,18 @@ function prepareChange(content, change) {
114
114
  const start = content.indexOf(oldString);
115
115
  return { start, end: start + oldString.length, oldString, newString };
116
116
  }
117
+ function proposalMatchesCurrentSource(cwd, changes) {
118
+ try {
119
+ for (const change of changes) {
120
+ const filePath = resolveProposedFile(cwd, change.filePath);
121
+ prepareChange(node_fs_1.default.readFileSync(filePath, "utf8"), change);
122
+ }
123
+ return true;
124
+ }
125
+ catch {
126
+ return false;
127
+ }
128
+ }
117
129
  function readEnvFile(cwd, filename) {
118
130
  try {
119
131
  return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
@@ -136,6 +148,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
136
148
  const wss = new ws_1.WebSocketServer({ server: bridgeServer });
137
149
  let overlaySocket = null;
138
150
  let lastSnapshot = [];
151
+ let lastEditRequest = null;
152
+ let lastEditConfig = null;
153
+ let reviewRefreshAttempts = 0;
139
154
  const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
140
155
  const fileEnv = envRoots.reduce((values, root) => ({
141
156
  ...values,
@@ -254,6 +269,43 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
254
269
  if (socket.readyState === socket.OPEN)
255
270
  socket.send(JSON.stringify({ type: "git_state", git }));
256
271
  });
272
+ const runEditReview = (request, config, statusMessage) => {
273
+ activeAgentController?.abort();
274
+ const controller = new AbortController();
275
+ activeAgentController = controller;
276
+ if (statusMessage && socket.readyState === socket.OPEN) {
277
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message: statusMessage }));
278
+ }
279
+ void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail) => {
280
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
281
+ socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
282
+ }
283
+ })
284
+ .then((proposal) => {
285
+ if (controller.signal.aborted || socket.readyState !== socket.OPEN)
286
+ return;
287
+ if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
288
+ if (reviewRefreshAttempts < 1) {
289
+ reviewRefreshAttempts += 1;
290
+ runEditReview(request, config, "The source changed while the proposal was being prepared. Refreshing the review…");
291
+ }
292
+ else {
293
+ socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "The source is still changing. Stop the dev-server edit or try the request again." }));
294
+ }
295
+ return;
296
+ }
297
+ socket.send(JSON.stringify({ type: "agent_status", status: "review", message: proposal.summary, changes: proposal.changes }));
298
+ })
299
+ .catch((error) => {
300
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
301
+ socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The agent could not prepare a change." }));
302
+ }
303
+ })
304
+ .finally(() => {
305
+ if (activeAgentController === controller)
306
+ activeAgentController = null;
307
+ });
308
+ };
257
309
  socket.on("message", async (raw) => {
258
310
  const msg = JSON.parse(raw.toString());
259
311
  if (msg.type === "edit" || msg.type === "ask") {
@@ -304,31 +356,13 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
304
356
  socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Add a supported agent key: GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." }));
305
357
  return;
306
358
  }
307
- activeAgentController?.abort();
308
- const controller = new AbortController();
309
- activeAgentController = controller;
310
359
  const selectedConfig = localProvider
311
360
  ? { provider: cliProvider, model: msg.model }
312
361
  : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
313
- void (0, agent_1.proposeChanges)(cwd, msg, selectedConfig, controller.signal, (message, detail) => {
314
- if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
315
- socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
316
- }
317
- })
318
- .then((proposal) => {
319
- if (!controller.signal.aborted)
320
- socket.send(JSON.stringify({ type: "agent_status", status: "review", message: proposal.summary, changes: proposal.changes }));
321
- })
322
- .catch((error) => {
323
- if (controller.signal.aborted)
324
- return;
325
- const message = error instanceof Error ? error.message : "The agent could not prepare a change.";
326
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message }));
327
- })
328
- .finally(() => {
329
- if (activeAgentController === controller)
330
- activeAgentController = null;
331
- });
362
+ lastEditRequest = msg;
363
+ lastEditConfig = selectedConfig;
364
+ reviewRefreshAttempts = 0;
365
+ runEditReview(msg, selectedConfig);
332
366
  }
333
367
  else if (msg.type === "stop") {
334
368
  activeAgentController?.abort();
@@ -463,7 +497,14 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
463
497
  for (const snapshot of lastSnapshot)
464
498
  node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
465
499
  lastSnapshot = [];
466
- socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The change could not be applied." }));
500
+ const message = error instanceof Error ? error.message : "The change could not be applied.";
501
+ const sourceChanged = message.includes("The source changed after the suggestion was generated");
502
+ if (sourceChanged && lastEditRequest && lastEditConfig && reviewRefreshAttempts < 1) {
503
+ runEditReview(lastEditRequest, lastEditConfig, "The source changed. Refreshing the review against the current file…");
504
+ }
505
+ else {
506
+ socket.send(JSON.stringify({ type: "agent_status", status: "error", message }));
507
+ }
467
508
  }
468
509
  }
469
510
  else if (msg.type === "undo") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
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",