@lasso-ai/cli 1.4.5 → 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.
@@ -14,10 +14,10 @@ const node_path_1 = __importDefault(require("node:path"));
14
14
  const node_child_process_1 = require("node:child_process");
15
15
  const node_util_1 = require("node:util");
16
16
  const chalk_1 = __importDefault(require("chalk"));
17
- const agent_1 = require("./agent");
18
17
  const project_1 = require("./project");
19
18
  const auth_1 = require("./auth");
20
19
  const snapshot_1 = require("./snapshot");
20
+ const runtime_client_1 = require("./runtime-client");
21
21
  exports.DEFAULT_BRIDGE_PORT = 3056;
22
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
23
23
  async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
@@ -48,361 +48,7 @@ async function getGitState(cwd) {
48
48
  const remote = await gitCommand(cwd, ["remote", "get-url", "origin"]).catch(() => "");
49
49
  return { isRepo: true, branch, status, hasChanges: status.length > 0, hasRemote: Boolean(remote), remote };
50
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) {
51
+ function readEnvFile(filename, cwd) {
406
52
  try {
407
53
  return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
408
54
  const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
@@ -419,15 +65,60 @@ function readEnvFile(cwd, filename) {
419
65
  return {};
420
66
  }
421
67
  }
68
+ function detectProjectFramework(cwd) {
69
+ try {
70
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8"));
71
+ if (pkg.dependencies?.next || pkg.devDependencies?.next)
72
+ return "next";
73
+ if (pkg.dependencies?.react || pkg.devDependencies?.react)
74
+ return "react";
75
+ if (pkg.dependencies?.vue || pkg.devDependencies?.vue)
76
+ return "vue";
77
+ if (pkg.dependencies?.svelte || pkg.devDependencies?.svelte)
78
+ return "svelte";
79
+ if (pkg.dependencies?.solid || pkg.devDependencies?.solid)
80
+ return "solid";
81
+ }
82
+ catch { }
83
+ return "unknown";
84
+ }
85
+ // Helper to extract SourceChange from runtime session
86
+ function extractChangesFromSession(session) {
87
+ if (!session)
88
+ return [];
89
+ const changes = [];
90
+ for (const action of session.actions || []) {
91
+ if (action.type === "edit_file" && action.data) {
92
+ changes.push({
93
+ filePath: action.data.filePath,
94
+ oldString: action.data.oldString,
95
+ newString: action.data.newString,
96
+ });
97
+ }
98
+ else if (action.type === "create_file" && action.data) {
99
+ changes.push({
100
+ filePath: action.data.filePath,
101
+ oldString: "",
102
+ newString: action.data.content || "",
103
+ });
104
+ }
105
+ else if (action.type === "delete_file" && action.data) {
106
+ changes.push({
107
+ filePath: action.data.filePath,
108
+ oldString: "(deleted)",
109
+ newString: "",
110
+ });
111
+ }
112
+ }
113
+ return changes;
114
+ }
422
115
  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
116
+ const bridgeServer = node_http_1.default.createServer();
424
117
  const wss = new ws_1.WebSocketServer({ server: bridgeServer });
425
118
  let overlaySocket = null;
426
119
  const taskSnapshots = new Map();
427
- const editRequests = new Map();
428
- const editConfigs = new Map();
429
- const reviewRefreshAttempts = new Map();
430
- const buildErrorContext = new Map();
120
+ const activeSessions = new Map();
121
+ const taskControllers = new Map();
431
122
  const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
432
123
  const fileEnv = envRoots.reduce((values, root) => ({
433
124
  ...values,
@@ -435,7 +126,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
435
126
  ...readEnvFile(root, ".env.local"),
436
127
  ...readEnvFile(root, ".env.production"),
437
128
  }), {});
438
- // Prioritize CLI auth credentials over env vars
439
129
  const cliCredentials = (0, auth_1.loadCredentials)();
440
130
  const resolvedLassoKey = cliCredentials?.apiKey ||
441
131
  (0, project_1.resolveLassoApiKey)(fileEnv) ||
@@ -447,164 +137,41 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
447
137
  fileEnv.NEXT_LASSO_API_KEY ||
448
138
  "";
449
139
  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: [] };
140
+ const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv) || "https://api.lasso.byorello.space";
141
+ const runtimeClient = new runtime_client_1.RuntimeClient(serverUrl, resolvedLassoKey || undefined);
142
+ // Load project info
143
+ const workspaceConfig = await (0, project_1.resolveWorkspaceConfig)(fileEnv);
144
+ const projectId = workspaceConfig?.workspaceId || "";
145
+ const framework = detectProjectFramework(cwd);
146
+ // Fetch models
147
+ let models = [];
571
148
  try {
572
- dynamicModels = await (0, project_1.fetchModels)(fileEnv);
149
+ const modelsResponse = await (0, project_1.fetchModels)(fileEnv);
150
+ const allModels = [
151
+ ...(modelsResponse.anthropic || []),
152
+ ...(modelsResponse.openai || []),
153
+ ...(modelsResponse.google || []),
154
+ ...(modelsResponse.nvidia || []),
155
+ ];
156
+ models = allModels.map((m) => ({
157
+ id: m.id,
158
+ label: m.name || m.id,
159
+ provider: m.provider,
160
+ locked: false,
161
+ }));
573
162
  }
