@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/auth.js
CHANGED
|
@@ -26,13 +26,7 @@ const CREDENTIALS_FILE = node_path_1.default.join(CREDENTIALS_DIR, "credentials.
|
|
|
26
26
|
* local development.
|
|
27
27
|
*/
|
|
28
28
|
function serverUrlFrom(fileEnv) {
|
|
29
|
-
return (process.env.LASSO_AUTH_URL ||
|
|
30
|
-
fileEnv.LASSO_AUTH_URL ||
|
|
31
|
-
process.env.LASSO_API_URL ||
|
|
32
|
-
fileEnv.LASSO_API_URL ||
|
|
33
|
-
process.env.LASSO_SERVER_URL ||
|
|
34
|
-
fileEnv.LASSO_SERVER_URL ||
|
|
35
|
-
(process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://api.lasso.byorello.space")).replace(/\/$/, "");
|
|
29
|
+
return (process.env.LASSO_AUTH_URL || fileEnv.LASSO_AUTH_URL || process.env.LASSO_API_URL || fileEnv.LASSO_API_URL || process.env.LASSO_SERVER_URL || fileEnv.LASSO_SERVER_URL || (process.env.NODE_ENV === "development" ? "http://localhost:3005" : "https://api.lasso.byorello.space")).replace(/\/$/, "");
|
|
36
30
|
}
|
|
37
31
|
function fetchWithTimeout(url, init, timeoutMs = 8000) {
|
|
38
32
|
const controller = new AbortController();
|
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -96,6 +96,11 @@ export type BridgeMessage = {
|
|
|
96
96
|
type: "agent_prompt_response";
|
|
97
97
|
taskId: string;
|
|
98
98
|
response: string;
|
|
99
|
+
} | {
|
|
100
|
+
type: "steer";
|
|
101
|
+
taskId: string;
|
|
102
|
+
queueId: string;
|
|
103
|
+
prompt: string;
|
|
99
104
|
} | {
|
|
100
105
|
type: "list_page_folders";
|
|
101
106
|
path?: string;
|
|
@@ -221,6 +226,12 @@ export type ServerBridgeMessage = {
|
|
|
221
226
|
kind: "permission" | "input" | "multiselect";
|
|
222
227
|
options?: string[];
|
|
223
228
|
};
|
|
229
|
+
} | {
|
|
230
|
+
type: "steer_result";
|
|
231
|
+
taskId: string;
|
|
232
|
+
queueId: string;
|
|
233
|
+
success: boolean;
|
|
234
|
+
error?: string;
|
|
224
235
|
} | {
|
|
225
236
|
type: "page_folders";
|
|
226
237
|
path: string;
|
package/dist/cli/bridge.js
CHANGED
|
@@ -35,37 +35,6 @@ function toRuntimeConversation(messages) {
|
|
|
35
35
|
}));
|
|
36
36
|
return turns.length > 0 ? turns : undefined;
|
|
37
37
|
}
|
|
38
|
-
function isConversationalPrompt(prompt) {
|
|
39
|
-
const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
|
|
40
|
-
// 1. Direct greetings and conversational niceties
|
|
41
|
-
const greetings = [
|
|
42
|
-
"hi", "hello", "hey", "heya", "howdy", "sup", "yo",
|
|
43
|
-
"good morning", "good afternoon", "good evening",
|
|
44
|
-
"thanks", "thank you", "thx", "ty", "cool", "awesome", "great", "nice",
|
|
45
|
-
"who are you", "what are you", "what can you do", "help", "help me",
|
|
46
|
-
"what is lasso", "how do you work", "how does this work",
|
|
47
|
-
];
|
|
48
|
-
if (greetings.includes(p))
|
|
49
|
-
return true;
|
|
50
|
-
// Short greetings
|
|
51
|
-
if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
|
|
52
|
-
const buildKeywords = /\b(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)\b/i;
|
|
53
|
-
if (!buildKeywords.test(p))
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
// 2. Action verbs indicating deliberate code changes
|
|
57
|
-
const mutationVerbs = /\b(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code|replace|set|switch|swap|rewrite|restyle|rebuild|apply)\b/i;
|
|
58
|
-
// 3. Question / informational prefixes
|
|
59
|
-
const infoPrefixes = /^(what|how|why|when|where|who|which|can you explain|explain|describe|tell me|check out|check|look at|summarize|give me an overview|overview|inspect)\b/i;
|
|
60
|
-
if (infoPrefixes.test(p) && !mutationVerbs.test(p)) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
// 4. Questions about the project / app / page without mutation verbs
|
|
64
|
-
if (/\b(about|doing|does this|is this|mean|purpose|work|repo|project|codebase|app|application|page|overview|summary)\b/i.test(p) && !mutationVerbs.test(p)) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
38
|
function cleanConversationReply(value) {
|
|
70
39
|
const text = typeof value === "string" ? value.trim() : "";
|
|
71
40
|
if (!text)
|
|
@@ -91,7 +60,7 @@ function cleanConversationReply(value) {
|
|
|
91
60
|
async function restartBridge(port = exports.DEFAULT_BRIDGE_PORT) {
|
|
92
61
|
try {
|
|
93
62
|
const response = await fetch(`http://127.0.0.1:${port}/__lasso/bridge/restart`, { method: "POST" });
|
|
94
|
-
const body = await response.json().catch(() => ({}));
|
|
63
|
+
const body = (await response.json().catch(() => ({})));
|
|
95
64
|
if (!response.ok || !body.ok)
|
|
96
65
|
return { ok: false, error: body.error || "The bridge rejected the restart request." };
|
|
97
66
|
return { ok: true };
|
|
@@ -118,7 +87,10 @@ async function getGitState(cwd) {
|
|
|
118
87
|
}
|
|
119
88
|
function readEnvFile(filename, cwd) {
|
|
120
89
|
try {
|
|
121
|
-
return node_fs_1.default
|
|
90
|
+
return node_fs_1.default
|
|
91
|
+
.readFileSync(node_path_1.default.join(cwd, filename), "utf8")
|
|
92
|
+
.split(/\r?\n/)
|
|
93
|
+
.reduce((values, line) => {
|
|
122
94
|
const match = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
123
95
|
if (match) {
|
|
124
96
|
let value = match[2].trim();
|
|
@@ -245,15 +217,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
245
217
|
});
|
|
246
218
|
}
|
|
247
219
|
const cliCredentials = (0, auth_1.loadCredentials)();
|
|
248
|
-
const resolvedLassoKey = cliCredentials?.apiKey ||
|
|
249
|
-
(0, project_1.resolveLassoApiKey)(fileEnv) ||
|
|
250
|
-
process.env.LASSO_API_KEY ||
|
|
251
|
-
fileEnv.LASSO_API_KEY ||
|
|
252
|
-
process.env.VITE_LASSO_API_KEY ||
|
|
253
|
-
process.env.NEXT_LASSO_API_KEY ||
|
|
254
|
-
fileEnv.VITE_LASSO_API_KEY ||
|
|
255
|
-
fileEnv.NEXT_LASSO_API_KEY ||
|
|
256
|
-
"";
|
|
220
|
+
const resolvedLassoKey = cliCredentials?.apiKey || (0, project_1.resolveLassoApiKey)(fileEnv) || process.env.LASSO_API_KEY || fileEnv.LASSO_API_KEY || process.env.VITE_LASSO_API_KEY || process.env.NEXT_LASSO_API_KEY || fileEnv.VITE_LASSO_API_KEY || fileEnv.NEXT_LASSO_API_KEY || "";
|
|
257
221
|
const lassoKeyConfigured = Boolean(resolvedLassoKey);
|
|
258
222
|
const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv) || "https://api.lasso.byorello.space";
|
|
259
223
|
const runtimeClient = new runtime_client_1.RuntimeClient(serverUrl, resolvedLassoKey || undefined);
|
|
@@ -265,12 +229,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
265
229
|
let models = [];
|
|
266
230
|
try {
|
|
267
231
|
const modelsResponse = await (0, project_1.fetchModels)(fileEnv);
|
|
268
|
-
const allModels = [
|
|
269
|
-
...(modelsResponse.anthropic || []),
|
|
270
|
-
...(modelsResponse.openai || []),
|
|
271
|
-
...(modelsResponse.google || []),
|
|
272
|
-
...(modelsResponse.nvidia || []),
|
|
273
|
-
];
|
|
232
|
+
const allModels = [...(modelsResponse.anthropic || []), ...(modelsResponse.openai || []), ...(modelsResponse.google || []), ...(modelsResponse.nvidia || [])];
|
|
274
233
|
models = allModels.map((m) => ({
|
|
275
234
|
id: m.id,
|
|
276
235
|
label: m.name || m.id,
|
|
@@ -311,6 +270,11 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
311
270
|
const target = node_path_1.default.resolve(root, relative && relative !== "." ? relative : "");
|
|
312
271
|
return target === root || target.startsWith(`${root}${node_path_1.default.sep}`) ? target : null;
|
|
313
272
|
};
|
|
273
|
+
const resolveWorkspacePath = (relative) => {
|
|
274
|
+
if (typeof relative !== "string" || !relative || node_path_1.default.isAbsolute(relative))
|
|
275
|
+
return null;
|
|
276
|
+
return resolveInsideProject(node_path_1.default.resolve(cwd), relative);
|
|
277
|
+
};
|
|
314
278
|
const pageEntries = (root, relative) => {
|
|
315
279
|
const directory = resolveInsideProject(root, relative);
|
|
316
280
|
if (!directory)
|
|
@@ -328,7 +292,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
328
292
|
}
|
|
329
293
|
}
|
|
330
294
|
catch { }
|
|
331
|
-
return entries.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1);
|
|
295
|
+
return entries.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "directory" ? -1 : 1));
|
|
332
296
|
};
|
|
333
297
|
bridgeServer.on("request", (req, res) => {
|
|
334
298
|
if (req.method !== "POST" || req.url?.split("?", 1)[0] !== "/__lasso/bridge/restart")
|
|
@@ -358,11 +322,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
358
322
|
const summary = event.data?.summary;
|
|
359
323
|
if (summary) {
|
|
360
324
|
const raw = event.data?.raw;
|
|
361
|
-
const detail = typeof raw === "string" && raw.trim()
|
|
362
|
-
? raw.length > 800
|
|
363
|
-
? `${raw.slice(0, 800)}…`
|
|
364
|
-
: raw
|
|
365
|
-
: undefined;
|
|
325
|
+
const detail = typeof raw === "string" && raw.trim() ? (raw.length > 800 ? `${raw.slice(0, 800)}…` : raw) : undefined;
|
|
366
326
|
socket.send(JSON.stringify({
|
|
367
327
|
type: "agent_status",
|
|
368
328
|
taskId,
|
|
@@ -420,17 +380,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
420
380
|
case "verification_completed": {
|
|
421
381
|
const data = event.data || {};
|
|
422
382
|
const passed = data.success ?? data.passed;
|
|
423
|
-
const summaries = (data.observations || [])
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const errList = (data.errors || [])
|
|
427
|
-
.map((e) => e?.message)
|
|
428
|
-
.filter(Boolean);
|
|
429
|
-
const message = summaries.length
|
|
430
|
-
? summaries.join(" · ")
|
|
431
|
-
: passed
|
|
432
|
-
? "Checks passed"
|
|
433
|
-
: "Checks failed";
|
|
383
|
+
const summaries = (data.observations || []).map((o) => o?.summary).filter(Boolean);
|
|
384
|
+
const errList = (data.errors || []).map((e) => e?.message).filter(Boolean);
|
|
385
|
+
const message = summaries.length ? summaries.join(" · ") : passed ? "Checks passed" : "Checks failed";
|
|
434
386
|
socket.send(JSON.stringify({
|
|
435
387
|
type: "agent_status",
|
|
436
388
|
taskId,
|
|
@@ -470,7 +422,8 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
470
422
|
const messages = finalSession.conversation || [];
|
|
471
423
|
const reply = [...messages]
|
|
472
424
|
.reverse()
|
|
473
|
-
.find((message) => message?.role === "assistant" && typeof message.content === "string")
|
|
425
|
+
.find((message) => message?.role === "assistant" && typeof message.content === "string")
|
|
426
|
+
?.content?.trim();
|
|
474
427
|
if (reply) {
|
|
475
428
|
socket.send(JSON.stringify({
|
|
476
429
|
type: "assistant_message",
|
|
@@ -620,7 +573,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
620
573
|
const controller = new AbortController();
|
|
621
574
|
taskControllers.set(taskId, controller);
|
|
622
575
|
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
623
|
-
if (
|
|
576
|
+
if (message.type === "ask" && !isLocalAgent) {
|
|
624
577
|
await handleHostedConversation(taskId, instruction, model, provider, messages, socket, controller.signal, message.conversationId);
|
|
625
578
|
return;
|
|
626
579
|
}
|
|
@@ -644,30 +597,35 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
644
597
|
projectId,
|
|
645
598
|
framework,
|
|
646
599
|
hasTypeScript: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json")),
|
|
647
|
-
hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) &&
|
|
648
|
-
node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
|
|
600
|
+
hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) && node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
|
|
649
601
|
},
|
|
650
602
|
model: {
|
|
651
603
|
provider: (provider || "anthropic"),
|
|
652
604
|
id: model || "claude-3-5-sonnet-20241022",
|
|
653
605
|
},
|
|
654
|
-
selection: context
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
606
|
+
selection: context
|
|
607
|
+
? {
|
|
608
|
+
element: element
|
|
609
|
+
? {
|
|
610
|
+
tag: element.tag,
|
|
611
|
+
text: element.label,
|
|
612
|
+
source: element.sourceHint,
|
|
613
|
+
attributes: context.attributes,
|
|
614
|
+
}
|
|
615
|
+
: undefined,
|
|
616
|
+
viewport: context?.viewport,
|
|
617
|
+
route: context?.position?.route,
|
|
618
|
+
screenshot: context?.screenshots?.full,
|
|
619
|
+
}
|
|
620
|
+
: undefined,
|
|
621
|
+
backendConfig: isLocalAgent
|
|
622
|
+
? {
|
|
623
|
+
type: "local_agent",
|
|
624
|
+
provider: provider,
|
|
625
|
+
modelId: backendModelId,
|
|
626
|
+
projectPath: cwd,
|
|
627
|
+
}
|
|
628
|
+
: undefined,
|
|
671
629
|
};
|
|
672
630
|
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
673
631
|
const session = await facade.createSession(sessionInput);
|
|
@@ -690,13 +648,49 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
690
648
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
691
649
|
}));
|
|
692
650
|
}
|
|
651
|
+
finally {
|
|
652
|
+
if (taskControllers.get(taskId) === controller)
|
|
653
|
+
taskControllers.delete(taskId);
|
|
654
|
+
}
|
|
693
655
|
}
|
|
694
656
|
// Handler for apply - applies approved pending changes, then runs server-side
|
|
695
657
|
// verification. On failure a corrective run is started automatically.
|
|
696
658
|
async function handleApply(message, socket, cwd) {
|
|
697
659
|
const { taskId, changes } = message;
|
|
698
660
|
const session = activeSessions.get(taskId);
|
|
699
|
-
const
|
|
661
|
+
const facade = taskFacades.get(taskId) || runtimeClient;
|
|
662
|
+
if (!session || !Array.isArray(changes)) {
|
|
663
|
+
socket.send(JSON.stringify({
|
|
664
|
+
type: "agent_status",
|
|
665
|
+
taskId,
|
|
666
|
+
status: "error",
|
|
667
|
+
message: "There is no reviewable agent session for this task.",
|
|
668
|
+
}));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
// The UI is only an approval surface. Never trust its diff as the source
|
|
672
|
+
// of truth; reload the authoritative diff captured by the runtime.
|
|
673
|
+
const latestSession = await facade.getSession(session.id);
|
|
674
|
+
const authoritative = (latestSession.metadata?.pendingChanges || []);
|
|
675
|
+
if (!Array.isArray(authoritative) || authoritative.length === 0) {
|
|
676
|
+
socket.send(JSON.stringify({
|
|
677
|
+
type: "agent_status",
|
|
678
|
+
taskId,
|
|
679
|
+
status: "error",
|
|
680
|
+
message: "This task has no pending changes to apply.",
|
|
681
|
+
}));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const filePaths = authoritative.map((c) => c.filePath);
|
|
685
|
+
if (authoritative.some((change) => !resolveWorkspacePath(change.filePath))) {
|
|
686
|
+
socket.send(JSON.stringify({
|
|
687
|
+
type: "agent_status",
|
|
688
|
+
taskId,
|
|
689
|
+
status: "error",
|
|
690
|
+
message: "The review contains an invalid workspace path.",
|
|
691
|
+
}));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
700
694
|
// The server rolled the workspace back before review, so the disk still
|
|
701
695
|
// holds the originals — capture them now (if the review didn't already).
|
|
702
696
|
if (filePaths.length > 0 && (!taskSnapshots.has(taskId) || (taskSnapshots.get(taskId) || []).length === 0)) {
|
|
@@ -713,9 +707,9 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
713
707
|
}));
|
|
714
708
|
}
|
|
715
709
|
if (filePaths.length > 0) {
|
|
716
|
-
(0, snapshot_1.createSnapshot)(cwd, taskId, `Apply changes for task ${taskId}`, filePaths, undefined, session
|
|
710
|
+
(0, snapshot_1.createSnapshot)(cwd, taskId, `Apply changes for task ${taskId}`, filePaths, undefined, session.id);
|
|
717
711
|
}
|
|
718
|
-
const applyError = applyChanges(
|
|
712
|
+
const applyError = applyChanges(authoritative, cwd);
|
|
719
713
|
if (applyError) {
|
|
720
714
|
// Roll back partial application so the project stays consistent
|
|
721
715
|
const originals = taskSnapshots.get(taskId) || [];
|
|
@@ -744,7 +738,6 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
744
738
|
// Post-apply verification (inconclusive when the engine can't run —
|
|
745
739
|
// never reported as a failure of the changes themselves).
|
|
746
740
|
let validation;
|
|
747
|
-
const facade = taskFacades.get(taskId) || runtimeClient;
|
|
748
741
|
if (session) {
|
|
749
742
|
try {
|
|
750
743
|
const result = await facade.verifySession(session.id, cwd);
|
|
@@ -771,12 +764,13 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
771
764
|
/** Apply approved changes to disk. Returns an error message or null. */
|
|
772
765
|
function applyChanges(changes, cwd) {
|
|
773
766
|
for (const change of changes) {
|
|
774
|
-
const filePath =
|
|
767
|
+
const filePath = resolveWorkspacePath(change.filePath);
|
|
768
|
+
if (!filePath)
|
|
769
|
+
return `Cannot apply ${String(change.filePath)}: invalid workspace path.`;
|
|
775
770
|
const dir = node_path_1.default.dirname(filePath);
|
|
776
771
|
const oldString = typeof change.oldString === "string" ? change.oldString : "";
|
|
777
772
|
const newString = typeof change.newString === "string" ? change.newString : "";
|
|
778
|
-
const operation = change.operation ||
|
|
779
|
-
(oldString === "" && newString === "" ? "deleted" : oldString === "" ? "created" : "modified");
|
|
773
|
+
const operation = change.operation || (oldString === "" && newString === "" ? "deleted" : oldString === "" ? "created" : "modified");
|
|
780
774
|
try {
|
|
781
775
|
if (!node_fs_1.default.existsSync(dir)) {
|
|
782
776
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -846,16 +840,27 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
846
840
|
const { taskId } = message;
|
|
847
841
|
const snapshot = taskSnapshots.get(taskId) || [];
|
|
848
842
|
for (const item of snapshot) {
|
|
843
|
+
const filePath = resolveWorkspacePath(item.filePath);
|
|
844
|
+
if (!filePath) {
|
|
845
|
+
socket.send(JSON.stringify({
|
|
846
|
+
type: "agent_status",
|
|
847
|
+
taskId,
|
|
848
|
+
status: "error",
|
|
849
|
+
message: `Cannot undo ${item.filePath}: invalid workspace path.`,
|
|
850
|
+
}));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
849
853
|
if (item.content === null) {
|
|
850
854
|
// File didn't exist, delete if it was created
|
|
851
855
|
try {
|
|
852
|
-
node_fs_1.default.unlinkSync(
|
|
856
|
+
node_fs_1.default.unlinkSync(filePath);
|
|
853
857
|
}
|
|
854
858
|
catch { }
|
|
855
859
|
}
|
|
856
860
|
else {
|
|
857
861
|
// Restore original content
|
|
858
|
-
node_fs_1.default.
|
|
862
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
863
|
+
node_fs_1.default.writeFileSync(filePath, item.content);
|
|
859
864
|
}
|
|
860
865
|
}
|
|
861
866
|
socket.send(JSON.stringify({
|
|
@@ -895,6 +900,40 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
895
900
|
message: "Agent stopped",
|
|
896
901
|
}));
|
|
897
902
|
}
|
|
903
|
+
/** Forward an overlay answer to the runtime's pending user question. */
|
|
904
|
+
async function handleAgentPromptResponse(message, socket) {
|
|
905
|
+
const session = activeSessions.get(message.taskId);
|
|
906
|
+
const facade = taskFacades.get(message.taskId);
|
|
907
|
+
if (!session || !facade) {
|
|
908
|
+
throw new Error("No active agent session is waiting for a response.");
|
|
909
|
+
}
|
|
910
|
+
if (!facade.respondToPrompt) {
|
|
911
|
+
throw new Error("This agent backend does not support interactive prompts.");
|
|
912
|
+
}
|
|
913
|
+
await facade.respondToPrompt(session.id, message.response);
|
|
914
|
+
socket.send(JSON.stringify({
|
|
915
|
+
type: "agent_status",
|
|
916
|
+
taskId: message.taskId,
|
|
917
|
+
status: "working",
|
|
918
|
+
message: "Continuing with your response…",
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
/** Queue a prompt for the active runtime's next model turn. */
|
|
922
|
+
async function handleSteer(message, socket) {
|
|
923
|
+
const session = activeSessions.get(message.taskId);
|
|
924
|
+
const facade = taskFacades.get(message.taskId);
|
|
925
|
+
if (!session || !facade?.steerSession) {
|
|
926
|
+
socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: false, error: "This agent backend does not support live steering." }));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
await facade.steerSession(session.id, message.prompt);
|
|
931
|
+
socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: true }));
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: false, error: error instanceof Error ? error.message : "Failed to steer the agent." }));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
898
937
|
// Handler for switch_model
|
|
899
938
|
async function handleSwitchModel(message, socket) {
|
|
900
939
|
const { taskId, provider, modelId } = message;
|
|
@@ -945,15 +984,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
945
984
|
taskControllers.get(taskId)?.abort();
|
|
946
985
|
const controller = new AbortController();
|
|
947
986
|
taskControllers.set(taskId, controller);
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
// there are no canned assistant responses in the bridge.
|
|
987
|
+
// All oneshot prompts go through the selected agent/model. The agent
|
|
988
|
+
// decides whether the request needs project work or a conversational reply.
|
|
951
989
|
try {
|
|
952
990
|
const isLocalAgent = ["claude_code", "opencode", "codex_cli", "cursor"].includes(provider || "");
|
|
953
|
-
if (isConversationalPrompt(prompt) && !isLocalAgent) {
|
|
954
|
-
await handleHostedConversation(taskId, prompt, model, provider, message.messages, socket, controller.signal, message.conversationId);
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
991
|
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
958
992
|
const sessionInput = {
|
|
959
993
|
userPrompt: prompt,
|
|
@@ -961,19 +995,20 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
961
995
|
projectId,
|
|
962
996
|
framework,
|
|
963
997
|
hasTypeScript: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json")),
|
|
964
|
-
hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) &&
|
|
965
|
-
node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
|
|
998
|
+
hasReact: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "package.json")) && node_fs_1.default.readFileSync(node_path_1.default.join(cwd, "package.json"), "utf8").includes("react"),
|
|
966
999
|
},
|
|
967
1000
|
model: {
|
|
968
1001
|
provider: (provider || "anthropic"),
|
|
969
1002
|
id: model || "claude-3-5-sonnet-20241022",
|
|
970
1003
|
},
|
|
971
|
-
backendConfig: isLocalAgent
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1004
|
+
backendConfig: isLocalAgent
|
|
1005
|
+
? {
|
|
1006
|
+
type: "local_agent",
|
|
1007
|
+
provider: provider,
|
|
1008
|
+
modelId: backendModelId,
|
|
1009
|
+
projectPath: cwd,
|
|
1010
|
+
}
|
|
1011
|
+
: undefined,
|
|
977
1012
|
};
|
|
978
1013
|
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
979
1014
|
const session = await facade.createSession(sessionInput);
|
|
@@ -993,6 +1028,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
993
1028
|
message: error instanceof Error ? error.message : "Unknown oneshot error",
|
|
994
1029
|
}));
|
|
995
1030
|
}
|
|
1031
|
+
finally {
|
|
1032
|
+
if (taskControllers.get(taskId) === controller)
|
|
1033
|
+
taskControllers.delete(taskId);
|
|
1034
|
+
}
|
|
996
1035
|
}
|
|
997
1036
|
// Handler for restore_snapshot
|
|
998
1037
|
async function handleRestoreSnapshot(message, socket, cwd) {
|
|
@@ -1098,25 +1137,25 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1098
1137
|
overlaySocket = socket;
|
|
1099
1138
|
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
1100
1139
|
const creds = (0, auth_1.loadCredentials)();
|
|
1101
|
-
const userInfo = creds?.userName
|
|
1102
|
-
? { name: creds.userName, email: creds.userEmail }
|
|
1103
|
-
: (process.env.USER ? { name: process.env.USER } : null);
|
|
1140
|
+
const userInfo = creds?.userName ? { name: creds.userName, email: creds.userEmail } : process.env.USER ? { name: process.env.USER } : null;
|
|
1104
1141
|
socket.send(JSON.stringify({
|
|
1105
1142
|
type: "config",
|
|
1106
1143
|
apiKeyConfigured: lassoKeyConfigured,
|
|
1107
1144
|
agentConfigured: lassoKeyConfigured,
|
|
1108
1145
|
models,
|
|
1109
1146
|
collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null,
|
|
1110
|
-
user: userInfo
|
|
1147
|
+
user: userInfo,
|
|
1111
1148
|
}));
|
|
1112
1149
|
void getGitState(cwd).then((git) => {
|
|
1113
1150
|
if (socket.readyState === socket.OPEN)
|
|
1114
1151
|
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
1115
1152
|
});
|
|
1116
1153
|
socket.on("message", async (raw) => {
|
|
1117
|
-
|
|
1118
|
-
console.log("[lasso] received from overlay:", msg.type);
|
|
1154
|
+
let taskId;
|
|
1119
1155
|
try {
|
|
1156
|
+
const msg = JSON.parse(raw.toString());
|
|
1157
|
+
taskId = msg.taskId;
|
|
1158
|
+
console.log("[lasso] received from overlay:", msg.type);
|
|
1120
1159
|
switch (msg.type) {
|
|
1121
1160
|
case "hello":
|
|
1122
1161
|
break;
|
|
@@ -1133,6 +1172,12 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1133
1172
|
case "stop":
|
|
1134
1173
|
await handleStop(msg, socket);
|
|
1135
1174
|
break;
|
|
1175
|
+
case "agent_prompt_response":
|
|
1176
|
+
await handleAgentPromptResponse(msg, socket);
|
|
1177
|
+
break;
|
|
1178
|
+
case "steer":
|
|
1179
|
+
await handleSteer(msg, socket);
|
|
1180
|
+
break;
|
|
1136
1181
|
case "switch_model":
|
|
1137
1182
|
await handleSwitchModel(msg, socket);
|
|
1138
1183
|
break;
|
|
@@ -1228,6 +1273,17 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1228
1273
|
// daemon log so it's diagnosable.
|
|
1229
1274
|
console.error("[overlay error]", msg.selectionId || "", msg.details || msg.message || msg);
|
|
1230
1275
|
break;
|
|
1276
|
+
case "page_context_response": {
|
|
1277
|
+
const pending = pendingBrowserContext.get(msg.requestId);
|
|
1278
|
+
if (!pending)
|
|
1279
|
+
break;
|
|
1280
|
+
pendingBrowserContext.delete(msg.requestId);
|
|
1281
|
+
if (msg.error)
|
|
1282
|
+
pending.reject(new Error(msg.error));
|
|
1283
|
+
else
|
|
1284
|
+
pending.resolve(msg.pageContext);
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1231
1287
|
default:
|
|
1232
1288
|
console.warn("Unknown bridge message type:", msg.type);
|
|
1233
1289
|
}
|
|
@@ -1236,7 +1292,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1236
1292
|
console.error("Error handling bridge message:", error);
|
|
1237
1293
|
socket.send(JSON.stringify({
|
|
1238
1294
|
type: "agent_status",
|
|
1239
|
-
taskId
|
|
1295
|
+
taskId,
|
|
1240
1296
|
status: "error",
|
|
1241
1297
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
1242
1298
|
}));
|
|
@@ -1245,6 +1301,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1245
1301
|
socket.on("close", () => {
|
|
1246
1302
|
if (socket === overlaySocket)
|
|
1247
1303
|
overlaySocket = null;
|
|
1304
|
+
for (const [requestId, pending] of pendingBrowserContext) {
|
|
1305
|
+
pending.reject(new Error("The overlay disconnected before browser context was returned."));
|
|
1306
|
+
pendingBrowserContext.delete(requestId);
|
|
1307
|
+
}
|
|
1248
1308
|
});
|
|
1249
1309
|
});
|
|
1250
1310
|
bridgeServer.listen(bridgePort, "127.0.0.1", () => {
|
|
@@ -86,7 +86,8 @@ async function downloadMkcert() {
|
|
|
86
86
|
const tempPath = node_path_1.default.join(tempDir, binaryName);
|
|
87
87
|
console.log(`[Lasso] Downloading mkcert from ${downloadUrl}...`);
|
|
88
88
|
const file = node_fs_1.default.createWriteStream(tempPath);
|
|
89
|
-
protocol
|
|
89
|
+
protocol
|
|
90
|
+
.get(downloadUrl, (response) => {
|
|
90
91
|
if (response.statusCode !== 200) {
|
|
91
92
|
node_fs_1.default.unlinkSync(tempPath);
|
|
92
93
|
return resolve({ ok: false, error: `Failed to download mkcert: ${response.statusCode}` });
|
|
@@ -98,7 +99,8 @@ async function downloadMkcert() {
|
|
|
98
99
|
console.log(`[Lasso] ✓ mkcert downloaded to ${tempPath}`);
|
|
99
100
|
resolve({ ok: true, path: tempPath });
|
|
100
101
|
});
|
|
101
|
-
})
|
|
102
|
+
})
|
|
103
|
+
.on("error", (error) => {
|
|
102
104
|
node_fs_1.default.unlinkSync(tempPath);
|
|
103
105
|
resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
104
106
|
});
|
|
@@ -260,11 +262,7 @@ async function ensureHostCertificate(onProgress) {
|
|
|
260
262
|
const configFile = node_path_1.default.join(node_os_1.default.tmpdir(), `lasso-host-${process.pid}.cnf`);
|
|
261
263
|
try {
|
|
262
264
|
onProgress?.("Creating self-signed certificate with OpenSSL...");
|
|
263
|
-
node_fs_1.default.writeFileSync(configFile, [
|
|
264
|
-
"[req]", "distinguished_name = req_distinguished_name", "x509_extensions = v3_req", "prompt = no",
|
|
265
|
-
"[req_distinguished_name]", "CN = *.lasso", "[v3_req]",
|
|
266
|
-
"subjectAltName = DNS:*.lasso,DNS:localhost,IP:127.0.0.1,IP:::1", "",
|
|
267
|
-
].join("\n"), { mode: 0o600 });
|
|
265
|
+
node_fs_1.default.writeFileSync(configFile, ["[req]", "distinguished_name = req_distinguished_name", "x509_extensions = v3_req", "prompt = no", "[req_distinguished_name]", "CN = *.lasso", "[v3_req]", "subjectAltName = DNS:*.lasso,DNS:localhost,IP:127.0.0.1,IP:::1", ""].join("\n"), { mode: 0o600 });
|
|
268
266
|
(0, node_child_process_1.execFileSync)("openssl", ["req", "-x509", "-nodes", "-newkey", "rsa:2048", "-keyout", exports.TLS_KEY_FILE, "-out", exports.TLS_CERT_FILE, "-days", "825", "-config", configFile], { stdio: "ignore" });
|
|
269
267
|
node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
|
|
270
268
|
onProgress?.("✓ SSL certificate ready (self-signed)");
|
package/dist/cli/host/daemon.js
CHANGED
|
@@ -158,7 +158,9 @@ class LassoHost {
|
|
|
158
158
|
route: "POST /_host/register",
|
|
159
159
|
handler: async (req, res) => {
|
|
160
160
|
const body = await readBody(req);
|
|
161
|
-
const domain = String(body.domain || "")
|
|
161
|
+
const domain = String(body.domain || "")
|
|
162
|
+
.trim()
|
|
163
|
+
.toLowerCase();
|
|
162
164
|
const directory = String(body.directory || "").trim();
|
|
163
165
|
const projectId = String(body.projectId || "").trim();
|
|
164
166
|
if (!(0, registry_1.validateDomain)(domain))
|
|
@@ -183,7 +185,9 @@ class LassoHost {
|
|
|
183
185
|
route: "POST /_host/unregister",
|
|
184
186
|
handler: async (req, res) => {
|
|
185
187
|
const body = await readBody(req);
|
|
186
|
-
const domain = String(body.domain || "")
|
|
188
|
+
const domain = String(body.domain || "")
|
|
189
|
+
.trim()
|
|
190
|
+
.toLowerCase();
|
|
187
191
|
if (!domain)
|
|
188
192
|
return writeJson(res, 400, { ok: false, error: "A domain is required." });
|
|
189
193
|
const had = domain in this.registry;
|
|
@@ -199,7 +203,9 @@ class LassoHost {
|
|
|
199
203
|
route: "POST /_host/stop",
|
|
200
204
|
handler: async (req, res) => {
|
|
201
205
|
const body = await readBody(req);
|
|
202
|
-
const domain = String(body.domain || "")
|
|
206
|
+
const domain = String(body.domain || "")
|
|
207
|
+
.trim()
|
|
208
|
+
.toLowerCase();
|
|
203
209
|
const runtime = this.running.get(domain);
|
|
204
210
|
if (!runtime)
|
|
205
211
|
return writeJson(res, 200, { ok: true, domain, status: "stopped" });
|
|
@@ -229,7 +235,9 @@ class LassoHost {
|
|
|
229
235
|
route: "POST /_host/restart-project",
|
|
230
236
|
handler: async (req, res) => {
|
|
231
237
|
const body = await readBody(req);
|
|
232
|
-
const domain = String(body.domain || "")
|
|
238
|
+
const domain = String(body.domain || "")
|
|
239
|
+
.trim()
|
|
240
|
+
.toLowerCase();
|
|
233
241
|
if (!domain)
|
|
234
242
|
return writeJson(res, 400, { ok: false, error: "A project domain is required." });
|
|
235
243
|
const runtime = this.running.get(domain);
|
|
@@ -377,7 +385,10 @@ class LassoHost {
|
|
|
377
385
|
this.log(`proxy listening on 127.0.0.1:${this.opts.proxyPort}`);
|
|
378
386
|
if (!certificate.ok)
|
|
379
387
|
return resolve({});
|
|
380
|
-
this.secureServer.once("error", (error) => {
|
|
388
|
+
this.secureServer.once("error", (error) => {
|
|
389
|
+
this.log(`https proxy failed: ${line(error)}`);
|
|
390
|
+
resolve({});
|
|
391
|
+
});
|
|
381
392
|
this.secureServer.listen(this.opts.httpsPort, "127.0.0.1", () => {
|
|
382
393
|
this.log(`https proxy listening on 127.0.0.1:${this.opts.httpsPort}`);
|
|
383
394
|
resolve({});
|
package/dist/cli/host/dns.js
CHANGED
|
@@ -200,7 +200,10 @@ class WindowsResolver {
|
|
|
200
200
|
const match = content.match(new RegExp(`${HOSTS_BLOCK_START}\\n([\\s\\S]*?)\\n${HOSTS_BLOCK_END}`));
|
|
201
201
|
if (!match)
|
|
202
202
|
return [];
|
|
203
|
-
return match[1]
|
|
203
|
+
return match[1]
|
|
204
|
+
.split(/\r?\n/)
|
|
205
|
+
.map((line) => line.trim())
|
|
206
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
204
207
|
}
|
|
205
208
|
catch {
|
|
206
209
|
return [];
|