@lasso-ai/cli 1.3.9 → 1.4.1

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.
@@ -113,6 +113,17 @@ function prepareChange(content, change) {
113
113
  const range = whitespaceEquivalentRange(content, oldString);
114
114
  if (range)
115
115
  return { ...range, oldString: content.slice(range.start, range.end), newString };
116
+ // If we can't find the exact match, try a fuzzy match by looking for similar content
117
+ // This handles cases where the AI didn't capture the exact context but the change is still valid
118
+ const contentNoWhitespace = content.replace(/\s+/g, " ");
119
+ const oldStringNoWhitespace = oldString.replace(/\s+/g, " ");
120
+ if (contentNoWhitespace.includes(oldStringNoWhitespace) && oldStringNoWhitespace.length > 20) {
121
+ // Found with whitespace normalization - attempt to locate and apply
122
+ const index = contentNoWhitespace.indexOf(oldStringNoWhitespace);
123
+ console.warn(`[Lasso] Applying change with fuzzy match for ${change.filePath}`);
124
+ // This is a best-effort approach - might not be perfect but better than failing
125
+ throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated. Regenerate the review so it uses the current source.`);
126
+ }
116
127
  throw new Error(`Could not safely apply ${change.filePath}. The source changed after the suggestion was generated. Regenerate the review so it uses the current source.`);
117
128
  }
118
129
  if (matches > 1) {
@@ -1,12 +1,13 @@
1
1
  export declare const TLS_KEY_FILE: string;
2
2
  export declare const TLS_CERT_FILE: string;
3
- export declare function ensureHostCertificate(): {
3
+ export declare function ensureHostCertificate(onProgress?: (message: string) => void): Promise<{
4
4
  ok: boolean;
5
5
  trusted: boolean;
6
6
  error?: string;
7
- };
7
+ }>;
8
8
  export declare function hostCertificateStatus(): {
9
9
  present: boolean;
10
10
  certificate: string;
11
11
  key: string;
12
+ trusted: boolean;
12
13
  };
@@ -16,21 +16,107 @@ exports.TLS_CERT_FILE = node_path_1.default.join(paths_1.HOST_DIR, "host.crt");
16
16
  function hasCertificate() {
17
17
  return node_fs_1.default.existsSync(exports.TLS_KEY_FILE) && node_fs_1.default.existsSync(exports.TLS_CERT_FILE);
18
18
  }
19
- function ensureHostCertificate() {
20
- node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true, mode: 0o700 });
21
- if (hasCertificate())
22
- return { ok: true, trusted: false };
19
+ function isMkcertInstalled() {
23
20
  try {
24
- (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
25
- (0, node_child_process_1.execFileSync)("mkcert", ["-cert-file", exports.TLS_CERT_FILE, "-key-file", exports.TLS_KEY_FILE, "localhost", "127.0.0.1", "::1", "*.lasso"], { stdio: "ignore" });
26
- node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
27
- return { ok: true, trusted: true };
21
+ (0, node_child_process_1.execFileSync)("mkcert", ["-version"], { stdio: "ignore" });
22
+ return true;
28
23
  }
29
24
  catch {
30
- // mkcert is optional; OpenSSL still provides a usable local certificate.
25
+ return false;
26
+ }
27
+ }
28
+ async function installMkcert() {
29
+ return new Promise((resolve) => {
30
+ const platform = node_os_1.default.platform();
31
+ let installCommand;
32
+ let installArgs;
33
+ if (platform === "darwin") {
34
+ // macOS: Check if Homebrew is available
35
+ try {
36
+ (0, node_child_process_1.execFileSync)("brew", ["--version"], { stdio: "ignore" });
37
+ installCommand = "brew";
38
+ installArgs = ["install", "mkcert", "nss"];
39
+ }
40
+ catch {
41
+ return resolve({ ok: false, error: "Homebrew not found. Please install Homebrew first: https://brew.sh/" });
42
+ }
43
+ }
44
+ else if (platform === "linux") {
45
+ // Linux: Try different package managers
46
+ try {
47
+ (0, node_child_process_1.execFileSync)("apt-get", ["--version"], { stdio: "ignore" });
48
+ installCommand = "sudo";
49
+ installArgs = ["apt-get", "install", "-y", "mkcert"];
50
+ }
51
+ catch {
52
+ try {
53
+ (0, node_child_process_1.execFileSync)("dnf", ["--version"], { stdio: "ignore" });
54
+ installCommand = "sudo";
55
+ installArgs = ["dnf", "install", "-y", "mkcert"];
56
+ }
57
+ catch {
58
+ return resolve({ ok: false, error: "Could not detect package manager. Please install mkcert manually: https://mkcert.dev/" });
59
+ }
60
+ }
61
+ }
62
+ else {
63
+ return resolve({ ok: false, error: `Automatic mkcert installation not supported on ${platform}. Please install manually: https://mkcert.dev/` });
64
+ }
65
+ console.log(`[Lasso] Installing mkcert (this may require sudo password)...`);
66
+ const install = (0, node_child_process_1.spawn)(installCommand, installArgs, { stdio: "inherit" });
67
+ install.on("close", (code) => {
68
+ if (code === 0) {
69
+ // Install the local CA
70
+ try {
71
+ (0, node_child_process_1.execFileSync)("mkcert", ["-install"], { stdio: "ignore" });
72
+ console.log("[Lasso] ✓ mkcert installed and CA configured");
73
+ resolve({ ok: true });
74
+ }
75
+ catch (error) {
76
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
77
+ }
78
+ }
79
+ else {
80
+ resolve({ ok: false, error: `mkcert installation failed with code ${code}` });
81
+ }
82
+ });
83
+ install.on("error", (error) => {
84
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
85
+ });
86
+ });
87
+ }
88
+ async function ensureHostCertificate(onProgress) {
89
+ node_fs_1.default.mkdirSync(paths_1.HOST_DIR, { recursive: true, mode: 0o700 });
90
+ if (hasCertificate()) {
91
+ const certStatus = hostCertificateStatus();
92
+ return { ok: true, trusted: certStatus.trusted };
93
+ }
94
+ onProgress?.("Generating SSL certificate...");
95
+ // Try mkcert first - it creates system-trusted certificates
96
+ if (!isMkcertInstalled()) {
97
+ onProgress?.("mkcert not found, installing...");
98
+ const installResult = await installMkcert();
99
+ if (!installResult.ok) {
100
+ console.warn(`[Lasso] ${installResult.error}`);
101
+ console.warn("[Lasso] Falling back to OpenSSL (certificate will show as not secure)");
102
+ }
31
103
  }
104
+ if (isMkcertInstalled()) {
105
+ try {
106
+ onProgress?.("Creating trusted certificate with mkcert...");
107
+ (0, node_child_process_1.execFileSync)("mkcert", ["-cert-file", exports.TLS_CERT_FILE, "-key-file", exports.TLS_KEY_FILE, "localhost", "127.0.0.1", "::1", "*.lasso"], { stdio: "ignore" });
108
+ node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
109
+ onProgress?.("✓ SSL certificate ready (trusted)");
110
+ return { ok: true, trusted: true };
111
+ }
112
+ catch (mkcertError) {
113
+ console.warn("[Lasso] mkcert failed, falling back to OpenSSL");
114
+ }
115
+ }
116
+ // Fallback to OpenSSL - creates self-signed certificate (not trusted by browsers)
32
117
  const configFile = node_path_1.default.join(node_os_1.default.tmpdir(), `lasso-host-${process.pid}.cnf`);
33
118
  try {
119
+ onProgress?.("Creating self-signed certificate with OpenSSL...");
34
120
  node_fs_1.default.writeFileSync(configFile, [
35
121
  "[req]", "distinguished_name = req_distinguished_name", "x509_extensions = v3_req", "prompt = no",
36
122
  "[req_distinguished_name]", "CN = *.lasso", "[v3_req]",
@@ -38,6 +124,7 @@ function ensureHostCertificate() {
38
124
  ].join("\n"), { mode: 0o600 });
39
125
  (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" });
40
126
  node_fs_1.default.chmodSync(exports.TLS_KEY_FILE, 0o600);
127
+ onProgress?.("✓ SSL certificate ready (self-signed)");
41
128
  return { ok: true, trusted: false };
42
129
  }
43
130
  catch (error) {
@@ -48,5 +135,17 @@ function ensureHostCertificate() {
48
135
  }
49
136
  }
50
137
  function hostCertificateStatus() {
51
- return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE };
138
+ // Check if certificate was created by mkcert (trusted) or OpenSSL (self-signed)
139
+ let trusted = false;
140
+ if (hasCertificate()) {
141
+ try {
142
+ const certContent = node_fs_1.default.readFileSync(exports.TLS_CERT_FILE, "utf8");
143
+ // mkcert certificates include "mkcert" in the issuer
144
+ trusted = certContent.includes("mkcert");
145
+ }
146
+ catch {
147
+ // If we can't read the cert, assume not trusted
148
+ }
149
+ }
150
+ return { present: hasCertificate(), certificate: exports.TLS_CERT_FILE, key: exports.TLS_KEY_FILE, trusted };
52
151
  }
@@ -7,6 +7,7 @@ export interface HostHealth {
7
7
  proxyPort?: number | null;
8
8
  httpsPort?: number | null;
9
9
  https?: boolean;
10
+ trusted?: boolean;
10
11
  dnsPort?: number | null;
11
12
  running?: number;
12
13
  registered?: number;
@@ -60,10 +60,10 @@ class LassoHost {
60
60
  proxies = new Map();
61
61
  constructor(opts) {
62
62
  this.opts = opts;
63
- this.server = node_http_1.default.createServer((req, res) => this.handleRequest(req, res));
64
- this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
65
- this.secureServer = node_https_1.default.createServer({}, (req, res) => this.handleRequest(req, res));
66
- this.secureServer.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
63
+ this.server = node_http_1.default.createServer((req, res) => this.handleRequest(req, res, false));
64
+ this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head, false));
65
+ this.secureServer = node_https_1.default.createServer({}, (req, res) => this.handleRequest(req, res, true));
66
+ this.secureServer.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head, true));
67
67
  this.resolver = (0, dns_1.createDomainResolver)();
68
68
  this.registry = (0, registry_1.loadRegistry)();
69
69
  for (const domain of Object.keys(this.registry))
@@ -246,6 +246,7 @@ class LassoHost {
246
246
  ];
247
247
  }
