@lasso-ai/cli 1.2.6 → 1.3.1

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
@@ -14,12 +14,57 @@ exports.runLocalAgentPrompt = runLocalAgentPrompt;
14
14
  exports.proposeChanges = proposeChanges;
15
15
  exports.answerQuestion = answerQuestion;
16
16
  exports.generateCommitMessage = generateCommitMessage;
17
- const promises_1 = __importDefault(require("node:fs/promises"));
17
+ const node_fs_1 = __importDefault(require("node:fs"));
18
18
  const node_path_1 = __importDefault(require("node:path"));
19
19
  const node_child_process_1 = require("node:child_process");
20
20
  const node_util_1 = require("node:util");
21
+ const auth_1 = require("./auth");
21
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
22
23
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
24
+ function detectProjectStructure(cwd) {
25
+ const pkgPath = node_path_1.default.join(cwd, "package.json");
26
+ let framework = "unknown";
27
+ const directories = [];
28
+ const appDirs = [];
29
+ if (node_fs_1.default.existsSync(pkgPath)) {
30
+ try {
31
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
32
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
33
+ if (deps.next)
34
+ framework = "Next.js";
35
+ else if (deps.vite)
36
+ framework = "Vite";
37
+ else if (deps.react)
38
+ framework = "React";
39
+ }
40
+ catch {
41
+ // Ignore errors
42
+ }
43
+ }
44
+ // Scan for important directories
45
+ const scanDirs = ["src", "app", "pages", "web", "lib", "components"];
46
+ for (const dir of scanDirs) {
47
+ if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, dir))) {
48
+ directories.push(dir);
49
+ if (dir === "app")
50
+ appDirs.push(dir);
51
+ // Check for src/app structure
52
+ if (dir === "src" && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "src", "app"))) {
53
+ directories.push("src/app");
54
+ appDirs.push("src/app");
55
+ }
56
+ }
57
+ }
58
+ // Determine source root
59
+ let sourceRoot = ".";
60
+ if (directories.includes("src/app"))
61
+ sourceRoot = "src";
62
+ else if (directories.includes("app"))
63
+ sourceRoot = ".";
64
+ else if (directories.includes("src"))
65
+ sourceRoot = "src";
66
+ return { framework, sourceRoot, directories, multipleAppDirs: appDirs };
67
+ }
23
68
  async function installPackages(cwd, packages, onProgress) {
24
69
  const validPkgs = [...new Set(packages
25
70
  .map((p) => p.trim().replace(/^['"`]|['"`]$/g, ""))
@@ -30,7 +75,7 @@ async function installPackages(cwd, packages, onProgress) {
30
75
  let existingDeps = {};
31
76
  try {
32
77
  const pkgJsonPath = node_path_1.default.join(cwd, "package.json");
33
- const raw = await promises_1.default.readFile(pkgJsonPath, "utf8");
78
+ const raw = node_fs_1.default.readFileSync(pkgJsonPath, "utf8");
34
79
  const parsed = JSON.parse(raw);
35
80
  existingDeps = { ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) };
36
81
  }
@@ -41,7 +86,7 @@ async function installPackages(cwd, packages, onProgress) {
41
86
  // Detect package manager
42
87
  let cmd = "npm install";
43
88
  try {
44
- const rootFiles = await promises_1.default.readdir(cwd);
89
+ const rootFiles = node_fs_1.default.readdirSync(cwd);
45
90
  if (rootFiles.includes("bun.lockb") || rootFiles.includes("bun.lock"))
46
91
  cmd = "bun add";
47
92
  else if (rootFiles.includes("pnpm-lock.yaml"))
@@ -164,7 +209,7 @@ const ignored = new Set(["node_modules", ".git", ".next", "dist", "build", ".tur
164
209
  const sourceExtensions = /\.(tsx?|jsx?|vue|svelte|css|scss|html)$/i;
165
210
  const layoutFiles = /^(layout|_layout|_app|app|_document|document|template|_template)\.(tsx?|jsx?)$/i;
166
211
  const sourceFileCache = new Map();
167
- async function sourceFiles(directory, budget = { remaining: 25 }) {
212
+ function sourceFiles(directory, budget = { remaining: 25 }) {
168
213
  if (budget.remaining <= 0)
169
214
  return [];
170
215
  const cached = sourceFileCache.get(directory);
@@ -173,7 +218,7 @@ async function sourceFiles(directory, budget = { remaining: 25 }) {
173
218
  budget.remaining -= files.length;
174
219
  return files;
175
220
  }
176
- const entries = await promises_1.default.readdir(directory, { withFileTypes: true });
221
+ const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
177
222
  const files = [];
178
223
  const layoutFilesList = [];
179
224
  const otherFiles = [];
@@ -184,7 +229,7 @@ async function sourceFiles(directory, budget = { remaining: 25 }) {
184
229
  if (budget.remaining <= 0)
185
230
  break;
186
231
  if (entry.isDirectory()) {
187
- const nested = await sourceFiles(fullPath, budget);
232
+ const nested = sourceFiles(fullPath, budget);
188
233
  files.push(...nested);
189
234
  }
190
235
  else if (sourceExtensions.test(entry.name)) {
@@ -202,10 +247,21 @@ async function sourceFiles(directory, budget = { remaining: 25 }) {
202
247
  sourceFileCache.set(directory, { expiresAt: Date.now() + 30000, files });
203
248
  return files;
204
249
  }
205
- async function contextFor(cwd, element) {
250
+ function contextFor(cwd, element) {
206
251
  const needle = element.label.replace(/^[^.#]+[.#]?/, "");
207
252
  const hintedPath = element.sourceHint?.split(":")[0];
208
253
  const sourceFile = hintedPath ? node_path_1.default.basename(hintedPath) : "";
254
+ // Add project structure information at the top
255
+ const structure = detectProjectStructure(cwd);
256
+ let projectStructureInfo = `PROJECT STRUCTURE:
257
+ - Framework: ${structure.framework}
258
+ - Source root: ${structure.sourceRoot}
259
+ - Directories found: ${structure.directories.join(", ") || "root"}
260
+ - Multiple app directories: ${structure.multipleAppDirs.length > 0 ? structure.multipleAppDirs.join(", ") : "none"}
261
+ - IMPORTANT: When creating files, use the existing directory structure. Do not create duplicate directories (e.g., if "src/app/" exists, use it instead of creating a new "app/").
262
+ - Path aliases: Check tsconfig.json or jsconfig.json for path aliases like "@/components"
263
+
264
+ `;
209
265
  // Extract unique class names and attributes from the element for better matching
210
266
  const classMatches = element.html?.match(/class="([^"]+)"/);
211
267
  const classNames = classMatches ? classMatches[1].split(/\s+/).filter((c) => c.length > 3) : [];
@@ -222,20 +278,20 @@ async function contextFor(cwd, element) {
222
278
  candidates.push(node_path_1.default.join(cwd, normalizedHint.slice(srcIndex + 1)));
223
279
  for (const candidate of candidates) {
224
280
  try {
225
- const content = await promises_1.default.readFile(candidate, "utf8");
226
- return `FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
281
+ const content = node_fs_1.default.readFileSync(candidate, "utf8");
282
+ return `${projectStructureInfo}FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
227
283
  }
228
284
  catch {
229
285
  // The runtime source hint can point to a different checkout.
230
286
  }
231
287
  }
232
288
  }
233
- const files = await sourceFiles(cwd);
289
+ const files = sourceFiles(cwd);
234
290
  const snippets = [];
235
291
  const scoredFiles = [];
236
- const results = await Promise.all(files.map(async (file) => {
292
+ const results = files.map((file) => {
237
293
  try {
238
- const content = await promises_1.default.readFile(file, "utf8");
294
+ const content = node_fs_1.default.readFileSync(file, "utf8");
239
295
  let score = 0;
240
296
  // Score files based on relevance
241
297
  if (sourceFile && file.endsWith(sourceFile))
@@ -263,7 +319,7 @@ async function contextFor(cwd, element) {
263
319
  // A file can disappear while a dev server is rebuilding; skip it.
264
320
  }
265
321
  return null;
266
- }));
322
+ }).filter((r) => r !== null);
267
323
  // Sort by score and take top files
268
324
  const sortedResults = results.filter((r) => r !== null).sort((a, b) => b.score - a.score);
269
325
  for (const result of sortedResults) {
@@ -271,7 +327,7 @@ async function contextFor(cwd, element) {
271
327
  snippets.push(`FILE: ${node_path_1.default.relative(cwd, result.file)}\n${result.content.slice(0, 6000)}`);
272
328
  }
273
329
  }
274
- return snippets.join("\n\n---\n\n");
330
+ return projectStructureInfo + snippets.join("\n\n---\n\n");
275
331
  }
276
332
  function jsonObjectCandidates(text) {
277
333
  const candidates = [];
@@ -307,8 +363,18 @@ function jsonObjectCandidates(text) {
307
363
  return candidates;
308
364
  }
309
365
  function jsonFrom(text) {
310
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
311
- const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(text)];
366
+ // Remove common progress prefixes and tool output noise
367
+ const cleaned = text
368
+ .replace(/^(Thinking|Working|Planning|Analyzing|Executing|Searching|Scanning)\.\.\.?$/gm, '')
369
+ .replace(/^›/gm, '')
370
+ .replace(/^>/gm, '')
371
+ .replace(/^(grep|find|cat|ls|cd|npm|pnpm|yarn|git).*$/gm, '')
372
+ .replace(/^src\/.*:\d+:.*$/gm, '')
373
+ .replace(/^(The agent|Error|Warning|Info|Note):.*$/gm, '')
374
+ .replace(/^(Progress|Activity|Output|Result):.*$/gm, '')
375
+ .trim();
376
+ const fenced = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
377
+ const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(cleaned)];
312
378
  for (const candidate of candidates) {
313
379
  try {
314
380
  const parsed = JSON.parse(candidate.trim());
@@ -584,6 +650,18 @@ const AGENT_FAILURE_HINTS = [
584
650
  pattern: /failed to fetch models\.dev/i,
585
651
  message: "The local agent could not reach models.dev to refresh its model list. Check the network or proxy it uses, then retry.",
586
652
  },
653
+ {
654
+ pattern: /quota exceeded|quota limit|out of quota|no quota|quota.*limit|usage.*quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted/i,
655
+ message: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.",
656
+ },
657
+ {
658
+ pattern: /payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i,
659
+ message: "Your subscription or plan requires an upgrade. Please check your billing settings and upgrade to continue.",
660
+ },
661
+ {
662
+ pattern: /api.*key.*invalid|api.*key.*expired|invalid.*api.*key|expired.*api.*key|authentication.*failed|unauthorized|forbidden|401|403/i,
663
+ message: "Authentication failed. Check your API key or sign in to your account, then retry.",
664
+ },
587
665
  ];
588
666
  function agentFailureHint(line) {
589
667
  return AGENT_FAILURE_HINTS.find((hint) => hint.pattern.test(line))?.message;
@@ -732,6 +810,10 @@ function localAgentError(command, stderr, exitCode) {
732
810
  if (command === "codex" && output.includes("mcp.canva.com") && output.includes("invalid_token")) {
733
811
  return "Codex started, but the Canva MCP connection has an expired OAuth token. Re-authenticate or remove the Canva MCP server from Codex, then retry.";
734
812
  }
813
+ // Check for quota/plan exhaustion in the output
814
+ if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted/i.test(output)) {
815
+ return `${command === "opencode" ? "OpenCode" : command === "cursor" ? "Cursor" : command} request failed: Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.`;
816
+ }
735
817
  return `${command} exited with code ${exitCode}${output ? `: ${output.slice(0, 500)}` : ""}`;
736
818
  }
737
819
  function localModel(provider, model) {
@@ -849,7 +931,14 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
849
931
  activeAgentInputs.delete(taskId);
850
932
  signal?.removeEventListener("abort", abort);
851
933
  if (timedOut) {
852
- reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
934
+ const stderrTail = pendingStderr.trim();
935
+ // Check if timeout was due to quota exhaustion
936
+ if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted/i.test(stderrTail)) {
937
+ reject(new Error(`${label} did not finish within 5 minutes: Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.`));
938
+ }
939
+ else {
940
+ reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
941
+ }
853
942
  return;
854
943
  }
855
944
  const stdoutTail = pendingStdout.trim();
@@ -900,7 +989,7 @@ async function runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, task
900
989
  return extractLocalAgentText(result.stdout, provider);
901
990
  }
902
991
  async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
903
- const context = await contextFor(cwd, input.element);
992
+ const context = contextFor(cwd, input.element);
904
993
  const runtimeErrors = input.context?.runtimeErrors || [];
905
994
  const visualContext = input.context ? { ...input.context, screenshots: undefined, runtimeErrors: undefined } : undefined;
906
995
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
@@ -911,6 +1000,8 @@ async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt)
911
1000
  const runtimeErrorHint = runtimeErrors.length
912
1001
  ? `\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")}`
913
1002
  : "";
1003
+ // Detect project structure for instruction
1004
+ const structure = detectProjectStructure(cwd);
914
1005
  const instruction = `Selection context:
915
1006
  ${JSON.stringify(input.element, null, 2)}
916
1007
  ${JSON.stringify(visualContext, null, 2)}
@@ -928,6 +1019,8 @@ Relevant source context:
928
1019
  ${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}
929
1020
 
930
1021
  CRITICAL INSTRUCTIONS:
1022
+ 0. PROJECT STRUCTURE AWARENESS: This project has specific directory structure. Before creating or moving files, verify the actual structure. Do not assume standard locations.
1023
+ ${structure.multipleAppDirs.length > 0 ? ` WARNING: Multiple app directories detected: ${structure.multipleAppDirs.join(", ")}. Use the correct one for this project.\n` : ""}
931
1024
  1. You are an autonomous agent with the ability to install npm packages and edit source code.
932
1025
  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"]).
933
1026
  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".
@@ -948,6 +1041,9 @@ CRITICAL INSTRUCTIONS:
948
1041
  }
949
1042
  Do NOT return prose or markdown outside the JSON.`;
950
1043
  const callServerGateway = async (gatewayKey) => {
1044
+ // Use CLI auth key if available, otherwise use the provided gateway key
1045
+ const cliCredentials = (0, auth_1.loadCredentials)();
1046
+ const apiKeyToUse = cliCredentials?.apiKey || gatewayKey;
951
1047
  let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
952
1048
  if (serverUrl.includes("collab.lasso.byorello.space")) {
953
1049
  serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
@@ -964,7 +1060,7 @@ Do NOT return prose or markdown outside the JSON.`;
964
1060
  const response = await fetch(endpoint, {
965
1061
  method: "POST",
966
1062
  signal,
967
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1063
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
968
1064
  body: JSON.stringify({
969
1065
  sessionId: input.taskId,
970
1066
  prompt: instruction,
@@ -974,7 +1070,12 @@ Do NOT return prose or markdown outside the JSON.`;
974
1070
  });
975
1071
  if (!response.ok) {
976
1072
  const errData = await response.json().catch(() => ({}));
977
- throw new Error(errData.error || `Lasso Agent Gateway request failed (${response.status}).`);
1073
+ const errorMessage = errData.error || `Lasso Agent Gateway request failed (${response.status})`;
1074
+ // Check for quota/plan exhaustion
1075
+ if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted|payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i.test(errorMessage)) {
1076
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1077
+ }
1078
+ throw new Error(errorMessage);
978
1079
  }
979
1080
  const data = await response.json();
980
1081
  // If the server-side agent reported failure, surface it as a thrown error so the
@@ -1007,7 +1108,12 @@ Do NOT return prose or markdown outside the JSON.`;
1007
1108
  });
1008
1109
  if (!response.ok) {
1009
1110
  const errData = await response.json().catch(() => ({}));
1010
- throw new Error(errData.error || `Lasso Gateway generate failed (${response.status}).`);
1111
+ const errorMessage = errData.error || `Lasso Gateway generate failed (${response.status})`;
1112
+ // Check for quota/plan exhaustion
1113
+ if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted|payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i.test(errorMessage)) {
1114
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1115
+ }
1116
+ throw new Error(errorMessage);
1011
1117
  }
1012
1118
  const data = await response.json();
1013
1119
  if (!data.text?.trim())
@@ -1018,7 +1124,9 @@ Do NOT return prose or markdown outside the JSON.`;
1018
1124
  proposal = await proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
1019
1125
  }
1020
1126
  else {
1021
- const isLassoKey = config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1127
+ // Check if using Lasso platform key (from CLI auth or env)
1128
+ const cliCredentials = (0, auth_1.loadCredentials)();
1129
+ const isLassoKey = cliCredentials?.apiKey || config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1022
1130
  const system = `You are Lasso, an autonomous agentic AI coding assistant.
1023
1131
  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.
1024
1132
 
@@ -1051,8 +1159,17 @@ RULES:
1051
1159
  - If you already have sufficient context to fulfill the user's request immediately, return "propose_changes" (or standard {"summary": "...", "packages": [...], "changes": [...]}) directly!
1052
1160
  - 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.
1053
1161
  - Each oldString must match the existing file context exactly.
1054
- - Return ONLY valid JSON with no markdown formatting or prose outside the JSON.`;
1055
- 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");
1162
+ - CRITICAL: Return ONLY valid JSON with no markdown formatting, no code blocks, no prose, no thinking tags, and no progress output. Just the JSON object.
1163
+ - Do NOT include phrases like "Thinking...", "Working...", or any progress indicators in your response.
1164
+ - Do NOT wrap the JSON in code blocks. Return raw JSON only.`;
1165
+ const model = config.model;
1166
+ if (!model) {
1167
+ return {
1168
+ summary: "No model selected. Please select a model in settings.",
1169
+ changes: [],
1170
+ packages: [],
1171
+ };
1172
+ }
1056
1173
  const image = input.context?.screenshots?.element || input.context?.screenshots?.full;
1057
1174
  const imageData = image?.replace(/^data:image\/[^;]+;base64,/, "");
1058
1175
  const imageMime = image?.match(/^data:(image\/[^;]+);base64,/)?.[1] || "image/jpeg";
@@ -1068,7 +1185,8 @@ RULES:
1068
1185
  // If using a Lasso platform key, route this turn through the /generate endpoint
1069
1186
  // (full agentic loop via Lasso gateway instead of one-shot session endpoint)
1070
1187
  if (isLassoKey && config.provider !== "ollama") {
1071
- rawText = await callGatewayGenerate(config.apiKey, system, currentPrompt);
1188
+ const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1189
+ rawText = await callGatewayGenerate(apiKeyToUse, system, currentPrompt);
1072
1190
  }
1073
1191
  else {
1074
1192
  // Provider-direct inference path
@@ -1106,6 +1224,10 @@ RULES:
1106
1224
  });
1107
1225
  }
1108
1226
  if (!response.ok) {
1227
+ // Check for quota/plan exhaustion before fallback
1228
+ if (response.status === 429 || response.status === 402 || response.status === 403) {
1229
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1230
+ }
1109
1231
  if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1110
1232
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1111
1233
  proposal = await callServerGateway(config.lassoKey);
@@ -1125,6 +1247,10 @@ RULES:
1125
1247
  }
1126
1248
  }
1127
1249
  catch (err) {
1250
+ // Check for quota/plan exhaustion before fallback
1251
+ if (err.message?.includes("429") || err.message?.includes("402") || err.message?.includes("403")) {
1252
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1253
+ }
1128
1254
  if (!isLassoKey && config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
1129
1255
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1130
1256
  proposal = await callServerGateway(config.lassoKey);
@@ -1189,13 +1315,16 @@ RULES:
1189
1315
  return finalProposal;
1190
1316
  }
1191
1317
  async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
1192
- const context = await contextFor(cwd, input.element);
1318
+ const context = contextFor(cwd, input.element);
1193
1319
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
1194
1320
  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."}`;
1195
1321
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
1196
1322
  return (await runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, input.taskId, onPrompt)).trim();
1197
1323
  }
1198
1324
  const callServerGatewayAnswer = async (gatewayKey) => {
1325
+ // Use CLI auth key if available, otherwise use the provided gateway key
1326
+ const cliCredentials = (0, auth_1.loadCredentials)();
1327
+ const apiKeyToUse = cliCredentials?.apiKey || gatewayKey;
1199
1328
  let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
1200
1329
  if (serverUrl.includes("collab.lasso.byorello.space")) {
1201
1330
  serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
@@ -1204,7 +1333,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1204
1333
  const response = await fetch(endpoint, {
1205
1334
  method: "POST",
1206
1335
  signal,
1207
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1336
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
1208
1337
  body: JSON.stringify({
1209
1338
  provider: config.provider,
1210
1339
  model: config.model,
@@ -1214,18 +1343,29 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1214
1343
  });
1215
1344
  if (!response.ok) {
1216
1345
  const errData = await response.json().catch(() => ({}));
1217
- throw new Error(errData.error || `Lasso Agent Gateway request failed (${response.status}).`);
1346
+ const errorMessage = errData.error || `Lasso Agent Gateway request failed (${response.status})`;
1347
+ // Check for quota/plan exhaustion
1348
+ if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted|payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i.test(errorMessage)) {
1349
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1350
+ }
1351
+ throw new Error(errorMessage);
1218
1352
  }
1219
1353
  const data = await response.json();
1220
1354
  if (!data.text?.trim())
1221
1355
  throw new Error("The agent returned an empty answer.");
1222
1356
  return data.text.trim();
1223
1357
  };
1224
- const isLassoKey = config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1358
+ // Check if using Lasso platform key (from CLI auth or env)
1359
+ const cliCredentials = (0, auth_1.loadCredentials)();
1360
+ const isLassoKey = cliCredentials?.apiKey || config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1225
1361
  if (isLassoKey && config.provider !== "ollama") {
1226
- return await callServerGatewayAnswer(config.apiKey);
1362
+ const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1363
+ return await callServerGatewayAnswer(apiKeyToUse);
1364
+ }
1365
+ const model = config.model;
1366
+ if (!model) {
1367
+ return "No model selected. Please select a model in settings.";
1227
1368
  }
1228
- 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");
1229
1369
  let response;
1230
1370
  try {
1231
1371
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
@@ -1248,6 +1388,10 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1248
1388
  });
1249
1389
  }
1250
1390
  if (!response.ok) {
1391
+ // Check for quota/plan exhaustion before fallback
1392
+ if (response.status === 429 || response.status === 402 || response.status === 403) {
1393
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1394
+ }
1251
1395
  if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1252
1396
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1253
1397
  return await callServerGatewayAnswer(config.lassoKey);
@@ -148,6 +148,8 @@ type ModelOption = {
148
148
  id: string;
149
149
  label: string;
150
150
  provider: "anthropic" | "openai" | "google" | "ollama" | "nvidia" | "cli";
151
+ locked?: boolean;
152
+ lockedReason?: string;
151
153
  };
152
154
  type PageEntry = {
153
155
  name: string;
@@ -217,6 +219,11 @@ export type ServerBridgeMessage = {
217
219
  type: "applied";
218
220
  taskId?: string;
219
221
  message: string;
222
+ validation?: {
223
+ passed: boolean;
224
+ errors: string[];
225
+ command: string;
226
+ };
220
227
  } | {
221
228
  type: "undone";
222
229
  taskId?: string;
@@ -233,8 +240,8 @@ export declare function restartBridge(port?: number): Promise<{
233
240
  ok: boolean;
234
241
  error?: string;
235
242
  }>;
236
- export declare function startBridge(cwd?: string, collabConfig?: CollabConfig | null, bridgePort?: number): {
243
+ export declare function startBridge(cwd?: string, collabConfig?: CollabConfig | null, bridgePort?: number): Promise<{
237
244
  send: (msg: ServerBridgeMessage) => void;
238
245
  close(): void;
239
- };
246
+ }>;
240
247
  export {};