@lasso-ai/cli 1.2.2 → 1.2.4

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 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(/&amp;/g, "&")
108
+ .replace(/&quot;/g, '"')
109
+ .replace(/&#39;/g, "'")
110
+ .replace(/&lt;/g, "<")
111
+ .replace(/&gt;/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
- return { summary: parsed.summary || "The proposed source changes are ready for review.", changes: parsed.changes };
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 visualContext = input.context ? { ...input.context, screenshots: undefined } : undefined;
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 instruction = `Selection context:\n${JSON.stringify(input.element, null, 2)}\n${JSON.stringify(visualContext, null, 2)}\n\nConversation history:\n${history}\n\nPrevious change history for this selection:\n${priorChanges}\n\nRelevant source context:\n${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}\n\nCRITICAL: You MUST return ONLY JSON with actual file changes. Do NOT explain what you see or describe the code. Always propose concrete edits when given an instruction. Return JSON format: {"summary":"brief action taken","changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}`;
718
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
719
- return proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
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
- const system = "You are Lasso, a source-code editing agent. Your ONLY job is to propose concrete file changes. NEVER explain, describe, or analyze code without proposing edits. ALWAYS return valid JSON with at least one change when the user requests a modification. Each oldString must occur exactly once in its file. Never rewrite whole files. Keep changes focused and minimal. If you cannot find the exact text to change, look harder at the provided context - do not give up and explain instead.";
722
- 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");
723
- const image = input.context?.screenshots?.element || input.context?.screenshots?.full;
724
- const imageData = image?.replace(/^data:image\/[^;]+;base64,/, "");
725
- const imageMime = image?.match(/^data:(image\/[^;]+);base64,/)?.[1] || "image/jpeg";
726
- let response;
727
- if (config.provider === "openai" || config.provider === "ollama") {
728
- const content = image ? [{ type: "text", text: instruction }, { type: "image_url", image_url: { url: image } }] : instruction;
729
- response = await fetch(`${config.baseUrl || "https://api.openai.com/v1"}/chat/completions`, {
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 ${config.apiKey || ""}` },
733
- body: JSON.stringify({ model, temperature: 0.1, messages: [{ role: "system", content: system }, { role: "user", content }] }),
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
- else if (config.provider === "google") {
737
- const parts = [{ text: instruction }];
738
- if (imageData)
739
- parts.push({ inlineData: { mimeType: imageMime, data: imageData } });
740
- response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
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({ systemInstruction: { parts: [{ text: system }] }, contents: [{ role: "user", parts }] }),
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 content = [{ type: "text", text: instruction }];
749
- if (imageData)
750
- content.push({ type: "image", source: { type: "base64", media_type: imageMime, data: imageData } });
751
- response = await fetch("https://api.anthropic.com/v1/messages", {
752
- method: "POST",
753
- signal,
754
- headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
755
- body: JSON.stringify({ model, max_tokens: 4096, system, messages: [{ role: "user", content }] }),
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
- if (!response.ok)
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 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");
782
- let response;
783
- if (config.provider === "openai" || config.provider === "ollama") {
784
- response = await fetch(`${config.baseUrl || "https://api.openai.com/v1"}/chat/completions`, {
785
- method: "POST", signal, headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
786
- body: JSON.stringify({ model, temperature: 0.2, messages: [{ role: "system", content: "Answer conversationally. Do not edit files or return JSON." }, { role: "user", content: prompt }] }),
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
- else if (config.provider === "google") {
790
- response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
791
- method: "POST", signal, headers: { "content-type": "application/json" },
792
- body: JSON.stringify({ systemInstruction: { parts: [{ text: "Answer conversationally. Do not edit files or return JSON." }] }, contents: [{ role: "user", parts: [{ text: prompt }] }] }),
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
- else {
796
- response = await fetch("https://api.anthropic.com/v1/messages", {
797
- method: "POST", signal, headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
798
- body: JSON.stringify({ model, max_tokens: 2048, system: "Answer conversationally. Do not edit files or return JSON.", messages: [{ role: "user", content: prompt }] }),
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, {