@lasso-ai/cli 1.0.23 → 1.0.25
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 +106 -78
- package/package.json +1 -1
package/dist/cli/agent.js
CHANGED
|
@@ -351,59 +351,117 @@ function localCommand(provider, model, prompt) {
|
|
|
351
351
|
return { command: "claude", args: ["-p", prompt || "", "--output-format", "stream-json", "--verbose", "--permission-mode", "plan", "--max-turns", "3", ...(selectedModel ? ["--model", selectedModel] : [])] };
|
|
352
352
|
}
|
|
353
353
|
if (provider === "opencode") {
|
|
354
|
-
return { command: "opencode", args: ["run", "--format", "json", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
|
|
354
|
+
return { command: "opencode", args: ["run", "--format", "json", "--print-logs", "--log-level", "INFO", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), 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
|
+
if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
|
|
399
|
+
onProgress?.("OpenCode · provider rejected the request; stopping this task…");
|
|
400
|
+
child.kill("SIGTERM");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
child.stdout.on("data", (chunk) => consume(chunk, "stdout"));
|
|
406
|
+
child.stderr.on("data", (chunk) => consume(chunk, "stderr"));
|
|
407
|
+
child.once("spawn", () => onProgress?.(`${label} process started${child.pid ? ` · PID ${child.pid}` : ""}`));
|
|
408
|
+
const heartbeat = setInterval(() => {
|
|
409
|
+
if (!signal?.aborted)
|
|
410
|
+
onProgress?.(`${label} is still running${child.pid ? ` · PID ${child.pid}` : ""}…`);
|
|
411
|
+
}, 15000);
|
|
412
|
+
heartbeat.unref();
|
|
413
|
+
const abort = () => child.kill("SIGTERM");
|
|
414
|
+
if (signal?.aborted)
|
|
415
|
+
abort();
|
|
416
|
+
else
|
|
417
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
418
|
+
child.once("error", (error) => {
|
|
419
|
+
if (settled)
|
|
420
|
+
return;
|
|
421
|
+
settled = true;
|
|
422
|
+
clearTimeout(timeout);
|
|
423
|
+
clearInterval(heartbeat);
|
|
424
|
+
signal?.removeEventListener("abort", abort);
|
|
425
|
+
reject(error);
|
|
426
|
+
});
|
|
427
|
+
child.once("close", (code) => {
|
|
428
|
+
if (settled)
|
|
429
|
+
return;
|
|
430
|
+
settled = true;
|
|
431
|
+
clearTimeout(timeout);
|
|
432
|
+
clearInterval(heartbeat);
|
|
433
|
+
signal?.removeEventListener("abort", abort);
|
|
434
|
+
if (timedOut) {
|
|
435
|
+
reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const stdoutTail = pendingStdout.trim();
|
|
439
|
+
const stderrTail = pendingStderr.trim();
|
|
440
|
+
if (stdoutTail) {
|
|
441
|
+
stdout += pendingStdout;
|
|
442
|
+
const progress = progressFromLine(stdoutTail, provider);
|
|
443
|
+
if (progress)
|
|
444
|
+
onProgress?.(progress.message, progress.detail);
|
|
445
|
+
}
|
|
446
|
+
if (stderrTail) {
|
|
447
|
+
const progress = progressFromLine(stderrTail, provider);
|
|
448
|
+
if (progress)
|
|
449
|
+
onProgress?.(progress.message, progress.detail);
|
|
450
|
+
}
|
|
451
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
358
455
|
async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
|
|
359
456
|
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
457
|
const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
|
|
361
458
|
const local = localCommand(config.provider, config.model, prompt);
|
|
362
459
|
const command = local.command;
|
|
363
460
|
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);
|
|
461
|
+
const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress);
|
|
462
|
+
if (result.exitCode !== 0)
|
|
463
|
+
throw new Error(localAgentError(command, result.stderr, result.exitCode));
|
|
464
|
+
return extractLocalAgentProposal(result.stdout, config.provider);
|
|
407
465
|
}
|
|
408
466
|
async function proposeChanges(cwd, input, config, signal, onProgress) {
|
|
409
467
|
onProgress?.("Reading source context…");
|
|
@@ -474,40 +532,10 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
|
|
|
474
532
|
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
533
|
if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
|
|
476
534
|
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();
|
|
535
|
+
const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress);
|
|
536
|
+
if (result.exitCode !== 0)
|
|
537
|
+
throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
|
|
538
|
+
return extractLocalAgentText(result.stdout, config.provider).trim();
|
|
511
539
|
}
|
|
512
540
|
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
541
|
let response;
|
package/package.json
CHANGED