574
- catch (error) {
575
- console.warn('[Lasso] Failed to fetch dynamic models, using fallback');
163
+ catch {
164
+ models = [
165
+ { id: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet", provider: "anthropic", locked: false },
166
+ { id: "gpt-4o", label: "GPT-4o", provider: "openai", locked: false },
167
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google", locked: false },
168
+ ];
576
169
  }
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
170
  const pageIgnoredDirs = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "out", "coverage", ".vercel", ".cache"]);
602
171
  const resolveInsideProject = (root, relative) => {
603
172
  const target = node_path_1.default.resolve(root, relative && relative !== "." ? relative : "");
604
173
  return target === root || target.startsWith(`${root}${node_path_1.default.sep}`) ? target : null;
605
174
  };
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
175
  const pageEntries = (root, relative) => {
609
176
  const directory = resolveInsideProject(root, relative);
610
177
  if (!directory)
@@ -621,730 +188,607 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
621
188
  });
622
189
  }
623
190
  }
624
- catch {
625
- // Folders that cannot be read are treated as empty.
626
- }
191
+ catch { }
627
192
  return entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
628
193
  };
629
194
  bridgeServer.on("request", (req, res) => {
630
195
  if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
631
196
  return;
632
- activeAgentController?.abort();
633
197
  for (const controller of taskControllers.values())
634
198
  controller.abort();
635
199
  taskControllers.clear();
636
- activeAgentController = null;
637
200
  for (const socket of wss.clients)
638
201
  socket.close(1000, "Bridge restarted by the CLI");
639
202
  res.writeHead(200, { "content-type": "application/json" });
640
203
  res.end(JSON.stringify({ ok: true }));
641
204
  });
