@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.
- package/dist/cli/agent.js +100 -77
- 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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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