@lasso-ai/cli 1.0.23 → 1.0.24

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.
Files changed (2) hide show
  1. package/dist/cli/agent.js +100 -77
  2. package/package.json +1 -1
package/dist/cli/agent.js CHANGED
@@ -355,55 +355,108 @@ function localCommand(provider, model, prompt) {
355
355
  }
356
356
  return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
357
357
  }
358
+ function runLocalCommand(cwd, command, args, provider, signal, onProgress) {
359
+ return new Promise((resolve, reject) => {
360
+ const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
361
+ const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : "Claude Code";
362
+ let stdout = "";
363
+ let stderr = "";
364
+ let pendingStdout = "";
365
+ let pendingStderr = "";
366
+ let settled = false;
367
+ let timedOut = false;
368
+ const timeout = setTimeout(() => {
369
+ timedOut = true;
370
+ onProgress?.(`${label} did not respond; stopping this task…`);
371
+ child.kill("SIGTERM");
372
+ setTimeout(() => child.kill("SIGKILL"), 3000).unref();
373
+ }, 5 * 60 * 1000);
374
+ timeout.unref();
375
+ const consume = (chunk, stream) => {
376
+ const value = String(chunk);
377
+ if (stream === "stdout") {
378
+ stdout += value;
379
+ pendingStdout += value;
380
+ }
381
+ else {
382
+ stderr += value;
383
+ pendingStderr += value;
384
+ }
385
+ const pending = stream === "stdout" ? pendingStdout : pendingStderr;
386
+ const lines = pending.split(/\r?\n/);
387
+ const remainder = lines.pop() || "";
388
+ if (stream === "stdout")
389
+ pendingStdout = remainder;
390
+ else
391
+ pendingStderr = remainder;
392
+ for (const line of lines) {
393
+ const progress = progressFromLine(line, provider);
394
+ if (progress)
395
+ onProgress?.(progress.message, progress.detail);
396
+ else if (stream === "stderr" && line.trim())
397
+ onProgress?.(`${label} · ${snippet(line, 110)}`);
398
+ }
399
+ };
400
+ child.stdout.on("data", (chunk) => consume(chunk, "stdout"));
401
+ child.stderr.on("data", (chunk) => consume(chunk, "stderr"));
402
+ child.once("spawn", () => onProgress?.(`${label} process started${child.pid ? ` · PID ${child.pid}` : ""}`));
403
+ const heartbeat = setInterval(() => {
404
+ if (!signal?.aborted)
405
+ onProgress?.(`${label} is still running${child.pid ? ` · PID ${child.pid}` : ""}…`);
406
+ }, 15000);
407
+ heartbeat.unref();
408
+ const abort = () => child.kill("SIGTERM");
409
+ if (signal?.aborted)
410
+ abort();
411
+ else
412
+ signal?.addEventListener("abort", abort, { once: true });
413
+ child.once("error", (error) => {
414
+ if (settled)
415
+ return;
416
+ settled = true;
417
+ clearTimeout(timeout);
418
+ clearInterval(heartbeat);
419
+ signal?.removeEventListener("abort", abort);
420
+ reject(error);
421
+ });
422
+ child.once("close", (code) => {
423
+ if (settled)
424
+ return;
425
+ settled = true;
426
+ clearTimeout(timeout);
427
+ clearInterval(heartbeat);
428
+ signal?.removeEventListener("abort", abort);
429
+ if (timedOut) {
430
+ reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
431
+ return;
432
+ }
433
+ const stdoutTail = pendingStdout.trim();
434
+ const stderrTail = pendingStderr.trim();
435
+ if (stdoutTail) {
436
+ stdout += pendingStdout;
437
+ const progress = progressFromLine(stdoutTail, provider);
438
+ if (progress)
439
+ onProgress?.(progress.message, progress.detail);
440
+ }
441
+ if (stderrTail) {
442
+ const progress = progressFromLine(stderrTail, provider);
443
+ if (progress)
444
+ onProgress?.(progress.message, progress.detail);
445
+ }
446
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
447
+ });
448
+ });
449
+ }
358
450
  async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
359
451
  const outputContract = `Return ONLY valid JSON in this exact shape: {"summary":"short explanation","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}. Treat the supplied source context as read-only. Before returning, verify every oldString against that context. Use project-relative paths only. Do not edit files, run write commands, commit, or produce markdown fences.`;
