@lasso-ai/cli 1.5.0 → 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 +1 -0
- package/dist/cli/backends.d.ts +53 -0
- package/dist/cli/backends.js +267 -0
- package/dist/cli/bridge.d.ts +8 -5
- package/dist/cli/bridge.js +357 -151
- package/dist/cli/host/install.js +12 -1
- package/dist/cli/local-runtime.d.ts +28 -0
- package/dist/cli/local-runtime.js +294 -0
- package/dist/cli/oneshot.js +28 -10
- package/dist/cli/runtime-client.d.ts +91 -10
- package/dist/cli/runtime-client.js +38 -3
- 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
|
@@ -18,6 +18,8 @@ const project_1 = require("./project");
|
|
|
18
18
|
const auth_1 = require("./auth");
|
|
19
19
|
const snapshot_1 = require("./snapshot");
|
|
20
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) {
|
|
@@ -82,31 +84,50 @@ function detectProjectFramework(cwd) {
|
|
|
82
84
|
catch { }
|
|
83
85
|
return "unknown";
|
|
84
86
|
}
|
|
85
|
-
// Helper to extract
|
|
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).
|
|
86
90
|
function extractChangesFromSession(session) {
|
|
87
91
|
if (!session)
|
|
88
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
|
+
}
|
|
89
103
|
const changes = [];
|
|
90
104
|
for (const action of session.actions || []) {
|
|
91
|
-
|
|
105
|
+
const data = (action.data || {});
|
|
106
|
+
const filePath = (data.filePath || data.path);
|
|
107
|
+
if (!filePath)
|
|
108
|
+
continue;
|
|
109
|
+
if (action.type === "edit_file") {
|
|
92
110
|
changes.push({
|
|
93
|
-
filePath
|
|
94
|
-
oldString:
|
|
95
|
-
newString:
|
|
111
|
+
filePath,
|
|
112
|
+
oldString: data.oldString ?? "",
|
|
113
|
+
newString: data.newString ?? "",
|
|
114
|
+
operation: "modified",
|
|
96
115
|
});
|
|
97
116
|
}
|
|
98
|
-
else if (action.type === "create_file"
|
|
117
|
+
else if (action.type === "create_file") {
|
|
99
118
|
changes.push({
|
|
100
|
-
filePath
|
|
119
|
+
filePath,
|
|
101
120
|
oldString: "",
|
|
102
|
-
newString:
|
|
121
|
+
newString: data.content ?? "",
|
|
122
|
+
operation: "created",
|
|
103
123
|
});
|
|
104
124
|
}
|
|
105
|
-
else if (action.type === "delete_file"
|
|
125
|
+
else if (action.type === "delete_file") {
|
|
106
126
|
changes.push({
|
|
107
|
-
filePath
|
|
108
|
-
oldString: "(deleted)",
|
|
127
|
+
filePath,
|
|
128
|
+
oldString: data.oldContent ?? "(deleted)",
|
|
109
129
|
newString: "",
|
|
130
|
+
operation: "deleted",
|
|
110
131
|
});
|
|
111
132
|
}
|
|
112
133
|
}
|
|
@@ -118,7 +139,11 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
118
139
|
let overlaySocket = null;
|
|
119
140
|
const taskSnapshots = new Map();
|
|
120
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();
|
|
121
145
|
const taskControllers = new Map();
|
|
146
|
+
const correctiveAttempts = new Map();
|
|
122
147
|
const envRoots = [cwd, node_path_1.default.join(cwd, "web"), node_path_1.default.join(cwd, "server")];
|
|
123
148
|
const fileEnv = envRoots.reduce((values, root) => ({
|
|
124
149
|
...values,
|
|
@@ -167,6 +192,27 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
167
192
|
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google", locked: false },
|
|
168
193
|
];
|
|
169
194
|
}
|
|
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",
|
|
202
|
+
};
|
|
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
|
+
}
|
|
170
216
|
const pageIgnoredDirs = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo", "out", "coverage", ".vercel", ".cache"]);
|
|
171
217
|
const resolveInsideProject = (root, relative) => {
|
|
172
218
|
const target = node_path_1.default.resolve(root, relative && relative !== "." ? relative : "");
|
|
@@ -202,6 +248,128 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
202
248
|
res.writeHead(200, { "content-type": "application/json" });
|
|
203
249
|
res.end(JSON.stringify({ ok: true }));
|
|
204
250
|
});
|
|
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;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
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) {
|
|
338
|
+
try {
|
|
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
|
+
}));
|
|
363
|
+
}
|
|
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
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
205
373
|
// Handler for edit/ask - uses runtime
|
|
206
374
|
async function handleEditOrAsk(message, socket, taskId, instruction, model, provider, context, element, messages) {
|
|
207
375
|
taskControllers.get(taskId)?.abort();
|
|
@@ -214,6 +382,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
214
382
|
message: "Starting...",
|
|
215
383
|
}));
|
|
216
384
|
try {
|
|
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 || "");
|
|
217
389
|
const sessionInput = {
|
|
218
390
|
userPrompt: instruction,
|
|
219
391
|
project: {
|
|
@@ -238,103 +410,21 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
238
410
|
route: context?.position?.route,
|
|
239
411
|
screenshot: context?.screenshots?.full,
|
|
240
412
|
} : undefined,
|
|
413
|
+
backendConfig: isLocalAgent ? {
|
|
414
|
+
type: "local_agent",
|
|
415
|
+
provider: provider,
|
|
416
|
+
modelId: backendModelId,
|
|
417
|
+
projectPath: cwd,
|
|
418
|
+
} : undefined,
|
|
241
419
|
};
|
|
242
|
-
const
|
|
420
|
+
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
421
|
+
const session = await facade.createSession(sessionInput);
|
|
243
422
|
activeSessions.set(taskId, session);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
await
|
|
248
|
-
|
|
249
|
-
return;
|
|
250
|
-
switch (event.type) {
|
|
251
|
-
case "agent_started":
|
|
252
|
-
case "agent_reasoning":
|
|
253
|
-
socket.send(JSON.stringify({
|
|
254
|
-
type: "agent_status",
|
|
255
|
-
taskId,
|
|
256
|
-
status: "thinking",
|
|
257
|
-
message: event.data.summary || "Working...",
|
|
258
|
-
}));
|
|
259
|
-
break;
|
|
260
|
-
case "tool_started":
|
|
261
|
-
socket.send(JSON.stringify({
|
|
262
|
-
type: "agent_status",
|
|
263
|
-
taskId,
|
|
264
|
-
status: "working",
|
|
265
|
-
message: `Running ${event.data.tool || "tool"}...`,
|
|
266
|
-
}));
|
|
267
|
-
break;
|
|
268
|
-
case "tool_completed":
|
|
269
|
-
socket.send(JSON.stringify({
|
|
270
|
-
type: "agent_status",
|
|
271
|
-
taskId,
|
|
272
|
-
status: "working",
|
|
273
|
-
message: "Completed tool",
|
|
274
|
-
}));
|
|
275
|
-
break;
|
|
276
|
-
case "phase_changed":
|
|
277
|
-
const phase = event.data.phase;
|
|
278
|
-
socket.send(JSON.stringify({
|
|
279
|
-
type: "agent_status",
|
|
280
|
-
taskId,
|
|
281
|
-
status: "working",
|
|
282
|
-
message: `Phase: ${phase}`,
|
|
283
|
-
}));
|
|
284
|
-
break;
|
|
285
|
-
case "verification_started":
|
|
286
|
-
socket.send(JSON.stringify({
|
|
287
|
-
type: "agent_status",
|
|
288
|
-
taskId,
|
|
289
|
-
status: "review",
|
|
290
|
-
message: "Verifying changes...",
|
|
291
|
-
}));
|
|
292
|
-
break;
|
|
293
|
-
case "verification_completed":
|
|
294
|
-
const passed = event.data.passed;
|
|
295
|
-
socket.send(JSON.stringify({
|
|
296
|
-
type: "agent_status",
|
|
297
|
-
taskId,
|
|
298
|
-
status: passed ? "working" : "error",
|
|
299
|
-
message: passed ? "Verification passed" : "Verification failed",
|
|
300
|
-
}));
|
|
301
|
-
break;
|
|
302
|
-
case "agent_completed":
|
|
303
|
-
runtimeClient.getSession(session.id).then((finalSession) => {
|
|
304
|
-
const changes = extractChangesFromSession(finalSession);
|
|
305
|
-
// Create snapshot with affected files
|
|
306
|
-
const filePaths = changes.map(c => c.filePath);
|
|
307
|
-
if (filePaths.length > 0) {
|
|
308
|
-
(0, snapshot_1.createSnapshot)(cwd, taskId, `Runtime session: ${instruction}`, filePaths, undefined, session.id);
|
|
309
|
-
}
|
|
310
|
-
socket.send(JSON.stringify({
|
|
311
|
-
type: "agent_status",
|
|
312
|
-
taskId,
|
|
313
|
-
status: "review",
|
|
314
|
-
message: "Review changes",
|
|
315
|
-
changes,
|
|
316
|
-
}));
|
|
317
|
-
});
|
|
318
|
-
break;
|
|
319
|
-
case "error":
|
|
320
|
-
socket.send(JSON.stringify({
|
|
321
|
-
type: "agent_status",
|
|
322
|
-
taskId,
|
|
323
|
-
status: "error",
|
|
324
|
-
message: event.data.error || "Unknown error",
|
|
325
|
-
}));
|
|
326
|
-
break;
|
|
327
|
-
case "waiting_for_user":
|
|
328
|
-
socket.send(JSON.stringify({
|
|
329
|
-
type: "agent_prompt",
|
|
330
|
-
taskId,
|
|
331
|
-
prompt: {
|
|
332
|
-
message: event.data?.question || "Input required",
|
|
333
|
-
kind: "input",
|
|
334
|
-
},
|
|
335
|
-
}));
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
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);
|
|
338
428
|
}, controller.signal);
|
|
339
429
|
}
|
|
340
430
|
catch (error) {
|
|
@@ -346,55 +436,155 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
346
436
|
}));
|
|
347
437
|
}
|
|
348
438
|
}
|
|
349
|
-
// Handler for apply - applies pending changes
|
|
439
|
+
// Handler for apply - applies approved pending changes, then runs server-side
|
|
440
|
+
// verification. On failure a corrective run is started automatically.
|
|
350
441
|
async function handleApply(message, socket, cwd) {
|
|
351
442
|
const { taskId, changes } = message;
|
|
352
443
|
const session = activeSessions.get(taskId);
|
|
353
|
-
const snapshot = taskSnapshots.get(taskId) || [];
|
|
354
|
-
// Create snapshot before applying changes
|
|
355
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;
|
|
451
|
+
try {
|
|
452
|
+
content = node_fs_1.default.readFileSync(abs, "utf8");
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
content = null;
|
|
456
|
+
}
|
|
457
|
+
return { filePath: change.filePath, content };
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
356
460
|
if (filePaths.length > 0) {
|
|
357
461
|
(0, snapshot_1.createSnapshot)(cwd, taskId, `Apply changes for task ${taskId}`, filePaths, undefined, session?.id);
|
|
358
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) {
|
|
468
|
+
try {
|
|
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
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
}
|
|
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
|
+
};
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
validation = undefined;
|
|
504
|
+
}
|
|
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) {
|
|
359
518
|
for (const change of changes) {
|
|
360
519
|
const filePath = node_path_1.default.join(cwd, change.filePath);
|
|
361
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");
|
|
362
525
|
try {
|
|
363
526
|
if (!node_fs_1.default.existsSync(dir)) {
|
|
364
527
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
365
528
|
}
|
|
366
|
-
if (
|
|
367
|
-
// Delete file
|
|
529
|
+
if (operation === "deleted") {
|
|
368
530
|
if (node_fs_1.default.existsSync(filePath)) {
|
|
369
531
|
node_fs_1.default.unlinkSync(filePath);
|
|
370
532
|
}
|
|
371
533
|
}
|
|
372
|
-
else if (
|
|
373
|
-
|
|
374
|
-
node_fs_1.default.writeFileSync(filePath, change.newString);
|
|
534
|
+
else if (operation === "created" || (oldString === "" && operation !== "modified")) {
|
|
535
|
+
node_fs_1.default.writeFileSync(filePath, newString);
|
|
375
536
|
}
|
|
376
537
|
else {
|
|
377
|
-
//
|
|
538
|
+
// Full-content replacement; split/join avoids String.replace's
|
|
539
|
+
// special $-pattern handling in the replacement text.
|
|
378
540
|
const content = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
379
|
-
|
|
380
|
-
|
|
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);
|
|
381
552
|
}
|
|
382
553
|
}
|
|
383
554
|
catch (error) {
|
|
384
|
-
|
|
385
|
-
type: "agent_status",
|
|
386
|
-
taskId,
|
|
387
|
-
status: "error",
|
|
388
|
-
message: `Failed to apply ${change.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
389
|
-
}));
|
|
390
|
-
return;
|
|
555
|
+
return `Failed to apply ${change.filePath}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
391
556
|
}
|
|
392
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);
|
|
393
569
|
socket.send(JSON.stringify({
|
|
394
|
-
type: "
|
|
570
|
+
type: "agent_status",
|
|
395
571
|
taskId,
|
|
396
|
-
|
|
572
|
+
status: "thinking",
|
|
573
|
+
message: "Fixing verification errors...",
|
|
397
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
|
+
}
|
|
398
588
|
}
|
|
399
589
|
// Handler for undo - restores snapshot
|
|
400
590
|
async function handleUndo(message, socket, cwd) {
|
|
@@ -424,13 +614,24 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
424
614
|
const { taskId } = message;
|
|
425
615
|
if (taskId) {
|
|
426
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(() => { });
|
|
427
621
|
activeSessions.delete(taskId);
|
|
622
|
+
taskFacades.delete(taskId);
|
|
428
623
|
}
|
|
429
624
|
else {
|
|
430
625
|
for (const controller of taskControllers.values())
|
|
431
626
|
controller.abort();
|
|
432
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
|
+
}
|
|
433
633
|
activeSessions.clear();
|
|
634
|
+
taskFacades.clear();
|
|
434
635
|
}
|
|
435
636
|
socket.send(JSON.stringify({
|
|
436
637
|
type: "agent_status",
|
|
@@ -453,10 +654,19 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
453
654
|
return;
|
|
454
655
|
}
|
|
455
656
|
try {
|
|
456
|
-
|
|
657
|
+
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
658
|
+
const facade = taskFacades.get(taskId) || runtimeClient;
|
|
659
|
+
await facade.switchModel(session.id, {
|
|
457
660
|
provider: provider,
|
|
458
661
|
id: modelId,
|
|
459
|
-
}
|
|
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);
|
|
460
670
|
socket.send(JSON.stringify({
|
|
461
671
|
type: "agent_status",
|
|
462
672
|
taskId,
|
|
@@ -487,6 +697,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
487
697
|
message: "Starting oneshot...",
|
|
488
698
|
}));
|
|
489
699
|
try {
|
|
700
|
+
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
701
|
+
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
490
702
|
const sessionInput = {
|
|
491
703
|
userPrompt: prompt,
|
|
492
704
|
project: {
|
|
@@ -500,32 +712,26 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
500
712
|
provider: (provider || "anthropic"),
|
|
501
713
|
id: model || "claude-3-5-sonnet-20241022",
|
|
502
714
|
},
|
|
715
|
+
backendConfig: isLocalAgent ? {
|
|
716
|
+
type: "local_agent",
|
|
717
|
+
provider: provider,
|
|
718
|
+
modelId: backendModelId,
|
|
719
|
+
projectPath: cwd,
|
|
720
|
+
} : undefined,
|
|
503
721
|
};
|
|
504
|
-
const
|
|
722
|
+
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
723
|
+
const session = await facade.createSession(sessionInput);
|
|
505
724
|
activeSessions.set(taskId, session);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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);
|
|
509
730
|
if (event.type === "agent_completed") {
|
|
510
|
-
runtimeClient.getSession(session.id).then((finalSession) => {
|
|
511
|
-
const changes = extractChangesFromSession(finalSession);
|
|
512
|
-
const filePaths = changes.map(c => c.filePath);
|
|
513
|
-
if (filePaths.length > 0) {
|
|
514
|
-
(0, snapshot_1.createSnapshot)(cwd, taskId, `Oneshot: ${instruction}`, filePaths, undefined, session.id);
|
|
515
|
-
}
|
|
516
|
-
socket.send(JSON.stringify({
|
|
517
|
-
type: "assistant_message",
|
|
518
|
-
taskId,
|
|
519
|
-
message: "Oneshot completed",
|
|
520
|
-
}));
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
else if (event.type === "error") {
|
|
524
731
|
socket.send(JSON.stringify({
|
|
525
|
-
type: "
|
|
732
|
+
type: "assistant_message",
|
|
526
733
|
taskId,
|
|
527
|
-
|
|
528
|
-
message: event.data.error || "Runtime error",
|
|
734
|
+
message: "Oneshot completed",
|
|
529
735
|
}));
|
|
530
736
|
}
|
|
531
737
|
}, controller.signal);
|
|
@@ -543,7 +749,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
543
749
|
async function handleRestoreSnapshot(message, socket, cwd) {
|
|
544
750
|
const { snapshotId } = message;
|
|
545
751
|
try {
|
|
546
|
-
await (0, snapshot_1.restoreSnapshot)(
|
|
752
|
+
await (0, snapshot_1.restoreSnapshot)(cwd, snapshotId);
|
|
547
753
|
socket.send(JSON.stringify({
|
|
548
754
|
type: "agent_status",
|
|
549
755
|
status: "working",
|
|
@@ -579,7 +785,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
579
785
|
async function handleDeleteSnapshot(message, socket, cwd) {
|
|
580
786
|
const { snapshotId } = message;
|
|
581
787
|
try {
|
|
582
|
-
await (0, snapshot_1.deleteSnapshot)(
|
|
788
|
+
await (0, snapshot_1.deleteSnapshot)(cwd, snapshotId);
|
|
583
789
|
socket.send(JSON.stringify({
|
|
584
790
|
type: "agent_status",
|
|
585
791
|
status: "working",
|