642
- async function localModels() {
643
- const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
205
+ // Handler for edit/ask - uses runtime
206
+ async function handleEditOrAsk(message, socket, taskId, instruction, model, provider, context, element, messages) {
207
+ taskControllers.get(taskId)?.abort();
208
+ const controller = new AbortController();
209
+ taskControllers.set(taskId, controller);
210
+ socket.send(JSON.stringify({
211
+ type: "agent_status",
212
+ taskId,
213
+ status: "thinking",
214
+ message: "Starting...",
215
+ }));
644
216
  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" }));
217
+ const sessionInput = {
218
+ userPrompt: instruction,
219
+ project: {
220
+ projectId,
221
+ framework,
222
+ hasTypeScript: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json")),
223
+ hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) &&
224
+ node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
225
+ },
226
+ model: {
227
+ provider: (provider || "anthropic"),
228
+ id: model || "claude-3-5-sonnet-20241022",
229
+ },
230
+ selection: context ? {
231
+ element: element ? {
232
+ tag: element.tag,
233
+ text: element.label,
234
+ source: element.sourceHint,
235
+ attributes: context.attributes,
236
+ } : undefined,
237
+ viewport: context?.viewport,
238
+ route: context?.position?.route,
239
+ screenshot: context?.screenshots?.full,
240
+ } : undefined,
241
+ };
242
+ const session = await runtimeClient.createSession(sessionInput);
243
+ activeSessions.set(taskId, session);
244
+ // Create snapshot before changes
245
+ taskSnapshots.set(taskId, []);
246
+ // Note: Snapshot will be created after changes are extracted from session
247
+ await runtimeClient.runSession(session.id, cwd, (event) => {
248
+ if (controller.signal.aborted)
249
+ return;
250
+ switch (event.type) {
251
+ case "agent_started":
252
+ case "agent_reasoning":
253
+ socket.send(JSON.stringify({
254
+ type: "agent_status",
255
+ taskId,
256
+ status: "thinking",
257
+ message: event.data.summary || "Working...",
258
+ }));
259
+ break;
260
+ case "tool_started":
261
+ socket.send(JSON.stringify({
262
+ type: "agent_status",
263
+ taskId,
264
+ status: "working",
265
+ message: `Running ${event.data.tool || "tool"}...`,
266
+ }));
267
+ break;
268
+ case "tool_completed":
269
+ socket.send(JSON.stringify({
270
+ type: "agent_status",
271
+ taskId,
272
+ status: "working",
273
+ message: "Completed tool",
274
+ }));
275
+ break;
276
+ case "phase_changed":
277
+ const phase = event.data.phase;
278
+ socket.send(JSON.stringify({
279
+ type: "agent_status",
280
+ taskId,
281
+ status: "working",
282
+ message: `Phase: ${phase}`,
283
+ }));
284
+ break;
285
+ case "verification_started":
286
+ socket.send(JSON.stringify({
287
+ type: "agent_status",
288
+ taskId,
289
+ status: "review",
290
+ message: "Verifying changes...",
291
+ }));
292
+ break;
293
+ case "verification_completed":
294
+ const passed = event.data.passed;
295
+ socket.send(JSON.stringify({
296
+ type: "agent_status",
297
+ taskId,
298
+ status: passed ? "working" : "error",
299
+ message: passed ? "Verification passed" : "Verification failed",
300
+ }));
301
+ break;
302
+ case "agent_completed":
303
+ runtimeClient.getSession(session.id).then((finalSession) => {
304
+ const changes = extractChangesFromSession(finalSession);
305
+ // Create snapshot with affected files
306
+ const filePaths = changes.map(c => c.filePath);
307
+ if (filePaths.length > 0) {
308
+ (0, snapshot_1.createSnapshot)(cwd, taskId, `Runtime session: ${instruction}`, filePaths, undefined, session.id);
309
+ }
310
+ socket.send(JSON.stringify({
311
+ type: "agent_status",
312
+ taskId,
313
+ status: "review",
314
+ message: "Review changes",
315
+ changes,
316
+ }));
317
+ });
318
+ break;
319
+ case "error":
320
+ socket.send(JSON.stringify({
321
+ type: "agent_status",
322
+ taskId,
323
+ status: "error",
324
+ message: event.data.error || "Unknown error",
325
+ }));
326
+ break;
327
+ case "waiting_for_user":
328
+ socket.send(JSON.stringify({
329
+ type: "agent_prompt",
330
+ taskId,
331
+ prompt: {
332
+ message: event.data?.question || "Input required",
333
+ kind: "input",
334
+ },
335
+ }));
336
+ break;
337
+ }
338
+ }, controller.signal);
650
339
  }