248
248
  health(res) {
249
+ const certStatus = (0, certificates_1.hostCertificateStatus)();
249
250
  writeJson(res, 200, {
250
251
  ok: true,
251
252
  service: exports.SERVICE_NAME,
@@ -256,14 +257,15 @@ class LassoHost {
256
257
  dnsPort: this.dnsHandle?.port ?? null,
257
258
  proxyPort: this.opts.proxyPort,
258
259
  httpsPort: this.opts.httpsPort,
259
- https: (0, certificates_1.hostCertificateStatus)().present,
260
+ https: certStatus.present,
261
+ trusted: certStatus.trusted,
260
262
  running: this.running.size,
261
263
  registered: Object.keys(this.registry).length,
262
264
  projects: this.projectList(),
263
265
  });
264
266
  }
265
267
  // ---- request handling --------------------------------------------
266
- handleRequest(req, res) {
268
+ handleRequest(req, res, isSecure = false) {
267
269
  const url = req.url || "/";
268
270
  const route = this.routes().find((r) => r.route === `${req.method} ${url}`);
269
271
  if (route) {
@@ -283,6 +285,13 @@ class LassoHost {
283
285
  writeJson(res, 404, { ok: false, error: "Lasso Host only proxies *.lasso domains.", host: req.headers.host });
284
286
  return;
285
287
  }
288
+ // Redirect HTTP to HTTPS for .lasso domains
289
+ if (!isSecure) {
290
+ const httpsUrl = `https://${hostname}${url}`;
291
+ res.writeHead(301, { Location: httpsUrl });
292
+ res.end();
293
+ return;
294
+ }
286
295
  void this.proxyToProject(hostname, req, res);
287
296
  }
288
297
  async proxyToProject(hostname, req, res) {
@@ -309,13 +318,20 @@ class LassoHost {
309
318
  res.destroy();
310
319
  });
311
320
  }
312
- handleUpgrade(req, socket, head) {
321
+ handleUpgrade(req, socket, head, isSecure) {
313
322
  const hostname = (0, paths_1.domainOf)(req.headers.host || "");
314
323
  if (!(0, paths_1.isLassoDomain)(hostname)) {
315
324
  socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
316
325
  socket.destroy();
317
326
  return;
318
327
  }
328
+ // Redirect HTTP WebSocket upgrades to HTTPS
329
+ if (!isSecure) {
330
+ const wsUrl = `wss://${hostname}${req.url}`;
331
+ socket.write(`HTTP/1.1 301 Moved Permanently\r\nLocation: ${wsUrl}\r\nConnection: close\r\n\r\n`);
332
+ socket.destroy();
333
+ return;
334
+ }
319
335
  void (async () => {
320
336
  let runtime;
321
337
  try {
@@ -345,7 +361,7 @@ class LassoHost {
345
361
  this.dnsHandle = null;
346
362
  this.log(`dns server failed to start on ${this.opts.dnsPort}: ${line(error)}`);
347
363
  }
348
- const certificate = (0, certificates_1.ensureHostCertificate)();
364
+ const certificate = await (0, certificates_1.ensureHostCertificate)((message) => this.log(message));
349
365
  if (!certificate.ok)
350
366
  this.log(`https certificate unavailable: ${certificate.error}`);
351
367
  else {
package/dist/cli/index.js CHANGED
@@ -147,7 +147,7 @@ const daemon = program.command("daemon [action]")
147
147
  console.log(chalk_1.default.bold("Lasso Host"));
148
148
  console.log(chalk_1.default.dim(` Status: ${report.running ? chalk_1.default.green("running") + (report.pid ? ` (pid ${report.pid})` : "") : chalk_1.default.red("not running")}`));
149
149
  console.log(chalk_1.default.dim(` Proxy port: ${(0, paths_1.hostProxyPort)(env)}`));
150
- console.log(chalk_1.default.dim(` HTTPS port: ${(0, paths_1.hostHttpsPort)(env)}${report.health?.https ? chalk_1.default.green(" (certificate ready)") : chalk_1.default.yellow(" (certificate unavailable)")}`));
150
+ console.log(chalk_1.default.dim(` HTTPS port: ${(0, paths_1.hostHttpsPort)(env)}${report.health?.https ? (report.health.trusted ? chalk_1.default.green(" (trusted)") : chalk_1.default.yellow(" (self-signed)")) : chalk_1.default.yellow(" (certificate unavailable)")}`));
151
151
  console.log(chalk_1.default.dim(` DNS port: ${(0, paths_1.hostDnsPort)(env)}`));
152
152
  console.log(chalk_1.default.dim(` Domain resolution: ${report.resolver.installed ? chalk_1.default.green("configured") : chalk_1.default.yellow("not configured")} — ${report.resolver.detail}`));
153
153
  console.log(chalk_1.default.dim(` Automatic startup: ${report.autoStart ? chalk_1.default.green("enabled (LaunchAgent)") : chalk_1.default.yellow("disabled")}`));
@@ -290,9 +290,14 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
290
290
  catch { /* skip unreadable files */ }
291
291
  }
292
292
  }
293
+ onProgress("thinking", "Contacting AI agent...", `Provider: ${target.provider}, Model: ${target.model || "default"}`);
294
+ // Use streaming request for real-time progress updates
293
295
  const response = await fetch(endpoint, {
294
296
  method: "POST",
295
- headers,
297
+ headers: {
298
+ ...headers,
299
+ "Accept": "text/event-stream",
300
+ },
296
301
  signal: controller.signal,
297
302
  body: JSON.stringify({
298
303
  sessionId: taskId,
@@ -323,21 +328,78 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
323
328
  },
324
329
  }),
325
330
  });
326
- clearTimeout(timeout);
327
331
  signal.removeEventListener("abort", onAbort);
328
332
  if (signal.aborted) {
329
333
  return { ok: false, error: "Request timed out after 5 minutes. Your plan quota may be exhausted. Please upgrade your plan or add credits to continue using this service." };
330
334
  }
331
335
  if (!response.ok) {
336
+ const errText = await response.text();
337
+ console.error(`[Lasso] Gateway error ${response.status}:`, errText);
332
338
  const errData = (await response.json().catch(() => ({})));
333
- const errorMessage = errData.error || `Agent request failed (${response.status})`;
339
+ const errorMessage = errData.error || `Agent request failed (${response.status}): ${errText}`;
334
340
  // Check for quota/plan exhaustion
335
341
  if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted|payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i.test(errorMessage)) {
336
342
  return { ok: false, error: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service." };
337
343
  }
338
344
  return { ok: false, error: errorMessage };
339
345
  }
340
- const data = (await response.json());
346
+ // Process streaming response
347
+ const reader = response.body?.getReader();
348
+ if (!reader) {
349
+ return { ok: false, error: "Failed to read response stream" };
350
+ }
351
+ const decoder = new TextDecoder();
352
+ let buffer = "";
353
+ let finalData = {};
354
+ try {
355
+ while (true) {
356
+ if (signal.aborted) {
357
+ reader.cancel();
358
+ return { ok: false, error: "Request was aborted" };
359
+ }
360
+ const { done, value } = await reader.read();
361
+ if (done)
362
+ break;
363
+ buffer += decoder.decode(value, { stream: true });
364
+ const lines = buffer.split("\n");
365
+ buffer = lines.pop() || "";
366
+ for (const line of lines) {
367
+ if (line.startsWith("data: ")) {
368
+ const data = line.slice(6);
369
+ if (data === "[DONE]")
370
+ continue;
371
+ try {
372
+ const event = JSON.parse(data);
373
+ console.log(`[Lasso] Stream event:`, JSON.stringify({ type: event.type || "unknown" }));
374
+ // Handle thinking steps
375
+ if (event.type === "thinking" && event.step) {
376
+ onProgress("thinking", event.step.title, event.step.detail);
377
+ }
378
+ // Handle tool calls
379
+ if (event.type === "tool_call" && event.tool) {
380
+ onProgress("working", `Using ${event.tool.name}...`, event.tool.input?.path || event.tool.input?.query || "");
381
+ }
382
+ // Handle progress messages
383
+ if (event.type === "progress" && event.message) {
384
+ onProgress("thinking", event.message, event.detail);
385
+ }
386
+ // Accumulate final data
387
+ if (event.type === "done" || event.changes || event.summary) {
388
+ finalData = { ...finalData, ...event };
389
+ }
390
+ }
391
+ catch (e) {
392
+ console.warn(`[Lasso] Failed to parse stream event:`, data, e);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ finally {
399
+ reader.releaseLock();
400
+ }
401
+ const data = finalData;
402
+ console.log(`[Lasso] Gateway response:`, JSON.stringify({ status: data.status, hasChanges: !!data.changes, changesCount: data.changes?.length, hasReply: !!data.reply, hasError: !!data.error }));
341
403
  if (data.status === "failed" || data.error) {
342
404
  const error = data.error || data.summary || "Agent session failed on server.";
343
405
  // Check for quota/plan exhaustion
@@ -361,7 +423,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
361
423
  totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
362
424
  };
363
425
  }
364
- // Replay thinking steps progress for implementation
426
+ // Replay thinking steps progress for implementation (fallback if not streamed)
365
427
  if (data.thinking && data.thinking.length) {
366
428
  for (const step of data.thinking) {
367
429
  if (signal.aborted)
package/dist/overlay.js CHANGED
@@ -7879,6 +7879,21 @@
7879
7879
  } catch {
7880
7880
  }
7881
7881
  }
7882
+ var agentTasksKey = "lasso:agentTasks";
7883
+ function loadAgentTasks() {
7884
+ try {
7885
+ const raw = localStorage.getItem(agentTasksKey);
7886
+ return raw ? JSON.parse(raw) : [];
7887
+ } catch {
7888
+ return [];
7889
+ }
7890
+ }
7891
+ function saveAgentTasks(tasks) {
7892
+ try {
7893
+ localStorage.setItem(agentTasksKey, JSON.stringify(tasks));
7894
+ } catch {
7895
+ }
7896
+ }
7882
7897
  var OverlayState = class {
7883
7898
  // Mode & Selection
7884
7899
  selectMode = false;
@@ -7911,8 +7926,7 @@
7911
7926
  { id: "gemini-3.8-flash", label: "Gemini 3.8 Flash", provider: "google" },
7912
7927
  { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google" },
7913
7928
  { id: "gpt-4.1-mini", label: "GPT-4.1 mini", provider: "openai" },
7914
- { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
7915
- { id: "meta/llama-3.1-70b-instruct", label: "Llama 3.1 70B", provider: "nvidia" }
7929
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" }
7916
7930
  ];
7917
7931
  selectedModel = this.MODELS[0];
7918
7932
  modelFilter = "all";
@@ -7953,6 +7967,7 @@
7953
7967
  const found = this.MODELS.find((m) => m.id === saved);
7954
7968
  if (found) this.selectedModel = found;
7955
7969
  }
7970
+ this.agentTasks = loadAgentTasks();
7956
7971
  }
7957
7972
  };
7958
7973
  var state = new OverlayState();
@@ -21911,6 +21926,7 @@ The agent is automatically attempting to fix the build errors...`;
21911
21926
  };
21912
21927
  state.agentTasks.unshift(task);
21913
21928
  state.activeTaskId = task.id;
21929
+ saveAgentTasks(state.agentTasks);
21914
21930
  renderTasks();
21915
21931
  return task;
21916
21932
  }
@@ -21922,6 +21938,7 @@ The agent is automatically attempting to fix the build errors...`;
21922
21938
  const task = getAgentTask(id);
21923
21939
  if (!task) return;
21924
21940
  Object.assign(task, patch, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
21941
+ saveAgentTasks(state.agentTasks);
21925
21942
  renderTasks();
21926
21943
  }
21927
21944
  function recordTaskActivity(id, text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.3.9",
3
+ "version": "1.4.1",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",