@lasso-ai/cli 1.0.18 → 1.0.20
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/bridge.d.ts +10 -0
- package/dist/cli/bridge.js +59 -44
- package/dist/overlay.js +550 -128
- package/package.json +1 -1
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type BridgeMessage = {
|
|
|
14
14
|
from: "overlay" | "cli";
|
|
15
15
|
} | {
|
|
16
16
|
type: "edit";
|
|
17
|
+
taskId: string;
|
|
17
18
|
instruction: string;
|
|
18
19
|
model: string;
|
|
19
20
|
provider?: "anthropic" | "openai" | "google" | "ollama" | "cli";
|
|
@@ -48,6 +49,7 @@ export type BridgeMessage = {
|
|
|
48
49
|
};
|
|
49
50
|
} | {
|
|
50
51
|
type: "ask";
|
|
52
|
+
taskId: string;
|
|
51
53
|
question: string;
|
|
52
54
|
model: string;
|
|
53
55
|
provider?: "anthropic" | "openai" | "google" | "ollama" | "cli";
|
|
@@ -81,11 +83,14 @@ export type BridgeMessage = {
|
|
|
81
83
|
details: string;
|
|
82
84
|
} | {
|
|
83
85
|
type: "apply";
|
|
86
|
+
taskId: string;
|
|
84
87
|
changes: SourceChange[];
|
|
85
88
|
} | {
|
|
86
89
|
type: "undo";
|
|
90
|
+
taskId?: string;
|
|
87
91
|
} | {
|
|
88
92
|
type: "stop";
|
|
93
|
+
taskId?: string;
|
|
89
94
|
} | {
|
|
90
95
|
type: "git_status";
|
|
91
96
|
} | {
|
|
@@ -101,6 +106,7 @@ export type BridgeMessage = {
|
|
|
101
106
|
type: "git_push";
|
|
102
107
|
} | {
|
|
103
108
|
type: "agent_status";
|
|
109
|
+
taskId?: string;
|
|
104
110
|
status: "thinking" | "working" | "review" | "error" | "stopped";
|
|
105
111
|
message: string;
|
|
106
112
|
} | {
|
|
@@ -134,18 +140,22 @@ export type ServerBridgeMessage = {
|
|
|
134
140
|
error?: string;
|
|
135
141
|
} | {
|
|
136
142
|
type: "agent_status";
|
|
143
|
+
taskId?: string;
|
|
137
144
|
status: "thinking" | "working" | "review" | "error" | "stopped";
|
|
138
145
|
message: string;
|
|
139
146
|
detail?: string;
|
|
140
147
|
changes?: SourceChange[];
|
|
141
148
|
} | {
|
|
142
149
|
type: "assistant_message";
|
|
150
|
+
taskId?: string;
|
|
143
151
|
message: string;
|
|
144
152
|
} | {
|
|
145
153
|
type: "applied";
|
|
154
|
+
taskId?: string;
|
|
146
155
|
message: string;
|
|
147
156
|
} | {
|
|
148
157
|
type: "undone";
|
|
158
|
+
taskId?: string;
|
|
149
159
|
message: string;
|
|
150
160
|
} | {
|
|
151
161
|
type: "transcribe_result";
|
package/dist/cli/bridge.js
CHANGED
|
@@ -147,10 +147,10 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
147
147
|
const bridgeServer = node_http_1.default.createServer(); // dedicated, empty HTTP server
|
|
148
148
|
const wss = new ws_1.WebSocketServer({ server: bridgeServer });
|
|
149
149
|
let overlaySocket = null;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
const taskSnapshots = new Map();
|
|
151
|
+
const editRequests = new Map();
|
|
152
|
+
const editConfigs = new Map();
|
|
153
|
+
const reviewRefreshAttempts = new Map();
|
|
154
154
|
const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
|
|
155
155
|
const fileEnv = envRoots.reduce((values, root) => ({
|
|
156
156
|
...values,
|
|
@@ -179,11 +179,15 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
179
179
|
};
|
|
180
180
|
})();
|
|
181
181
|
let activeAgentController = null;
|
|
182
|
+
const taskControllers = new Map();
|
|
182
183
|
let localAgents = new Set();
|
|
183
184
|
bridgeServer.on("request", (req, res) => {
|
|
184
185
|
if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
|
|
185
186
|
return;
|
|
186
187
|
activeAgentController?.abort();
|
|
188
|
+
for (const controller of taskControllers.values())
|
|
189
|
+
controller.abort();
|
|
190
|
+
taskControllers.clear();
|
|
187
191
|
activeAgentController = null;
|
|
188
192
|
for (const socket of wss.clients)
|
|
189
193
|
socket.close(1000, "Bridge restarted by the CLI");
|
|
@@ -270,40 +274,41 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
270
274
|
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
271
275
|
});
|
|
272
276
|
const runEditReview = (request, config, statusMessage) => {
|
|
273
|
-
|
|
277
|
+
taskControllers.get(request.taskId)?.abort();
|
|
274
278
|
const controller = new AbortController();
|
|
275
|
-
|
|
279
|
+
taskControllers.set(request.taskId, controller);
|
|
276
280
|
if (statusMessage && socket.readyState === socket.OPEN) {
|
|
277
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "working", message: statusMessage }));
|
|
281
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: statusMessage }));
|
|
278
282
|
}
|
|
279
283
|
void (0, agent_1.proposeChanges)(cwd, request, config, controller.signal, (message, detail) => {
|
|
280
284
|
if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
|
|
281
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
|
|
285
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message, detail }));
|
|
282
286
|
}
|
|
283
287
|
})
|
|
284
288
|
.then((proposal) => {
|
|
285
289
|
if (controller.signal.aborted || socket.readyState !== socket.OPEN)
|
|
286
290
|
return;
|
|
287
291
|
if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
|
|
293
|
+
if (refreshAttempts < 1) {
|
|
294
|
+
reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
|
|
290
295
|
runEditReview(request, config, "The source changed while the proposal was being prepared. Refreshing the review…");
|
|
291
296
|
}
|
|
292
297
|
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." }));
|
|
298
|
+
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." }));
|
|
294
299
|
}
|
|
295
300
|
return;
|
|
296
301
|
}
|
|
297
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "review", message: proposal.summary, changes: proposal.changes }));
|
|
302
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
|
|
298
303
|
})
|
|
299
304
|
.catch((error) => {
|
|
300
305
|
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." }));
|
|
306
|
+
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." }));
|
|
302
307
|
}
|
|
303
308
|
})
|
|
304
309
|
.finally(() => {
|
|
305
|
-
if (
|
|
306
|
-
|
|
310
|
+
if (taskControllers.get(request.taskId) === controller)
|
|
311
|
+
taskControllers.delete(request.taskId);
|
|
307
312
|
});
|
|
308
313
|
};
|
|
309
314
|
socket.on("message", async (raw) => {
|
|
@@ -318,56 +323,59 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
318
323
|
const localProvider = msg.provider === "cli";
|
|
319
324
|
const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
|
|
320
325
|
if (!lassoKeyConfigured && !localProvider) {
|
|
321
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
|
|
326
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set your Lasso API key before asking the hosted agent a question." }));
|
|
322
327
|
return;
|
|
323
328
|
}
|
|
324
329
|
if (!agentConfig && !localProvider) {
|
|
325
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "No hosted agent is configured. Select Claude Code/Codex or add a provider key." }));
|
|
330
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "No hosted agent is configured. Select Claude Code/Codex or add a provider key." }));
|
|
326
331
|
return;
|
|
327
332
|
}
|
|
328
|
-
|
|
333
|
+
taskControllers.get(msg.taskId)?.abort();
|
|
329
334
|
const controller = new AbortController();
|
|
330
|
-
|
|
335
|
+
taskControllers.set(msg.taskId, controller);
|
|
331
336
|
const selectedConfig = localProvider
|
|
332
337
|
? { provider: cliProvider, model: msg.model }
|
|
333
338
|
: { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
|
|
334
339
|
void (0, agent_1.answerQuestion)(cwd, { question: msg.question, context: msg.context, element: msg.element, messages: msg.messages }, selectedConfig, controller.signal, (message, detail) => {
|
|
335
340
|
if (!controller.signal.aborted && socket.readyState === socket.OPEN)
|
|
336
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "working", message, detail }));
|
|
341
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "working", message, detail }));
|
|
337
342
|
}).then((answer) => {
|
|
338
343
|
if (!controller.signal.aborted)
|
|
339
|
-
socket.send(JSON.stringify({ type: "assistant_message", message: answer }));
|
|
344
|
+
socket.send(JSON.stringify({ type: "assistant_message", taskId: msg.taskId, message: answer }));
|
|
340
345
|
}).catch((error) => {
|
|
341
346
|
if (!controller.signal.aborted)
|
|
342
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
|
|
347
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
|
|
343
348
|
}).finally(() => {
|
|
344
|
-
if (
|
|
345
|
-
|
|
349
|
+
if (taskControllers.get(msg.taskId) === controller)
|
|
350
|
+
taskControllers.delete(msg.taskId);
|
|
346
351
|
});
|
|
347
352
|
}
|
|
348
353
|
else if (msg.type === "edit") {
|
|
349
354
|
const localProvider = msg.provider === "cli";
|
|
350
355
|
const cliProvider = msg.model.startsWith("claude-code:") ? "claude-code" : msg.model.startsWith("opencode:") ? "opencode" : "codex";
|
|
351
356
|
if (!lassoKeyConfigured && !localProvider) {
|
|
352
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
|
|
357
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Set VITE_LASSO_API_KEY or NEXT_LASSO_API_KEY in your app environment before sending an edit." }));
|
|
353
358
|
return;
|
|
354
359
|
}
|
|
355
360
|
if (!agentConfig && !localProvider) {
|
|
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." }));
|
|
361
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message: "Add a supported agent key: GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY." }));
|
|
357
362
|
return;
|
|
358
363
|
}
|
|
359
364
|
const selectedConfig = localProvider
|
|
360
365
|
? { provider: cliProvider, model: msg.model }
|
|
361
366
|
: { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
reviewRefreshAttempts
|
|
367
|
+
editRequests.set(msg.taskId, msg);
|
|
368
|
+
editConfigs.set(msg.taskId, selectedConfig);
|
|
369
|
+
reviewRefreshAttempts.set(msg.taskId, 0);
|
|
365
370
|
runEditReview(msg, selectedConfig);
|
|
366
371
|
}
|
|
367
372
|
else if (msg.type === "stop") {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
373
|
+
if (msg.taskId)
|
|
374
|
+
taskControllers.get(msg.taskId)?.abort();
|
|
375
|
+
else
|
|
376
|
+
for (const controller of taskControllers.values())
|
|
377
|
+
controller.abort();
|
|
378
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "stopped", message: "Agent stopped." }));
|
|
371
379
|
}
|
|
372
380
|
else if (msg.type === "runtime_error") {
|
|
373
381
|
socket.send(JSON.stringify({ type: "agent_status", status: "error", message: `Runtime error detected${msg.selectionId ? ` for selection ${msg.selectionId}` : ""}: ${msg.details}` }));
|
|
@@ -468,7 +476,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
468
476
|
}
|
|
469
477
|
else if (msg.type === "apply") {
|
|
470
478
|
try {
|
|
471
|
-
|
|
479
|
+
const snapshots = [];
|
|
472
480
|
const planned = new Map();
|
|
473
481
|
for (const change of msg.changes) {
|
|
474
482
|
const filePath = resolveProposedFile(cwd, change.filePath);
|
|
@@ -483,36 +491,43 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
|
|
|
483
491
|
}
|
|
484
492
|
// Validate every change before writing any file, then apply each
|
|
485
493
|
// file's replacements from the end toward the beginning.
|
|
494
|
+
taskSnapshots.set(msg.taskId, snapshots);
|
|
486
495
|
for (const [filePath, changes] of planned) {
|
|
487
496
|
const content = changes[0].content;
|
|
488
|
-
|
|
497
|
+
snapshots.push({ filePath, content });
|
|
489
498
|
const nextContent = [...changes]
|
|
490
499
|
.sort((a, b) => b.start - a.start)
|
|
491
500
|
.reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content);
|
|
492
501
|
node_fs_1.default.writeFileSync(filePath, nextContent);
|
|
493
502
|
}
|
|
494
|
-
socket.send(JSON.stringify({ type: "applied", message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.` }));
|
|
503
|
+
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.` }));
|
|
495
504
|
}
|
|
496
505
|
catch (error) {
|
|
497
|
-
|
|
506
|
+
// Validation happens before writes, but restore this task's snapshot
|
|
507
|
+
// if a filesystem error occurs during the write phase.
|
|
508
|
+
for (const snapshot of taskSnapshots.get(msg.taskId) || [])
|
|
498
509
|
node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
|
|
499
|
-
|
|
510
|
+
taskSnapshots.delete(msg.taskId);
|
|
500
511
|
const message = error instanceof Error ? error.message : "The change could not be applied.";
|
|
501
512
|
const sourceChanged = message.includes("The source changed after the suggestion was generated");
|
|
502
|
-
|
|
503
|
-
|
|
513
|
+
const editRequest = editRequests.get(msg.taskId);
|
|
514
|
+
const editConfig = editConfigs.get(msg.taskId);
|
|
515
|
+
const refreshAttempts = reviewRefreshAttempts.get(msg.taskId) || 0;
|
|
516
|
+
if (sourceChanged && editRequest && editConfig && refreshAttempts < 1) {
|
|
517
|
+
reviewRefreshAttempts.set(msg.taskId, refreshAttempts + 1);
|
|
518
|
+
runEditReview(editRequest, editConfig, "The source changed. Refreshing the review against the current file…");
|
|
504
519
|
}
|
|
505
520
|
else {
|
|
506
|
-
socket.send(JSON.stringify({ type: "agent_status", status: "error", message }));
|
|
521
|
+
socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message }));
|
|
507
522
|
}
|
|
508
523
|
}
|
|
509
524
|
}
|
|
510
525
|
else if (msg.type === "undo") {
|
|
511
|
-
|
|
526
|
+
const snapshots = taskSnapshots.get(msg.taskId || "") || [];
|
|
527
|
+
for (const snapshot of snapshots)
|
|
512
528
|
node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
socket.send(JSON.stringify({ type: "undone", message: count ? "The accepted change was reverted." : "There is no accepted change to undo." }));
|
|
529
|
+
taskSnapshots.delete(msg.taskId || "");
|
|
530
|
+
socket.send(JSON.stringify({ type: "undone", taskId: msg.taskId, message: snapshots.length ? "The accepted change was reverted." : "There is no accepted change to undo." }));
|
|
516
531
|
}
|
|
517
532
|
});
|
|
518
533
|
socket.on("close", () => {
|