651
- catch {
652
- return [];
340
+ catch (error) {
341
+ socket.send(JSON.stringify({
342
+ type: "agent_status",
343
+ taskId,
344
+ status: "error",
345
+ message: error instanceof Error ? error.message : "Unknown error",
346
+ }));
653
347
  }
654
348
  }
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" }));
349
+ // Handler for apply - applies pending changes
350
+ async function handleApply(message, socket, cwd) {
351
+ const { taskId, changes } = message;
352
+ const session = activeSessions.get(taskId);
353
+ const snapshot = taskSnapshots.get(taskId) || [];
354
+ // Create snapshot before applying changes
355
+ const filePaths = changes.map((c) => c.filePath);
356
+ if (filePaths.length > 0) {
357
+ (0, snapshot_1.createSnapshot)(cwd, taskId, `Apply changes for task ${taskId}`, filePaths, undefined, session?.id);
665
358
  }
666
- catch {
667
- return [];
359
+ for (const change of changes) {
360
+ const filePath = node_path_1.default.join(cwd, change.filePath);
361
+ const dir = node_path_1.default.dirname(filePath);
362
+ try {
363
+ if (!node_fs_1.default.existsSync(dir)) {
364
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
365
+ }
366
+ if (change.oldString === "" && change.newString === "") {
367
+ // Delete file
368
+ if (node_fs_1.default.existsSync(filePath)) {
369
+ node_fs_1.default.unlinkSync(filePath);
370
+ }
371
+ }
372
+ else if (change.oldString === "") {
373
+ // Create file
374
+ node_fs_1.default.writeFileSync(filePath, change.newString);
375
+ }
376
+ else {
377
+ // Edit file
378
+ const content = node_fs_1.default.readFileSync(filePath, "utf8");
379
+ const newContent = content.replace(change.oldString, change.newString);
380
+ node_fs_1.default.writeFileSync(filePath, newContent);
381
+ }
382
+ }
383
+ catch (error) {
384
+ socket.send(JSON.stringify({
385
+ type: "agent_status",
386
+ taskId,
387
+ status: "error",
388
+ message: `Failed to apply ${change.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`,
389
+ }));
390
+ return;
391
+ }
668
392
  }
393
+ socket.send(JSON.stringify({
394
+ type: "applied",
395
+ taskId,
396
+ message: "Changes applied successfully",
397
+ }));
669
398
  }
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);
399
+ // Handler for undo - restores snapshot
400
+ async function handleUndo(message, socket, cwd) {
401
+ const { taskId } = message;
402
+ const snapshot = taskSnapshots.get(taskId) || [];
403
+ for (const item of snapshot) {
404
+ if (item.content === null) {
405
+ // File didn't exist, delete if it was created
406
+ try {
407
+ node_fs_1.default.unlinkSync(node_path_1.default.join(cwd, item.filePath));
408
+ }
409
+ catch { }
410
+ }
411
+ else {
412
+ // Restore original content
413
+ node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, item.filePath), item.content);
414
+ }
686
415
  }
