@lasso-ai/cli 1.4.4 → 1.5.0

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.
@@ -0,0 +1,1350 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_BRIDGE_PORT = void 0;
7
+ exports.restartBridge = restartBridge;
8
+ exports.startBridge = startBridge;
9
+ // src/cli/bridge.ts
10
+ const ws_1 = require("ws");
11
+ const node_http_1 = __importDefault(require("node:http"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const node_child_process_1 = require("node:child_process");
15
+ const node_util_1 = require("node:util");
16
+ const chalk_1 = __importDefault(require("chalk"));
17
+ const agent_1 = require("./agent");
18
+ const project_1 = require("./project");
19
+ const auth_1 = require("./auth");
20
+ const snapshot_1 = require("./snapshot");
21
+ exports.DEFAULT_BRIDGE_PORT = 3056;
22
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
23
+ async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
24
+ try {
25
+ const response = await fetch(`http://127.0.0.1:${port}/__lasso/bridge/restart`, { method: "POST" });
26
+ const body = await response.json().catch(() => ({}));
27
+ if (!response.ok || !body.ok)
28
+ return { ok: false, error: body.error || "The bridge rejected the restart request." };
29
+ return { ok: true };
30
+ }
31
+ catch {
32
+ return { ok: false, error: `No Lasso bridge is listening on 127.0.0.1:${port}. Start your project with ${chalk_1.default.cyan("lasso dev")} first.` };
33
+ }
34
+ }
35
+ async function gitCommand(cwd, args) {
36
+ const result = await execFileAsync("git", args, { cwd, maxBuffer: 1024 * 1024 });
37
+ return result.stdout.trim();
38
+ }
39
+ async function getGitState(cwd) {
40
+ try {
41
+ await gitCommand(cwd, ["rev-parse", "--is-inside-work-tree"]);
42
+ }
43
+ catch {
44
+ return { isRepo: false };
45
+ }
46
+ const status = (await gitCommand(cwd, ["status", "--short"])).split("\n").filter(Boolean);
47
+ const branch = await gitCommand(cwd, ["branch", "--show-current"]).catch(() => "");
48
+ const remote = await gitCommand(cwd, ["remote", "get-url", "origin"]).catch(() => "");
49
+ return { isRepo: true, branch, status, hasChanges: status.length > 0, hasRemote: Boolean(remote), remote };
50
+ }
51
+ function fileLineEnding(content) {
52
+ return content.includes("\r\n") ? "\r\n" : "\n";
53
+ }
54
+ function withLineEnding(value, lineEnding) {
55
+ return value.replace(/\r\n?|\n/g, lineEnding);
56
+ }
57
+ function resolveProposedFile(cwd, proposedPath, allowNew = false) {
58
+ const root = node_path_1.default.resolve(cwd);
59
+ const candidate = node_path_1.default.resolve(root, proposedPath);
60
+ if (candidate.startsWith(`${root}${node_path_1.default.sep}`) && node_fs_1.default.existsSync(candidate))
61
+ return candidate;
62
+ // Some React/Next source maps contain an absolute path from an older
63
+ // checkout. Rebase only the recognizable src/... suffix into this project.
64
+ const normalized = proposedPath.replace(/\\/g, "/");
65
+ const srcIndex = normalized.lastIndexOf("/src/");
66
+ if (srcIndex >= 0) {
67
+ const rebased = node_path_1.default.join(root, normalized.slice(srcIndex + 1));
68
+ if (rebased.startsWith(`${root}${node_path_1.default.sep}`) && node_fs_1.default.existsSync(rebased))
69
+ return rebased;
70
+ if (allowNew && (rebased.startsWith(`${root}${node_path_1.default.sep}`) || rebased === root))
71
+ return rebased;
72
+ }
73
+ if (candidate.startsWith(`${root}${node_path_1.default.sep}`) || candidate === root) {
74
+ if (allowNew)
75
+ return candidate;
76
+ throw new Error(`Could not find the proposed file in the current project: ${proposedPath}`);
77
+ }
78
+ throw new Error(`The proposed file is outside the current project: ${proposedPath}`);
79
+ }
80
+ function occurrenceCount(content, needle) {
81
+ if (!needle)
82
+ return 0;
83
+ let count = 0;
84
+ let from = 0;
85
+ while (true) {
86
+ const index = content.indexOf(needle, from);
87
+ if (index < 0)
88
+ return count;
89
+ count += 1;
90
+ from = index + needle.length;
91
+ }
92
+ }
93
+ function escapeRegExp(value) {
94
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
95
+ }
96
+ function whitespaceEquivalentRange(content, oldString) {
97
+ const trimmed = oldString.trim();
98
+ if (!trimmed)
99
+ return null;
100
+ const pattern = trimmed.split(/\s+/).map(escapeRegExp).join("\\s+");
101
+ const matches = Array.from(content.matchAll(new RegExp(pattern, "g")));
102
+ if (matches.length !== 1 || matches[0].index === undefined)
103
+ return null;
104
+ const start = matches[0].index;
105
+ return { start, end: start + matches[0][0].length };
106
+ }
107
+ function prepareChange(content, change) {
108
+ const lineEnding = fileLineEnding(content);
109
+ const oldString = withLineEnding(change.oldString, lineEnding);
110
+ const newString = withLineEnding(change.newString, lineEnding);
111
+ const matches = occurrenceCount(content, oldString);
112
+ if (matches === 0) {
113
+ const range = whitespaceEquivalentRange(content, oldString);
114
+ if (range)
115
+ return { ...range, oldString: content.slice(range.start, range.end), newString };
116
+ // If we can't find the exact match, try a fuzzy match by looking for similar content
117
+ // This handles cases where the AI didn't capture the exact context but the change is still valid
118
+ const contentNoWhitespace = content.replace(/\s+/g, " ");
119
+ const oldStringNoWhitespace = oldString.replace(/\s+/g, " ");
120
+ if (contentNoWhitespace.includes(oldStringNoWhitespace) && oldStringNoWhitespace.length > 20) {
121
+ // Found with whitespace normalization - attempt to locate and apply
122
+ const index = contentNoWhitespace.indexOf(oldStringNoWhitespace);
123
+ console.warn(`[Lasso] Applying change with fuzzy match for ${change.filePath}`);
124
+ // This is a best-effort approach - might not be perfect but better than failing
125
+ throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated. Regenerate the review so it uses the current source.`);
126
+ }
127
+ throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated. Regenerate the review so it uses the current source.`);
128
+ }
129
+ if (matches > 1) {
130
+ throw new Error(`Could not safely apply ${change.filePath}. The selected code is not unique (${matches} matches).`);
131
+ }
132
+ const start = content.indexOf(oldString);
133
+ return { start, end: start + oldString.length, oldString, newString };
134
+ }
135
+ function proposalMatchesCurrentSource(cwd, changes) {
136
+ try {
137
+ for (const change of changes) {
138
+ if (!change.oldString)
139
+ continue;
140
+ const filePath = resolveProposedFile(cwd, change.filePath);
141
+ const content = node_fs_1.default.readFileSync(filePath, "utf8");
142
+ // Try exact match first
143
+ try {
144
+ prepareChange(content, change);
145
+ }
146
+ catch {
147
+ // If exact match fails, try with normalized whitespace
148
+ const normalizedContent = content.replace(/\s+/g, " ").trim();
149
+ const normalizedOldString = change.oldString.replace(/\s+/g, " ").trim();
150
+ const normalizedNewString = change.newString.replace(/\s+/g, " ").trim();
151
+ if (!normalizedContent.includes(normalizedOldString)) {
152
+ throw new Error("Source content does not match proposed change (even with normalized whitespace)");
153
+ }
154
+ }
155
+ }
156
+ return true;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
162
+ async function triggerBuildFix(cwd, taskId, buildErrors, buildCommand, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext) {
163
+ const MAX_RETRIES = 3;
164
+ const context = buildErrorContext.get(taskId);
165
+ if (!context)
166
+ return;
167
+ context.retryCount += 1;
168
+ if (context.retryCount > MAX_RETRIES) {
169
+ // Max retries reached - revert changes and show error
170
+ for (const snapshot of taskSnapshots.get(taskId) || []) {
171
+ if (snapshot.content === null) {
172
+ if (node_fs_1.default.existsSync(snapshot.filePath))
173
+ node_fs_1.default.unlinkSync(snapshot.filePath);
174
+ }
175
+ else {
176
+ node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
177
+ }
178
+ }
179
+ taskSnapshots.delete(taskId);
180
+ buildErrorContext.delete(taskId);
181
+ socket.send(JSON.stringify({
182
+ type: "agent_status",
183
+ taskId,
184
+ status: "error",
185
+ 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` : ""}`
186
+ }));
187
+ return;
188
+ }
189
+ // Re-apply the original changes
190
+ try {
191
+ const planned = new Map();
192
+ for (const change of originalChanges) {
193
+ const isNew = !change.oldString;
194
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
195
+ const exists = node_fs_1.default.existsSync(filePath);
196
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
197
+ const prepared = (!exists || isNew)
198
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
199
+ : prepareChange(content, change);
200
+ const fileChanges = planned.get(filePath) || [];
201
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
202
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
203
+ }
204
+ fileChanges.push({ filePath, content, exists, ...prepared });
205
+ planned.set(filePath, fileChanges);
206
+ }
207
+ for (const [filePath, changes] of planned) {
208
+ const exists = changes[0].exists;
209
+ const content = changes[0].content;
210
+ const nextContent = exists
211
+ ? [...changes]
212
+ .sort((a, b) => b.start - a.start)
213
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
214
+ : changes[0].newString;
215
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
216
+ node_fs_1.default.writeFileSync(filePath, nextContent);
217
+ }
218
+ }
219
+ catch (applyError) {
220
+ socket.send(JSON.stringify({
221
+ type: "agent_status",
222
+ taskId,
223
+ status: "error",
224
+ message: `Failed to re-apply original changes: ${applyError instanceof Error ? applyError.message : "Unknown error"}`
225
+ }));
226
+ return;
227
+ }
228
+ // Run agent to fix build errors
229
+ const { runOneShotAgent } = await import("./oneshot.js");
230
+ 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}.`;
231
+ const controller = new AbortController();
232
+ taskControllers.set(taskId, controller);
233
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: `Fixing build errors (attempt ${context.retryCount}/${MAX_RETRIES})...` }));
234
+ try {
235
+ 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) => {
236
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
237
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
238
+ }
239
+ }, (prompt) => {
240
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
241
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId, prompt }));
242
+ }
243
+ }, undefined, "https://api.lasso.byorello.space", resolvedLassoKey || agentConfig?.apiKey || collabConfig?.apiKey, taskId, undefined);
244
+ if (result.ok && result.changes && result.changes.length > 0) {
245
+ // Apply the fix
246
+ const snapshots = [];
247
+ const planned = new Map();
248
+ for (const change of result.changes) {
249
+ const isNew = !change.oldString;
250
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
251
+ const exists = node_fs_1.default.existsSync(filePath);
252
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
253
+ const prepared = (!exists || isNew)
254
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
255
+ : prepareChange(content, change);
256
+ const fileChanges = planned.get(filePath) || [];
257
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
258
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
259
+ }
260
+ fileChanges.push({ filePath, content, exists, ...prepared });
261
+ planned.set(filePath, fileChanges);
262
+ }
263
+ taskSnapshots.set(taskId, snapshots);
264
+ for (const [filePath, changes] of planned) {
265
+ const exists = changes[0].exists;
266
+ const content = changes[0].content;
267
+ snapshots.push({ filePath, content: exists ? content : null });
268
+ const nextContent = exists
269
+ ? [...changes]
270
+ .sort((a, b) => b.start - a.start)
271
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
272
+ : changes[0].newString;
273
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
274
+ node_fs_1.default.writeFileSync(filePath, nextContent);
275
+ }
276
+ // Validate the fix
277
+ const validation = await validateBuild(cwd);
278
+ if (validation.passed) {
279
+ // Build fixed!
280
+ buildErrorContext.delete(taskId);
281
+ socket.send(JSON.stringify({
282
+ type: "applied",
283
+ taskId,
284
+ message: `Build errors fixed after ${context.retryCount} attempt${context.retryCount > 1 ? "s" : ""}. ${result.changes.length} file${result.changes.length === 1 ? "" : "s"} updated.`,
285
+ validation
286
+ }));
287
+ }
288
+ else {
289
+ // Still failing - trigger another retry
290
+ socket.send(JSON.stringify({
291
+ type: "agent_status",
292
+ taskId,
293
+ status: "working",
294
+ message: `⚠️ Build still failing. Retrying...`,
295
+ 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` : ""}`
296
+ }));
297
+ // Update context with new errors
298
+ buildErrorContext.set(taskId, {
299
+ errors: validation.errors,
300
+ command: validation.command,
301
+ originalChanges: originalChanges,
302
+ retryCount: context.retryCount
303
+ });
304
+ await triggerBuildFix(cwd, taskId, validation.errors, validation.command, originalChanges, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
305
+ }
306
+ }
307
+ else {
308
+ // Agent failed to produce a fix
309
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: result.error || "Failed to fix build errors" }));
310
+ }
311
+ }
312
+ catch (error) {
313
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "Build fix encountered an error" }));
314
+ }
315
+ finally {
316
+ if (taskControllers.get(taskId) === controller)
317
+ taskControllers.delete(taskId);
318
+ }
319
+ }
320
+ async function validateBuild(cwd) {
321
+ const pkgPath = node_path_1.default.join(cwd, "package.json");
322
+ if (!node_fs_1.default.existsSync(pkgPath)) {
323
+ return { passed: true, errors: [], command: "none" };
324
+ }
325
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
326
+ const scripts = pkg.scripts || {};
327
+ // Determine which build command to run
328
+ let buildCommand = "";
329
+ if (scripts.build) {
330
+ buildCommand = "build";
331
+ }
332
+ else if (scripts["build:cli"] || scripts["build:overlay"]) {
333
+ // Monorepo with separate build scripts
334
+ buildCommand = scripts["build:cli"] ? "build:cli" : "build:overlay";
335
+ }
336
+ else if (scripts.compile) {
337
+ buildCommand = "compile";
338
+ }
339
+ else if (scripts.check) {
340
+ buildCommand = "check";
341
+ }
342
+ else if (scripts.lint) {
343
+ buildCommand = "lint";
344
+ }
345
+ else if (scripts.typecheck) {
346
+ buildCommand = "typecheck";
347
+ }
348
+ else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"))) {
349
+ buildCommand = "tsc"; // Run TypeScript compiler directly
350
+ }
351
+ else {
352
+ // No suitable build command found
353
+ return { passed: true, errors: [], command: "none" };
354
+ }
355
+ try {
356
+ const result = await execFileAsync(buildCommand === "tsc" ? "tsc" : "npm", buildCommand === "tsc" ? [] : ["run", buildCommand], { cwd, maxBuffer: 1024 * 1024 * 10, timeout: 60000 });
357
+ const output = result.stdout + result.stderr;
358
+ const errors = [];
359
+ // Parse TypeScript errors
360
+ if (output.includes("error TS")) {
361
+ const tsErrors = output.match(/error TS\d+.*$/gm) || [];
362
+ errors.push(...tsErrors);
363
+ }
364
+ // Parse general build errors
365
+ if (output.includes("Error:") || output.includes("error:")) {
366
+ const lines = output.split("\n");
367
+ for (const line of lines) {
368
+ if (line.includes("Error:") || line.includes("error:")) {
369
+ errors.push(line.trim());
370
+ }
371
+ }
372
+ }
373
+ return {
374
+ passed: errors.length === 0,
375
+ errors,
376
+ command: buildCommand === "tsc" ? "tsc" : `npm run ${buildCommand}`
377
+ };
378
+ }
379
+ catch (error) {
380
+ const stderr = error.stderr || "";
381
+ const errors = [];
382
+ if (stderr.includes("error TS")) {
383
+ const tsErrors = stderr.match(/error TS\d+.*$/gm) || [];
384
+ errors.push(...tsErrors);
385
+ }
386
+ if (stderr.includes("Error:") || stderr.includes("error:")) {
387
+ const lines = stderr.split("\n");
388
+ for (const line of lines) {
389
+ if (line.includes("Error:") || line.includes("error:")) {
390
+ errors.push(line.trim());
391
+ }
392
+ }
393
+ }
394
+ // If no specific errors found, include the full stderr
395
+ if (errors.length === 0 && stderr) {
396
+ errors.push(stderr);
397
+ }
398
+ return {
399
+ passed: false,
400
+ errors,
401
+ command: buildCommand === "tsc" ? "tsc" : `npm run ${buildCommand}`
402
+ };
403
+ }
404
+ }
405
+ function readEnvFile(cwd, filename) {
406
+ try {
407
+ return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
408
+ const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
409
+ if (match) {
410
+ let value = match[2].trim();
411
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
412
+ value = value.slice(1, -1);
413
+ values[match[1]] = value.trim();
414
+ }
415
+ return values;
416
+ }, {});
417
+ }
418
+ catch {
419
+ return {};
420
+ }
421
+ }
422
+ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = exports.DEFAULT_BRIDGE_PORT) {
423
+ const bridgeServer = node_http_1.default.createServer(); // dedicated, empty HTTP server
424
+ const wss = new ws_1.WebSocketServer({ server: bridgeServer });
425
+ let overlaySocket = null;
426
+ const taskSnapshots = new Map();
427
+ const editRequests = new Map();
428
+ const editConfigs = new Map();
429
+ const reviewRefreshAttempts = new Map();
430
+ const buildErrorContext = new Map();
431
+ const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
432
+ const fileEnv = envRoots.reduce((values, root) => ({
433
+ ...values,
434
+ ...readEnvFile(root, ".env"),
435
+ ...readEnvFile(root, ".env.local"),
436
+ ...readEnvFile(root, ".env.production"),
437
+ }), {});
438
+ // Prioritize CLI auth credentials over env vars
439
+ const cliCredentials = (0, auth_1.loadCredentials)();
440
+ const resolvedLassoKey = cliCredentials?.apiKey ||
441
+ (0, project_1.resolveLassoApiKey)(fileEnv) ||
442
+ process.env.LASSO_API_KEY ||
443
+ fileEnv.LASSO_API_KEY ||
444
+ process.env.VITE_LASSO_API_KEY ||
445
+ process.env.NEXT_LASSO_API_KEY ||
446
+ fileEnv.VITE_LASSO_API_KEY ||
447
+ fileEnv.NEXT_LASSO_API_KEY ||
448
+ "";
449
+ const lassoKeyConfigured = Boolean(resolvedLassoKey);
450
+ // Task queue for concurrent execution
451
+ const MAX_CONCURRENT_TASKS = 3;
452
+ const taskQueue = [];
453
+ const runningTasks = new Set();
454
+ const taskPriority = new Map(); // Lower number = higher priority
455
+ // Will be set when WebSocket connects
456
+ let currentSocket = null;
457
+ function processTaskQueue() {
458
+ if (runningTasks.size >= MAX_CONCURRENT_TASKS || taskQueue.length === 0 || !currentSocket)
459
+ return;
460
+ const socket = currentSocket; // Capture non-null reference
461
+ taskQueue.sort((a, b) => {
462
+ const priorityA = taskPriority.get(a.taskId) ?? 999;
463
+ const priorityB = taskPriority.get(b.taskId) ?? 999;
464
+ return priorityA - priorityB;
465
+ });
466
+ const task = taskQueue.shift();
467
+ if (!task)
468
+ return;
469
+ runningTasks.add(task.taskId);
470
+ if (task.message.type === "edit") {
471
+ const request = task.message;
472
+ taskControllers.get(request.taskId)?.abort();
473
+ const controller = new AbortController();
474
+ taskControllers.set(request.taskId, controller);
475
+ if (socket.readyState === socket.OPEN) {
476
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: "Working…" }));
477
+ }
478
+ void (0, agent_1.proposeChanges)(cwd, request, task.config, controller.signal, (message, detail, level) => {
479
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
480
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: level === "error" ? "error" : "working", message, detail }));
481
+ }
482
+ }, (prompt) => {
483
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
484
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId: request.taskId, prompt }));
485
+ }
486
+ })
487
+ .then((proposal) => {
488
+ if (controller.signal.aborted || socket.readyState !== socket.OPEN)
489
+ return;
490
+ if (!proposalMatchesCurrentSource(cwd, proposal.changes)) {
491
+ const refreshAttempts = reviewRefreshAttempts.get(request.taskId) || 0;
492
+ const MAX_REFRESH_ATTEMPTS = 3;
493
+ if (refreshAttempts < MAX_REFRESH_ATTEMPTS) {
494
+ reviewRefreshAttempts.set(request.taskId, refreshAttempts + 1);
495
+ setTimeout(() => {
496
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
497
+ enqueueTask(request.taskId, request, task.config, 0);
498
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "working", message: `The source changed while the proposal was being prepared. Refreshing the review… (${refreshAttempts + 1}/${MAX_REFRESH_ATTEMPTS})` }));
499
+ }
500
+ }, 500 * (refreshAttempts + 1));
501
+ }
502
+ else {
503
+ 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." }));
504
+ }
505
+ return;
506
+ }
507
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "review", message: proposal.summary, changes: proposal.changes }));
508
+ })
509
+ .catch((error) => {
510
+ if (socket.readyState !== socket.OPEN)
511
+ return;
512
+ if (controller.signal.aborted) {
513
+ socket.send(JSON.stringify({ type: "agent_status", taskId: request.taskId, status: "stopped", message: "Agent stopped." }));
514
+ return;
515
+ }
516
+ 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." }));
517
+ })
518
+ .finally(() => {
519
+ if (taskControllers.get(request.taskId) === controller)
520
+ taskControllers.delete(request.taskId);
521
+ runningTasks.delete(request.taskId);
522
+ taskPriority.delete(request.taskId);
523
+ processTaskQueue();
524
+ });
525
+ }
526
+ else if (task.message.type === "ask") {
527
+ const controller = new AbortController();
528
+ taskControllers.set(task.taskId, controller);
529
+ socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: "working", message: "Working…" }));
530
+ void (0, agent_1.answerQuestion)(cwd, {
531
+ taskId: task.taskId,
532
+ question: task.message.question,
533
+ context: task.message.context,
534
+ element: task.message.element,
535
+ messages: task.message.messages,
536
+ }, task.config, controller.signal, (message, detail, level) => {
537
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
538
+ socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: level === "error" ? "error" : "working", message, detail }));
539
+ }
540
+ }, (prompt) => {
541
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
542
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId: task.taskId, prompt }));
543
+ }
544
+ }).then((answer) => {
545
+ if (!controller.signal.aborted)
546
+ socket.send(JSON.stringify({ type: "assistant_message", taskId: task.taskId, message: answer }));
547
+ }).catch((error) => {
548
+ if (!controller.signal.aborted)
549
+ socket.send(JSON.stringify({ type: "agent_status", taskId: task.taskId, status: "error", message: error instanceof Error ? error.message : "The agent could not answer." }));
550
+ }).finally(() => {
551
+ if (taskControllers.get(task.taskId) === controller)
552
+ taskControllers.delete(task.taskId);
553
+ runningTasks.delete(task.taskId);
554
+ taskPriority.delete(task.taskId);
555
+ processTaskQueue();
556
+ });
557
+ }
558
+ }
559
+ function enqueueTask(taskId, message, config, priority = 0) {
560
+ taskPriority.set(taskId, priority);
561
+ taskQueue.push({ taskId, message, config });
562
+ processTaskQueue();
563
+ }
564
+ // Fetch workspace configuration from server (dashboard-configured providers)
565
+ let workspaceConfig = null;
566
+ if (lassoKeyConfigured) {
567
+ workspaceConfig = await (0, project_1.resolveWorkspaceConfig)(fileEnv);
568
+ }
569
+ // Fetch available models from server
570
+ let dynamicModels = { anthropic: [], openai: [], google: [], nvidia: [] };
571
+ try {
572
+ dynamicModels = await (0, project_1.fetchModels)(fileEnv);
573
+ }
574
+ catch (error) {
575
+ console.warn('[Lasso] Failed to fetch dynamic models, using fallback');
576
+ }
577
+ const localProviderKeys = {
578
+ google: process.env.GOOGLE_GENERATIVE_AI_API_KEY || fileEnv.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY || fileEnv.GEMINI_API_KEY,
579
+ openai: process.env.OPENAI_API_KEY || fileEnv.OPENAI_API_KEY,
580
+ anthropic: process.env.ANTHROPIC_API_KEY || fileEnv.ANTHROPIC_API_KEY,
581
+ nvidia: process.env.NVIDIA_API_KEY || fileEnv.NVIDIA_API_KEY,
582
+ ollama: process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL ? "ollama" : undefined,
583
+ };
584
+ let agentConfig = (() => {
585
+ const provider = (process.env.LASSO_AGENT_PROVIDER || fileEnv.LASSO_AGENT_PROVIDER || process.env.AI_PROVIDER || fileEnv.AI_PROVIDER || "").toLowerCase();
586
+ const selectedProvider = provider === "google" || provider === "gemini" ? "google" : provider === "openai" ? "openai" : provider === "ollama" ? "ollama" : provider === "anthropic" ? "anthropic" : provider === "nvidia" ? "nvidia" : localProviderKeys.ollama ? "ollama" : localProviderKeys.nvidia ? "nvidia" : localProviderKeys.google ? "google" : localProviderKeys.openai ? "openai" : localProviderKeys.anthropic ? "anthropic" : (cliCredentials ? "anthropic" : "google");
587
+ const apiKey = selectedProvider === "ollama" ? "ollama" : localProviderKeys[selectedProvider];
588
+ // If user has CLI auth, use Lasso gateway by default
589
+ if (!apiKey && !lassoKeyConfigured)
590
+ return null;
591
+ return {
592
+ provider: selectedProvider,
593
+ apiKey: apiKey || resolvedLassoKey,
594
+ model: process.env.LASSO_AGENT_MODEL || fileEnv.LASSO_AGENT_MODEL || process.env.AI_MODEL || fileEnv.AI_MODEL,
595
+ baseUrl: selectedProvider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL ? `${(process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api").replace(/\/api\/?$/, "")}/v1` : undefined,
596
+ };
597
+ })();
598
+ let activeAgentController = null;
599
+ const taskControllers = new Map();
600
+ let localAgents = new Set();
601
+ const pageIgnoredDirs = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "out", "coverage", ".vercel", ".cache"]);
602
+ const resolveInsideProject = (root, relative) => {
603
+ const target = node_path_1.default.resolve(root, relative && relative !== "." ? relative : "");
604
+ return target === root || target.startsWith(`${root}${node_path_1.default.sep}`) ? target : null;
605
+ };
606
+ // Directories are returned first so the overlay can render an explorer-style
607
+ // tree that mirrors how VS Code orders folders above files.
608
+ const pageEntries = (root, relative) => {
609
+ const directory = resolveInsideProject(root, relative);
610
+ if (!directory)
611
+ return [];
612
+ let entries = [];
613
+ try {
614
+ for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true })) {
615
+ if (entry.name.startsWith(".") || (entry.isDirectory() && pageIgnoredDirs.has(entry.name)))
616
+ continue;
617
+ entries.push({
618
+ name: entry.name,
619
+ path: relative && relative !== "." ? node_path_1.default.join(relative, entry.name) : entry.name,
620
+ type: entry.isDirectory() ? "directory" : "file",
621
+ });
622
+ }
623
+ }
624
+ catch {
625
+ // Folders that cannot be read are treated as empty.
626
+ }
627
+ return entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
628
+ };
629
+ bridgeServer.on("request", (req, res) => {
630
+ if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
631
+ return;
632
+ activeAgentController?.abort();
633
+ for (const controller of taskControllers.values())
634
+ controller.abort();
635
+ taskControllers.clear();
636
+ activeAgentController = null;
637
+ for (const socket of wss.clients)
638
+ socket.close(1000, "Bridge restarted by the CLI");
639
+ res.writeHead(200, { "content-type": "application/json" });
640
+ res.end(JSON.stringify({ ok: true }));
641
+ });
642
+ async function localModels() {
643
+ const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
644
+ try {
645
+ const response = await fetch(`${base.replace(/\/$/, "")}/tags`);
646
+ if (!response.ok)
647
+ return [];
648
+ const payload = await response.json();
649
+ return (payload.models || []).filter((model) => model.name).map((model) => ({ id: model.name, label: `${model.name} · Local`, provider: "ollama" }));
650
+ }
651
+ catch {
652
+ return [];
653
+ }
654
+ }
655
+ async function openCodeModels() {
656
+ if (!localAgents.has("opencode"))
657
+ return [];
658
+ try {
659
+ const result = await execFileAsync("opencode", ["models"], { maxBuffer: 1024 * 1024 });
660
+ return result.stdout
661
+ .split(/\r?\n/)
662
+ .map((line) => line.trim())
663
+ .filter((line) => /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.:@/-]+$/.test(line))
664
+ .map((model) => ({ id: `opencode:${model}`, label: `OpenCode · ${model}`, provider: "cli" }));
665
+ }
666
+ catch {
667
+ return [];
668
+ }
669
+ }
670
+ async function cursorModels() {
671
+ if (!localAgents.has("cursor"))
672
+ return [];
673
+ try {
674
+ const result = await execFileAsync("cursor", ["model", "list"], { maxBuffer: 1024 * 1024 });
675
+ return result.stdout
676
+ .split(/\r?\n/)
677
+ .map((line) => line.trim())
678
+ .filter((line) => line && !line.startsWith("Available models"))
679
+ .map((line) => {
680
+ const parts = line.split(/\s+/);
681
+ const id = parts[0]?.trim();
682
+ const name = parts.slice(1).join(" ").trim();
683
+ return id ? { id: `cursor:${id}`, label: `Cursor · ${name}`, provider: "cli" } : null;
684
+ })
685
+ .filter((model) => model !== null);
686
+ }
687
+ catch {
688
+ return [];
689
+ }
690
+ }
691
+ const availableModels = async (collabConfig = null, workspaceConfig = null) => {
692
+ localAgents = await (0, agent_1.detectLocalAgents)();
693
+ const locals = await localModels();
694
+ const discoveredOpenCodeModels = await openCodeModels();
695
+ const discoveredCursorModels = await cursorModels();
696
+ if (locals.length && !agentConfig) {
697
+ const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
698
+ agentConfig = { provider: "ollama", apiKey: "ollama", model: process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL, baseUrl: `${base.replace(/\/api\/?$/, "")}/v1` };
699
+ }
700
+ // Check if user has CLI auth credentials (lasso auth login)
701
+ const cliCredentials = (0, auth_1.loadCredentials)();
702
+ const hasCliAuth = Boolean(cliCredentials?.apiKey);
703
+ // Prioritize workspace config (dashboard) over project config
704
+ // Workspace config is fetched independently of project config
705
+ const workspacePlan = (workspaceConfig?.plan || "free").toLowerCase();
706
+ const workspaceProviders = workspaceConfig?.configuredProviders || [];
707
+ const projectPlan = (collabConfig?.plan || "free").toLowerCase();
708
+ const projectProviders = collabConfig?.configuredProviders || [];
709
+ // Use the highest-tier plan available
710
+ const isPaidPlan = workspacePlan !== "free" || projectPlan !== "free";
711
+ // Use workspace providers if available, otherwise fall back to project providers
712
+ const configuredProviders = workspaceProviders.length > 0 ? workspaceProviders : projectProviders;
713
+ // Build model list from dynamic server fetch, falling back to hardcoded models
714
+ const allModels = [
715
+ // Dynamic models from server API
716
+ ...dynamicModels.anthropic.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
717
+ ...dynamicModels.openai.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
718
+ ...dynamicModels.google.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
719
+ ...dynamicModels.nvidia.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
720
+ // Fallback hardcoded models (only if dynamic fetch failed)
721
+ ...(dynamicModels.anthropic.length === 0 ? [
722
+ { id: "claude-opus-4-5-20251101", label: "Claude Opus 4.5", provider: "anthropic" },
723
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
724
+ { id: "claude-haiku-4-5-20251015", label: "Claude Haiku 4.5", provider: "anthropic" },
725
+ ] : []),
726
+ ...(dynamicModels.openai.length === 0 ? [
727
+ { id: "gpt-5.1", label: "GPT-5.1", provider: "openai" },
728
+ { id: "gpt-5.1-codex", label: "GPT-5.1 Codex", provider: "openai" },
729
+ { id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini", provider: "openai" },
730
+ { id: "o3", label: "OpenAI o3", provider: "openai" },
731
+ { id: "o3-mini", label: "OpenAI o3-mini", provider: "openai" },
732
+ ] : []),
733
+ ...(dynamicModels.google.length === 0 ? [
734
+ { id: "gemini-3-pro-preview", label: "Gemini 3 Pro", provider: "google" },
735
+ { id: "gemini-3-flash-preview", label: "Gemini 3 Flash", provider: "google" },
736
+ { id: "gemini-2.5-flash-preview-09-2025", label: "Gemini 2.5 Flash", provider: "google" },
737
+ { id: "gemini-2.5-flash-lite-preview-09-2025", label: "Gemini 2.5 Flash Lite", provider: "google" },
738
+ ] : []),
739
+ ...(dynamicModels.nvidia.length === 0 ? [
740
+ { id: "nvidia/llama-3.3-nemotron-super-49b-v1", label: "Llama 3.3 Nemotron Super 49B", provider: "nvidia" },
741
+ { id: "deepseek-ai/deepseek-r1-distill-llama-70b", label: "DeepSeek R1 Distill Llama 70B", provider: "nvidia" },
742
+ { id: "deepseek-ai/deepseek-r1-distill-llama-8b", label: "DeepSeek R1 Distill Llama 8B", provider: "nvidia" },
743
+ { id: "google/gemma-3-1b-it", label: "Gemma 3 1B Instruct", provider: "nvidia" },
744
+ { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B", provider: "nvidia" },
745
+ ] : []),
746
+ // Local CLI agents (always included)
747
+ ...(localAgents.has("claude-code") ? [
748
+ { id: "claude-code:sonnet", label: "Claude Code · Sonnet 4.5", provider: "cli" },
749
+ { id: "claude-code:opus", label: "Claude Code · Opus 4.5", provider: "cli" },
750
+ { id: "claude-code:haiku", label: "Claude Code · Haiku 4.5", provider: "cli" },
751
+ ] : []),
752
+ ...(localAgents.has("codex") ? [
753
+ { id: "codex:gpt-5.1", label: "Codex · GPT-5.1", provider: "cli" },
754
+ { id: "codex:gpt-5.1-codex", label: "Codex · GPT-5.1-Codex", provider: "cli" },
755
+ ] : []),
756
+ ...(localAgents.has("opencode") ? [
757
+ { id: "opencode:opencode/big-pickle", label: "OpenCode · Big Pickle", provider: "cli" },
758
+ { id: "opencode:deepseek/deepseek-chat", label: "OpenCode · DeepSeek Chat", provider: "cli" },
759
+ { id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet 4.5", provider: "cli" },
760
+ { id: "opencode:openai/gpt-5.1", label: "OpenCode · GPT-5.1", provider: "cli" },
761
+ ] : []),
762
+ ...(localAgents.has("cursor") ? [
763
+ { id: "cursor:claude-sonnet-4.5", label: "Cursor · Claude Sonnet 4.5", provider: "cli" },
764
+ { id: "cursor:claude-opus-4", label: "Cursor · Claude Opus 4", provider: "cli" },
765
+ { id: "cursor:claude-3.5-sonnet", label: "Cursor · Claude 3.5 Sonnet", provider: "cli" },
766
+ { id: "cursor:gpt-4o", label: "Cursor · GPT-4o", provider: "cli" },
767
+ { id: "cursor:gpt-4o-mini", label: "Cursor · GPT-4o mini", provider: "cli" },
768
+ ] : []),
769
+ ...discoveredOpenCodeModels,
770
+ ...discoveredCursorModels,
771
+ ...locals,
772
+ ];
773
+ // Lock models based on provider API key configuration
774
+ // Paid plans have all models unlocked
775
+ // Free plans only have models unlocked for providers with configured API keys
776
+ const providerKeyMapping = {
777
+ anthropic: "ANTHROPIC_API_KEY",
778
+ openai: "OPENAI_API_KEY",
779
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
780
+ nvidia: "NVIDIA_API_KEY",
781
+ ollama: "OLLAMA_BASE_URL",
782
+ };
783
+ return allModels.map((model) => {
784
+ if (model.provider === "cli") {
785
+ // CLI agents are always unlocked (user manages their own credentials)
786
+ return model;
787
+ }
788
+ if (isPaidPlan) {
789
+ // Paid plans have all hosted models unlocked
790
+ return model;
791
+ }
792
+ // Free plans: check if provider is configured
793
+ let isConfigured = false;
794
+ let lockedReason = "Configure your API key in the dashboard to use this model";
795
+ if (hasCliAuth) {
796
+ // User has CLI auth: check dashboard-configured providers first
797
+ isConfigured = configuredProviders.includes(model.provider);
798
+ lockedReason = "Configure this provider in your workspace dashboard to use this model";
799
+ }
800
+ // If not configured via dashboard, fall back to env-based detection
801
+ if (!isConfigured) {
802
+ const envKey = providerKeyMapping[model.provider];
803
+ isConfigured = Boolean(process.env[envKey]) ||
804
+ Boolean(fileEnv[envKey]) ||
805
+ Boolean(localProviderKeys[model.provider]);
806
+ if (!isConfigured) {
807
+ lockedReason = hasCliAuth
808
+ ? "Configure this provider in your workspace dashboard, or set the provider API key in your .env file"
809
+ : "Run `lasso auth login` to configure providers in your dashboard, or set the provider API key in your .env file";
810
+ }
811
+ else {
812
+ lockedReason = "";
813
+ }
814
+ }
815
+ if (!isConfigured) {
816
+ return {
817
+ ...model,
818
+ locked: true,
819
+ lockedReason: lockedReason || "Configure your API key to use this model",
820
+ };
821
+ }
822
+ return model;
823
+ });
824
+ };
825
+ wss.on("connection", (socket) => {
826
+ overlaySocket = socket;
827
+ currentSocket = socket;
828
+ console.log(chalk_1.default.green("✓") + " Overlay connected");
829
+ const creds = (0, auth_1.loadCredentials)();
830
+ const userInfo = creds?.userName
831
+ ? { name: creds.userName, email: creds.userEmail }
832
+ : (process.env.USER ? { name: process.env.USER } : null);
833
+ socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: lassoKeyConfigured || Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null, user: userInfo }));
834
+ void availableModels(collabConfig, workspaceConfig).then((models) => {
835
+ if (socket.readyState === socket.OPEN)
836
+ socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: lassoKeyConfigured || Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null, user: userInfo }));
837
+ });
838
+ void getGitState(cwd).then((git) => {
839
+ if (socket.readyState === socket.OPEN)
840
+ socket.send(JSON.stringify({ type: "git_state", git }));
841
+ });
842
+ socket.on("message", async (raw) => {
843
+ const msg = JSON.parse(raw.toString());
844
+ if (msg.type === "edit" || msg.type === "ask") {
845
+ console.log(`[lasso] received ${msg.type}:`, { model: msg.model, provider: msg.provider, selectionId: msg.context?.selectionId, messages: msg.messages?.length || 0, changes: msg.type === "edit" ? msg.changesHistory?.length || 0 : 0, screenshots: Boolean(msg.context?.screenshots?.full || msg.context?.screenshots?.element) });
846
+ }
847
+ else {
848
+ console.log("[lasso] received from overlay:", msg);
849
+ }
850
+ if (msg.type === "list_page_folders") {
851
+ const requested = msg.path || ".";
852
+ const root = node_path_1.default.resolve(cwd);
853
+ const directory = resolveInsideProject(root, requested);
854
+ if (!directory) {
855
+ socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: [], error: "That folder is outside the current project." }));
856
+ return;
857
+ }
858
+ socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: pageEntries(root, requested), project: node_path_1.default.basename(root) }));
859
+ return;
860
+ }
861
+ if (msg.type === "create_page_folder") {
862
+ const root = node_path_1.default.resolve(cwd);
863
+ const name = node_path_1.default.basename(msg.name || "");
864
+ const parent = resolveInsideProject(root, msg.parent || ".");
865
+ if (!parent || !name || name !== msg.name || name === "." || name === "..") {
866
+ socket.send(JSON.stringify({ type: "page_folder_created", path: msg.name || "", error: "Enter a folder name without slashes or dots." }));
867
+ return;
868
+ }
869
+ const target = node_path_1.default.resolve(parent, name);
870
+ if (!target.startsWith(`${root}${node_path_1.default.sep}`)) {
871
+ socket.send(JSON.stringify({ type: "page_folder_created", path: name, error: "Choose a folder inside the current project." }));
872
+ return;
873
+ }
874
+ try {
875
+ node_fs_1.default.mkdirSync(target);
876
+ socket.send(JSON.stringify({ type: "page_folder_created", path: node_path_1.default.relative(root, target) }));
877
+ }
878
+ catch (error) {
879
+ socket.send(JSON.stringify({
880
+ type: "page_folder_created",
881
+ path: node_path_1.default.relative(root, target),
882
+ error: error instanceof Error && error.message.includes("EEXIST") ? "A folder with that name already exists." : "The folder could not be created.",
883
+ }));
884
+ }
885
+ return;
886
+ }
887
+ if (msg.type === "create_page") {
888
+ const root = node_path_1.default.resolve(cwd);
889
+ const folder = resolveInsideProject(root, msg.folder || ".");
890
+ // A page name may contain "/" so the overlay can create nested routes in
891
+ // one step, but every segment is validated and resolved inside the project.
892
+ const segments = (msg.fileName || "").split("/").map((segment) => segment.trim()).filter(Boolean);
893
+ const fileName = segments.pop() || "";
894
+ if (!folder || !fileName || segments.some((segment) => segment === "." || segment === "..") || fileName === "." || fileName === "..") {
895
+ socket.send(JSON.stringify({ type: "page_created", path: msg.fileName || "", error: "Choose a valid project folder and file name." }));
896
+ return;
897
+ }
898
+ const target = node_path_1.default.resolve(folder, ...segments, fileName);
899
+ if (!target.startsWith(`${root}${node_path_1.default.sep}`)) {
900
+ socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "Choose a valid project folder and file name." }));
901
+ return;
902
+ }
903
+ try {
904
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(target), { recursive: true });
905
+ node_fs_1.default.writeFileSync(target, msg.content.slice(0, 200000), { encoding: "utf8", flag: "wx" });
906
+ socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root, target) }));
907
+ }
908
+ catch (error) {
909
+ socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root, target), error: error instanceof Error && error.message.includes("EEXIST") ? "A file with that name already exists." : "The page could not be created." }));
910
+ }
911
+ return;
912
+ }
913
+ if (msg.type === "ask") {
914
+ const localProvider = msg.provider === "cli";
915
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
916
+ if (!lassoKeyConfigured && !localProvider) {
917
+ 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." }));
918
+ return;
919
+ }
920
+ if (!agentConfig && !localProvider) {
921
+ 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." }));
922
+ return;
923
+ }
924
+ taskControllers.get(msg.taskId)?.abort();
925
+ const controller = new AbortController();
926
+ taskControllers.set(msg.taskId, controller);
927
+ let selectedConfig;
928
+ if (localProvider) {
929
+ selectedConfig = { provider: cliProvider, model: msg.model, lassoKey: resolvedLassoKey, serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv) };
930
+ }
931
+ else {
932
+ const requestedProvider = (msg.provider || agentConfig.provider);
933
+ const providerLocalKey = localProviderKeys[requestedProvider];
934
+ const apiKey = providerLocalKey || resolvedLassoKey;
935
+ const baseUrl = requestedProvider === "nvidia"
936
+ ? "https://integrate.api.nvidia.com/v1"
937
+ : requestedProvider === "ollama"
938
+ ? (agentConfig.baseUrl || "http://localhost:11434/v1")
939
+ : agentConfig.baseUrl;
940
+ selectedConfig = {
941
+ ...agentConfig,
942
+ provider: requestedProvider,
943
+ model: msg.model,
944
+ apiKey,
945
+ baseUrl,
946
+ lassoKey: resolvedLassoKey,
947
+ serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv),
948
+ };
949
+ }
950
+ // Enqueue task for concurrent execution
951
+ enqueueTask(msg.taskId, msg, selectedConfig, 1); // Priority 1 for ask tasks
952
+ }
953
+ else if (msg.type === "edit") {
954
+ const localProvider = msg.provider === "cli";
955
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
956
+ if (!lassoKeyConfigured && !localProvider) {
957
+ 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." }));
958
+ return;
959
+ }
960
+ if (!agentConfig && !localProvider) {
961
+ 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." }));
962
+ return;
963
+ }
964
+ let selectedConfig;
965
+ if (localProvider) {
966
+ selectedConfig = { provider: cliProvider, model: msg.model, lassoKey: resolvedLassoKey, serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv) };
967
+ }
968
+ else {
969
+ const requestedProvider = (msg.provider || agentConfig.provider);
970
+ const providerLocalKey = localProviderKeys[requestedProvider];
971
+ const apiKey = providerLocalKey || resolvedLassoKey;
972
+ const baseUrl = requestedProvider === "nvidia"
973
+ ? "https://integrate.api.nvidia.com/v1"
974
+ : requestedProvider === "ollama"
975
+ ? (agentConfig.baseUrl || "http://localhost:11434/v1")
976
+ : agentConfig.baseUrl;
977
+ selectedConfig = {
978
+ ...agentConfig,
979
+ provider: requestedProvider,
980
+ model: msg.model,
981
+ apiKey,
982
+ baseUrl,
983
+ lassoKey: resolvedLassoKey,
984
+ serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv),
985
+ };
986
+ }
987
+ editRequests.set(msg.taskId, msg);
988
+ editConfigs.set(msg.taskId, selectedConfig);
989
+ reviewRefreshAttempts.set(msg.taskId, 0);
990
+ // Enqueue task for concurrent execution with higher priority (0)
991
+ enqueueTask(msg.taskId, msg, selectedConfig, 0);
992
+ }
993
+ else if (msg.type === "agent_prompt_response") {
994
+ (0, agent_1.respondToAgentPrompt)(msg.taskId, msg.response);
995
+ }
996
+ else if (msg.type === "stop") {
997
+ if (msg.taskId) {
998
+ taskControllers.get(msg.taskId)?.abort();
999
+ // Remove from queue and running tasks
1000
+ const queueIndex = taskQueue.findIndex(t => t.taskId === msg.taskId);
1001
+ if (queueIndex !== -1)
1002
+ taskQueue.splice(queueIndex, 1);
1003
+ runningTasks.delete(msg.taskId);
1004
+ taskPriority.delete(msg.taskId);
1005
+ }
1006
+ else {
1007
+ for (const controller of taskControllers.values())
1008
+ controller.abort();
1009
+ taskQueue.length = 0;
1010
+ runningTasks.clear();
1011
+ taskPriority.clear();
1012
+ }
1013
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "stopped", message: "Agent stopped." }));
1014
+ }
1015
+ else if (msg.type === "runtime_error") {
1016
+ socket.send(JSON.stringify({ type: "agent_status", status: "error", message: `Runtime error detected${msg.selectionId ? ` for selection ${msg.selectionId}` : ""}: ${msg.details}` }));
1017
+ }
1018
+ else if (msg.type === "git_status") {
1019
+ socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
1020
+ }
1021
+ else if (msg.type === "git_generate_message") {
1022
+ const localProvider = msg.provider === "cli";
1023
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
1024
+ if (!agentConfig && !localProvider) {
1025
+ socket.send(JSON.stringify({ type: "git_commit_message", error: "No AI provider is configured." }));
1026
+ return;
1027
+ }
1028
+ const selectedConfig = localProvider
1029
+ ? { provider: cliProvider, model: msg.model }
1030
+ : { ...agentConfig, provider: (msg.provider || agentConfig.provider), model: msg.model };
1031
+ const controller = new AbortController();
1032
+ activeAgentController?.abort();
1033
+ activeAgentController = controller;
1034
+ socket.send(JSON.stringify({ type: "git_progress", message: "Generating a commit message…" }));
1035
+ void getGitState(cwd).then((git) => (0, agent_1.generateCommitMessage)(cwd, git.status || [], selectedConfig, controller.signal, (message, detail) => {
1036
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN)
1037
+ socket.send(JSON.stringify({ type: "git_progress", message: detail ? `${message} · ${detail}` : message }));
1038
+ })).then((message) => {
1039
+ if (!controller.signal.aborted)
1040
+ socket.send(JSON.stringify({ type: "git_commit_message", message }));
1041
+ }).catch((error) => {
1042
+ if (!controller.signal.aborted)
1043
+ socket.send(JSON.stringify({ type: "git_commit_message", error: error instanceof Error ? error.message : "Unable to generate a commit message." }));
1044
+ }).finally(() => { if (activeAgentController === controller)
1045
+ activeAgentController = null; });
1046
+ }
1047
+ else if (msg.type === "git_init") {
1048
+ try {
1049
+ await gitCommand(cwd, ["init"]);
1050
+ socket.send(JSON.stringify({ type: "git_result", message: "Git repository initialized." }));
1051
+ socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
1052
+ }
1053
+ catch (error) {
1054
+ socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Git could not be initialized." }));
1055
+ }
1056
+ }
1057
+ else if (msg.type === "git_commit") {
1058
+ try {
1059
+ const commitMessage = msg.message.trim().slice(0, 120);
1060
+ if (!commitMessage)
1061
+ throw new Error("Enter a commit message first.");
1062
+ await gitCommand(cwd, ["add", "-A"]);
1063
+ await gitCommand(cwd, ["commit", "-m", commitMessage]);
1064
+ socket.send(JSON.stringify({ type: "git_result", message: "Changes committed." }));
1065
+ socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
1066
+ }
1067
+ catch (error) {
1068
+ socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Changes could not be committed." }));
1069
+ }
1070
+ }
1071
+ else if (msg.type === "git_push") {
1072
+ try {
1073
+ await gitCommand(cwd, ["push"]);
1074
+ socket.send(JSON.stringify({ type: "git_result", message: "Changes pushed to the configured remote." }));
1075
+ socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
1076
+ }
1077
+ catch (error) {
1078
+ socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Changes could not be pushed." }));
1079
+ }
1080
+ }
1081
+ else if (msg.type === "transcribe") {
1082
+ const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv);
1083
+ const creds = (0, auth_1.loadCredentials)();
1084
+ const apiKey = resolvedLassoKey || agentConfig?.apiKey || collabConfig?.apiKey || creds?.apiKey;
1085
+ try {
1086
+ const resp = await fetch(`${serverUrl}/api/v1/transcribe`, {
1087
+ method: "POST",
1088
+ headers: {
1089
+ "Content-Type": "application/json",
1090
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
1091
+ },
1092
+ body: JSON.stringify({
1093
+ audio: msg.audio,
1094
+ mimeType: msg.mimeType,
1095
+ language: msg.language,
1096
+ }),
1097
+ });
1098
+ const data = await resp.json();
1099
+ socket.send(JSON.stringify({
1100
+ type: "transcribe_result",
1101
+ requestId: msg.requestId,
1102
+ success: Boolean(data.success),
1103
+ text: data.text || "",
1104
+ provider: data.provider || "gradium",
1105
+ error: data.message,
1106
+ }));
1107
+ }
1108
+ catch (error) {
1109
+ socket.send(JSON.stringify({
1110
+ type: "transcribe_result",
1111
+ requestId: msg.requestId,
1112
+ success: false,
1113
+ error: error instanceof Error ? error.message : "Failed to contact transcription service",
1114
+ }));
1115
+ }
1116
+ }
1117
+ else if (msg.type === "oneshot") {
1118
+ console.log("[lasso] one-shot request:", msg.prompt, msg.scope);
1119
+ const taskId = msg.taskId || `oneshot-${Date.now()}`;
1120
+ const localProvider = msg.provider === "cli" || msg.model?.startsWith("claude-code:") || msg.model?.startsWith("opencode:") || msg.model?.startsWith("cursor:") || msg.model?.startsWith("codex:");
1121
+ const cliProvider = msg.model?.startsWith("claude-code:") ? "claude-code" : msg.model?.startsWith("opencode:") ? "opencode" : msg.model?.startsWith("cursor:") ? "cursor" : "codex";
1122
+ const apiKey = resolvedLassoKey || agentConfig?.apiKey || collabConfig?.apiKey;
1123
+ if (!localProvider && !lassoKeyConfigured && !agentConfig && !apiKey) {
1124
+ socket.send(JSON.stringify({
1125
+ type: "agent_status",
1126
+ taskId,
1127
+ status: "error",
1128
+ message: "No AI provider or Lasso key configured. Sign in with 'lasso auth login' or configure a provider key in .env."
1129
+ }));
1130
+ return;
1131
+ }
1132
+ const provider = (msg.provider || agentConfig?.provider || "google");
1133
+ const model = msg.model || agentConfig?.model;
1134
+ if (!model) {
1135
+ socket.send(JSON.stringify({
1136
+ type: "error",
1137
+ message: "No model selected. Please select a model in settings."
1138
+ }));
1139
+ return;
1140
+ }
1141
+ const localKey = localProviderKeys[provider];
1142
+ const serverUrl = collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv);
1143
+ const targetApiUrl = (serverUrl.includes("collab.lasso.byorello.space") ? "https://api.lasso.byorello.space" : serverUrl).replace(/\/api\/v1\/?$/, "");
1144
+ const selectedConfig = localProvider
1145
+ ? { provider: cliProvider, model: msg.model || "claude-code:sonnet", lassoKey: resolvedLassoKey, serverUrl: targetApiUrl }
1146
+ : {
1147
+ ...(agentConfig || {}),
1148
+ provider,
1149
+ model,
1150
+ apiKey: localKey || apiKey,
1151
+ lassoKey: resolvedLassoKey,
1152
+ serverUrl: targetApiUrl,
1153
+ baseUrl: provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : provider === "ollama" ? `${(process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api").replace(/\/api\/?$/, "")}/v1` : undefined,
1154
+ };
1155
+ const controller = new AbortController();
1156
+ taskControllers.set(taskId, controller);
1157
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: "Planning your request..." }));
1158
+ const { runOneShotAgent } = await import("./oneshot.js");
1159
+ runOneShotAgent(cwd, msg.prompt, msg.scope || "project", selectedConfig, controller.signal, (status, message, detail) => {
1160
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
1161
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
1162
+ }
1163
+ }, (prompt) => {
1164
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
1165
+ socket.send(JSON.stringify({ type: "agent_prompt", taskId, prompt }));
1166
+ }
1167
+ }, msg.messages, targetApiUrl, apiKey, taskId, msg.pageContext).then((result) => {
1168
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
1169
+ if (result.ok) {
1170
+ const hasChanges = Array.isArray(result.changes) && result.changes.length > 0;
1171
+ socket.send(JSON.stringify({
1172
+ type: "agent_status",
1173
+ taskId,
1174
+ status: hasChanges ? "review" : "complete",
1175
+ message: result.summary,
1176
+ todo: result.todo,
1177
+ changes: result.changes,
1178
+ thinking: result.thinking,
1179
+ totalThinkingTimeMs: result.totalThinkingTimeMs,
1180
+ }));
1181
+ }
1182
+ else {
1183
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: result.error || "One-shot agent failed" }));
1184
+ }
1185
+ }
1186
+ }).catch((error) => {
1187
+ if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
1188
+ socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: error instanceof Error ? error.message : "One-shot agent encountered an error" }));
1189
+ }
1190
+ }).finally(() => {
1191
+ if (taskControllers.get(taskId) === controller)
1192
+ taskControllers.delete(taskId);
1193
+ });
1194
+ }
1195
+ else if (msg.type === "apply") {
1196
+ try {
1197
+ // Create snapshot before applying changes
1198
+ const filePaths = [...new Set(msg.changes.map(c => c.filePath))];
1199
+ const parentSnapshot = (0, snapshot_1.getSnapshotByTaskId)(cwd, msg.taskId);
1200
+ // Create pre-apply snapshot
1201
+ const preSnapshot = (0, snapshot_1.createSnapshot)(cwd, msg.taskId, `Pre-apply: ${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"}`, filePaths, parentSnapshot?.id);
1202
+ const snapshots = [];
1203
+ const planned = new Map();
1204
+ for (const change of msg.changes) {
1205
+ const isNew = !change.oldString;
1206
+ const filePath = resolveProposedFile(cwd, change.filePath, true);
1207
+ const exists = node_fs_1.default.existsSync(filePath);
1208
+ const content = exists ? node_fs_1.default.readFileSync(filePath, "utf8") : "";
1209
+ const prepared = (!exists || isNew)
1210
+ ? { start: 0, end: content.length, oldString: content, newString: change.newString }
1211
+ : prepareChange(content, change);
1212
+ const fileChanges = planned.get(filePath) || [];
1213
+ if (fileChanges.some((item) => prepared.start < item.end && item.start < prepared.end)) {
1214
+ throw new Error(`Could not safely apply ${change.filePath}. Proposed changes overlap.`);
1215
+ }
1216
+ fileChanges.push({ filePath, content, exists, ...prepared });
1217
+ planned.set(filePath, fileChanges);
1218
+ }
1219
+ // Apply each file's replacements from the end toward the beginning
1220
+ taskSnapshots.set(msg.taskId, snapshots);
1221
+ for (const [filePath, changes] of planned) {
1222
+ const exists = changes[0].exists;
1223
+ const content = changes[0].content;
1224
+ snapshots.push({ filePath, content: exists ? content : null });
1225
+ const nextContent = exists
1226
+ ? [...changes]
1227
+ .sort((a, b) => b.start - a.start)
1228
+ .reduce((value, change) => value.slice(0, change.start) + change.newString + value.slice(change.end), content)
1229
+ : changes[0].newString;
1230
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
1231
+ node_fs_1.default.writeFileSync(filePath, nextContent);
1232
+ }
1233
+ // Create post-apply snapshot
1234
+ (0, snapshot_1.createSnapshot)(cwd, msg.taskId, `Applied: ${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"}`, filePaths, preSnapshot.id);
1235
+ // Run build validation after applying changes
1236
+ const validation = await validateBuild(cwd);
1237
+ if (!validation.passed) {
1238
+ // Build failed - automatically trigger retry with build error context
1239
+ const errorContext = validation.errors.slice(0, 10).join("\n");
1240
+ socket.send(JSON.stringify({
1241
+ type: "agent_status",
1242
+ taskId: msg.taskId,
1243
+ status: "working",
1244
+ message: `⚠️ Build validation failed. Attempting to fix build errors automatically...`,
1245
+ detail: `Build command: ${validation.command}\nErrors:\n${errorContext}${validation.errors.length > 10 ? `\n... and ${validation.errors.length - 10} more errors` : ""}`
1246
+ }));
1247
+ // Store context for retry
1248
+ buildErrorContext.set(msg.taskId, {
1249
+ errors: validation.errors,
1250
+ command: validation.command,
1251
+ originalChanges: msg.changes,
1252
+ retryCount: 0
1253
+ });
1254
+ // Trigger automatic build fix
1255
+ await triggerBuildFix(cwd, msg.taskId, validation.errors, validation.command, msg.changes, resolvedLassoKey, agentConfig, collabConfig, socket, taskControllers, taskSnapshots, buildErrorContext);
1256
+ return;
1257
+ }
1258
+ socket.send(JSON.stringify({
1259
+ type: "applied",
1260
+ taskId: msg.taskId,
1261
+ message: `${msg.changes.length} file${msg.changes.length === 1 ? "" : "s"} updated. Your dev server will reload.`,
1262
+ validation
1263
+ }));
1264
+ }
1265
+ catch (error) {
1266
+ // Restore this task's snapshot if a filesystem error occurs
1267
+ for (const snapshot of taskSnapshots.get(msg.taskId) || []) {
1268
+ if (snapshot.content === null) {
1269
+ if (node_fs_1.default.existsSync(snapshot.filePath))
1270
+ node_fs_1.default.unlinkSync(snapshot.filePath);
1271
+ }
1272
+ else {
1273
+ node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
1274
+ }
1275
+ }
1276
+ taskSnapshots.delete(msg.taskId);
1277
+ const message = error instanceof Error ? error.message : "The change could not be applied.";
1278
+ socket.send(JSON.stringify({ type: "agent_status", taskId: msg.taskId, status: "error", message }));
1279
+ }
1280
+ finally {
1281
+ if (taskControllers.get(msg.taskId))
1282
+ taskControllers.delete(msg.taskId);
1283
+ }
1284
+ }
1285
+ else if (msg.type === "undo") {
1286
+ const snapshots = taskSnapshots.get(msg.taskId || "") || [];
1287
+ for (const snapshot of snapshots) {
1288
+ if (snapshot.content === null) {
1289
+ if (node_fs_1.default.existsSync(snapshot.filePath))
1290
+ node_fs_1.default.unlinkSync(snapshot.filePath);
1291
+ }
1292
+ else {
1293
+ node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
1294
+ }
1295
+ }
1296
+ taskSnapshots.delete(msg.taskId || "");
1297
+ socket.send(JSON.stringify({ type: "undone", taskId: msg.taskId, message: snapshots.length ? "The accepted change was reverted." : "There is no accepted change to undo." }));
1298
+ }
1299
+ else if (msg.type === "restore_snapshot") {
1300
+ const success = (0, snapshot_1.restoreSnapshot)(cwd, msg.snapshotId);
1301
+ socket.send(JSON.stringify({
1302
+ type: "agent_status",
1303
+ message: success ? "Snapshot restored successfully." : "Failed to restore snapshot."
1304
+ }));
1305
+ }
1306
+ else if (msg.type === "get_timeline") {
1307
+ const timeline = (0, snapshot_1.getTimeline)(cwd);
1308
+ socket.send(JSON.stringify({ type: "timeline", snapshots: timeline }));
1309
+ }
1310
+ else if (msg.type === "delete_snapshot") {
1311
+ const success = (0, snapshot_1.deleteSnapshot)(cwd, msg.snapshotId);
1312
+ socket.send(JSON.stringify({
1313
+ type: "agent_status",
1314
+ message: success ? "Snapshot deleted." : "Failed to delete snapshot."
1315
+ }));
1316
+ }
1317
+ });
1318
+ socket.on("close", () => {
1319
+ overlaySocket = null;
1320
+ currentSocket = null;
1321
+ console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
1322
+ });
1323
+ });
1324
+ let bridgeListening = false;
1325
+ bridgeServer.on("listening", () => {
1326
+ bridgeListening = true;
1327
+ });
1328
+ bridgeServer.on("error", (error) => {
1329
+ if (error.code === "EADDRINUSE") {
1330
+ console.warn(chalk_1.default.yellow("!") + ` Lasso bridge port ${bridgePort} is already in use; reusing the existing bridge.`);
1331
+ return;
1332
+ }
1333
+ console.error(chalk_1.default.red("Lasso bridge error:"), error.message);
1334
+ });
1335
+ bridgeServer.listen(bridgePort, "127.0.0.1");
1336
+ function send(msg) {
1337
+ const socket = currentSocket;
1338
+ if (socket && socket.readyState === socket.OPEN) {
1339
+ socket.send(JSON.stringify(msg));
1340
+ }
1341
+ }
1342
+ return {
1343
+ send,
1344
+ close() {
1345
+ if (bridgeListening)
1346
+ bridgeServer.close();
1347
+ wss.close();
1348
+ },
1349
+ };
1350
+ }