@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 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();
@@ -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;
@@ -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({
@@ -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
- // 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.
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
- type: "local_agent",
973
- provider: provider,
974
- modelId: backendModelId,
975
- projectPath: cwd,
976
- } : undefined,
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
- const msg = JSON.parse(raw.toString());
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: msg.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.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 [];