687
- catch {
688
- return [];
416
+ socket.send(JSON.stringify({
417
+ type: "undone",
418
+ taskId,
419
+ message: "Changes undone",
420
+ }));
421
+ }
422
+ // Handler for stop
423
+ async function handleStop(message, socket) {
424
+ const { taskId } = message;
425
+ if (taskId) {
426
+ taskControllers.get(taskId)?.abort();
427
+ activeSessions.delete(taskId);
689
428
  }
429
+ else {
430
+ for (const controller of taskControllers.values())
431
+ controller.abort();
432
+ taskControllers.clear();
433
+ activeSessions.clear();
434
+ }
435
+ socket.send(JSON.stringify({
436
+ type: "agent_status",
437
+ taskId,
438
+ status: "stopped",
439
+ message: "Agent stopped",
440
+ }));
690
441
  }
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` };
442
+ // Handler for switch_model
443
+ async function handleSwitchModel(message, socket) {
444
+ const { taskId, provider, modelId } = message;
445
+ const session = activeSessions.get(taskId);
446
+ if (!session) {
447
+ socket.send(JSON.stringify({
448
+ type: "agent_status",
449
+ taskId,
450
+ status: "error",
451
+ message: "No active session for this task",
452
+ }));
453
+ return;
699
454
  }
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";
455
+ try {
456
+ await runtimeClient.switchModel(session.id, {
457
+ provider: provider,
458
+ id: modelId,
459
+ });
460
+ socket.send(JSON.stringify({
461
+ type: "agent_status",
462
+ taskId,
463
+ status: "working",
464
+ message: `Switched to ${provider}/${modelId}`,
465
+ }));
466
+ }
467
+ catch (error) {
468
+ socket.send(JSON.stringify({
469
+ type: "agent_status",
470
+ taskId,
471
+ status: "error",
472
+ message: error instanceof Error ? error.message : "Failed to switch model",
473
+ }));
474
+ }
475
+ }
476
+ // Handler for oneshot
477
+ async function handleOneshot(message, socket, cwd) {
478
+ const { taskId, prompt, model, provider } = message;
479
+ const instruction = prompt;
480
+ taskControllers.get(taskId)?.abort();
481
+ const controller = new AbortController();
482
+ taskControllers.set(taskId, controller);
483
+ socket.send(JSON.stringify({
484
+ type: "agent_status",
485
+ taskId,
486
+ status: "thinking",
487
+ message: "Starting oneshot...",
488
+ }));
489
+ try {
490
+ const sessionInput = {
491
+ userPrompt: prompt,
492
+ project: {
493
+ projectId,
494
+ framework,
495
+ hasTypeScript: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json")),
496
+ hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) &&
497
+ node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
498
+ },
499
+ model: {
500
+ provider: (provider || "anthropic"),
501
+ id: model || "claude-3-5-sonnet-20241022",
502
+ },
503
+ };
504
+ const session = await runtimeClient.createSession(sessionInput);
505
+ activeSessions.set(taskId, session);
506
+ await runtimeClient.runSession(session.id, cwd, (event) => {
507
+ if (controller.signal.aborted)
508
+ return;
509
+ if (event.type === "agent_completed") {
510
+ runtimeClient.getSession(session.id).then((finalSession) => {
511
+ const changes = extractChangesFromSession(finalSession);
512
+ const filePaths = changes.map(c => c.filePath);
513
+ if (filePaths.length > 0) {
514
+ (0, snapshot_1.createSnapshot)(cwd, taskId, `Oneshot: ${instruction}`, filePaths, undefined, session.id);
515
+ }
516
+ socket.send(JSON.stringify({
517
+ type: "assistant_message",
518
+ taskId,
519
+ message: "Oneshot completed",
520
+ }));
521
+ });
810
522
  }
811
- else {
812
- lockedReason = "";
523
+ else if (event.type === "error") {
524
+ socket.send(JSON.stringify({
525
+ type: "agent_status",
526
+ taskId,
527
+ status: "error",
528
+ message: event.data.error || "Runtime error",
529
+ }));
813
530
  }
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
- };
531
+ }, controller.signal);
532
+ }
533
+ catch (error) {
534
+ socket.send(JSON.stringify({
535
+ type: "agent_status",
536
+ taskId,
537
+ status: "error",
538
+ message: error instanceof Error ? error.message : "Unknown oneshot error",
539
+ }));
540
+ }
541
+ }
542
+ // Handler for restore_snapshot
543
+ async function handleRestoreSnapshot(message, socket, cwd) {
544
+ const { snapshotId } = message;
545
+ try {
546
+ await (0, snapshot_1.restoreSnapshot)(snapshotId, cwd);
547
+ socket.send(JSON.stringify({
548
+ type: "agent_status",
549
+ status: "working",
550
+ message: "Snapshot restored",
551
+ }));
552
+ }
553
+ catch (error) {
554
+ socket.send(JSON.stringify({
555
+ type: "agent_status",
556
+ status: "error",
557
+ message: error instanceof Error ? error.message : "Failed to restore snapshot",
558
+ }));
559
+ }
560
+ }
561
+ // Handler for get_timeline
562
+ async function handleGetTimeline(socket, cwd) {
563
+ try {
564
+ const timeline = await (0, snapshot_1.getTimeline)(cwd);
565
+ socket.send(JSON.stringify({
566
+ type: "timeline",
567
+ snapshots: timeline,
568
+ }));
569
+ }
570
+ catch (error) {
571
+ socket.send(JSON.stringify({
572
+ type: "agent_status",
573
+ status: "error",
574
+ message: error instanceof Error ? error.message : "Failed to get timeline",
575
+ }));
576
+ }
577
+ }
578
+ // Handler for delete_snapshot
579
+ async function handleDeleteSnapshot(message, socket, cwd) {
580
+ const { snapshotId } = message;
581
+ try {
582
+ await (0, snapshot_1.deleteSnapshot)(snapshotId, cwd);
583
+ socket.send(JSON.stringify({
584
+ type: "agent_status",
585
+ status: "working",
586
+ message: "Snapshot deleted",
587
+ }));
588
+ }
589
+ catch (error) {
590
+ socket.send(JSON.stringify({
591
+ type: "agent_status",
592
+ status: "error",
593
+ message: error instanceof Error ? error.message : "Failed to delete snapshot",
594
+ }));
595
+ }
596
+ }
597
+ // Git handlers
598
+ async function handleGitStatus(socket, cwd) {
599
+ socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
600
+ }
601
+ async function handleGitGenerateMessage(message, socket, cwd) {
602
+ const git = await getGitState(cwd);
603
+ if (!git.isRepo) {
604
+ socket.send(JSON.stringify({ type: "git_commit_message", error: "Not a git repository" }));
605
+ return;
606
+ }
607
+ const diff = await gitCommand(cwd, ["diff", "--cached"]).catch(() => "");
608
+ if (!diff) {
609
+ socket.send(JSON.stringify({ type: "git_commit_message", error: "No staged changes" }));
610
+ return;
611
+ }
612
+ // TODO: Use runtime for commit message generation
613
+ socket.send(JSON.stringify({ type: "git_commit_message", message: "feat: changes" }));
614
+ }
615
+ async function handleGitInit(socket, cwd) {
616
+ try {
617
+ await gitCommand(cwd, ["init"]);
618
+ socket.send(JSON.stringify({ type: "git_result", message: "Git repository initialized" }));
619
+ }
620
+ catch (error) {
621
+ socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Failed to initialize git" }));
622
+ }
623
+ }
624
+ async function handleGitCommit(message, socket, cwd) {
625
+ try {
626
+ await gitCommand(cwd, ["commit", "-m", message.message]);
627
+ socket.send(JSON.stringify({ type: "git_result", message: "Changes committed" }));
628
+ }
629
+ catch (error) {
630
+ socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Failed to commit" }));
631
+ }
632
+ }
633
+ async function handleGitPush(socket, cwd) {
634
+ try {
635
+ await gitCommand(cwd, ["push"]);
636
+ socket.send(JSON.stringify({ type: "git_result", message: "Changes pushed" }));
637
+ }
638
+ catch (error) {
639
+ socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Failed to push" }));
640
+ }
641
+ }
825
642
  wss.on("connection", (socket) => {
826
643
  overlaySocket = socket;
827
- currentSocket = socket;
828
644
  console.log(chalk_1.default.green("✓") + " Overlay connected");
829
645
  const creds = (0, auth_1.loadCredentials)();
830
646
  const userInfo = creds?.userName
831
647
  ? { name: creds.userName, email: creds.userEmail }
832
648
  : (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
- });
649
+ socket.send(JSON.stringify({
650
+ type: "config",
651
+ apiKeyConfigured: lassoKeyConfigured,
652
+ agentConfigured: lassoKeyConfigured,
653
+ models,
654
+ collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null,
655
+ user: userInfo
656
+ }));
838
657
  void getGitState(cwd).then((git) => {
839
658
  if (socket.readyState === socket.OPEN)
840
659
  socket.send(JSON.stringify({ type: "git_state", git }));
841
660
  });
842
661
  socket.on("message", async (raw) => {
843
662
  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;
663
+ console.log("[lasso] received from overlay:", msg.type);
664
+ try {
665
+ switch (msg.type) {
666
+ case "hello":
667
+ break;
668
+ case "edit":
669
+ case "ask":
670
+ await handleEditOrAsk(msg, socket, msg.taskId, msg.type === "edit" ? msg.instruction : msg.question, msg.model, msg.provider || "anthropic", msg.context, msg.element, msg.messages);
671
+ break;
672
+ case "apply":
673
+ await handleApply(msg, socket, cwd);
674
+ break;
675
+ case "undo":
676
+ await handleUndo(msg, socket, cwd);
677
+ break;
678
+ case "stop":
679
+ await handleStop(msg, socket);
680
+ break;
681
+ case "switch_model":
682
+ await handleSwitchModel(msg, socket);
683
+ break;
684
+ case "oneshot":
685
+ await handleOneshot(msg, socket, cwd);
686
+ break;
687
+ case "restore_snapshot":
688
+ await handleRestoreSnapshot(msg, socket, cwd);
689
+ break;
690
+ case "get_timeline":
691
+ await handleGetTimeline(socket, cwd);
692
+ break;
693
+ case "delete_snapshot":
694
+ await handleDeleteSnapshot(msg, socket, cwd);
695
+ break;
696
+ case "git_status":
697
+ await handleGitStatus(socket, cwd);
698
+ break;
699
+ case "git_generate_message":
700
+ await handleGitGenerateMessage(msg, socket, cwd);
701
+ break;
702
+ case "git_init":
703
+ await handleGitInit(socket, cwd);
704
+ break;
705
+ case "git_commit":
706
+ await handleGitCommit(msg, socket, cwd);
707
+ break;
708
+ case "git_push":
709
+ await handleGitPush(socket, cwd);
710
+ break;
711
+ case "list_page_folders":
712
+ const requested = msg.path || ".";
713
+ const root = node_path_1.default.resolve(cwd);
714
+ const directory = resolveInsideProject(root, requested);
715
+ if (!directory) {
716
+ socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: [], error: "That folder is outside the current project." }));
717
+ return;
718
+ }
719
+ socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: pageEntries(root, requested), project: node_path_1.default.basename(root) }));
720
+ break;
721
+ case "create_page_folder":
722
+ const root2 = node_path_1.default.resolve(cwd);
723
+ const name = node_path_1.default.basename(msg.name || "");
724
+ const parent = resolveInsideProject(root2, msg.parent || ".");
725
+ if (!parent || !name || name !== msg.name || name === "." || name === "..") {
726
+ socket.send(JSON.stringify({ type: "page_folder_created", path: msg.name || "", error: "Enter a folder name without slashes or dots." }));
727
+ return;
728
+ }
729
+ const target = node_path_1.default.resolve(parent, name);
730
+ if (!target.startsWith(`${root2}${node_path_1.default.sep}`)) {
731
+ socket.send(JSON.stringify({ type: "page_folder_created", path: name, error: "Choose a folder inside the current project." }));
732
+ return;
733
+ }
734
+ try {
735
+ node_fs_1.default.mkdirSync(target);
736
+ socket.send(JSON.stringify({ type: "page_folder_created", path: node_path_1.default.relative(root2, target) }));
737
+ }
738
+ catch (error) {
1171
739
  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,
740
+ type: "page_folder_created",
741
+ path: node_path_1.default.relative(root2, target),
742
+ error: error instanceof Error && error.message.includes("EEXIST") ? "A folder with that name already exists." : "The folder could not be created.",
1180
743
  }));
1181
744
  }
1182
- else {
1183
- socket.send(JSON.stringify({ type: "agent_status", taskId, status: "error", message: result.error || "One-shot agent failed" }));
745
+ break;
746
+ case "create_page":
747
+ const root3 = node_path_1.default.resolve(cwd);
748
+ const folder = resolveInsideProject(root3, msg.folder || ".");
749
+ if (!folder) {
750
+ socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "That folder is outside the current project." }));
751
+ return;
1184
752
  }
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.`);
753
+ const targetFile = node_path_1.default.resolve(folder, msg.fileName);
754
+ if (!targetFile.startsWith(`${root3}${node_path_1.default.sep}`)) {
755
+ socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "Choose a location inside the current project." }));
756
+ return;
1215
757
  }
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);
758
+ try {
759
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
760
+ node_fs_1.default.writeFileSync(targetFile, msg.content);
761
+ socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root3, targetFile) }));
1271
762
  }
