@lasso-ai/cli 1.4.5 → 2.0.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 +10 -89
- package/dist/cli/agent.js +66 -1412
- package/dist/cli/backends.d.ts +53 -0
- package/dist/cli/backends.js +267 -0
- package/dist/cli/bridge-old.d.ts +259 -0
- package/dist/cli/bridge-old.js +1350 -0
- package/dist/cli/bridge.d.ts +22 -12
- package/dist/cli/bridge.js +824 -1174
- package/dist/cli/host/install.js +12 -1
- package/dist/cli/host/next-host-entry.js +0 -2
- package/dist/cli/host/vite-host-entry.js +1 -3
- package/dist/cli/local-runtime.d.ts +28 -0
- package/dist/cli/local-runtime.js +294 -0
- package/dist/cli/oneshot.js +177 -961
- package/dist/cli/runtime-client.d.ts +310 -0
- package/dist/cli/runtime-client.js +178 -0
- package/dist/cli/snapshot.d.ts +3 -1
- package/dist/cli/snapshot.js +8 -1
- package/dist/overlay.js +44 -28
- package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
- package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
- package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
- package/dist/runtime/__tests__/state-machine.test.js +107 -0
- package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
- package/dist/runtime/__tests__/verification-engine.test.js +91 -0
- package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
- package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
- package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
- package/dist/runtime/adapters/claude-code-adapter.js +230 -0
- package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
- package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
- package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
- package/dist/runtime/adapters/cursor-adapter.js +214 -0
- package/dist/runtime/adapters/factory.d.ts +19 -0
- package/dist/runtime/adapters/factory.js +48 -0
- package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
- package/dist/runtime/adapters/local-agent-adapter.js +408 -0
- package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
- package/dist/runtime/adapters/opencode-adapter.js +210 -0
- package/dist/runtime/agent-runtime.d.ts +286 -0
- package/dist/runtime/agent-runtime.js +8 -0
- package/dist/runtime/agent-state-machine.d.ts +67 -0
- package/dist/runtime/agent-state-machine.js +234 -0
- package/dist/runtime/backend-interface.d.ts +272 -0
- package/dist/runtime/backend-interface.js +38 -0
- package/dist/runtime/event-mapper.d.ts +11 -0
- package/dist/runtime/event-mapper.js +40 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +43 -0
- package/dist/runtime/local-session-runner.d.ts +60 -0
- package/dist/runtime/local-session-runner.js +185 -0
- package/dist/runtime/review.d.ts +24 -0
- package/dist/runtime/review.js +30 -0
- package/dist/runtime/verification-engine.d.ts +71 -0
- package/dist/runtime/verification-engine.js +374 -0
- package/dist/runtime/workspace-guard.d.ts +70 -0
- package/dist/runtime/workspace-guard.js +321 -0
- package/docs/local-coding-agents.md +183 -0
- package/package.json +22 -3
package/dist/cli/bridge.js
CHANGED
|
@@ -14,10 +14,12 @@ 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
|
+
const backends_1 = require("./backends");
|
|
22
|
+
const local_runtime_1 = require("./local-runtime");
|
|
21
23
|
exports.DEFAULT_BRIDGE_PORT = 3056;
|
|
22
24
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
23
25
|
async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
|
|
@@ -48,361 +50,7 @@ async function getGitState(cwd) {
|
|
|
48
50
|
const remote = await gitCommand(cwd, ["remote", "get-url", "origin"]).catch(() => "");
|
|
49
51
|
return { isRepo: true, branch, status, hasChanges: status.length > 0, hasRemote: Boolean(remote), remote };
|
|
50
52
|
}
|
|
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) {
|
|
53
|
+
function readEnvFile(filename, cwd) {
|
|
406
54
|
try {
|
|
407
55
|
return node_fs_1.default.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
|
|
408
56
|
const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
@@ -419,15 +67,83 @@ function readEnvFile(cwd, filename) {
|
|
|
419
67
|
return {};
|
|
420
68
|
}
|
|
421
69
|
}
|
|
70
|
+
function detectProjectFramework(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8"));
|
|
73
|
+
if (pkg.dependencies?.next || pkg.devDependencies?.next)
|
|
74
|
+
return "next";
|
|
75
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react)
|
|
76
|
+
return "react";
|
|
77
|
+
if (pkg.dependencies?.vue || pkg.devDependencies?.vue)
|
|
78
|
+
return "vue";
|
|
79
|
+
if (pkg.dependencies?.svelte || pkg.devDependencies?.svelte)
|
|
80
|
+
return "svelte";
|
|
81
|
+
if (pkg.dependencies?.solid || pkg.devDependencies?.solid)
|
|
82
|
+
return "solid";
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
return "unknown";
|
|
86
|
+
}
|
|
87
|
+
// Helper to extract review changes from a runtime session.
|
|
88
|
+
// Prefers the authoritative guard diff (session.metadata.pendingChanges) and
|
|
89
|
+
// falls back to normalizing recorded actions (accepting filePath OR path).
|
|
90
|
+
function extractChangesFromSession(session) {
|
|
91
|
+
if (!session)
|
|
92
|
+
return [];
|
|
93
|
+
const metadata = session.metadata;
|
|
94
|
+
const pending = metadata?.pendingChanges;
|
|
95
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
96
|
+
return pending.map((change) => ({
|
|
97
|
+
filePath: change.filePath,
|
|
98
|
+
oldString: change.oldString ?? "",
|
|
99
|
+
newString: change.newString ?? "",
|
|
100
|
+
operation: change.operation || (change.oldString === "" && change.newString === "" ? "deleted" : change.oldString === "" ? "created" : "modified"),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
const changes = [];
|
|
104
|
+
for (const action of session.actions || []) {
|
|
105
|
+
const data = (action.data || {});
|
|
106
|
+
const filePath = (data.filePath || data.path);
|
|
107
|
+
if (!filePath)
|
|
108
|
+
continue;
|
|
109
|
+
if (action.type === "edit_file") {
|
|
110
|
+
changes.push({
|
|
111
|
+
filePath,
|
|
112
|
+
oldString: data.oldString ?? "",
|
|
113
|
+
newString: data.newString ?? "",
|
|
114
|
+
operation: "modified",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else if (action.type === "create_file") {
|
|
118
|
+
changes.push({
|
|
119
|
+
filePath,
|
|
120
|
+
oldString: "",
|
|
121
|
+
newString: data.content ?? "",
|
|
122
|
+
operation: "created",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else if (action.type === "delete_file") {
|
|
126
|
+
changes.push({
|
|
127
|
+
filePath,
|
|
128
|
+
oldString: data.oldContent ?? "(deleted)",
|
|
129
|
+
newString: "",
|
|
130
|
+
operation: "deleted",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return changes;
|
|
135
|
+
}
|
|
422
136
|
async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = exports.DEFAULT_BRIDGE_PORT) {
|
|
423
|
-
const bridgeServer = node_http_1.default.createServer();
|
|
137
|
+
const bridgeServer = node_http_1.default.createServer();
|
|
424
138
|
const wss = new ws_1.WebSocketServer({ server: bridgeServer });
|
|
425
139
|
let overlaySocket = null;
|
|
426
140
|
const taskSnapshots = new Map();
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
141
|
+
const activeSessions = new Map();
|
|
142
|
+
// Per-task runtime facade: LocalRuntime for local coding agents (executed
|
|
143
|
+
// in this process), RuntimeClient for hosted models (via Lasso server).
|
|
144
|
+
const taskFacades = new Map();
|
|
145
|
+
const taskControllers = new Map();
|
|
146
|
+
const correctiveAttempts = new Map();
|
|
431
147
|
const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
|
|
432
148
|
const fileEnv = envRoots.reduce((values, root) => ({
|
|
433
149
|
...values,
|
|
@@ -435,7 +151,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
435
151
|
...readEnvFile(root, ".env.local"),
|
|
436
152
|
...readEnvFile(root, ".env.production"),
|
|
437
153
|
}), {});
|
|
438
|
-
// Prioritize CLI auth credentials over env vars
|
|
439
154
|
const cliCredentials = (0, auth_1.loadCredentials)();
|
|
440
155
|
const resolvedLassoKey = cliCredentials?.apiKey ||
|
|
441
156
|
(0, project_1.resolveLassoApiKey)(fileEnv) ||
|
|
@@ -447,164 +162,62 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
447
162
|
fileEnv.NEXT_LASSO_API_KEY ||
|
|
448
163
|
"";
|
|
449
164
|
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: [] };
|
|
165
|
+
const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv) || "https://api.lasso.byorello.space";
|
|
166
|
+
const runtimeClient = new runtime_client_1.RuntimeClient(serverUrl, resolvedLassoKey || undefined);
|
|
167
|
+
// Load project info
|
|
168
|
+
const workspaceConfig = await (0, project_1.resolveWorkspaceConfig)(fileEnv);
|
|
169
|
+
const projectId = workspaceConfig?.workspaceId || "";
|
|
170
|
+
const framework = detectProjectFramework(cwd);
|
|
171
|
+
// Fetch models
|
|
172
|
+
let models = [];
|
|
571
173
|
try {
|
|
572
|
-
|
|
174
|
+
const modelsResponse = await (0, project_1.fetchModels)(fileEnv);
|
|
175
|
+
const allModels = [
|
|
176
|
+
...(modelsResponse.anthropic || []),
|
|
177
|
+
...(modelsResponse.openai || []),
|
|
178
|
+
...(modelsResponse.google || []),
|
|
179
|
+
...(modelsResponse.nvidia || []),
|
|
180
|
+
];
|
|
181
|
+
models = allModels.map((m) => ({
|
|
182
|
+
id: m.id,
|
|
183
|
+
label: m.name || m.id,
|
|
184
|
+
provider: m.provider,
|
|
185
|
+
locked: false,
|
|
186
|
+
}));
|
|
573
187
|
}
|
|
574
|
-
catch
|
|
575
|
-
|
|
188
|
+
catch {
|
|
189
|
+
models = [
|
|
190
|
+
{ id: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet", provider: "anthropic", locked: false },
|
|
191
|
+
{ id: "gpt-4o", label: "GPT-4o", provider: "openai", locked: false },
|
|
192
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google", locked: false },
|
|
193
|
+
];
|
|
576
194
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
195
|
+
// Detect local coding agents (installed + authenticated) on this machine
|
|
196
|
+
const detectedBackends = await (0, backends_1.detectBackends)();
|
|
197
|
+
const BACKEND_LABELS = {
|
|
198
|
+
claude_code: "Claude Code",
|
|
199
|
+
opencode: "OpenCode",
|
|
200
|
+
codex_cli: "Codex CLI",
|
|
201
|
+
cursor: "Cursor",
|
|
583
202
|
};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
})();
|
|
598
|
-
let activeAgentController = null;
|
|
599
|
-
const taskControllers = new Map();
|
|
600
|
-
let localAgents = new Set();
|
|
203
|
+
for (const backend of detectedBackends) {
|
|
204
|
+
if (!backend.installed)
|
|
205
|
+
continue;
|
|
206
|
+
for (const option of backend.models) {
|
|
207
|
+
models.push({
|
|
208
|
+
id: option.id,
|
|
209
|
+
label: `${BACKEND_LABELS[backend.provider]} · ${option.label}`,
|
|
210
|
+
provider: backend.provider,
|
|
211
|
+
locked: !backend.authenticated,
|
|
212
|
+
lockedReason: backend.authenticated ? undefined : backend.error || `${BACKEND_LABELS[backend.provider]} is not authenticated.`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
601
216
|
const pageIgnoredDirs = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "out", "coverage", ".vercel", ".cache"]);
|
|
602
217
|
const resolveInsideProject = (root, relative) => {
|
|
603
218
|
const target = node_path_1.default.resolve(root, relative && relative !== "." ? relative : "");
|
|
604
219
|
return target === root || target.startsWith(`${root}${node_path_1.default.sep}`) ? target : null;
|
|
605
220
|
};
|
|
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
221
|
const pageEntries = (root, relative) => {
|
|
609
222
|
const directory = resolveInsideProject(root, relative);
|
|
610
223
|
if (!directory)
|
|
@@ -621,730 +234,767 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
621
234
|
});
|
|
622
235
|
}
|
|
623
236
|
}
|
|
624
|
-
catch {
|
|
625
|
-
// Folders that cannot be read are treated as empty.
|
|
626
|
-
}
|
|
237
|
+
catch { }
|
|
627
238
|
return entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
|
|
628
239
|
};
|
|
629
240
|
bridgeServer.on("request", (req, res) => {
|
|
630
241
|
if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
|
|
631
242
|
return;
|
|
632
|
-
activeAgentController?.abort();
|
|
633
243
|
for (const controller of taskControllers.values())
|
|
634
244
|
controller.abort();
|
|
635
245
|
taskControllers.clear();
|
|
636
|
-
activeAgentController = null;
|
|
637
246
|
for (const socket of wss.clients)
|
|
638
247
|
socket.close(1000, "Bridge restarted by the CLI");
|
|
639
248
|
res.writeHead(200, { "content-type": "application/json" });
|
|
640
249
|
res.end(JSON.stringify({ ok: true }));
|
|
641
250
|
});
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
251
|
+
// Shared runtime event handling for edit/ask, oneshot, and corrective runs.
|
|
252
|
+
// review=true opens the diff review on agent_completed (edit/ask flow).
|
|
253
|
+
function handleRuntimeEvent(event, socket, taskId, session, instruction, review) {
|
|
254
|
+
if (taskControllers.get(taskId)?.signal.aborted)
|
|
255
|
+
return;
|
|
256
|
+
switch (event.type) {
|
|
257
|
+
case "agent_started":
|
|
258
|
+
case "agent_reasoning":
|
|
259
|
+
socket.send(JSON.stringify({
|
|
260
|
+
type: "agent_status",
|
|
261
|
+
taskId,
|
|
262
|
+
status: "thinking",
|
|
263
|
+
message: event.data.summary || "Working...",
|
|
264
|
+
}));
|
|
265
|
+
break;
|
|
266
|
+
case "tool_started":
|
|
267
|
+
socket.send(JSON.stringify({
|
|
268
|
+
type: "agent_status",
|
|
269
|
+
taskId,
|
|
270
|
+
status: "working",
|
|
271
|
+
message: `Running ${event.data.tool || event.data.actionType || "tool"}...`,
|
|
272
|
+
}));
|
|
273
|
+
break;
|
|
274
|
+
case "tool_completed":
|
|
275
|
+
socket.send(JSON.stringify({
|
|
276
|
+
type: "agent_status",
|
|
277
|
+
taskId,
|
|
278
|
+
status: "working",
|
|
279
|
+
message: "Completed tool",
|
|
280
|
+
}));
|
|
281
|
+
break;
|
|
282
|
+
case "phase_changed": {
|
|
283
|
+
const phase = event.data.phase;
|
|
284
|
+
socket.send(JSON.stringify({
|
|
285
|
+
type: "agent_status",
|
|
286
|
+
taskId,
|
|
287
|
+
status: "working",
|
|
288
|
+
message: `Phase: ${phase}`,
|
|
289
|
+
}));
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "verification_started":
|
|
293
|
+
socket.send(JSON.stringify({
|
|
294
|
+
type: "agent_status",
|
|
295
|
+
taskId,
|
|
296
|
+
status: "review",
|
|
297
|
+
message: "Verifying changes...",
|
|
298
|
+
}));
|
|
299
|
+
break;
|
|
300
|
+
case "verification_completed": {
|
|
301
|
+
const passed = event.data.success ?? event.data.passed;
|
|
302
|
+
socket.send(JSON.stringify({
|
|
303
|
+
type: "agent_status",
|
|
304
|
+
taskId,
|
|
305
|
+
status: passed ? "working" : "error",
|
|
306
|
+
message: passed ? "Verification passed" : "Verification failed",
|
|
307
|
+
}));
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "agent_completed":
|
|
311
|
+
if (!review)
|
|
312
|
+
break;
|
|
313
|
+
void handleReviewReady(session, socket, taskId, instruction);
|
|
314
|
+
break;
|
|
315
|
+
case "error":
|
|
316
|
+
socket.send(JSON.stringify({
|
|
317
|
+
type: "agent_status",
|
|
318
|
+
taskId,
|
|
319
|
+
status: "error",
|
|
320
|
+
message: event.data.error || "Unknown error",
|
|
321
|
+
}));
|
|
322
|
+
break;
|
|
323
|
+
case "waiting_for_user":
|
|
324
|
+
socket.send(JSON.stringify({
|
|
325
|
+
type: "agent_prompt",
|
|
326
|
+
taskId,
|
|
327
|
+
prompt: {
|
|
328
|
+
message: event.data?.question || "Input required",
|
|
329
|
+
kind: "input",
|
|
330
|
+
},
|
|
331
|
+
}));
|
|
332
|
+
break;
|
|
653
333
|
}
|
|
654
334
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
335
|
+
// The server rolled the workspace back before review, so the current files
|
|
336
|
+
// are the originals: capture them for undo, then open the review panel.
|
|
337
|
+
async function handleReviewReady(session, socket, taskId, instruction) {
|
|
658
338
|
try {
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
.
|
|
664
|
-
|
|
339
|
+
const facade = taskFacades.get(taskId) || runtimeClient;
|
|
340
|
+
const finalSession = await facade.getSession(session.id);
|
|
341
|
+
const changes = extractChangesFromSession(finalSession);
|
|
342
|
+
if (changes.length > 0) {
|
|
343
|
+
taskSnapshots.set(taskId, changes.map((change) => {
|
|
344
|
+
const abs = node_path_1.default.join(cwd, change.filePath);
|
|
345
|
+
let content = null;
|
|
346
|
+
try {
|
|
347
|
+
content = node_fs_1.default.readFileSync(abs, "utf8");
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
content = null; // did not exist before the task (created file)
|
|
351
|
+
}
|
|
352
|
+
return { filePath: change.filePath, content };
|
|
353
|
+
}));
|
|
354
|
+
(0, snapshot_1.createSnapshot)(cwd, taskId, instruction, changes.map((change) => change.filePath), undefined, session.id);
|
|
355
|
+
}
|
|
356
|
+
socket.send(JSON.stringify({
|
|
357
|
+
type: "agent_status",
|
|
358
|
+
taskId,
|
|
359
|
+
status: "review",
|
|
360
|
+
message: changes.length > 0 ? "Review changes" : "No file changes were produced",
|
|
361
|
+
changes,
|
|
362
|
+
}));
|
|
665
363
|
}
|
|
666
|
-
catch {
|
|
667
|
-
|
|
364
|
+
catch (error) {
|
|
365
|
+
socket.send(JSON.stringify({
|
|
366
|
+
type: "agent_status",
|
|
367
|
+
taskId,
|
|
368
|
+
status: "error",
|
|
369
|
+
message: error instanceof Error ? error.message : "Failed to load review changes",
|
|
370
|
+
}));
|
|
668
371
|
}
|
|
669
372
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
373
|
+
// Handler for edit/ask - uses runtime
|
|
374
|
+
async function handleEditOrAsk(message, socket, taskId, instruction, model, provider, context, element, messages) {
|
|
375
|
+
taskControllers.get(taskId)?.abort();
|
|
376
|
+
const controller = new AbortController();
|
|
377
|
+
taskControllers.set(taskId, controller);
|
|
378
|
+
socket.send(JSON.stringify({
|
|
379
|
+
type: "agent_status",
|
|
380
|
+
taskId,
|
|
381
|
+
status: "thinking",
|
|
382
|
+
message: "Starting...",
|
|
383
|
+
}));
|
|
673
384
|
try {
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
385
|
+
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
386
|
+
// Overlay model option ids are prefixed ("claude_code:sonnet") — the
|
|
387
|
+
// agent CLIs need the unprefixed/normalized form.
|
|
388
|
+
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
389
|
+
const sessionInput = {
|
|
390
|
+
userPrompt: instruction,
|
|
391
|
+
project: {
|
|
392
|
+
projectId,
|
|
393
|
+
framework,
|
|
394
|
+
hasTypeScript: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json")),
|
|
395
|
+
hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) &&
|
|
396
|
+
node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
|
|
397
|
+
},
|
|
398
|
+
model: {
|
|
399
|
+
provider: (provider || "anthropic"),
|
|
400
|
+
id: model || "claude-3-5-sonnet-20241022",
|
|
401
|
+
},
|
|
402
|
+
selection: context ? {
|
|
403
|
+
element: element ? {
|
|
404
|
+
tag: element.tag,
|
|
405
|
+
text: element.label,
|
|
406
|
+
source: element.sourceHint,
|
|
407
|
+
attributes: context.attributes,
|
|
408
|
+
} : undefined,
|
|
409
|
+
viewport: context?.viewport,
|
|
410
|
+
route: context?.position?.route,
|
|
411
|
+
screenshot: context?.screenshots?.full,
|
|
412
|
+
} : undefined,
|
|
413
|
+
backendConfig: isLocalAgent ? {
|
|
414
|
+
type: "local_agent",
|
|
415
|
+
provider: provider,
|
|
416
|
+
modelId: backendModelId,
|
|
417
|
+
projectPath: cwd,
|
|
418
|
+
} : undefined,
|
|
419
|
+
};
|
|
420
|
+
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
421
|
+
const session = await facade.createSession(sessionInput);
|
|
422
|
+
activeSessions.set(taskId, session);
|
|
423
|
+
taskFacades.set(taskId, facade);
|
|
424
|
+
// Originals are captured when the review is ready (the workspace was
|
|
425
|
+
// already rolled back at that point).
|
|
426
|
+
await facade.runSession(session.id, cwd, (event) => {
|
|
427
|
+
handleRuntimeEvent(event, socket, taskId, session, `Runtime session: ${instruction}`, true);
|
|
428
|
+
}, controller.signal);
|
|
686
429
|
}
|
|
687
|
-
catch {
|
|
688
|
-
|
|
430
|
+
catch (error) {
|
|
431
|
+
socket.send(JSON.stringify({
|
|
432
|
+
type: "agent_status",
|
|
433
|
+
taskId,
|
|
434
|
+
status: "error",
|
|
435
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
436
|
+
}));
|
|
689
437
|
}
|
|
690
438
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
// Prioritize workspace config (dashboard) over project config
|
|
704
|
-
// Workspace config is fetched independently of project config
|
|
705
|
-
const workspacePlan = (workspaceConfig?.plan || "free").toLowerCase();
|
|
706
|
-
const workspaceProviders = workspaceConfig?.configuredProviders || [];
|
|
707
|
-
const projectPlan = (collabConfig?.plan || "free").toLowerCase();
|
|
708
|
-
const projectProviders = collabConfig?.configuredProviders || [];
|
|
709
|
-
// Use the highest-tier plan available
|
|
710
|
-
const isPaidPlan = workspacePlan !== "free" || projectPlan !== "free";
|
|
711
|
-
// Use workspace providers if available, otherwise fall back to project providers
|
|
712
|
-
const configuredProviders = workspaceProviders.length > 0 ? workspaceProviders : projectProviders;
|
|
713
|
-
// Build model list from dynamic server fetch, falling back to hardcoded models
|
|
714
|
-
const allModels = [
|
|
715
|
-
// Dynamic models from server API
|
|
716
|
-
...dynamicModels.anthropic.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
|
|
717
|
-
...dynamicModels.openai.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
|
|
718
|
-
...dynamicModels.google.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
|
|
719
|
-
...dynamicModels.nvidia.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
|
|
720
|
-
// Fallback hardcoded models (only if dynamic fetch failed)
|
|
721
|
-
...(dynamicModels.anthropic.length === 0 ? [
|
|
722
|
-
{ id: "claude-opus-4-5-20251101", label: "Claude Opus 4.5", provider: "anthropic" },
|
|
723
|
-
{ id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
|
|
724
|
-
{ id: "claude-haiku-4-5-20251015", label: "Claude Haiku 4.5", provider: "anthropic" },
|
|
725
|
-
] : []),
|
|
726
|
-
...(dynamicModels.openai.length === 0 ? [
|
|
727
|
-
{ id: "gpt-5.1", label: "GPT-5.1", provider: "openai" },
|
|
728
|
-
{ id: "gpt-5.1-codex", label: "GPT-5.1 Codex", provider: "openai" },
|
|
729
|
-
{ id: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex Mini", provider: "openai" },
|
|
730
|
-
{ id: "o3", label: "OpenAI o3", provider: "openai" },
|
|
731
|
-
{ id: "o3-mini", label: "OpenAI o3-mini", provider: "openai" },
|
|
732
|
-
] : []),
|
|
733
|
-
...(dynamicModels.google.length === 0 ? [
|
|
734
|
-
{ id: "gemini-3-pro-preview", label: "Gemini 3 Pro", provider: "google" },
|
|
735
|
-
{ id: "gemini-3-flash-preview", label: "Gemini 3 Flash", provider: "google" },
|
|
736
|
-
{ id: "gemini-2.5-flash-preview-09-2025", label: "Gemini 2.5 Flash", provider: "google" },
|
|
737
|
-
{ id: "gemini-2.5-flash-lite-preview-09-2025", label: "Gemini 2.5 Flash Lite", provider: "google" },
|
|
738
|
-
] : []),
|
|
739
|
-
...(dynamicModels.nvidia.length === 0 ? [
|
|
740
|
-
{ id: "nvidia/llama-3.3-nemotron-super-49b-v1", label: "Llama 3.3 Nemotron Super 49B", provider: "nvidia" },
|
|
741
|
-
{ id: "deepseek-ai/deepseek-r1-distill-llama-70b", label: "DeepSeek R1 Distill Llama 70B", provider: "nvidia" },
|
|
742
|
-
{ id: "deepseek-ai/deepseek-r1-distill-llama-8b", label: "DeepSeek R1 Distill Llama 8B", provider: "nvidia" },
|
|
743
|
-
{ id: "google/gemma-3-1b-it", label: "Gemma 3 1B Instruct", provider: "nvidia" },
|
|
744
|
-
{ id: "openai/gpt-oss-120b", label: "GPT-OSS 120B", provider: "nvidia" },
|
|
745
|
-
] : []),
|
|
746
|
-
// Local CLI agents (always included)
|
|
747
|
-
...(localAgents.has("claude-code") ? [
|
|
748
|
-
{ id: "claude-code:sonnet", label: "Claude Code · Sonnet 4.5", provider: "cli" },
|
|
749
|
-
{ id: "claude-code:opus", label: "Claude Code · Opus 4.5", provider: "cli" },
|
|
750
|
-
{ id: "claude-code:haiku", label: "Claude Code · Haiku 4.5", provider: "cli" },
|
|
751
|
-
] : []),
|
|
752
|
-
...(localAgents.has("codex") ? [
|
|
753
|
-
{ id: "codex:gpt-5.1", label: "Codex · GPT-5.1", provider: "cli" },
|
|
754
|
-
{ id: "codex:gpt-5.1-codex", label: "Codex · GPT-5.1-Codex", provider: "cli" },
|
|
755
|
-
] : []),
|
|
756
|
-
...(localAgents.has("opencode") ? [
|
|
757
|
-
{ id: "opencode:opencode/big-pickle", label: "OpenCode · Big Pickle", provider: "cli" },
|
|
758
|
-
{ id: "opencode:deepseek/deepseek-chat", label: "OpenCode · DeepSeek Chat", provider: "cli" },
|
|
759
|
-
{ id: "opencode:anthropic/claude-sonnet-4-5", label: "OpenCode · Claude Sonnet 4.5", provider: "cli" },
|
|
760
|
-
{ id: "opencode:openai/gpt-5.1", label: "OpenCode · GPT-5.1", provider: "cli" },
|
|
761
|
-
] : []),
|
|
762
|
-
...(localAgents.has("cursor") ? [
|
|
763
|
-
{ id: "cursor:claude-sonnet-4.5", label: "Cursor · Claude Sonnet 4.5", provider: "cli" },
|
|
764
|
-
{ id: "cursor:claude-opus-4", label: "Cursor · Claude Opus 4", provider: "cli" },
|
|
765
|
-
{ id: "cursor:claude-3.5-sonnet", label: "Cursor · Claude 3.5 Sonnet", provider: "cli" },
|
|
766
|
-
{ id: "cursor:gpt-4o", label: "Cursor · GPT-4o", provider: "cli" },
|
|
767
|
-
{ id: "cursor:gpt-4o-mini", label: "Cursor · GPT-4o mini", provider: "cli" },
|
|
768
|
-
] : []),
|
|
769
|
-
...discoveredOpenCodeModels,
|
|
770
|
-
...discoveredCursorModels,
|
|
771
|
-
...locals,
|
|
772
|
-
];
|
|
773
|
-
// Lock models based on provider API key configuration
|
|
774
|
-
// Paid plans have all models unlocked
|
|
775
|
-
// Free plans only have models unlocked for providers with configured API keys
|
|
776
|
-
const providerKeyMapping = {
|
|
777
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
778
|
-
openai: "OPENAI_API_KEY",
|
|
779
|
-
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
780
|
-
nvidia: "NVIDIA_API_KEY",
|
|
781
|
-
ollama: "OLLAMA_BASE_URL",
|
|
782
|
-
};
|
|
783
|
-
return allModels.map((model) => {
|
|
784
|
-
if (model.provider === "cli") {
|
|
785
|
-
// CLI agents are always unlocked (user manages their own credentials)
|
|
786
|
-
return model;
|
|
787
|
-
}
|
|
788
|
-
if (isPaidPlan) {
|
|
789
|
-
// Paid plans have all hosted models unlocked
|
|
790
|
-
return model;
|
|
791
|
-
}
|
|
792
|
-
// Free plans: check if provider is configured
|
|
793
|
-
let isConfigured = false;
|
|
794
|
-
let lockedReason = "Configure your API key in the dashboard to use this model";
|
|
795
|
-
if (hasCliAuth) {
|
|
796
|
-
// User has CLI auth: check dashboard-configured providers first
|
|
797
|
-
isConfigured = configuredProviders.includes(model.provider);
|
|
798
|
-
lockedReason = "Configure this provider in your workspace dashboard to use this model";
|
|
799
|
-
}
|
|
800
|
-
// If not configured via dashboard, fall back to env-based detection
|
|
801
|
-
if (!isConfigured) {
|
|
802
|
-
const envKey = providerKeyMapping[model.provider];
|
|
803
|
-
isConfigured = Boolean(process.env[envKey]) ||
|
|
804
|
-
Boolean(fileEnv[envKey]) ||
|
|
805
|
-
Boolean(localProviderKeys[model.provider]);
|
|
806
|
-
if (!isConfigured) {
|
|
807
|
-
lockedReason = hasCliAuth
|
|
808
|
-
? "Configure this provider in your workspace dashboard, or set the provider API key in your .env file"
|
|
809
|
-
: "Run `lasso auth login` to configure providers in your dashboard, or set the provider API key in your .env file";
|
|
810
|
-
}
|
|
811
|
-
else {
|
|
812
|
-
lockedReason = "";
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (!isConfigured) {
|
|
816
|
-
return {
|
|
817
|
-
...model,
|
|
818
|
-
locked: true,
|
|
819
|
-
lockedReason: lockedReason || "Configure your API key to use this model",
|
|
820
|
-
};
|
|
821
|
-
}
|
|
822
|
-
return model;
|
|
823
|
-
});
|
|
824
|
-
};
|
|
825
|
-
wss.on("connection", (socket) => {
|
|
826
|
-
overlaySocket = socket;
|
|
827
|
-
currentSocket = socket;
|
|
828
|
-
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
829
|
-
const creds = (0, auth_1.loadCredentials)();
|
|
830
|
-
const userInfo = creds?.userName
|
|
831
|
-
? { name: creds.userName, email: creds.userEmail }
|
|
832
|
-
: (process.env.USER ? { name: process.env.USER } : null);
|
|
833
|
-
socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: lassoKeyConfigured || Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null, user: userInfo }));
|
|
834
|
-
void availableModels(collabConfig, workspaceConfig).then((models) => {
|
|
835
|
-
if (socket.readyState === socket.OPEN)
|
|
836
|
-
socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: lassoKeyConfigured || Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null, user: userInfo }));
|
|
837
|
-
});
|
|
838
|
-
void getGitState(cwd).then((git) => {
|
|
839
|
-
if (socket.readyState === socket.OPEN)
|
|
840
|
-
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
841
|
-
});
|
|
842
|
-
socket.on("message", async (raw) => {
|
|
843
|
-
const msg = JSON.parse(raw.toString());
|
|
844
|
-
if (msg.type === "edit" || msg.type === "ask") {
|
|
845
|
-
console.log(`[lasso] received ${msg.type}:`, { model: msg.model, provider: msg.provider, selectionId: msg.context?.selectionId, messages: msg.messages?.length || 0, changes: msg.type === "edit" ? msg.changesHistory?.length || 0 : 0, screenshots: Boolean(msg.context?.screenshots?.full || msg.context?.screenshots?.element) });
|
|
846
|
-
}
|
|
847
|
-
else {
|
|
848
|
-
console.log("[lasso] received from overlay:", msg);
|
|
849
|
-
}
|
|
850
|
-
if (msg.type === "list_page_folders") {
|
|
851
|
-
const requested = msg.path || ".";
|
|
852
|
-
const root = node_path_1.default.resolve(cwd);
|
|
853
|
-
const directory = resolveInsideProject(root, requested);
|
|
854
|
-
if (!directory) {
|
|
855
|
-
socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: [], error: "That folder is outside the current project." }));
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: pageEntries(root, requested), project: node_path_1.default.basename(root) }));
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (msg.type === "create_page_folder") {
|
|
862
|
-
const root = node_path_1.default.resolve(cwd);
|
|
863
|
-
const name = node_path_1.default.basename(msg.name || "");
|
|
864
|
-
const parent = resolveInsideProject(root, msg.parent || ".");
|
|
865
|
-
if (!parent || !name || name !== msg.name || name === "." || name === "..") {
|
|
866
|
-
socket.send(JSON.stringify({ type: "page_folder_created", path: msg.name || "", error: "Enter a folder name without slashes or dots." }));
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const target = node_path_1.default.resolve(parent, name);
|
|
870
|
-
if (!target.startsWith(`${root}${node_path_1.default.sep}`)) {
|
|
871
|
-
socket.send(JSON.stringify({ type: "page_folder_created", path: name, error: "Choose a folder inside the current project." }));
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
439
|
+
// Handler for apply - applies approved pending changes, then runs server-side
|
|
440
|
+
// verification. On failure a corrective run is started automatically.
|
|
441
|
+
async function handleApply(message, socket, cwd) {
|
|
442
|
+
const { taskId, changes } = message;
|
|
443
|
+
const session = activeSessions.get(taskId);
|
|
444
|
+
const filePaths = changes.map((c) => c.filePath);
|
|
445
|
+
// The server rolled the workspace back before review, so the disk still
|
|
446
|
+
// holds the originals — capture them now (if the review didn't already).
|
|
447
|
+
if (filePaths.length > 0 && (!taskSnapshots.has(taskId) || (taskSnapshots.get(taskId) || []).length === 0)) {
|
|
448
|
+
taskSnapshots.set(taskId, changes.map((change) => {
|
|
449
|
+
const abs = node_path_1.default.join(cwd, change.filePath);
|
|
450
|
+
let content = null;
|
|
874
451
|
try {
|
|
875
|
-
node_fs_1.default.
|
|
876
|
-
socket.send(JSON.stringify({ type: "page_folder_created", path: node_path_1.default.relative(root, target) }));
|
|
452
|
+
content = node_fs_1.default.readFileSync(abs, "utf8");
|
|
877
453
|
}
|
|
878
|
-
catch
|
|
879
|
-
|
|
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;
|
|
454
|
+
catch {
|
|
455
|
+
content = null;
|
|
902
456
|
}
|
|
457
|
+
return { filePath: change.filePath, content };
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
if (filePaths.length > 0) {
|
|
461
|
+
(0, snapshot_1.createSnapshot)(cwd, taskId, `Apply changes for task ${taskId}`, filePaths, undefined, session?.id);
|
|
462
|
+
}
|
|
463
|
+
const applyError = applyChanges(changes, cwd);
|
|
464
|
+
if (applyError) {
|
|
465
|
+
// Roll back partial application so the project stays consistent
|
|
466
|
+
const originals = taskSnapshots.get(taskId) || [];
|
|
467
|
+
for (const item of originals) {
|
|
903
468
|
try {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
};
|
|
469
|
+
const abs = node_path_1.default.join(cwd, item.filePath);
|
|
470
|
+
if (item.content === null) {
|
|
471
|
+
if (node_fs_1.default.existsSync(abs))
|
|
472
|
+
node_fs_1.default.unlinkSync(abs);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(abs), { recursive: true });
|
|
476
|
+
node_fs_1.default.writeFileSync(abs, item.content);
|
|
477
|
+
}
|
|
949
478
|
}
|
|
950
|
-
|
|
951
|
-
enqueueTask(msg.taskId, msg, selectedConfig, 1); // Priority 1 for ask tasks
|
|
479
|
+
catch { }
|
|
952
480
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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);
|
|
481
|
+
socket.send(JSON.stringify({
|
|
482
|
+
type: "agent_status",
|
|
483
|
+
taskId,
|
|
484
|
+
status: "error",
|
|
485
|
+
message: applyError,
|
|
486
|
+
}));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Post-apply verification (inconclusive when the engine can't run —
|
|
490
|
+
// never reported as a failure of the changes themselves).
|
|
491
|
+
let validation;
|
|
492
|
+
const facade = taskFacades.get(taskId) || runtimeClient;
|
|
493
|
+
if (session) {
|
|
494
|
+
try {
|
|
495
|
+
const result = await facade.verifySession(session.id, cwd);
|
|
496
|
+
validation = {
|
|
497
|
+
passed: result.passed,
|
|
498
|
+
errors: result.errors.map((e) => e.message),
|
|
499
|
+
command: result.command || "verification",
|
|
500
|
+
};
|
|
992
501
|
}
|
|
993
|
-
|
|
994
|
-
|
|
502
|
+
catch {
|
|
503
|
+
validation = undefined;
|
|
995
504
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
505
|
+
}
|
|
506
|
+
socket.send(JSON.stringify({
|
|
507
|
+
type: "applied",
|
|
508
|
+
taskId,
|
|
509
|
+
message: "Changes applied successfully",
|
|
510
|
+
...(validation ? { validation } : {}),
|
|
511
|
+
}));
|
|
512
|
+
if (validation && !validation.passed && session) {
|
|
513
|
+
await runCorrectiveFix(session, facade, taskId, socket);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Apply approved changes to disk. Returns an error message or null. */
|
|
517
|
+
function applyChanges(changes, cwd) {
|
|
518
|
+
for (const change of changes) {
|
|
519
|
+
const filePath = node_path_1.default.join(cwd, change.filePath);
|
|
520
|
+
const dir = node_path_1.default.dirname(filePath);
|
|
521
|
+
const oldString = typeof change.oldString === "string" ? change.oldString : "";
|
|
522
|
+
const newString = typeof change.newString === "string" ? change.newString : "";
|
|
523
|
+
const operation = change.operation ||
|
|
524
|
+
(oldString === "" && newString === "" ? "deleted" : oldString === "" ? "created" : "modified");
|
|
525
|
+
try {
|
|
526
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
527
|
+
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
1012
528
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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;
|
|
529
|
+
if (operation === "deleted") {
|
|
530
|
+
if (node_fs_1.default.existsSync(filePath)) {
|
|
531
|
+
node_fs_1.default.unlinkSync(filePath);
|
|
532
|
+
}
|
|
1027
533
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
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) }));
|
|
534
|
+
else if (operation === "created" || (oldString === "" && operation !== "modified")) {
|
|
535
|
+
node_fs_1.default.writeFileSync(filePath, newString);
|
|
1052
536
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
537
|
+
else {
|
|
538
|
+
// Full-content replacement; split/join avoids String.replace's
|
|
539
|
+
// special $-pattern handling in the replacement text.
|
|
540
|
+
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
541
|
+
let next;
|
|
542
|
+
if (content === oldString) {
|
|
543
|
+
next = newString;
|
|
544
|
+
}
|
|
545
|
+
else if (oldString && content.includes(oldString)) {
|
|
546
|
+
next = content.split(oldString).join(newString);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
return `Cannot apply ${change.filePath}: the file changed since the diff was generated.`;
|
|
550
|
+
}
|
|
551
|
+
node_fs_1.default.writeFileSync(filePath, next);
|
|
1055
552
|
}
|
|
1056
553
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
return `Failed to apply ${change.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
1070
556
|
}
|
|
1071
|
-
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
// Automatic corrective run after failed post-apply verification (bounded).
|
|
561
|
+
async function runCorrectiveFix(session, facade, taskId, socket) {
|
|
562
|
+
const attempts = correctiveAttempts.get(taskId) || 0;
|
|
563
|
+
if (attempts >= 2)
|
|
564
|
+
return;
|
|
565
|
+
correctiveAttempts.set(taskId, attempts + 1);
|
|
566
|
+
taskControllers.get(taskId)?.abort();
|
|
567
|
+
const controller = new AbortController();
|
|
568
|
+
taskControllers.set(taskId, controller);
|
|
569
|
+
socket.send(JSON.stringify({
|
|
570
|
+
type: "agent_status",
|
|
571
|
+
taskId,
|
|
572
|
+
status: "thinking",
|
|
573
|
+
message: "Fixing verification errors...",
|
|
574
|
+
}));
|
|
575
|
+
try {
|
|
576
|
+
await facade.runSession(session.id, cwd, (event) => {
|
|
577
|
+
handleRuntimeEvent(event, socket, taskId, session, "Corrective run", true);
|
|
578
|
+
}, controller.signal);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
socket.send(JSON.stringify({
|
|
582
|
+
type: "agent_status",
|
|
583
|
+
taskId,
|
|
584
|
+
status: "error",
|
|
585
|
+
message: error instanceof Error ? error.message : "Corrective run failed",
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Handler for undo - restores snapshot
|
|
590
|
+
async function handleUndo(message, socket, cwd) {
|
|
591
|
+
const { taskId } = message;
|
|
592
|
+
const snapshot = taskSnapshots.get(taskId) || [];
|
|
593
|
+
for (const item of snapshot) {
|
|
594
|
+
if (item.content === null) {
|
|
595
|
+
// File didn't exist, delete if it was created
|
|
1072
596
|
try {
|
|
1073
|
-
|
|
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." }));
|
|
597
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(cwd, item.filePath));
|
|
1079
598
|
}
|
|
599
|
+
catch { }
|
|
1080
600
|
}
|
|
1081
|
-
else
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
}
|
|
601
|
+
else {
|
|
602
|
+
// Restore original content
|
|
603
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, item.filePath), item.content);
|
|
1116
604
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
605
|
+
}
|
|
606
|
+
socket.send(JSON.stringify({
|
|
607
|
+
type: "undone",
|
|
608
|
+
taskId,
|
|
609
|
+
message: "Changes undone",
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
// Handler for stop
|
|
613
|
+
async function handleStop(message, socket) {
|
|
614
|
+
const { taskId } = message;
|
|
615
|
+
if (taskId) {
|
|
616
|
+
taskControllers.get(taskId)?.abort();
|
|
617
|
+
const session = activeSessions.get(taskId);
|
|
618
|
+
const facade = taskFacades.get(taskId);
|
|
619
|
+
if (session && facade)
|
|
620
|
+
void facade.cancelSession(session.id).catch(() => { });
|
|
621
|
+
activeSessions.delete(taskId);
|
|
622
|
+
taskFacades.delete(taskId);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
for (const controller of taskControllers.values())
|
|
626
|
+
controller.abort();
|
|
627
|
+
taskControllers.clear();
|
|
628
|
+
for (const [id, session] of activeSessions) {
|
|
629
|
+
const facade = taskFacades.get(id);
|
|
630
|
+
if (facade)
|
|
631
|
+
void facade.cancelSession(session.id).catch(() => { });
|
|
632
|
+
}
|
|
633
|
+
activeSessions.clear();
|
|
634
|
+
taskFacades.clear();
|
|
635
|
+
}
|
|
636
|
+
socket.send(JSON.stringify({
|
|
637
|
+
type: "agent_status",
|
|
638
|
+
taskId,
|
|
639
|
+
status: "stopped",
|
|
640
|
+
message: "Agent stopped",
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
// Handler for switch_model
|
|
644
|
+
async function handleSwitchModel(message, socket) {
|
|
645
|
+
const { taskId, provider, modelId } = message;
|
|
646
|
+
const session = activeSessions.get(taskId);
|
|
647
|
+
if (!session) {
|
|
648
|
+
socket.send(JSON.stringify({
|
|
649
|
+
type: "agent_status",
|
|
650
|
+
taskId,
|
|
651
|
+
status: "error",
|
|
652
|
+
message: "No active session for this task",
|
|
653
|
+
}));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
try {
|
|
657
|
+
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
658
|
+
const facade = taskFacades.get(taskId) || runtimeClient;
|
|
659
|
+
await facade.switchModel(session.id, {
|
|
660
|
+
provider: provider,
|
|
661
|
+
id: modelId,
|
|
662
|
+
}, undefined, isLocalAgent
|
|
663
|
+
? {
|
|
664
|
+
type: "local_agent",
|
|
665
|
+
provider: provider,
|
|
666
|
+
modelId: (0, backends_1.modelIdForBackend)(modelId || "", provider || ""),
|
|
667
|
+
projectPath: cwd,
|
|
668
|
+
}
|
|
669
|
+
: undefined);
|
|
670
|
+
socket.send(JSON.stringify({
|
|
671
|
+
type: "agent_status",
|
|
672
|
+
taskId,
|
|
673
|
+
status: "working",
|
|
674
|
+
message: `Switched to ${provider}/${modelId}`,
|
|
675
|
+
}));
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
socket.send(JSON.stringify({
|
|
679
|
+
type: "agent_status",
|
|
680
|
+
taskId,
|
|
681
|
+
status: "error",
|
|
682
|
+
message: error instanceof Error ? error.message : "Failed to switch model",
|
|
683
|
+
}));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Handler for oneshot
|
|
687
|
+
async function handleOneshot(message, socket, cwd) {
|
|
688
|
+
const { taskId, prompt, model, provider } = message;
|
|
689
|
+
const instruction = prompt;
|
|
690
|
+
taskControllers.get(taskId)?.abort();
|
|
691
|
+
const controller = new AbortController();
|
|
692
|
+
taskControllers.set(taskId, controller);
|
|
693
|
+
socket.send(JSON.stringify({
|
|
694
|
+
type: "agent_status",
|
|
695
|
+
taskId,
|
|
696
|
+
status: "thinking",
|
|
697
|
+
message: "Starting oneshot...",
|
|
698
|
+
}));
|
|
699
|
+
try {
|
|
700
|
+
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
701
|
+
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
702
|
+
const sessionInput = {
|
|
703
|
+
userPrompt: prompt,
|
|
704
|
+
project: {
|
|
705
|
+
projectId,
|
|
706
|
+
framework,
|
|
707
|
+
hasTypeScript: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json")),
|
|
708
|
+
hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) &&
|
|
709
|
+
node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
|
|
710
|
+
},
|
|
711
|
+
model: {
|
|
712
|
+
provider: (provider || "anthropic"),
|
|
713
|
+
id: model || "claude-3-5-sonnet-20241022",
|
|
714
|
+
},
|
|
715
|
+
backendConfig: isLocalAgent ? {
|
|
716
|
+
type: "local_agent",
|
|
717
|
+
provider: provider,
|
|
718
|
+
modelId: backendModelId,
|
|
719
|
+
projectPath: cwd,
|
|
720
|
+
} : undefined,
|
|
721
|
+
};
|
|
722
|
+
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
723
|
+
const session = await facade.createSession(sessionInput);
|
|
724
|
+
activeSessions.set(taskId, session);
|
|
725
|
+
taskFacades.set(taskId, facade);
|
|
726
|
+
await facade.runSession(session.id, cwd, (event) => {
|
|
727
|
+
// Oneshot results are reviewed exactly like edit/ask: the workspace is
|
|
728
|
+
// rolled back and only approval re-applies the changes.
|
|
729
|
+
handleRuntimeEvent(event, socket, taskId, session, `Oneshot: ${instruction}`, true);
|
|
730
|
+
if (event.type === "agent_completed") {
|
|
1124
731
|
socket.send(JSON.stringify({
|
|
1125
|
-
type: "
|
|
732
|
+
type: "assistant_message",
|
|
1126
733
|
taskId,
|
|
1127
|
-
|
|
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."
|
|
734
|
+
message: "Oneshot completed",
|
|
1138
735
|
}));
|
|
1139
|
-
return;
|
|
1140
736
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
737
|
+
}, controller.signal);
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
socket.send(JSON.stringify({
|
|
741
|
+
type: "agent_status",
|
|
742
|
+
taskId,
|
|
743
|
+
status: "error",
|
|
744
|
+
message: error instanceof Error ? error.message : "Unknown oneshot error",
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// Handler for restore_snapshot
|
|
749
|
+
async function handleRestoreSnapshot(message, socket, cwd) {
|
|
750
|
+
const { snapshotId } = message;
|
|
751
|
+
try {
|
|
752
|
+
await (0, snapshot_1.restoreSnapshot)(cwd, snapshotId);
|
|
753
|
+
socket.send(JSON.stringify({
|
|
754
|
+
type: "agent_status",
|
|
755
|
+
status: "working",
|
|
756
|
+
message: "Snapshot restored",
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
socket.send(JSON.stringify({
|
|
761
|
+
type: "agent_status",
|
|
762
|
+
status: "error",
|
|
763
|
+
message: error instanceof Error ? error.message : "Failed to restore snapshot",
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Handler for get_timeline
|
|
768
|
+
async function handleGetTimeline(socket, cwd) {
|
|
769
|
+
try {
|
|
770
|
+
const timeline = await (0, snapshot_1.getTimeline)(cwd);
|
|
771
|
+
socket.send(JSON.stringify({
|
|
772
|
+
type: "timeline",
|
|
773
|
+
snapshots: timeline,
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
socket.send(JSON.stringify({
|
|
778
|
+
type: "agent_status",
|
|
779
|
+
status: "error",
|
|
780
|
+
message: error instanceof Error ? error.message : "Failed to get timeline",
|
|
781
|
+
}));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
// Handler for delete_snapshot
|
|
785
|
+
async function handleDeleteSnapshot(message, socket, cwd) {
|
|
786
|
+
const { snapshotId } = message;
|
|
787
|
+
try {
|
|
788
|
+
await (0, snapshot_1.deleteSnapshot)(cwd, snapshotId);
|
|
789
|
+
socket.send(JSON.stringify({
|
|
790
|
+
type: "agent_status",
|
|
791
|
+
status: "working",
|
|
792
|
+
message: "Snapshot deleted",
|
|
793
|
+
}));
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
socket.send(JSON.stringify({
|
|
797
|
+
type: "agent_status",
|
|
798
|
+
status: "error",
|
|
799
|
+
message: error instanceof Error ? error.message : "Failed to delete snapshot",
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Git handlers
|
|
804
|
+
async function handleGitStatus(socket, cwd) {
|
|
805
|
+
socket.send(JSON.stringify({ type: "git_state", git: await getGitState(cwd) }));
|
|
806
|
+
}
|
|
807
|
+
async function handleGitGenerateMessage(message, socket, cwd) {
|
|
808
|
+
const git = await getGitState(cwd);
|
|
809
|
+
if (!git.isRepo) {
|
|
810
|
+
socket.send(JSON.stringify({ type: "git_commit_message", error: "Not a git repository" }));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const diff = await gitCommand(cwd, ["diff", "--cached"]).catch(() => "");
|
|
814
|
+
if (!diff) {
|
|
815
|
+
socket.send(JSON.stringify({ type: "git_commit_message", error: "No staged changes" }));
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
// TODO: Use runtime for commit message generation
|
|
819
|
+
socket.send(JSON.stringify({ type: "git_commit_message", message: "feat: changes" }));
|
|
820
|
+
}
|
|
821
|
+
async function handleGitInit(socket, cwd) {
|
|
822
|
+
try {
|
|
823
|
+
await gitCommand(cwd, ["init"]);
|
|
824
|
+
socket.send(JSON.stringify({ type: "git_result", message: "Git repository initialized" }));
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Failed to initialize git" }));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function handleGitCommit(message, socket, cwd) {
|
|
831
|
+
try {
|
|
832
|
+
await gitCommand(cwd, ["commit", "-m", message.message]);
|
|
833
|
+
socket.send(JSON.stringify({ type: "git_result", message: "Changes committed" }));
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Failed to commit" }));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async function handleGitPush(socket, cwd) {
|
|
840
|
+
try {
|
|
841
|
+
await gitCommand(cwd, ["push"]);
|
|
842
|
+
socket.send(JSON.stringify({ type: "git_result", message: "Changes pushed" }));
|
|
843
|
+
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
socket.send(JSON.stringify({ type: "git_result", error: error instanceof Error ? error.message : "Failed to push" }));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
wss.on("connection", (socket) => {
|
|
849
|
+
overlaySocket = socket;
|
|
850
|
+
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
851
|
+
const creds = (0, auth_1.loadCredentials)();
|
|
852
|
+
const userInfo = creds?.userName
|
|
853
|
+
? { name: creds.userName, email: creds.userEmail }
|
|
854
|
+
: (process.env.USER ? { name: process.env.USER } : null);
|
|
855
|
+
socket.send(JSON.stringify({
|
|
856
|
+
type: "config",
|
|
857
|
+
apiKeyConfigured: lassoKeyConfigured,
|
|
858
|
+
agentConfigured: lassoKeyConfigured,
|
|
859
|
+
models,
|
|
860
|
+
collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null,
|
|
861
|
+
user: userInfo
|
|
862
|
+
}));
|
|
863
|
+
void getGitState(cwd).then((git) => {
|
|
864
|
+
if (socket.readyState === socket.OPEN)
|
|
865
|
+
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
866
|
+
});
|
|
867
|
+
socket.on("message", async (raw) => {
|
|
868
|
+
const msg = JSON.parse(raw.toString());
|
|
869
|
+
console.log("[lasso] received from overlay:", msg.type);
|
|
870
|
+
try {
|
|
871
|
+
switch (msg.type) {
|
|
872
|
+
case "hello":
|
|
873
|
+
break;
|
|
874
|
+
case "edit":
|
|
875
|
+
case "ask":
|
|
876
|
+
await handleEditOrAsk(msg, socket, msg.taskId, msg.type === "edit" ? msg.instruction : msg.question, msg.model, msg.provider || "anthropic", msg.context, msg.element, msg.messages);
|
|
877
|
+
break;
|
|
878
|
+
case "apply":
|
|
879
|
+
await handleApply(msg, socket, cwd);
|
|
880
|
+
break;
|
|
881
|
+
case "undo":
|
|
882
|
+
await handleUndo(msg, socket, cwd);
|
|
883
|
+
break;
|
|
884
|
+
case "stop":
|
|
885
|
+
await handleStop(msg, socket);
|
|
886
|
+
break;
|
|
887
|
+
case "switch_model":
|
|
888
|
+
await handleSwitchModel(msg, socket);
|
|
889
|
+
break;
|
|
890
|
+
case "oneshot":
|
|
891
|
+
await handleOneshot(msg, socket, cwd);
|
|
892
|
+
break;
|
|
893
|
+
case "restore_snapshot":
|
|
894
|
+
await handleRestoreSnapshot(msg, socket, cwd);
|
|
895
|
+
break;
|
|
896
|
+
case "get_timeline":
|
|
897
|
+
await handleGetTimeline(socket, cwd);
|
|
898
|
+
break;
|
|
899
|
+
case "delete_snapshot":
|
|
900
|
+
await handleDeleteSnapshot(msg, socket, cwd);
|
|
901
|
+
break;
|
|
902
|
+
case "git_status":
|
|
903
|
+
await handleGitStatus(socket, cwd);
|
|
904
|
+
break;
|
|
905
|
+
case "git_generate_message":
|
|
906
|
+
await handleGitGenerateMessage(msg, socket, cwd);
|
|
907
|
+
break;
|
|
908
|
+
case "git_init":
|
|
909
|
+
await handleGitInit(socket, cwd);
|
|
910
|
+
break;
|
|
911
|
+
case "git_commit":
|
|
912
|
+
await handleGitCommit(msg, socket, cwd);
|
|
913
|
+
break;
|
|
914
|
+
case "git_push":
|
|
915
|
+
await handleGitPush(socket, cwd);
|
|
916
|
+
break;
|
|
917
|
+
case "list_page_folders":
|
|
918
|
+
const requested = msg.path || ".";
|
|
919
|
+
const root = node_path_1.default.resolve(cwd);
|
|
920
|
+
const directory = resolveInsideProject(root, requested);
|
|
921
|
+
if (!directory) {
|
|
922
|
+
socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: [], error: "That folder is outside the current project." }));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
socket.send(JSON.stringify({ type: "page_folders", path: requested, entries: pageEntries(root, requested), project: node_path_1.default.basename(root) }));
|
|
926
|
+
break;
|
|
927
|
+
case "create_page_folder":
|
|
928
|
+
const root2 = node_path_1.default.resolve(cwd);
|
|
929
|
+
const name = node_path_1.default.basename(msg.name || "");
|
|
930
|
+
const parent = resolveInsideProject(root2, msg.parent || ".");
|
|
931
|
+
if (!parent || !name || name !== msg.name || name === "." || name === "..") {
|
|
932
|
+
socket.send(JSON.stringify({ type: "page_folder_created", path: msg.name || "", error: "Enter a folder name without slashes or dots." }));
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const target = node_path_1.default.resolve(parent, name);
|
|
936
|
+
if (!target.startsWith(`${root2}${node_path_1.default.sep}`)) {
|
|
937
|
+
socket.send(JSON.stringify({ type: "page_folder_created", path: name, error: "Choose a folder inside the current project." }));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
node_fs_1.default.mkdirSync(target);
|
|
942
|
+
socket.send(JSON.stringify({ type: "page_folder_created", path: node_path_1.default.relative(root2, target) }));
|
|
943
|
+
}
|
|
944
|
+
catch (error) {
|
|
1171
945
|
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,
|
|
946
|
+
type: "page_folder_created",
|
|
947
|
+
path: node_path_1.default.relative(root2, target),
|
|
948
|
+
error: error instanceof Error && error.message.includes("EEXIST") ? "A folder with that name already exists." : "The folder could not be created.",
|
|
1180
949
|
}));
|
|
1181
950
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
951
|
+
break;
|
|
952
|
+
case "create_page":
|
|
953
|
+
const root3 = node_path_1.default.resolve(cwd);
|
|
954
|
+
const folder = resolveInsideProject(root3, msg.folder || ".");
|
|
955
|
+
if (!folder) {
|
|
956
|
+
socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "That folder is outside the current project." }));
|
|
957
|
+
return;
|
|
1184
958
|
}
|
|
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.`);
|
|
959
|
+
const targetFile = node_path_1.default.resolve(folder, msg.fileName);
|
|
960
|
+
if (!targetFile.startsWith(`${root3}${node_path_1.default.sep}`)) {
|
|
961
|
+
socket.send(JSON.stringify({ type: "page_created", path: msg.fileName, error: "Choose a location inside the current project." }));
|
|
962
|
+
return;
|
|
1215
963
|
}
|
|
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);
|
|
964
|
+
try {
|
|
965
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
|
|
966
|
+
node_fs_1.default.writeFileSync(targetFile, msg.content);
|
|
967
|
+
socket.send(JSON.stringify({ type: "page_created", path: node_path_1.default.relative(root3, targetFile) }));
|
|
1271
968
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
969
|
+
catch (error) {
|
|
970
|
+
socket.send(JSON.stringify({
|
|
971
|
+
type: "page_created",
|
|
972
|
+
path: node_path_1.default.relative(root3, targetFile),
|
|
973
|
+
error: error instanceof Error ? error.message : "The file could not be created.",
|
|
974
|
+
}));
|
|
1274
975
|
}
|
|
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);
|
|
976
|
+
break;
|
|
977
|
+
default:
|
|
978
|
+
console.warn("Unknown bridge message type:", msg.type);
|
|
1283
979
|
}
|
|
1284
980
|
}
|
|
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);
|
|
981
|
+
catch (error) {
|
|
982
|
+
console.error("Error handling bridge message:", error);
|
|
1312
983
|
socket.send(JSON.stringify({
|
|
1313
984
|
type: "agent_status",
|
|
1314
|
-
|
|
985
|
+
taskId: msg.taskId,
|
|
986
|
+
status: "error",
|
|
987
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
1315
988
|
}));
|
|
1316
989
|
}
|
|
1317
990
|
});
|
|
1318
991
|
socket.on("close", () => {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
console.log(chalk_1.default.yellow("!") + " Overlay disconnected");
|
|
992
|
+
if (socket === overlaySocket)
|
|
993
|
+
overlaySocket = null;
|
|
1322
994
|
});
|
|
1323
995
|
});
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
bridgeListening = true;
|
|
996
|
+
bridgeServer.listen(bridgePort, "127.0.0.1", () => {
|
|
997
|
+
console.log(chalk_1.default.green("✓") + ` Bridge listening on ${chalk_1.default.cyan(`http://127.0.0.1:${bridgePort}`)}`);
|
|
1327
998
|
});
|
|
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
|
-
};
|
|
999
|
+
return { ok: true };
|
|
1350
1000
|
}
|