@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.
- package/dist/cli/agent.d.ts +9 -89
- package/dist/cli/agent.js +66 -1412
- package/dist/cli/bridge-old.d.ts +259 -0
- package/dist/cli/bridge-old.js +1350 -0
- package/dist/cli/bridge.d.ts +19 -12
- package/dist/cli/bridge.js +625 -1181
- package/dist/cli/host/next-host-entry.js +0 -2
- package/dist/cli/host/vite-host-entry.js +1 -3
- package/dist/cli/oneshot.js +159 -961
- package/dist/cli/runtime-client.d.ts +229 -0
- package/dist/cli/runtime-client.js +143 -0
- package/dist/cli/snapshot.d.ts +3 -1
- package/dist/cli/snapshot.js +8 -1
- package/package.json +1 -1
package/dist/cli/bridge.js
CHANGED
|
@@ -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
|
|
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();
|
|
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
|
|
428
|
-
const
|
|
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
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
|
575
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
688
|
-
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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({
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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: "
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
1321
|
-
console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
|
|
786
|
+
if (socket === overlaySocket)
|
|
787
|
+
overlaySocket = null;
|
|
1322
788
|
});
|
|
1323
789
|
});
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
-
|
|
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
|
}
|