@lasso-ai/cli 2.1.14 → 2.1.16
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/auth.js +1 -7
- package/dist/cli/bridge.d.ts +11 -0
- package/dist/cli/bridge.js +177 -117
- package/dist/cli/host/certificates.js +5 -7
- package/dist/cli/host/daemon.js +16 -5
- package/dist/cli/host/dns.js +4 -1
- package/dist/cli/host/install.js +1 -1
- package/dist/cli/host/next-host-entry.js +1 -3
- package/dist/cli/host/vite-host-entry.js +1 -7
- package/dist/cli/index.js +2 -1
- package/dist/cli/local-runtime.js +3 -8
- package/dist/cli/project.js +8 -12
- package/dist/cli/runtime-client.d.ts +10 -0
- package/dist/cli/runtime-client.js +107 -66
- package/dist/cli/server/vite.js +1 -1
- package/dist/cli/snapshot.js +13 -3
- package/dist/overlay.js +274 -292
- package/dist/runtime/__tests__/adapter-normalization.test.js +7 -1
- package/dist/runtime/action-executor.js +3 -15
- package/dist/runtime/adapters/claude-code-adapter.js +6 -13
- package/dist/runtime/adapters/codex-cli-adapter.js +1 -5
- package/dist/runtime/adapters/local-agent-adapter.js +7 -14
- package/dist/runtime/adapters/opencode-adapter.js +3 -12
- package/dist/runtime/agent-runtime.d.ts +3 -1
- package/dist/runtime/agent-state-machine.js +2 -5
- package/dist/runtime/review.js +3 -7
- package/dist/runtime/verification-engine.d.ts +10 -0
- package/dist/runtime/verification-engine.js +83 -35
- package/dist/runtime/workspace-guard.js +1 -6
- package/package.json +4 -3
package/dist/cli/host/install.js
CHANGED
|
@@ -51,7 +51,7 @@ function spawnDaemon(env) {
|
|
|
51
51
|
console.log(chalk_1.default.yellow(`!`) + ` Lasso Host is running version ${health.version} but installed version is ${CURRENT_VERSION}. Restarting...`);
|
|
52
52
|
await stopDaemon(env);
|
|
53
53
|
// Wait a moment for the daemon to stop
|
|
54
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
55
55
|
}
|
|
56
56
|
else {
|
|
57
57
|
return { ok: true, alreadyRunning: true };
|
|
@@ -109,9 +109,7 @@ void (async () => {
|
|
|
109
109
|
}
|
|
110
110
|
const forward = (attempt = 0) => {
|
|
111
111
|
proxy.web(req, res, { headers: { "accept-encoding": "identity" } }, (error) => {
|
|
112
|
-
const retryable = error.code === "ECONNREFUSED" ||
|
|
113
|
-
error.code === "ECONNRESET" ||
|
|
114
|
-
error.code === "EPIPE";
|
|
112
|
+
const retryable = error.code === "ECONNREFUSED" || error.code === "ECONNRESET" || error.code === "EPIPE";
|
|
115
113
|
if (!res.headersSent && req.method !== "POST" && retryable && attempt < 12) {
|
|
116
114
|
setTimeout(() => forward(attempt + 1), 250);
|
|
117
115
|
return;
|
|
@@ -37,13 +37,7 @@ void (async () => {
|
|
|
37
37
|
host: "127.0.0.1",
|
|
38
38
|
port,
|
|
39
39
|
strictPort: true,
|
|
40
|
-
allowedHosts: [
|
|
41
|
-
".lasso",
|
|
42
|
-
"lasso",
|
|
43
|
-
"localhost",
|
|
44
|
-
"127.0.0.1",
|
|
45
|
-
...(process.env.LASSO_HOST_ALLOWED_HOSTS ? process.env.LASSO_HOST_ALLOWED_HOSTS.split(",") : []),
|
|
46
|
-
],
|
|
40
|
+
allowedHosts: [".lasso", "lasso", "localhost", "127.0.0.1", ...(process.env.LASSO_HOST_ALLOWED_HOSTS ? process.env.LASSO_HOST_ALLOWED_HOSTS.split(",") : [])],
|
|
47
41
|
},
|
|
48
42
|
plugins: [lassoOverlayPlugin()],
|
|
49
43
|
});
|
package/dist/cli/index.js
CHANGED
|
@@ -30,7 +30,8 @@ function projectEnv() {
|
|
|
30
30
|
return (0, project_1.readProjectEnv)(process.cwd());
|
|
31
31
|
}
|
|
32
32
|
const bridge = program.command("bridge").description("Manage the Lasso overlay bridge");
|
|
33
|
-
bridge
|
|
33
|
+
bridge
|
|
34
|
+
.command("restart")
|
|
34
35
|
.description("Reset the active bridge connection and reconnect the overlay")
|
|
35
36
|
.option("--port <port>", "Bridge port", "3056")
|
|
36
37
|
.action(async (options) => {
|
|
@@ -101,9 +101,7 @@ class LocalRuntime {
|
|
|
101
101
|
try {
|
|
102
102
|
// Fresh capture per run — corrective re-runs capture the post-apply state.
|
|
103
103
|
const guard = await index_js_1.WorkspaceGuard.capture(projectPath);
|
|
104
|
-
record.backend =
|
|
105
|
-
record.backend ||
|
|
106
|
-
(await (0, index_js_1.createLocalAgentBackend)({ ...record.backendConfig, type: "local_agent", projectPath }));
|
|
104
|
+
record.backend = record.backend || (await (0, index_js_1.createLocalAgentBackend)({ ...record.backendConfig, type: "local_agent", projectPath }));
|
|
107
105
|
const previousErrors = (record.errors || []);
|
|
108
106
|
const externalSessionId = typeof record.metadata.backendSessionId === "string" ? record.metadata.backendSessionId : undefined;
|
|
109
107
|
const runner = new index_js_1.LocalSessionRunner({
|
|
@@ -173,8 +171,7 @@ class LocalRuntime {
|
|
|
173
171
|
pendingChanges: outcome.changes,
|
|
174
172
|
binaryChanges: outcome.binaryPaths,
|
|
175
173
|
};
|
|
176
|
-
record.status =
|
|
177
|
-
outcome.status === "waiting" ? "waiting" : outcome.status === "completed" ? "completed" : "failed";
|
|
174
|
+
record.status = outcome.status === "waiting" ? "waiting" : outcome.status === "completed" ? "completed" : "failed";
|
|
178
175
|
record.phase = "complete";
|
|
179
176
|
record.updatedAt = new Date();
|
|
180
177
|
}
|
|
@@ -257,9 +254,7 @@ class LocalRuntime {
|
|
|
257
254
|
previousErrors: [],
|
|
258
255
|
currentPhase: "execute",
|
|
259
256
|
constraints: {},
|
|
260
|
-
sessionRef: typeof record.metadata.backendSessionId === "string"
|
|
261
|
-
? { id: record.metadata.backendSessionId }
|
|
262
|
-
: undefined,
|
|
257
|
+
sessionRef: typeof record.metadata.backendSessionId === "string" ? { id: record.metadata.backendSessionId } : undefined,
|
|
263
258
|
});
|
|
264
259
|
}
|
|
265
260
|
record.backend = nextBackend;
|
package/dist/cli/project.js
CHANGED
|
@@ -29,7 +29,10 @@ exports.LASSO_CONFIG_FILE = "lasso.config.json";
|
|
|
29
29
|
const PROJECT_STATE_FILE = ".lasso-project.json";
|
|
30
30
|
function readEnvFile(filename, cwd) {
|
|
31
31
|
try {
|
|
32
|
-
return node_fs_1.default
|
|
32
|
+
return node_fs_1.default
|
|
33
|
+
.readFileSync(node_path_1.default.join(cwd, filename), "utf8")
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.reduce((values, line) => {
|
|
33
36
|
const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
34
37
|
if (match) {
|
|
35
38
|
let value = match[2].trim();
|
|
@@ -202,7 +205,7 @@ async function initProject(cwd, fileEnv, forcedDomain) {
|
|
|
202
205
|
}
|
|
203
206
|
const registry = (0, registry_1.loadRegistry)();
|
|
204
207
|
const existingHost = (0, registry_1.findByDirectory)(registry, cwd);
|
|
205
|
-
const domain = forcedDomain || (existingConfig?.domain ? existingConfig.domain :
|
|
208
|
+
const domain = forcedDomain || (existingConfig?.domain ? existingConfig.domain : existingHost ? existingHost.domain : (0, registry_1.generateUniqueDomain)(cwd, registry));
|
|
206
209
|
payload.domain = domain;
|
|
207
210
|
let apiKey = resolveLassoApiKey(fileEnv);
|
|
208
211
|
if (!apiKey)
|
|
@@ -322,11 +325,7 @@ async function resolveCollabSession(cwd, fileEnv, projectId) {
|
|
|
322
325
|
}
|
|
323
326
|
if (!response || !response.ok) {
|
|
324
327
|
const status = response?.status;
|
|
325
|
-
const reason = status === 401
|
|
326
|
-
? "Your Lasso API key is invalid or inactive."
|
|
327
|
-
: status === 403
|
|
328
|
-
? "This project belongs to another workspace."
|
|
329
|
-
: `The server rejected the project session (${status || "unreachable"}).`;
|
|
328
|
+
const reason = status === 401 ? "Your Lasso API key is invalid or inactive." : status === 403 ? "This project belongs to another workspace." : `The server rejected the project session (${status || "unreachable"}).`;
|
|
330
329
|
console.warn(chalk_1.default.yellow("!") + ` Realtime collaboration unavailable: ${reason}`);
|
|
331
330
|
return config;
|
|
332
331
|
}
|
|
@@ -341,10 +340,7 @@ async function resolveCollabSession(cwd, fileEnv, projectId) {
|
|
|
341
340
|
config.registeredAt = new Date().toISOString();
|
|
342
341
|
config.plan = body.project?.plan || "free";
|
|
343
342
|
config.configuredProviders = body.project?.configuredProviders || [];
|
|
344
|
-
console.log(chalk_1.default.green("✓") +
|
|
345
|
-
" Project session opened " +
|
|
346
|
-
chalk_1.default.cyan(config.name || projectId) +
|
|
347
|
-
`${config.workspaceId ? ` · workspace ${config.workspaceId}` : ""}`);
|
|
343
|
+
console.log(chalk_1.default.green("✓") + " Project session opened " + chalk_1.default.cyan(config.name || projectId) + `${config.workspaceId ? ` · workspace ${config.workspaceId}` : ""}`);
|
|
348
344
|
return config;
|
|
349
345
|
}
|
|
350
346
|
catch (error) {
|
|
@@ -425,7 +421,7 @@ async function fetchModels(fileEnv) {
|
|
|
425
421
|
if (!response.ok) {
|
|
426
422
|
return { anthropic: [], openai: [], google: [], nvidia: [] };
|
|
427
423
|
}
|
|
428
|
-
const body = await response.json();
|
|
424
|
+
const body = (await response.json());
|
|
429
425
|
return body;
|
|
430
426
|
}
|
|
431
427
|
catch {
|
|
@@ -157,6 +157,7 @@ export interface RuntimeFacade {
|
|
|
157
157
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
|
|
158
158
|
providerApiKey?: string;
|
|
159
159
|
browserContextProvider?: BrowserContextProvider;
|
|
160
|
+
devServerUrl?: string;
|
|
160
161
|
}): Promise<void>;
|
|
161
162
|
getSession(sessionId: string): Promise<RuntimeSession>;
|
|
162
163
|
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
@@ -166,6 +167,8 @@ export interface RuntimeFacade {
|
|
|
166
167
|
name?: string;
|
|
167
168
|
}, apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
|
|
168
169
|
cancelSession(sessionId: string): Promise<void>;
|
|
170
|
+
respondToPrompt?(sessionId: string, response: string): Promise<void>;
|
|
171
|
+
steerSession?(sessionId: string, prompt: string): Promise<void>;
|
|
169
172
|
}
|
|
170
173
|
export type RuntimeEvent = {
|
|
171
174
|
type: "agent_started";
|
|
@@ -315,7 +318,10 @@ export declare class RuntimeClient {
|
|
|
315
318
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
|
|
316
319
|
providerApiKey?: string;
|
|
317
320
|
browserContextProvider?: BrowserContextProvider;
|
|
321
|
+
devServerUrl?: string;
|
|
318
322
|
}): Promise<void>;
|
|
323
|
+
/** Parse and dispatch one complete SSE frame. */
|
|
324
|
+
private processSseFrame;
|
|
319
325
|
/** Execute a tool_call request locally against the real project. */
|
|
320
326
|
private executeToolCallLocally;
|
|
321
327
|
/** Post a tool result back to the server (resolves its pending request). */
|
|
@@ -324,6 +330,10 @@ export declare class RuntimeClient {
|
|
|
324
330
|
* Cancel a running session
|
|
325
331
|
*/
|
|
326
332
|
cancelSession(sessionId: string): Promise<void>;
|
|
333
|
+
/** Resume a hosted runtime that is waiting for an answer from the user. */
|
|
334
|
+
respondToPrompt(sessionId: string, response: string): Promise<void>;
|
|
335
|
+
/** Queue a prompt for the next turn of an active hosted runtime. */
|
|
336
|
+
steerSession(sessionId: string, prompt: string): Promise<void>;
|
|
327
337
|
/**
|
|
328
338
|
* Switch model/backend for a session
|
|
329
339
|
*/
|
|
@@ -36,7 +36,7 @@ class RuntimeClient {
|
|
|
36
36
|
method: "POST",
|
|
37
37
|
headers: {
|
|
38
38
|
"Content-Type": "application/json",
|
|
39
|
-
...(this.apiKey && {
|
|
39
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
40
40
|
},
|
|
41
41
|
body: JSON.stringify(input),
|
|
42
42
|
});
|
|
@@ -53,13 +53,13 @@ class RuntimeClient {
|
|
|
53
53
|
async getSession(sessionId) {
|
|
54
54
|
const response = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}`, {
|
|
55
55
|
headers: {
|
|
56
|
-
...(this.apiKey && {
|
|
56
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
57
57
|
},
|
|
58
58
|
});
|
|
59
59
|
if (!response.ok) {
|
|
60
60
|
throw new Error(`Failed to get session: ${response.statusText}`);
|
|
61
61
|
}
|
|
62
|
-
const data = await response.json();
|
|
62
|
+
const data = (await response.json());
|
|
63
63
|
const local = this.localReviewCache.get(sessionId);
|
|
64
64
|
if (local && local.length > 0) {
|
|
65
65
|
data.metadata = { ...(data.metadata || {}), pendingChanges: local };
|
|
@@ -79,20 +79,19 @@ class RuntimeClient {
|
|
|
79
79
|
*/
|
|
80
80
|
async runSession(sessionId, projectPath, onEvent, signal, opts) {
|
|
81
81
|
// CLI-side snapshot: nothing the agent does is left on disk before review.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
console.error("WorkspaceGuard capture failed:", error);
|
|
88
|
-
}
|
|
82
|
+
// Review safety is mandatory. Never allow an agent to run if the
|
|
83
|
+
// pre-run workspace capture could not be established.
|
|
84
|
+
const guard = await index_js_1.WorkspaceGuard.capture(projectPath);
|
|
89
85
|
const executor = new index_js_1.ActionExecutor(projectPath, opts?.browserContextProvider);
|
|
90
|
-
const verifier = new index_js_1.VerificationEngine(projectPath
|
|
86
|
+
const verifier = new index_js_1.VerificationEngine(projectPath, {
|
|
87
|
+
devServerUrl: opts?.devServerUrl || process.env.LASSO_DEV_SERVER_URL,
|
|
88
|
+
browserContextProvider: opts?.browserContextProvider,
|
|
89
|
+
});
|
|
91
90
|
// Idempotent: called on agent_completed AND on stream failure, so the
|
|
92
91
|
// diff is always cached before the safety-net restore wipes the edits.
|
|
93
92
|
let finalized = false;
|
|
94
93
|
const finalizeGuard = async () => {
|
|
95
|
-
if (
|
|
94
|
+
if (finalized)
|
|
96
95
|
return;
|
|
97
96
|
finalized = true;
|
|
98
97
|
try {
|
|
@@ -100,8 +99,8 @@ class RuntimeClient {
|
|
|
100
99
|
const applyable = changes.filter((c) => !c.binary);
|
|
101
100
|
const pending = applyable.map((change) => ({
|
|
102
101
|
filePath: change.filePath,
|
|
103
|
-
oldString: change.operation === "created" ? "" : change.oldContent ?? "",
|
|
104
|
-
newString: change.operation === "deleted" ? "" : change.newContent ?? "",
|
|
102
|
+
oldString: change.operation === "created" ? "" : (change.oldContent ?? ""),
|
|
103
|
+
newString: change.operation === "deleted" ? "" : (change.newContent ?? ""),
|
|
105
104
|
operation: change.operation,
|
|
106
105
|
}));
|
|
107
106
|
this.localReviewCache.set(sessionId, pending);
|
|
@@ -125,59 +124,46 @@ class RuntimeClient {
|
|
|
125
124
|
const result = await this.executeToolCallLocally(request, executor, verifier);
|
|
126
125
|
await this.postToolResult(sessionId, result);
|
|
127
126
|
};
|
|
128
|
-
|
|
129
|
-
method: "POST",
|
|
130
|
-
headers: {
|
|
131
|
-
"Content-Type": "application/json",
|
|
132
|
-
...(this.apiKey && { "Authorization": `Bearer ${this.apiKey}` }),
|
|
133
|
-
},
|
|
134
|
-
// providerApiKey is the user's own model-provider key (BYOK): the
|
|
135
|
-
// server calls the hosted model with it and never stores it.
|
|
136
|
-
body: JSON.stringify({ projectPath, apiKey: opts?.providerApiKey }),
|
|
137
|
-
signal,
|
|
138
|
-
});
|
|
139
|
-
if (!response.ok) {
|
|
140
|
-
throw new Error(`Failed to run session: ${response.statusText}`);
|
|
141
|
-
}
|
|
142
|
-
const reader = response.body?.getReader();
|
|
143
|
-
if (!reader) {
|
|
144
|
-
throw new Error("No response body");
|
|
145
|
-
}
|
|
146
|
-
const decoder = new TextDecoder();
|
|
127
|
+
let reader;
|
|
147
128
|
try {
|
|
129
|
+
const response = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/run`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
134
|
+
},
|
|
135
|
+
// providerApiKey is the user's own model-provider key (BYOK): the
|
|
136
|
+
// server calls the hosted model with it and never stores it.
|
|
137
|
+
body: JSON.stringify({ projectPath, apiKey: opts?.providerApiKey }),
|
|
138
|
+
signal,
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
throw new Error(`Failed to run session: ${response.statusText}`);
|
|
142
|
+
}
|
|
143
|
+
reader = response.body?.getReader();
|
|
144
|
+
if (!reader) {
|
|
145
|
+
throw new Error("No response body");
|
|
146
|
+
}
|
|
147
|
+
const decoder = new TextDecoder();
|
|
148
|
+
let buffer = "";
|
|
148
149
|
while (true) {
|
|
149
150
|
const { done, value } = await reader.read();
|
|
150
151
|
if (done)
|
|
151
152
|
break;
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const data = line.slice(6);
|
|
158
|
-
if (data === "[DONE]")
|
|
159
|
-
continue;
|
|
160
|
-
try {
|
|
161
|
-
const event = JSON.parse(data);
|
|
162
|
-
if (event.type === "tool_call") {
|
|
163
|
-
// Execute locally and answer the server — never forwarded raw.
|
|
164
|
-
await handleToolCall(event.data);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (event.type === "agent_completed") {
|
|
168
|
-
// Diff + rollback BEFORE the review flow reads the session.
|
|
169
|
-
await finalizeGuard();
|
|
170
|
-
}
|
|
171
|
-
await onEvent(event);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
console.error("Failed to parse SSE event:", error);
|
|
175
|
-
}
|
|
153
|
+
buffer += decoder.decode(value, { stream: true });
|
|
154
|
+
const frames = buffer.split(/\r?\n\r?\n/);
|
|
155
|
+
buffer = frames.pop() || "";
|
|
156
|
+
for (const frame of frames) {
|
|
157
|
+
await this.processSseFrame(frame, handleToolCall, finalizeGuard, onEvent);
|
|
176
158
|
}
|
|
177
159
|
}
|
|
160
|
+
buffer += decoder.decode();
|
|
161
|
+
if (buffer.trim()) {
|
|
162
|
+
await this.processSseFrame(buffer, handleToolCall, finalizeGuard, onEvent);
|
|
163
|
+
}
|
|
178
164
|
}
|
|
179
165
|
finally {
|
|
180
|
-
reader
|
|
166
|
+
reader?.releaseLock();
|
|
181
167
|
// Stream died before agent_completed (network cut, server crash, model
|
|
182
168
|
// error surfaced as an error event): cache the diff first so the user's
|
|
183
169
|
// changes are recoverable, then restore the workspace. On the normal
|
|
@@ -185,6 +171,29 @@ class RuntimeClient {
|
|
|
185
171
|
await finalizeGuard();
|
|
186
172
|
}
|
|
187
173
|
}
|
|
174
|
+
/** Parse and dispatch one complete SSE frame. */
|
|
175
|
+
async processSseFrame(frame, handleToolCall, finalizeGuard, onEvent) {
|
|
176
|
+
const data = frame
|
|
177
|
+
.split(/\r?\n/)
|
|
178
|
+
.filter((line) => line.startsWith("data:"))
|
|
179
|
+
.map((line) => line.slice(5).trimStart())
|
|
180
|
+
.join("\n");
|
|
181
|
+
if (!data || data === "[DONE]")
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
const event = JSON.parse(data);
|
|
185
|
+
if (event.type === "tool_call") {
|
|
186
|
+
await handleToolCall(event.data);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (event.type === "agent_completed")
|
|
190
|
+
await finalizeGuard();
|
|
191
|
+
await onEvent(event);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
console.error("Failed to process SSE event:", error);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
188
197
|
/** Execute a tool_call request locally against the real project. */
|
|
189
198
|
async executeToolCallLocally(request, executor, verifier) {
|
|
190
199
|
try {
|
|
@@ -207,7 +216,8 @@ class RuntimeClient {
|
|
|
207
216
|
output: result.output,
|
|
208
217
|
error: result.error,
|
|
209
218
|
observations: result.observation
|
|
210
|
-
? [
|
|
219
|
+
? [
|
|
220
|
+
{
|
|
211
221
|
id: node_crypto_1.default.randomUUID(),
|
|
212
222
|
type: result.observation.type,
|
|
213
223
|
summary: result.observation.summary,
|
|
@@ -215,7 +225,8 @@ class RuntimeClient {
|
|
|
215
225
|
timestamp: Date.now(),
|
|
216
226
|
phase: "execute",
|
|
217
227
|
actionId: request.action.id,
|
|
218
|
-
}
|
|
228
|
+
},
|
|
229
|
+
]
|
|
219
230
|
: undefined,
|
|
220
231
|
};
|
|
221
232
|
}
|
|
@@ -234,7 +245,7 @@ class RuntimeClient {
|
|
|
234
245
|
method: "POST",
|
|
235
246
|
headers: {
|
|
236
247
|
"Content-Type": "application/json",
|
|
237
|
-
...(this.apiKey && {
|
|
248
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
238
249
|
},
|
|
239
250
|
body: JSON.stringify(result),
|
|
240
251
|
});
|
|
@@ -253,13 +264,43 @@ class RuntimeClient {
|
|
|
253
264
|
const response = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/cancel`, {
|
|
254
265
|
method: "POST",
|
|
255
266
|
headers: {
|
|
256
|
-
...(this.apiKey && {
|
|
267
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
257
268
|
},
|
|
258
269
|
});
|
|
259
270
|
if (!response.ok) {
|
|
260
271
|
throw new Error(`Failed to cancel session: ${response.statusText}`);
|
|
261
272
|
}
|
|
262
273
|
}
|
|
274
|
+
/** Resume a hosted runtime that is waiting for an answer from the user. */
|
|
275
|
+
async respondToPrompt(sessionId, response) {
|
|
276
|
+
const result = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/user-response`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ response }),
|
|
283
|
+
});
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
const body = (await result.json().catch(() => ({})));
|
|
286
|
+
throw new Error(body.error || `Failed to submit agent response: ${result.statusText}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Queue a prompt for the next turn of an active hosted runtime. */
|
|
290
|
+
async steerSession(sessionId, prompt) {
|
|
291
|
+
const result = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/steer`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: {
|
|
294
|
+
"Content-Type": "application/json",
|
|
295
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
296
|
+
},
|
|
297
|
+
body: JSON.stringify({ prompt }),
|
|
298
|
+
});
|
|
299
|
+
if (!result.ok) {
|
|
300
|
+
const body = (await result.json().catch(() => ({})));
|
|
301
|
+
throw new Error(body.error || `Failed to queue steering prompt: ${result.statusText}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
263
304
|
/**
|
|
264
305
|
* Switch model/backend for a session
|
|
265
306
|
*/
|
|
@@ -268,7 +309,7 @@ class RuntimeClient {
|
|
|
268
309
|
method: "POST",
|
|
269
310
|
headers: {
|
|
270
311
|
"Content-Type": "application/json",
|
|
271
|
-
...(this.apiKey && {
|
|
312
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
272
313
|
},
|
|
273
314
|
body: JSON.stringify({ ...model, apiKey, backendConfig }),
|
|
274
315
|
});
|
|
@@ -295,7 +336,7 @@ class RuntimeClient {
|
|
|
295
336
|
method: "POST",
|
|
296
337
|
headers: {
|
|
297
338
|
"Content-Type": "application/json",
|
|
298
|
-
...(this.apiKey && {
|
|
339
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
299
340
|
},
|
|
300
341
|
body: JSON.stringify({ projectPath, localResults: { passed, errors, observations } }),
|
|
301
342
|
});
|
|
@@ -317,7 +358,7 @@ class RuntimeClient {
|
|
|
317
358
|
const url = `${this.serverUrl}/api/v1/agent-runtime/backends${projectPath ? `?projectPath=${encodeURIComponent(projectPath)}` : ""}`;
|
|
318
359
|
const response = await fetch(url, {
|
|
319
360
|
headers: {
|
|
320
|
-
...(this.apiKey && {
|
|
361
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
321
362
|
},
|
|
322
363
|
});
|
|
323
364
|
if (!response.ok) {
|
|
@@ -332,7 +373,7 @@ class RuntimeClient {
|
|
|
332
373
|
const response = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}`, {
|
|
333
374
|
method: "DELETE",
|
|
334
375
|
headers: {
|
|
335
|
-
...(this.apiKey && {
|
|
376
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
336
377
|
},
|
|
337
378
|
});
|
|
338
379
|
if (!response.ok) {
|
package/dist/cli/server/vite.js
CHANGED
package/dist/cli/snapshot.js
CHANGED
|
@@ -20,6 +20,16 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
20
20
|
const SNAPSHOTS_DIR = ".lasso/snapshots";
|
|
21
21
|
const TIMELINE_FILE = ".lasso/timeline.json";
|
|
22
22
|
const MAX_SNAPSHOTS = 50;
|
|
23
|
+
function resolveSnapshotPath(cwd, filePath) {
|
|
24
|
+
if (!filePath || node_path_1.default.isAbsolute(filePath))
|
|
25
|
+
throw new Error(`Invalid snapshot path: ${filePath}`);
|
|
26
|
+
const root = node_path_1.default.resolve(cwd);
|
|
27
|
+
const fullPath = node_path_1.default.resolve(root, filePath);
|
|
28
|
+
if (fullPath !== root && !fullPath.startsWith(`${root}${node_path_1.default.sep}`)) {
|
|
29
|
+
throw new Error(`Snapshot path escapes workspace: ${filePath}`);
|
|
30
|
+
}
|
|
31
|
+
return fullPath;
|
|
32
|
+
}
|
|
23
33
|
// Ensure snapshots directory exists
|
|
24
34
|
function ensureSnapshotsDir(cwd) {
|
|
25
35
|
const dir = node_path_1.default.join(cwd, SNAPSHOTS_DIR);
|
|
@@ -66,7 +76,7 @@ function createSnapshot(cwd, taskId, summary, filePaths, parentSnapshotId, sessi
|
|
|
66
76
|
const files = [];
|
|
67
77
|
const timestamp = new Date().toISOString();
|
|
68
78
|
for (const filePath of filePaths) {
|
|
69
|
-
const fullPath =
|
|
79
|
+
const fullPath = resolveSnapshotPath(cwd, filePath);
|
|
70
80
|
let content = null;
|
|
71
81
|
let hash = "";
|
|
72
82
|
try {
|
|
@@ -118,7 +128,7 @@ function restoreSnapshot(cwd, snapshotId) {
|
|
|
118
128
|
}
|
|
119
129
|
try {
|
|
120
130
|
for (const file of snapshot.files) {
|
|
121
|
-
const fullPath =
|
|
131
|
+
const fullPath = resolveSnapshotPath(cwd, file.filePath);
|
|
122
132
|
if (file.content === null) {
|
|
123
133
|
// File didn't exist, delete if it exists now
|
|
124
134
|
if (node_fs_1.default.existsSync(fullPath)) {
|
|
@@ -143,7 +153,7 @@ function restoreSnapshot(cwd, snapshotId) {
|
|
|
143
153
|
function detectConflicts(cwd, expectedFiles) {
|
|
144
154
|
const conflicts = [];
|
|
145
155
|
for (const expected of expectedFiles) {
|
|
146
|
-
const fullPath =
|
|
156
|
+
const fullPath = resolveSnapshotPath(cwd, expected.filePath);
|
|
147
157
|
const expectedHash = expected.hash || (expected.content ? hashContent(expected.content) : "");
|
|
148
158
|
try {
|
|
149
159
|
if (!node_fs_1.default.existsSync(fullPath)) {
|