360
452
  const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
361
453
  const local = localCommand(config.provider, config.model, prompt);
362
454
  const command = local.command;
363
455
  const args = local.args;
364
- const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
365
- let stdout = "";
366
- let stderr = "";
367
- let pending = "";
368
- const consume = (chunk) => {
369
- pending += String(chunk);
370
- const lines = pending.split(/\r?\n/);
371
- pending = lines.pop() || "";
372
- for (const line of lines) {
373
- stdout += `${line}\n`;
374
- const progress = progressFromLine(line, config.provider);
375
- if (progress)
376
- onProgress?.(progress.message, progress.detail);
377
- }
378
- };
379
- child.stdout.on("data", consume);
380
- child.stderr.on("data", (chunk) => {
381
- const text = String(chunk);
382
- stderr += text;
383
- for (const line of text.split(/\r?\n/)) {
384
- const progress = progressFromLine(line, config.provider);
385
- if (progress)
386
- onProgress?.(progress.message, progress.detail);
387
- }
388
- });
389
- if (signal) {
390
- if (signal.aborted)
391
- child.kill("SIGTERM");
392
- signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
393
- }
394
- const exitCode = await new Promise((resolve, reject) => {
395
- child.once("error", reject);
396
- child.once("close", (code) => resolve(code ?? 1));
397
- });
398
- if (pending.trim()) {
399
- stdout += pending;
400
- const progress = progressFromLine(pending, config.provider);
401
- if (progress)
402
- onProgress?.(progress.message, progress.detail);
403
- }
404
- if (exitCode !== 0)
405
- throw new Error(localAgentError(command, stderr, exitCode));
406
- return extractLocalAgentProposal(stdout, config.provider);
456
+ const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress);
457
+ if (result.exitCode !== 0)
458
+ throw new Error(localAgentError(command, result.stderr, result.exitCode));
459
+ return extractLocalAgentProposal(result.stdout, config.provider);
407
460
  }
408
461
  async function proposeChanges(cwd, input, config, signal, onProgress) {
409
462
  onProgress?.("Reading source context…");
@@ -474,40 +527,10 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
474
527
  const prompt = `Answer the user's question conversationally and directly. Do not propose file changes and do not return JSON. If the question is about the selected UI, use the selection and source context below.\n\nUser question:\n${input.question}\n\nSelected element:\n${JSON.stringify(input.element, null, 2)}\n\nVisual context:\n${JSON.stringify({ ...input.context, screenshots: undefined }, null, 2)}\n\nConversation:\n${history}\n\nRelevant source context:\n${context || "No matching source context was found."}`;
475
528
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
476
529
  const local = localCommand(config.provider, config.model, prompt);
477
- const command = local.command;
478
- const args = local.args;
479
- const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
480
- let output = "";
481
- let pending = "";
482
- let stderr = "";
483
- const consume = (chunk) => {
484
- pending += String(chunk);
485
- const lines = pending.split(/\r?\n/);
486
- pending = lines.pop() || "";
487
- for (const line of lines) {
488
- output += `${line}\n`;
489
- const progress = progressFromLine(line, config.provider);
490
- if (progress)
491
- onProgress?.(progress.message, progress.detail);
492
- }
493
- };
494
- child.stdout.on("data", consume);
495
- child.stderr.on("data", (chunk) => { stderr += String(chunk); });
496
- if (signal)
497
- signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
498
- const code = await new Promise((resolve, reject) => {
499
- child.once("error", reject);
500
- child.once("close", (status) => resolve(status ?? 1));
501
- });
502
- if (pending.trim()) {
503
- output += pending;
504
- const progress = progressFromLine(pending, config.provider);
505
- if (progress)
506
- onProgress?.(progress.message, progress.detail);
507
- }
508
- if (code !== 0)
509
- throw new Error(localAgentError(command, stderr, code));
510
- return extractLocalAgentText(output, config.provider).trim();
530
+ const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress);
531
+ if (result.exitCode !== 0)
532
+ throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
533
+ return extractLocalAgentText(result.stdout, config.provider).trim();
511
534
  }
512
535
  const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : "claude-sonnet-4-20250514");
513
536
  let response;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
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",