@lasso-ai/cli 1.0.22 → 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 +119 -79
- package/package.json +1 -1
package/dist/cli/agent.js
CHANGED
|
@@ -28,22 +28,31 @@ async function detectLocalAgents() {
|
|
|
28
28
|
const ignored = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo"]);
|
|
29
29
|
const sourceExtensions = /\.(tsx?|jsx?|vue|svelte|css|scss|html)$/i;
|
|
30
30
|
const sourceFileCache = new Map();
|
|
31
|
-
async function sourceFiles(directory) {
|
|
31
|
+
async function sourceFiles(directory, budget = { remaining: 40 }) {
|
|
32
|
+
if (budget.remaining <= 0)
|
|
33
|
+
return [];
|
|
32
34
|
const cached = sourceFileCache.get(directory);
|
|
33
|
-
if (cached && cached.expiresAt > Date.now())
|
|
34
|
-
|
|
35
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
36
|
+
const files = cached.files.slice(0, budget.remaining);
|
|
37
|
+
budget.remaining -= files.length;
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
35
40
|
const entries = await promises_1.default.readdir(directory, { withFileTypes: true });
|
|
36
41
|
const files = [];
|
|
37
42
|
for (const entry of entries) {
|
|
38
43
|
if (ignored.has(entry.name) || entry.name.startsWith("."))
|
|
39
44
|
continue;
|
|
40
45
|
const fullPath = node_path_1.default.join(directory, entry.name);
|
|
41
|
-
if (
|
|
42
|
-
files.push(...(await sourceFiles(fullPath)));
|
|
43
|
-
else if (sourceExtensions.test(entry.name))
|
|
44
|
-
files.push(fullPath);
|
|
45
|
-
if (files.length >= 40)
|
|
46
|
+
if (budget.remaining <= 0)
|
|
46
47
|
break;
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
const nested = await sourceFiles(fullPath, budget);
|
|
50
|
+
files.push(...nested);
|
|
51
|
+
}
|
|
52
|
+
else if (sourceExtensions.test(entry.name)) {
|
|
53
|
+
files.push(fullPath);
|
|
54
|
+
budget.remaining -= 1;
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
sourceFileCache.set(directory, { expiresAt: Date.now() + 5000, files });
|
|
49
58
|
return files;
|
|
@@ -346,52 +355,113 @@ function localCommand(provider, model, prompt) {
|
|
|
346
355
|
}
|
|
347
356
|
return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
|
|
348
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
|
+
}
|
|
349
450
|
async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress) {
|
|
350
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.`;
|
|
351
452
|
const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
|
|
352
453
|
const local = localCommand(config.provider, config.model, prompt);
|
|
353
454
|
const command = local.command;
|
|
354
455
|
const args = local.args;
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const consume = (chunk) => {
|
|
360
|
-
pending += String(chunk);
|
|
361
|
-
const lines = pending.split(/\r?\n/);
|
|
362
|
-
pending = lines.pop() || "";
|
|
363
|
-
for (const line of lines) {
|
|
364
|
-
stdout += `${line}\n`;
|
|
365
|
-
const progress = progressFromLine(line, config.provider);
|
|
366
|
-
if (progress)
|
|
367
|
-
onProgress?.(progress.message, progress.detail);
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
child.stdout.on("data", consume);
|
|
371
|
-
child.stderr.on("data", (chunk) => {
|
|
372
|
-
stderr += String(chunk);
|
|
373
|
-
});
|
|
374
|
-
if (signal) {
|
|
375
|
-
if (signal.aborted)
|
|
376
|
-
child.kill("SIGTERM");
|
|
377
|
-
signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
378
|
-
}
|
|
379
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
380
|
-
child.once("error", reject);
|
|
381
|
-
child.once("close", (code) => resolve(code ?? 1));
|
|
382
|
-
});
|
|
383
|
-
if (pending.trim()) {
|
|
384
|
-
stdout += pending;
|
|
385
|
-
const progress = progressFromLine(pending, config.provider);
|
|
386
|
-
if (progress)
|
|
387
|
-
onProgress?.(progress.message, progress.detail);
|
|
388
|
-
}
|
|
389
|
-
if (exitCode !== 0)
|
|
390
|
-
throw new Error(localAgentError(command, stderr, exitCode));
|
|
391
|
-
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);
|
|
392
460
|
}
|
|
393
461
|
async function proposeChanges(cwd, input, config, signal, onProgress) {
|
|
462
|
+
onProgress?.("Reading source context…");
|
|
394
463
|
const context = await contextFor(cwd, input.element);
|
|
464
|
+
onProgress?.("Starting the coding agent…");
|
|
395
465
|
const visualContext = input.context ? { ...input.context, screenshots: undefined } : undefined;
|
|
396
466
|
const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
|
|
397
467
|
const priorChanges = input.changesHistory?.length ? JSON.stringify(input.changesHistory, null, 2) : "None";
|
|
@@ -457,40 +527,10 @@ async function answerQuestion(cwd, input, config, signal, onProgress) {
|
|
|
457
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."}`;
|
|
458
528
|
if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode") {
|
|
459
529
|
const local = localCommand(config.provider, config.model, prompt);
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
let pending = "";
|
|
465
|
-
let stderr = "";
|
|
466
|
-
const consume = (chunk) => {
|
|
467
|
-
pending += String(chunk);
|
|
468
|
-
const lines = pending.split(/\r?\n/);
|
|
469
|
-
pending = lines.pop() || "";
|
|
470
|
-
for (const line of lines) {
|
|
471
|
-
output += `${line}\n`;
|
|
472
|
-
const progress = progressFromLine(line, config.provider);
|
|
473
|
-
if (progress)
|
|
474
|
-
onProgress?.(progress.message, progress.detail);
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
child.stdout.on("data", consume);
|
|
478
|
-
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
|
479
|
-
if (signal)
|
|
480
|
-
signal.addEventListener("abort", () => child.kill("SIGTERM"), { once: true });
|
|
481
|
-
const code = await new Promise((resolve, reject) => {
|
|
482
|
-
child.once("error", reject);
|
|
483
|
-
child.once("close", (status) => resolve(status ?? 1));
|
|
484
|
-
});
|
|
485
|
-
if (pending.trim()) {
|
|
486
|
-
output += pending;
|
|
487
|
-
const progress = progressFromLine(pending, config.provider);
|
|
488
|
-
if (progress)
|
|
489
|
-
onProgress?.(progress.message, progress.detail);
|
|
490
|
-
}
|
|
491
|
-
if (code !== 0)
|
|
492
|
-
throw new Error(localAgentError(command, stderr, code));
|
|
493
|
-
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();
|
|
494
534
|
}
|
|
495
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");
|
|
496
536
|
let response;
|
package/package.json
CHANGED