@lasso-ai/cli 2.1.13 → 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 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();
@@ -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.readFileSync(node_path_1.default.join(cwd, filename), "utf8").split(/\r?\n/).reduce((values, line) => {
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
- .map((o) => o?.summary)
425
- .filter(Boolean);
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")?.content?.trim();
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 ((message.type === "ask" || isConversationalPrompt(instruction)) && !isLocalAgent) {
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
- element: element ? {
656
- tag: element.tag,
657
- text: element.label,
658
- source: element.sourceHint,
659
- attributes: context.attributes,
660
- } : undefined,
661
- viewport: context?.viewport,
662
- route: context?.position?.route,
663
- screenshot: context?.screenshots?.full,
664
- } : undefined,
665
- backendConfig: isLocalAgent ? {
666
- type: "local_agent",
667
- provider: provider,
668
- modelId: backendModelId,
669
- projectPath: cwd,
670
- } : undefined,
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 filePaths = changes.map((c) => c.filePath);
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?.id);
710
+ (0, snapshot_1.createSnapshot)(cwd, taskId, `Apply changes for task ${taskId}`, filePaths, undefined, session.id);
717
711
  }
718
- const applyError = applyChanges(changes, cwd);
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 = node_path_1.default.join(cwd, change.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(node_path_1.default.join(cwd, item.filePath));
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.writeFileSync(node_path_1.default.join(cwd, item.filePath), item.content);
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
- // Greetings and conversational messages still go through the selected
949
- // agent/model. The overlay only skips expensive page capture for them;
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
- type: "local_agent",
973
- provider: provider,
974
- modelId: backendModelId,
975
- projectPath: cwd,
976
- } : undefined,
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
- const msg = JSON.parse(raw.toString());
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: msg.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.get(downloadUrl, (response) => {
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
- }).on("error", (error) => {
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)");
@@ -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 || "").trim().toLowerCase();
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 || "").trim().toLowerCase();
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 || "").trim().toLowerCase();
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 || "").trim().toLowerCase();
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) => { this.log(`https proxy failed: ${line(error)}`); resolve({}); });
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({});
@@ -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].split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
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 [];
@@ -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.command("restart")
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) => {