@lasso-ai/cli 2.1.14 → 2.1.15
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.js +137 -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 +2 -0
- package/dist/cli/runtime-client.js +73 -65
- package/dist/cli/server/vite.js +1 -1
- package/dist/cli/snapshot.js +13 -3
- package/dist/overlay.js +139 -282
- 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-state-machine.js +2 -5
- package/dist/runtime/review.js +3 -7
- 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.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({
|
|
@@ -945,15 +950,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
945
950
|
taskControllers.get(taskId)?.abort();
|
|
946
951
|
const controller = new AbortController();
|
|
947
952
|
taskControllers.set(taskId, controller);
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
// there are no canned assistant responses in the bridge.
|
|
953
|
+
// All oneshot prompts go through the selected agent/model. The agent
|
|
954
|
+
// decides whether the request needs project work or a conversational reply.
|
|
951
955
|
try {
|
|
952
956
|
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
957
|
const backendModelId = (0, backends_1.modelIdForBackend)(model || "", provider || "");
|
|
958
958
|
const sessionInput = {
|
|
959
959
|
userPrompt: prompt,
|
|
@@ -961,19 +961,20 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
961
961
|
projectId,
|
|
962
962
|
framework,
|
|
963
963
|
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"),
|
|
964
|
+
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
965
|
},
|
|
967
966
|
model: {
|
|
968
967
|
provider: (provider || "anthropic"),
|
|
969
968
|
id: model || "claude-3-5-sonnet-20241022",
|
|
970
969
|
},
|
|
971
|
-
backendConfig: isLocalAgent
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
970
|
+
backendConfig: isLocalAgent
|
|
971
|
+
? {
|
|
972
|
+
type: "local_agent",
|
|
973
|
+
provider: provider,
|
|
974
|
+
modelId: backendModelId,
|
|
975
|
+
projectPath: cwd,
|
|
976
|
+
}
|
|
977
|
+
: undefined,
|
|
977
978
|
};
|
|
978
979
|
const facade = isLocalAgent ? (0, local_runtime_1.getLocalRuntime)() : runtimeClient;
|
|
979
980
|
const session = await facade.createSession(sessionInput);
|
|
@@ -993,6 +994,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
993
994
|
message: error instanceof Error ? error.message : "Unknown oneshot error",
|
|
994
995
|
}));
|
|
995
996
|
}
|
|
997
|
+
finally {
|
|
998
|
+
if (taskControllers.get(taskId) === controller)
|
|
999
|
+
taskControllers.delete(taskId);
|
|
1000
|
+
}
|
|
996
1001
|
}
|
|
997
1002
|
// Handler for restore_snapshot
|
|
998
1003
|
async function handleRestoreSnapshot(message, socket, cwd) {
|
|
@@ -1098,25 +1103,25 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1098
1103
|
overlaySocket = socket;
|
|
1099
1104
|
console.log(chalk_1.default.green("✓") + " Overlay connected");
|
|
1100
1105
|
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);
|
|
1106
|
+
const userInfo = creds?.userName ? { name: creds.userName, email: creds.userEmail } : process.env.USER ? { name: process.env.USER } : null;
|
|
1104
1107
|
socket.send(JSON.stringify({
|
|
1105
1108
|
type: "config",
|
|
1106
1109
|
apiKeyConfigured: lassoKeyConfigured,
|
|
1107
1110
|
agentConfigured: lassoKeyConfigured,
|
|
1108
1111
|
models,
|
|
1109
1112
|
collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null,
|
|
1110
|
-
user: userInfo
|
|
1113
|
+
user: userInfo,
|
|
1111
1114
|
}));
|
|
1112
1115
|
void getGitState(cwd).then((git) => {
|
|
1113
1116
|
if (socket.readyState === socket.OPEN)
|
|
1114
1117
|
socket.send(JSON.stringify({ type: "git_state", git }));
|
|
1115
1118
|
});
|
|
1116
1119
|
socket.on("message", async (raw) => {
|
|
1117
|
-
|
|
1118
|
-
console.log("[lasso] received from overlay:", msg.type);
|
|
1120
|
+
let taskId;
|
|
1119
1121
|
try {
|
|
1122
|
+
const msg = JSON.parse(raw.toString());
|
|
1123
|
+
taskId = msg.taskId;
|
|
1124
|
+
console.log("[lasso] received from overlay:", msg.type);
|
|
1120
1125
|
switch (msg.type) {
|
|
1121
1126
|
case "hello":
|
|
1122
1127
|
break;
|
|
@@ -1228,6 +1233,17 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1228
1233
|
// daemon log so it's diagnosable.
|
|
1229
1234
|
console.error("[overlay error]", msg.selectionId || "", msg.details || msg.message || msg);
|
|
1230
1235
|
break;
|
|
1236
|
+
case "page_context_response": {
|
|
1237
|
+
const pending = pendingBrowserContext.get(msg.requestId);
|
|
1238
|
+
if (!pending)
|
|
1239
|
+
break;
|
|
1240
|
+
pendingBrowserContext.delete(msg.requestId);
|
|
1241
|
+
if (msg.error)
|
|
1242
|
+
pending.reject(new Error(msg.error));
|
|
1243
|
+
else
|
|
1244
|
+
pending.resolve(msg.pageContext);
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1231
1247
|
default:
|
|
1232
1248
|
console.warn("Unknown bridge message type:", msg.type);
|
|
1233
1249
|
}
|
|
@@ -1236,7 +1252,7 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1236
1252
|
console.error("Error handling bridge message:", error);
|
|
1237
1253
|
socket.send(JSON.stringify({
|
|
1238
1254
|
type: "agent_status",
|
|
1239
|
-
taskId
|
|
1255
|
+
taskId,
|
|
1240
1256
|
status: "error",
|
|
1241
1257
|
message: error instanceof Error ? error.message : "Unknown error",
|
|
1242
1258
|
}));
|
|
@@ -1245,6 +1261,10 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1245
1261
|
socket.on("close", () => {
|
|
1246
1262
|
if (socket === overlaySocket)
|
|
1247
1263
|
overlaySocket = null;
|
|
1264
|
+
for (const [requestId, pending] of pendingBrowserContext) {
|
|
1265
|
+
pending.reject(new Error("The overlay disconnected before browser context was returned."));
|
|
1266
|
+
pendingBrowserContext.delete(requestId);
|
|
1267
|
+
}
|
|
1248
1268
|
});
|
|
1249
1269
|
});
|
|
1250
1270
|
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 [];
|
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) => {
|