@lasso-ai/cli 1.2.3 → 1.2.5
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/ARCHITECTURE.md +10 -3
- package/CHANGELOG.md +38 -0
- package/README.md +23 -3
- package/dist/cli/agent.d.ts +38 -2
- package/dist/cli/agent.js +521 -72
- package/dist/cli/auth.js +3 -1
- package/dist/cli/bridge.d.ts +22 -6
- package/dist/cli/bridge.js +182 -49
- package/dist/cli/index.js +10 -0
- package/dist/cli/oneshot.d.ts +19 -1
- package/dist/cli/oneshot.js +611 -106
- package/dist/cli/project.d.ts +2 -1
- package/dist/cli/project.js +71 -26
- package/dist/overlay.js +8668 -6431
- package/package.json +1 -1
package/dist/cli/agent.js
CHANGED
|
@@ -3,8 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.installPackages = installPackages;
|
|
6
7
|
exports.respondToAgentPrompt = respondToAgentPrompt;
|
|
8
|
+
exports.waitForAgentPrompt = waitForAgentPrompt;
|
|
9
|
+
exports.executeWebSearch = executeWebSearch;
|
|
10
|
+
exports.executeAgentCommand = executeAgentCommand;
|
|
7
11
|
exports.detectLocalAgents = detectLocalAgents;
|
|
12
|
+
exports.parseAgentAction = parseAgentAction;
|
|
8
13
|
exports.proposeChanges = proposeChanges;
|
|
9
14
|
exports.answerQuestion = answerQuestion;
|
|
10
15
|
exports.generateCommitMessage = generateCommitMessage;
|
|
@@ -13,14 +18,134 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
13
18
|
const node_child_process_1 = require("node:child_process");
|
|
14
19
|
const node_util_1 = require("node:util");
|
|
15
20
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
21
|
+
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
22
|
+
async function installPackages(cwd, packages, onProgress) {
|
|
23
|
+
const validPkgs = [...new Set(packages
|
|
24
|
+
.map((p) => p.trim().replace(/^['"`]|['"`]$/g, ""))
|
|
25
|
+
.filter((p) => /^(@[a-z0-9\-~][a-z0-9\-_.]*\/)?[a-z0-9\-~][a-z0-9\-_.]*$/i.test(p)))];
|
|
26
|
+
if (validPkgs.length === 0)
|
|
27
|
+
return [];
|
|
28
|
+
// Check package.json to see what is already installed
|
|
29
|
+
let existingDeps = {};
|
|
30
|
+
try {
|
|
31
|
+
const pkgJsonPath = node_path_1.default.join(cwd, "package.json");
|
|
32
|
+
const raw = await promises_1.default.readFile(pkgJsonPath, "utf8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
existingDeps = { ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) };
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
const needed = validPkgs.filter((pkg) => !existingDeps[pkg]);
|
|
38
|
+
if (needed.length === 0)
|
|
39
|
+
return validPkgs;
|
|
40
|
+
// Detect package manager
|
|
41
|
+
let cmd = "npm install";
|
|
42
|
+
try {
|
|
43
|
+
const rootFiles = await promises_1.default.readdir(cwd);
|
|
44
|
+
if (rootFiles.includes("bun.lockb") || rootFiles.includes("bun.lock"))
|
|
45
|
+
cmd = "bun add";
|
|
46
|
+
else if (rootFiles.includes("pnpm-lock.yaml"))
|
|
47
|
+
cmd = "pnpm add";
|
|
48
|
+
else if (rootFiles.includes("yarn.lock"))
|
|
49
|
+
cmd = "yarn add";
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
const commandStr = `${cmd} ${needed.join(" ")}`;
|
|
53
|
+
onProgress?.(`Installing dependencies (${needed.join(", ")})…`, commandStr, "working");
|
|
54
|
+
try {
|
|
55
|
+
await execAsync(commandStr, { cwd, timeout: 120000 });
|
|
56
|
+
onProgress?.(`Installed ${needed.join(", ")}`, undefined, "working");
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
onProgress?.(`Package installation failed: ${err.message}`, undefined, "error");
|
|
60
|
+
}
|
|
61
|
+
return validPkgs;
|
|
62
|
+
}
|
|
16
63
|
const activeAgentInputs = new Map();
|
|
17
64
|
function respondToAgentPrompt(taskId, value) {
|
|
18
65
|
const respond = activeAgentInputs.get(taskId);
|
|
19
66
|
if (!respond)
|
|
20
67
|
return false;
|
|
21
68
|
respond(value);
|
|
69
|
+
activeAgentInputs.delete(taskId);
|
|
22
70
|
return true;
|
|
23
71
|
}
|
|
72
|
+
function waitForAgentPrompt(taskId, timeoutMs = 120000) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
activeAgentInputs.delete(taskId);
|
|
76
|
+
resolve("Skip");
|
|
77
|
+
}, timeoutMs);
|
|
78
|
+
activeAgentInputs.set(taskId, (value) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
activeAgentInputs.delete(taskId);
|
|
81
|
+
resolve(value);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function executeWebSearch(query, maxResults = 5) {
|
|
86
|
+
try {
|
|
87
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
headers: {
|
|
90
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
91
|
+
"Accept": "text/html",
|
|
92
|
+
},
|
|
93
|
+
signal: AbortSignal.timeout(10000),
|
|
94
|
+
});
|
|
95
|
+
if (!res.ok)
|
|
96
|
+
return [];
|
|
97
|
+
const html = await res.text();
|
|
98
|
+
const results = [];
|
|
99
|
+
const resultRegex = /<a class="result__snippet[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
100
|
+
let match;
|
|
101
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
|
|
102
|
+
const rawHref = match[1];
|
|
103
|
+
const uddgMatch = rawHref.match(/uddg=([^&]+)/);
|
|
104
|
+
const targetUrl = uddgMatch ? decodeURIComponent(uddgMatch[1]) : rawHref;
|
|
105
|
+
const cleanSnippet = match[2]
|
|
106
|
+
.replace(/<[^>]*>/g, "")
|
|
107
|
+
.replace(/&/g, "&")
|
|
108
|
+
.replace(/"/g, '"')
|
|
109
|
+
.replace(/'/g, "'")
|
|
110
|
+
.replace(/</g, "<")
|
|
111
|
+
.replace(/>/g, ">")
|
|
112
|
+
.replace(/\s+/g, " ")
|
|
113
|
+
.trim();
|
|
114
|
+
if (cleanSnippet && targetUrl.startsWith("http")) {
|
|
115
|
+
results.push({
|
|
116
|
+
title: cleanSnippet.slice(0, 80),
|
|
117
|
+
url: targetUrl,
|
|
118
|
+
snippet: cleanSnippet,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const BLOCKED_COMMANDS = /\b(rm\s+-rf\s+\/|shutdown|reboot|sudo|mkfs|:\(\)\{\|:&\}|chmod\s+-R\s+777\s+\/)\b/i;
|
|
129
|
+
async function executeAgentCommand(cwd, command, timeout = 30000) {
|
|
130
|
+
if (BLOCKED_COMMANDS.test(command)) {
|
|
131
|
+
return { stdout: "", stderr: "Command rejected for safety reasons.", exitCode: 1 };
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const { stdout, stderr } = await execAsync(command, { cwd, timeout });
|
|
135
|
+
return {
|
|
136
|
+
stdout: stdout.slice(0, 8000),
|
|
137
|
+
stderr: stderr.slice(0, 2000),
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
return {
|
|
143
|
+
stdout: (err.stdout || "").slice(0, 8000),
|
|
144
|
+
stderr: (err.stderr || err.message || "").slice(0, 2000),
|
|
145
|
+
exitCode: err.code || 1,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
24
149
|
async function detectLocalAgents() {
|
|
25
150
|
const found = new Set();
|
|
26
151
|
for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"], ["cursor", "cursor"]]) {
|
|
@@ -193,7 +318,14 @@ function jsonFrom(text) {
|
|
|
193
318
|
throw new Error("The agent returned an invalid file change.");
|
|
194
319
|
}
|
|
195
320
|
}
|
|
196
|
-
|
|
321
|
+
const packages = Array.isArray(parsed.packages)
|
|
322
|
+
? parsed.packages.map((p) => String(p).trim()).filter(Boolean)
|
|
323
|
+
: [];
|
|
324
|
+
return {
|
|
325
|
+
summary: parsed.summary || (packages.length > 0 ? `Installed ${packages.join(", ")} and updated components.` : "The proposed source changes are ready for review."),
|
|
326
|
+
packages,
|
|
327
|
+
changes: parsed.changes,
|
|
328
|
+
};
|
|
197
329
|
}
|
|
198
330
|
catch (error) {
|
|
199
331
|
if (error instanceof Error && error.message === "The agent returned an invalid file change.")
|
|
@@ -202,6 +334,50 @@ function jsonFrom(text) {
|
|
|
202
334
|
}
|
|
203
335
|
throw new Error("The agent returned no valid reviewable changes. Progress output may have been mixed with the final JSON.");
|
|
204
336
|
}
|
|
337
|
+
function parseAgentAction(text) {
|
|
338
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
|
|
339
|
+
const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(text)];
|
|
340
|
+
for (const candidate of candidates) {
|
|
341
|
+
try {
|
|
342
|
+
const parsed = JSON.parse(candidate.trim());
|
|
343
|
+
if (parsed.action === "run_command" && typeof parsed.command === "string" && parsed.command.trim()) {
|
|
344
|
+
return { type: "run_command", command: parsed.command.trim(), thought: parsed.thought };
|
|
345
|
+
}
|
|
346
|
+
if (parsed.action === "web_search" && typeof parsed.query === "string" && parsed.query.trim()) {
|
|
347
|
+
return { type: "web_search", query: parsed.query.trim(), thought: parsed.thought };
|
|
348
|
+
}
|
|
349
|
+
if (parsed.action === "ask_user" && typeof parsed.question === "string" && parsed.question.trim()) {
|
|
350
|
+
return {
|
|
351
|
+
type: "ask_user",
|
|
352
|
+
question: parsed.question.trim(),
|
|
353
|
+
kind: parsed.kind === "multiselect" ? "multiselect" : parsed.kind === "input" ? "input" : "permission",
|
|
354
|
+
options: Array.isArray(parsed.options) ? parsed.options.map(String) : undefined,
|
|
355
|
+
thought: parsed.thought,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(parsed.changes)) {
|
|
359
|
+
for (const change of parsed.changes) {
|
|
360
|
+
if (!change.filePath || typeof change.oldString !== "string" || typeof change.newString !== "string") {
|
|
361
|
+
throw new Error("The agent returned an invalid file change.");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const packages = Array.isArray(parsed.packages) ? parsed.packages.map((p) => String(p).trim()).filter(Boolean) : [];
|
|
365
|
+
return {
|
|
366
|
+
type: "proposal",
|
|
367
|
+
summary: parsed.summary || (packages.length > 0 ? `Installed ${packages.join(", ")} and updated components.` : "The proposed source changes are ready for review."),
|
|
368
|
+
packages,
|
|
369
|
+
changes: parsed.changes,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof Error && error.message === "The agent returned an invalid file change.")
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const fallback = jsonFrom(text);
|
|
379
|
+
return { type: "proposal", ...fallback };
|
|
380
|
+
}
|
|
205
381
|
function extractLocalAgentText(raw, provider) {
|
|
206
382
|
if (provider === "claude-code") {
|
|
207
383
|
try {
|
|
@@ -696,7 +872,7 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
|
|
|
696
872
|
});
|
|
697
873
|
}
|
|
698
874
|
async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, taskId, onPrompt) {
|
|
699
|
-
const outputContract = `CRITICAL: You MUST return ONLY valid JSON with actual file changes. NEVER explain, describe, or analyze code without proposing edits. ALWAYS return JSON with at least one change when given a modification instruction. Format: {"summary":"brief action taken","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. If you cannot find the exact text to change, search the provided context more carefully - do not give up and explain instead.`;
|
|
875
|
+
const outputContract = `CRITICAL: You MUST return ONLY valid JSON with actual file changes. NEVER explain, describe, or analyze code without proposing edits. If third-party npm packages are needed, list them in "packages". ALWAYS return JSON with at least one change when given a modification instruction. Format: {"summary":"brief action taken","packages":["optional-package-name"],"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. If you cannot find the exact text to change, search the provided context more carefully - do not give up and explain instead.`;
|
|
700
876
|
const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
|
|
701
877
|
const local = localCommand(config.provider, config.model, prompt);
|
|
702
878
|
const command = local.command;
|
|
@@ -708,64 +884,292 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
|
|
|
708
884
|
}
|
|
709
885
|
async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
|
|
710
886
|
const context = await contextFor(cwd, input.element);
|
|
711
|
-
const
|
|
887
|
+
const runtimeErrors = input.context?.runtimeErrors || [];
|
|
888
|
+
const visualContext = input.context ? { ...input.context, screenshots: undefined, runtimeErrors: undefined } : undefined;
|
|
712
889
|
const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
|
|
713
890
|
const priorChanges = input.changesHistory?.length ? JSON.stringify(input.changesHistory, null, 2) : "None";
|
|
714
891
|
const dragHint = input.context?.drag
|
|
715
892
|
? `\n\nDRAG REPOSITIONING TASK:\nThe user dragged this element by dx: ${input.context.drag.delta?.dx ?? 0}px, dy: ${input.context.drag.delta?.dy ?? 0}px to target coordinates (left: ${input.context.drag.targetRect?.left ?? 0}px, top: ${input.context.drag.targetRect?.top ?? 0}px). Modify the source code (CSS classes, Tailwind classes, flex/grid alignment, margin offsets, or positioning properties) so the element is accurately rendered at this target position.`
|
|
716
893
|
: "";
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
894
|
+
const runtimeErrorHint = runtimeErrors.length
|
|
895
|
+
? `\n\nLIVE CONSOLE/RUNTIME ERRORS (reported by the user's browser — the page is currently broken by these):\n${runtimeErrors.map((error) => `! ${error}`).join("\n")}`
|
|
896
|
+
: "";
|
|
897
|
+
const instruction = `Selection context:
|
|
898
|
+
${JSON.stringify(input.element, null, 2)}
|
|
899
|
+
${JSON.stringify(visualContext, null, 2)}
|
|
900
|
+
|
|
901
|
+
User instruction:
|
|
902
|
+
${input.instruction}
|
|
903
|
+
|
|
904
|
+
Conversation history:
|
|
905
|
+
${history}
|
|
906
|
+
|
|
907
|
+
Previous change history for this selection:
|
|
908
|
+
${priorChanges}
|
|
909
|
+
|
|
910
|
+
Relevant source context:
|
|
911
|
+
${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}
|
|
912
|
+
|
|
913
|
+
CRITICAL INSTRUCTIONS:
|
|
914
|
+
1. You are an autonomous agent with the ability to install npm packages and edit source code.
|
|
915
|
+
2. If the user's request requires or asks for third-party libraries, icon packs, animation tools, or utility packages (e.g. icon libraries, motion, charts, UI primitives, etc.), determine the best npm package for this project and declare them in the "packages" array (e.g. ["@iconify/react", "@hugeicons/react"]).
|
|
916
|
+
3. In the "changes" array, propose concrete, minimal edits to the source code to implement the request. You may freely import and use the packages you specified in "packages".
|
|
917
|
+
4. If no new packages are required, "packages" should be empty [].
|
|
918
|
+
5. If LIVE CONSOLE/RUNTIME ERRORS are listed, fixing them is part of this task — the user's page is currently broken by them.
|
|
919
|
+
6. Never guess a package's export names. Before importing a named export (an icon, a component, a hook), verify it actually exists in the installed package (run_command grepping node_modules, or the package docs) — importing a non-existent export throws a SyntaxError and kills the whole page.
|
|
920
|
+
7. You MUST return ONLY valid JSON in this exact structure:
|
|
921
|
+
{
|
|
922
|
+
"summary": "Brief explanation of what was done",
|
|
923
|
+
"packages": ["package-name-1", "package-name-2"],
|
|
924
|
+
"changes": [
|
|
925
|
+
{
|
|
926
|
+
"filePath": "relative/path/to/file.tsx",
|
|
927
|
+
"oldString": "exact existing text",
|
|
928
|
+
"newString": "replacement text"
|
|
720
929
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
930
|
+
]
|
|
931
|
+
}
|
|
932
|
+
Do NOT return prose or markdown outside the JSON.`;
|
|
933
|
+
const callServerGateway = async (gatewayKey) => {
|
|
934
|
+
let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
|
|
935
|
+
if (serverUrl.includes("collab.lasso.byorello.space")) {
|
|
936
|
+
serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
|
|
937
|
+
}
|
|
938
|
+
const endpoint = `${serverUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, "")}/api/v1/agent/session`;
|
|
939
|
+
const sourceHints = {};
|
|
940
|
+
const fileMatch = context?.match(/^FILE:\s*([^\r\n]+)\r?\n([\s\S]*)$/);
|
|
941
|
+
if (fileMatch) {
|
|
942
|
+
sourceHints[fileMatch[1].trim()] = fileMatch[2].slice(0, 16000);
|
|
943
|
+
}
|
|
944
|
+
else if (context) {
|
|
945
|
+
sourceHints["selection-context"] = context.slice(0, 16000);
|
|
946
|
+
}
|
|
947
|
+
const response = await fetch(endpoint, {
|
|
730
948
|
method: "POST",
|
|
731
949
|
signal,
|
|
732
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${
|
|
733
|
-
body: JSON.stringify({
|
|
950
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
|
|
951
|
+
body: JSON.stringify({
|
|
952
|
+
sessionId: input.taskId,
|
|
953
|
+
prompt: instruction,
|
|
954
|
+
model: { id: config.model, provider: config.provider },
|
|
955
|
+
context: { sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined },
|
|
956
|
+
}),
|
|
734
957
|
});
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
958
|
+
if (!response.ok) {
|
|
959
|
+
const errData = await response.json().catch(() => ({}));
|
|
960
|
+
throw new Error(errData.error || `Lasso Agent Gateway request failed (${response.status}).`);
|
|
961
|
+
}
|
|
962
|
+
const data = await response.json();
|
|
963
|
+
// If the server-side agent reported failure, surface it as a thrown error so the
|
|
964
|
+
// bridge catches it and shows status="error" / "Retry" button, not status="review".
|
|
965
|
+
if (data.status === "failed") {
|
|
966
|
+
throw new Error(data.error || data.summary || "Agent encountered an issue while generating changes.");
|
|
967
|
+
}
|
|
968
|
+
return { summary: data.summary || "Done", packages: data.packages || [], changes: data.changes || [] };
|
|
969
|
+
};
|
|
970
|
+
let proposal = null;
|
|
971
|
+
// Helper: call /api/v1/agent/generate for one turn of the agentic loop using the Lasso gateway.
|
|
972
|
+
// This allows paid/hosted model users to run the full agentic loop (web_search, run_command,
|
|
973
|
+
// ask_user, propose_changes) rather than being shunted to the one-shot /session endpoint.
|
|
974
|
+
const callGatewayGenerate = async (gatewayKey, systemPrompt, turnPrompt) => {
|
|
975
|
+
let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
|
|
976
|
+
if (serverUrl.includes("collab.lasso.byorello.space")) {
|
|
977
|
+
serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
|
|
978
|
+
}
|
|
979
|
+
const endpoint = `${serverUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, "")}/api/v1/agent/generate`;
|
|
980
|
+
const response = await fetch(endpoint, {
|
|
741
981
|
method: "POST",
|
|
742
982
|
signal,
|
|
743
|
-
headers: { "content-type": "application/json" },
|
|
744
|
-
body: JSON.stringify({
|
|
983
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
provider: config.provider,
|
|
986
|
+
model: config.model,
|
|
987
|
+
system: systemPrompt,
|
|
988
|
+
prompt: turnPrompt,
|
|
989
|
+
}),
|
|
745
990
|
});
|
|
991
|
+
if (!response.ok) {
|
|
992
|
+
const errData = await response.json().catch(() => ({}));
|
|
993
|
+
throw new Error(errData.error || `Lasso Gateway generate failed (${response.status}).`);
|
|
994
|
+
}
|
|
995
|
+
const data = await response.json();
|
|
996
|
+
if (!data.text?.trim())
|
|
997
|
+
throw new Error("The agent returned an empty response.");
|
|
998
|
+
return data.text.trim();
|
|
999
|
+
};
|
|
1000
|
+
if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
|
|
1001
|
+
proposal = await proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
|
|
746
1002
|
}
|
|
747
1003
|
else {
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1004
|
+
const isLassoKey = config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
|
|
1005
|
+
const system = `You are Lasso, an autonomous agentic AI coding assistant.
|
|
1006
|
+
You have the ability to run inspection commands, search the live web for documentation or libraries, ask the user for input or single/multi-selection choices, install npm packages, and edit source code.
|
|
1007
|
+
|
|
1008
|
+
AVAILABLE ACTIONS:
|
|
1009
|
+
1. "run_command": Run safe inspection commands in the project (e.g. grep, find, ls, git status, cat) to investigate files or exports.
|
|
1010
|
+
Format: {"action": "run_command", "command": "grep -rn 'search_term' src/", "thought": "Why you need to run this command"}
|
|
1011
|
+
|
|
1012
|
+
2. "web_search": Search the live web to find real-time documentation, package details, or API signatures rather than assuming.
|
|
1013
|
+
Format: {"action": "web_search", "query": "lucide react icons documentation", "thought": "Why you need to search"}
|
|
1014
|
+
|
|
1015
|
+
3. "ask_user": Ask the user for clarification, single-choice, or multi-selection input when their intent is ambiguous.
|
|
1016
|
+
Format: {"action": "ask_user", "question": "Which icon library would you prefer?", "kind": "multiselect" | "single_select" | "input", "options": ["Option 1", "Option 2"], "thought": "Why you need user choice"}
|
|
1017
|
+
|
|
1018
|
+
4. "propose_changes": Finalize and propose the exact file edits and required npm packages.
|
|
1019
|
+
Format:
|
|
1020
|
+
{
|
|
1021
|
+
"action": "propose_changes",
|
|
1022
|
+
"summary": "Brief explanation of what was done",
|
|
1023
|
+
"packages": ["package-name-1", "package-name-2"],
|
|
1024
|
+
"changes": [
|
|
1025
|
+
{
|
|
1026
|
+
"filePath": "relative/path/to/file.tsx",
|
|
1027
|
+
"oldString": "exact existing text",
|
|
1028
|
+
"newString": "replacement text"
|
|
1029
|
+
}
|
|
1030
|
+
]
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
RULES:
|
|
1034
|
+
- If you already have sufficient context to fulfill the user's request immediately, return "propose_changes" (or standard {"summary": "...", "packages": [...], "changes": [...]}) directly!
|
|
1035
|
+
- If live console/runtime errors are provided, resolving them is part of the task. Use run_command to verify package exports and file contents before proposing changes instead of guessing.
|
|
1036
|
+
- Each oldString must match the existing file context exactly.
|
|
1037
|
+
- Return ONLY valid JSON with no markdown formatting or prose outside the JSON.`;
|
|
1038
|
+
const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "meta/llama-3.2-11b-vision-instruct" : "claude-sonnet-4-20250514");
|
|
1039
|
+
const image = input.context?.screenshots?.element || input.context?.screenshots?.full;
|
|
1040
|
+
const imageData = image?.replace(/^data:image\/[^;]+;base64,/, "");
|
|
1041
|
+
const imageMime = image?.match(/^data:(image\/[^;]+);base64,/)?.[1] || "image/jpeg";
|
|
1042
|
+
const MAX_TURNS = 5;
|
|
1043
|
+
let turn = 0;
|
|
1044
|
+
const toolLogs = [];
|
|
1045
|
+
while (turn < MAX_TURNS) {
|
|
1046
|
+
turn++;
|
|
1047
|
+
const toolContext = toolLogs.length > 0 ? `\n\nTOOL EXECUTION & USER INTERACTION HISTORY:\n${toolLogs.join("\n\n")}` : "";
|
|
1048
|
+
const currentPrompt = `${instruction}${toolContext}`;
|
|
1049
|
+
let rawText = "";
|
|
1050
|
+
try {
|
|
1051
|
+
// If using a Lasso platform key, route this turn through the /generate endpoint
|
|
1052
|
+
// (full agentic loop via Lasso gateway instead of one-shot session endpoint)
|
|
1053
|
+
if (isLassoKey && config.provider !== "ollama") {
|
|
1054
|
+
rawText = await callGatewayGenerate(config.apiKey, system, currentPrompt);
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
// Provider-direct inference path
|
|
1058
|
+
let response;
|
|
1059
|
+
if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
|
|
1060
|
+
const content = image && turn === 1 ? [{ type: "text", text: currentPrompt }, { type: "image_url", image_url: { url: image } }] : currentPrompt;
|
|
1061
|
+
const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
|
|
1062
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
1063
|
+
method: "POST",
|
|
1064
|
+
signal,
|
|
1065
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
|
|
1066
|
+
body: JSON.stringify({ model, temperature: 0.1, messages: [{ role: "system", content: system }, { role: "user", content }] }),
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
else if (config.provider === "google") {
|
|
1070
|
+
const parts = [{ text: currentPrompt }];
|
|
1071
|
+
if (imageData && turn === 1)
|
|
1072
|
+
parts.push({ inlineData: { mimeType: imageMime, data: imageData } });
|
|
1073
|
+
response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
|
|
1074
|
+
method: "POST",
|
|
1075
|
+
signal,
|
|
1076
|
+
headers: { "content-type": "application/json" },
|
|
1077
|
+
body: JSON.stringify({ systemInstruction: { parts: [{ text: system }] }, contents: [{ role: "user", parts }] }),
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
const content = [{ type: "text", text: currentPrompt }];
|
|
1082
|
+
if (imageData && turn === 1)
|
|
1083
|
+
content.push({ type: "image", source: { type: "base64", media_type: imageMime, data: imageData } });
|
|
1084
|
+
response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1085
|
+
method: "POST",
|
|
1086
|
+
signal,
|
|
1087
|
+
headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
|
|
1088
|
+
body: JSON.stringify({ model, max_tokens: 4096, system, messages: [{ role: "user", content }] }),
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
if (!response.ok) {
|
|
1092
|
+
if (config.lassoKey && (response.status === 429 || response.status === 401)) {
|
|
1093
|
+
onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
|
|
1094
|
+
proposal = await callServerGateway(config.lassoKey);
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
throw new Error(`Agent request failed (${response.status}).`);
|
|
1098
|
+
}
|
|
1099
|
+
const payload = await response.json();
|
|
1100
|
+
const contentCandidate = (config.provider === "openai" || config.provider === "nvidia" || config.provider === "ollama")
|
|
1101
|
+
? payload.choices?.[0]?.message?.content
|
|
1102
|
+
: config.provider === "google"
|
|
1103
|
+
? payload.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("")
|
|
1104
|
+
: payload.content?.find((item) => item.type === "text")?.text;
|
|
1105
|
+
rawText = contentCandidate || "";
|
|
1106
|
+
if (!rawText)
|
|
1107
|
+
throw new Error("The agent returned an empty response.");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
if (!isLassoKey && config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
|
|
1112
|
+
onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
|
|
1113
|
+
proposal = await callServerGateway(config.lassoKey);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
throw err;
|
|
1117
|
+
}
|
|
1118
|
+
const action = parseAgentAction(rawText);
|
|
1119
|
+
if (action.type === "run_command") {
|
|
1120
|
+
onProgress?.("Running command…", `$ ${action.command}`, "working");
|
|
1121
|
+
const res = await executeAgentCommand(cwd, action.command);
|
|
1122
|
+
const outputSnippet = res.stdout || res.stderr || "(no output)";
|
|
1123
|
+
onProgress?.(`Command completed: ${action.command}`, outputSnippet.slice(0, 150), "working");
|
|
1124
|
+
toolLogs.push(`Turn ${turn} Command executed: ${action.command}\nExit code: ${res.exitCode}\nOutput:\n${outputSnippet.slice(0, 3000)}`);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (action.type === "web_search") {
|
|
1128
|
+
onProgress?.("Searching the web…", `🔍 ${action.query}`, "working");
|
|
1129
|
+
const results = await executeWebSearch(action.query);
|
|
1130
|
+
const summary = results.length > 0
|
|
1131
|
+
? results.map((r, i) => `[${i + 1}] ${r.title}\nURL: ${r.url}\n${r.snippet}`).join("\n\n")
|
|
1132
|
+
: "No web results found.";
|
|
1133
|
+
onProgress?.(`Web search complete: ${action.query}`, `${results.length} results found`, "working");
|
|
1134
|
+
toolLogs.push(`Turn ${turn} Web search for "${action.query}":\n${summary.slice(0, 3000)}`);
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (action.type === "ask_user") {
|
|
1138
|
+
if (onPrompt && input.taskId) {
|
|
1139
|
+
onProgress?.("Waiting for your response…", action.question, "working");
|
|
1140
|
+
onPrompt({
|
|
1141
|
+
message: action.question,
|
|
1142
|
+
kind: action.kind || "permission",
|
|
1143
|
+
options: action.options,
|
|
1144
|
+
});
|
|
1145
|
+
const answer = await waitForAgentPrompt(input.taskId);
|
|
1146
|
+
onProgress?.(`Answer received: ${answer}`, undefined, "working");
|
|
1147
|
+
toolLogs.push(`Turn ${turn} Asked user: "${action.question}"\nUser response: ${answer}`);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (action.type === "proposal") {
|
|
1152
|
+
proposal = {
|
|
1153
|
+
summary: action.summary,
|
|
1154
|
+
packages: action.packages,
|
|
1155
|
+
changes: action.changes,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
proposal = { summary: "Waiting for user input.", changes: [] };
|
|
1160
|
+
}
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
if (!proposal) {
|
|
1164
|
+
proposal = { summary: "Completed agent task.", changes: [] };
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const finalProposal = proposal || { summary: "Completed agent task.", changes: [] };
|
|
1168
|
+
// Agentic execution: If the AI decided packages are needed, install them now
|
|
1169
|
+
if (finalProposal.packages && finalProposal.packages.length > 0) {
|
|
1170
|
+
await installPackages(cwd, finalProposal.packages, onProgress);
|
|
757
1171
|
}
|
|
758
|
-
|
|
759
|
-
throw new Error(`Agent request failed (${response.status}).`);
|
|
760
|
-
const payload = await response.json();
|
|
761
|
-
const text = config.provider === "openai"
|
|
762
|
-
? payload.choices?.[0]?.message?.content
|
|
763
|
-
: config.provider === "google"
|
|
764
|
-
? payload.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("")
|
|
765
|
-
: payload.content?.find((item) => item.type === "text")?.text;
|
|
766
|
-
if (!text)
|
|
767
|
-
throw new Error("The agent returned an empty response.");
|
|
768
|
-
return jsonFrom(text);
|
|
1172
|
+
return finalProposal;
|
|
769
1173
|
}
|
|
770
1174
|
async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
|
|
771
1175
|
const context = await contextFor(cwd, input.element);
|
|
@@ -778,37 +1182,82 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
|
|
|
778
1182
|
throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
|
|
779
1183
|
return extractLocalAgentText(result.stdout, config.provider).trim();
|
|
780
1184
|
}
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1185
|
+
const callServerGatewayAnswer = async (gatewayKey) => {
|
|
1186
|
+
let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
|
|
1187
|
+
if (serverUrl.includes("collab.lasso.byorello.space")) {
|
|
1188
|
+
serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
|
|
1189
|
+
}
|
|
1190
|
+
const endpoint = `${serverUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, "")}/api/v1/agent/generate`;
|
|
1191
|
+
const response = await fetch(endpoint, {
|
|
1192
|
+
method: "POST",
|
|
1193
|
+
signal,
|
|
1194
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
|
|
1195
|
+
body: JSON.stringify({
|
|
1196
|
+
provider: config.provider,
|
|
1197
|
+
model: config.model,
|
|
1198
|
+
system: "Answer conversationally and directly. Do not edit files or return JSON.",
|
|
1199
|
+
prompt,
|
|
1200
|
+
}),
|
|
787
1201
|
});
|
|
1202
|
+
if (!response.ok) {
|
|
1203
|
+
const errData = await response.json().catch(() => ({}));
|
|
1204
|
+
throw new Error(errData.error || `Lasso Agent Gateway request failed (${response.status}).`);
|
|
1205
|
+
}
|
|
1206
|
+
const data = await response.json();
|
|
1207
|
+
if (!data.text?.trim())
|
|
1208
|
+
throw new Error("The agent returned an empty answer.");
|
|
1209
|
+
return data.text.trim();
|
|
1210
|
+
};
|
|
1211
|
+
const isLassoKey = config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
|
|
1212
|
+
if (isLassoKey && config.provider !== "ollama") {
|
|
1213
|
+
return await callServerGatewayAnswer(config.apiKey);
|
|
788
1214
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1215
|
+
const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "meta/llama-3.2-11b-vision-instruct" : "claude-sonnet-4-20250514");
|
|
1216
|
+
let response;
|
|
1217
|
+
try {
|
|
1218
|
+
if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
|
|
1219
|
+
const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
|
|
1220
|
+
response = await fetch(`${baseUrl}/chat/completions`, {
|
|
1221
|
+
method: "POST", signal, headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
|
|
1222
|
+
body: JSON.stringify({ model, temperature: 0.2, messages: [{ role: "system", content: "Answer conversationally. Do not edit files or return JSON." }, { role: "user", content: prompt }] }),
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
else if (config.provider === "google") {
|
|
1226
|
+
response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
|
|
1227
|
+
method: "POST", signal, headers: { "content-type": "application/json" },
|
|
1228
|
+
body: JSON.stringify({ systemInstruction: { parts: [{ text: "Answer conversationally. Do not edit files or return JSON." }] }, contents: [{ role: "user", parts: [{ text: prompt }] }] }),
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
1233
|
+
method: "POST", signal, headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
|
|
1234
|
+
body: JSON.stringify({ model, max_tokens: 2048, system: "Answer conversationally. Do not edit files or return JSON.", messages: [{ role: "user", content: prompt }] }),
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
if (!response.ok) {
|
|
1238
|
+
if (config.lassoKey && (response.status === 429 || response.status === 401)) {
|
|
1239
|
+
onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
|
|
1240
|
+
return await callServerGatewayAnswer(config.lassoKey);
|
|
1241
|
+
}
|
|
1242
|
+
throw new Error(`Agent request failed (${response.status}).`);
|
|
1243
|
+
}
|
|
1244
|
+
const payload = await response.json();
|
|
1245
|
+
const answer = config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia"
|
|
1246
|
+
? payload.choices?.[0]?.message?.content
|
|
1247
|
+
: config.provider === "google"
|
|
1248
|
+
? payload.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("")
|
|
1249
|
+
: payload.content?.find((item) => item.type === "text")?.text;
|
|
1250
|
+
if (!answer?.trim())
|
|
1251
|
+
throw new Error("The agent returned an empty answer.");
|
|
1252
|
+
return answer.trim();
|
|
794
1253
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
1254
|
+
catch (err) {
|
|
1255
|
+
if (config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
|
|
1256
|
+
onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
|
|
1257
|
+
return await callServerGatewayAnswer(config.lassoKey);
|
|
1258
|
+
}
|
|
1259
|
+
throw err;
|
|
800
1260
|
}
|
|
801
|
-
if (!response.ok)
|
|
802
|
-
throw new Error(`Agent request failed (${response.status}).`);
|
|
803
|
-
const payload = await response.json();
|
|
804
|
-
const answer = config.provider === "openai" || config.provider === "ollama"
|
|
805
|
-
? payload.choices?.[0]?.message?.content
|
|
806
|
-
: config.provider === "google"
|
|
807
|
-
? payload.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("")
|
|
808
|
-
: payload.content?.find((item) => item.type === "text")?.text;
|
|
809
|
-
if (!answer?.trim())
|
|
810
|
-
throw new Error("The agent returned an empty answer.");
|
|
811
|
-
return answer.trim();
|
|
812
1261
|
}
|
|
813
1262
|
async function generateCommitMessage(cwd, status, config, signal, onProgress) {
|
|
814
1263
|
const answer = await answerQuestion(cwd, {
|