1272
- else {
1273
- node_fs_1.default.writeFileSync(snapshot.filePath, snapshot.content);
763
+ catch (error) {
764
+ socket.send(JSON.stringify({
765
+ type: "page_created",
766
+ path: node_path_1.default.relative(root3, targetFile),
767
+ error: error instanceof Error ? error.message : "The file could not be created.",
768
+ }));
1274
769
  }
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);
770
+ break;
771
+ default:
772
+ console.warn("Unknown bridge message type:", msg.type);
1283
773
  }
1284
774
  }
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);
775
+ catch (error) {
776
+ console.error("Error handling bridge message:", error);
1312
777
  socket.send(JSON.stringify({
1313
778
  type: "agent_status",
1314
- message: success ? "Snapshot deleted." : "Failed to delete snapshot."
779
+ taskId: msg.taskId,
780
+ status: "error",
781
+ message: error instanceof Error ? error.message : "Unknown error",
1315
782
  }));
1316
783
  }
1317
784
  });
1318
785
  socket.on("close", () => {
1319
- overlaySocket = null;
1320
- currentSocket = null;
1321
- console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
786
+ if (socket === overlaySocket)
787
+ overlaySocket = null;
1322
788
  });
1323
789
  });
1324
- let bridgeListening = false;
1325
- bridgeServer.on("listening", () => {
1326
- bridgeListening = true;
790
+ bridgeServer.listen(bridgePort, "127.0.0.1", () => {
791
+ console.log(chalk_1.default.green("✓") + ` Bridge listening on ${chalk_1.default.cyan(`http://127.0.0.1:${bridgePort}`)}`);
1327
792
  });
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
- };
793
+ return { ok: true };
1350
794
  }