@lasso-ai/cli 1.2.6 → 1.3.0

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
@@ -18,6 +18,7 @@ const promises_1 = __importDefault(require("node:fs/promises"));
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);
23
24
  async function installPackages(cwd, packages, onProgress) {
@@ -307,8 +308,18 @@ function jsonObjectCandidates(text) {
307
308
  return candidates;
308
309
  }
309
310
  function jsonFrom(text) {
310
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
311
- const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(text)];
311
+ // Remove common progress prefixes and tool output noise
312
+ const cleaned = text
313
+ .replace(/^(Thinking|Working|Planning|Analyzing|Executing|Searching|Scanning)\.\.\.?$/gm, '')
314
+ .replace(/^›/gm, '')
315
+ .replace(/^>/gm, '')
316
+ .replace(/^(grep|find|cat|ls|cd|npm|pnpm|yarn|git).*$/gm, '')
317
+ .replace(/^src\/.*:\d+:.*$/gm, '')
318
+ .replace(/^(The agent|Error|Warning|Info|Note):.*$/gm, '')
319
+ .replace(/^(Progress|Activity|Output|Result):.*$/gm, '')
320
+ .trim();
321
+ const fenced = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
322
+ const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(cleaned)];
312
323
  for (const candidate of candidates) {
313
324
  try {
314
325
  const parsed = JSON.parse(candidate.trim());
@@ -584,6 +595,18 @@ const AGENT_FAILURE_HINTS = [
584
595
  pattern: /failed to fetch models\.dev/i,
585
596
  message: "The local agent could not reach models.dev to refresh its model list. Check the network or proxy it uses, then retry.",
586
597
  },
598
+ {
599
+ 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,
600
+ message: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.",
601
+ },
602
+ {
603
+ pattern: /payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i,
604
+ message: "Your subscription or plan requires an upgrade. Please check your billing settings and upgrade to continue.",
605
+ },
606
+ {
607
+ pattern: /api.*key.*invalid|api.*key.*expired|invalid.*api.*key|expired.*api.*key|authentication.*failed|unauthorized|forbidden|401|403/i,
608
+ message: "Authentication failed. Check your API key or sign in to your account, then retry.",
609
+ },
587
610
  ];
588
611
  function agentFailureHint(line) {
589
612
  return AGENT_FAILURE_HINTS.find((hint) => hint.pattern.test(line))?.message;
@@ -732,6 +755,10 @@ function localAgentError(command, stderr, exitCode) {
732
755
  if (command === "codex" && output.includes("mcp.canva.com") && output.includes("invalid_token")) {
733
756
  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
757
  }
758
+ // Check for quota/plan exhaustion in the output
759
+ 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)) {
760
+ 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.`;
761
+ }
735
762
  return `${command} exited with code ${exitCode}${output ? `: ${output.slice(0, 500)}` : ""}`;
736
763
  }
737
764
  function localModel(provider, model) {
@@ -849,7 +876,14 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
849
876
  activeAgentInputs.delete(taskId);
850
877
  signal?.removeEventListener("abort", abort);
851
878
  if (timedOut) {
852
- reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
879
+ const stderrTail = pendingStderr.trim();
880
+ // Check if timeout was due to quota exhaustion
881
+ 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)) {
882
+ 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.`));
883
+ }
884
+ else {
885
+ reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
886
+ }
853
887
  return;
854
888
  }
855
889
  const stdoutTail = pendingStdout.trim();
@@ -948,6 +982,9 @@ CRITICAL INSTRUCTIONS:
948
982
  }
949
983
  Do NOT return prose or markdown outside the JSON.`;
950
984
  const callServerGateway = async (gatewayKey) => {
985
+ // Use CLI auth key if available, otherwise use the provided gateway key
986
+ const cliCredentials = (0, auth_1.loadCredentials)();
987
+ const apiKeyToUse = cliCredentials?.apiKey || gatewayKey;
951
988
  let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
952
989
  if (serverUrl.includes("collab.lasso.byorello.space")) {
953
990
  serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
@@ -964,7 +1001,7 @@ Do NOT return prose or markdown outside the JSON.`;
964
1001
  const response = await fetch(endpoint, {
965
1002
  method: "POST",
966
1003
  signal,
967
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1004
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
968
1005
  body: JSON.stringify({
969
1006
  sessionId: input.taskId,
970
1007
  prompt: instruction,
@@ -974,7 +1011,12 @@ Do NOT return prose or markdown outside the JSON.`;
974
1011
  });
975
1012
  if (!response.ok) {
976
1013
  const errData = await response.json().catch(() => ({}));
977
- throw new Error(errData.error || `Lasso Agent Gateway request failed (${response.status}).`);
1014
+ const errorMessage = errData.error || `Lasso Agent Gateway request failed (${response.status})`;
1015
+ // Check for quota/plan exhaustion
1016
+ 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)) {
1017
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1018
+ }
1019
+ throw new Error(errorMessage);
978
1020
  }
979
1021
  const data = await response.json();
980
1022
  // If the server-side agent reported failure, surface it as a thrown error so the
@@ -1007,7 +1049,12 @@ Do NOT return prose or markdown outside the JSON.`;
1007
1049
  });
1008
1050
  if (!response.ok) {
1009
1051
  const errData = await response.json().catch(() => ({}));
1010
- throw new Error(errData.error || `Lasso Gateway generate failed (${response.status}).`);
1052
+ const errorMessage = errData.error || `Lasso Gateway generate failed (${response.status})`;
1053
+ // Check for quota/plan exhaustion
1054
+ 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)) {
1055
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1056
+ }
1057
+ throw new Error(errorMessage);
1011
1058
  }
1012
1059
  const data = await response.json();
1013
1060
  if (!data.text?.trim())
@@ -1018,7 +1065,9 @@ Do NOT return prose or markdown outside the JSON.`;
1018
1065
  proposal = await proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
1019
1066
  }
1020
1067
  else {
1021
- const isLassoKey = config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1068
+ // Check if using Lasso platform key (from CLI auth or env)
1069
+ const cliCredentials = (0, auth_1.loadCredentials)();
1070
+ const isLassoKey = cliCredentials?.apiKey || config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1022
1071
  const system = `You are Lasso, an autonomous agentic AI coding assistant.
1023
1072
  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
1073
 
@@ -1051,8 +1100,17 @@ RULES:
1051
1100
  - If you already have sufficient context to fulfill the user's request immediately, return "propose_changes" (or standard {"summary": "...", "packages": [...], "changes": [...]}) directly!
1052
1101
  - 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
1102
  - 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");
1103
+ - 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.
1104
+ - Do NOT include phrases like "Thinking...", "Working...", or any progress indicators in your response.
1105
+ - Do NOT wrap the JSON in code blocks. Return raw JSON only.`;
1106
+ const model = config.model;
1107
+ if (!model) {
1108
+ return {
1109
+ summary: "No model selected. Please select a model in settings.",
1110
+ changes: [],
1111
+ packages: [],
1112
+ };
1113
+ }
1056
1114
  const image = input.context?.screenshots?.element || input.context?.screenshots?.full;
1057
1115
  const imageData = image?.replace(/^data:image\/[^;]+;base64,/, "");
1058
1116
  const imageMime = image?.match(/^data:(image\/[^;]+);base64,/)?.[1] || "image/jpeg";
@@ -1068,7 +1126,8 @@ RULES:
1068
1126
  // If using a Lasso platform key, route this turn through the /generate endpoint
1069
1127
  // (full agentic loop via Lasso gateway instead of one-shot session endpoint)
1070
1128
  if (isLassoKey && config.provider !== "ollama") {
1071
- rawText = await callGatewayGenerate(config.apiKey, system, currentPrompt);
1129
+ const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1130
+ rawText = await callGatewayGenerate(apiKeyToUse, system, currentPrompt);
1072
1131
  }
1073
1132
  else {
1074
1133
  // Provider-direct inference path
@@ -1106,6 +1165,10 @@ RULES:
1106
1165
  });
1107
1166
  }
1108
1167
  if (!response.ok) {
1168
+ // Check for quota/plan exhaustion before fallback
1169
+ if (response.status === 429 || response.status === 402 || response.status === 403) {
1170
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1171
+ }
1109
1172
  if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1110
1173
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1111
1174
  proposal = await callServerGateway(config.lassoKey);
@@ -1125,6 +1188,10 @@ RULES:
1125
1188
  }
1126
1189
  }
1127
1190
  catch (err) {
1191
+ // Check for quota/plan exhaustion before fallback
1192
+ if (err.message?.includes("429") || err.message?.includes("402") || err.message?.includes("403")) {
1193
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1194
+ }
1128
1195
  if (!isLassoKey && config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
1129
1196
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1130
1197
  proposal = await callServerGateway(config.lassoKey);
@@ -1196,6 +1263,9 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1196
1263
  return (await runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, input.taskId, onPrompt)).trim();
1197
1264
  }
1198
1265
  const callServerGatewayAnswer = async (gatewayKey) => {
1266
+ // Use CLI auth key if available, otherwise use the provided gateway key
1267
+ const cliCredentials = (0, auth_1.loadCredentials)();
1268
+ const apiKeyToUse = cliCredentials?.apiKey || gatewayKey;
1199
1269
  let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
1200
1270
  if (serverUrl.includes("collab.lasso.byorello.space")) {
1201
1271
  serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
@@ -1204,7 +1274,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1204
1274
  const response = await fetch(endpoint, {
1205
1275
  method: "POST",
1206
1276
  signal,
1207
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1277
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
1208
1278
  body: JSON.stringify({
1209
1279
  provider: config.provider,
1210
1280
  model: config.model,
@@ -1214,18 +1284,29 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1214
1284
  });
1215
1285
  if (!response.ok) {
1216
1286
  const errData = await response.json().catch(() => ({}));
1217
- throw new Error(errData.error || `Lasso Agent Gateway request failed (${response.status}).`);
1287
+ const errorMessage = errData.error || `Lasso Agent Gateway request failed (${response.status})`;
1288
+ // Check for quota/plan exhaustion
1289
+ 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)) {
1290
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1291
+ }
1292
+ throw new Error(errorMessage);
1218
1293
  }
1219
1294
  const data = await response.json();
1220
1295
  if (!data.text?.trim())
1221
1296
  throw new Error("The agent returned an empty answer.");
1222
1297
  return data.text.trim();
1223
1298
  };
1224
- const isLassoKey = config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1299
+ // Check if using Lasso platform key (from CLI auth or env)
1300
+ const cliCredentials = (0, auth_1.loadCredentials)();
1301
+ const isLassoKey = cliCredentials?.apiKey || config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1225
1302
  if (isLassoKey && config.provider !== "ollama") {
1226
- return await callServerGatewayAnswer(config.apiKey);
1303
+ const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1304
+ return await callServerGatewayAnswer(apiKeyToUse);
1305
+ }
1306
+ const model = config.model;
1307
+ if (!model) {
1308
+ return "No model selected. Please select a model in settings.";
1227
1309
  }
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
1310
  let response;
1230
1311
  try {
1231
1312
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
@@ -1248,6 +1329,10 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1248
1329
  });
1249
1330
  }
1250
1331
  if (!response.ok) {
1332
+ // Check for quota/plan exhaustion before fallback
1333
+ if (response.status === 429 || response.status === 402 || response.status === 403) {
1334
+ throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1335
+ }
1251
1336
  if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1252
1337
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1253
1338
  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;
@@ -233,8 +235,8 @@ export declare function restartBridge(port?: number): Promise<{
233
235
  ok: boolean;
234
236
  error?: string;
235
237
  }>;
236
- export declare function startBridge(cwd?: string, collabConfig?: CollabConfig | null, bridgePort?: number): {
238
+ export declare function startBridge(cwd?: string, collabConfig?: CollabConfig | null, bridgePort?: number): Promise<{
237
239
  send: (msg: ServerBridgeMessage) => void;
238
240
  close(): void;
239
- };
241
+ }>;
240
242
  export {};
@@ -164,7 +164,7 @@ function readEnvFile(cwd, filename) {
164
164
  return {};
165
165
  }
166
166
  }
167
- function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = exports.DEFAULT_BRIDGE_PORT) {
167
+ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = exports.DEFAULT_BRIDGE_PORT) {
168
168
  const bridgeServer = node_http_1.default.createServer(); // dedicated, empty HTTP server
169
169
  const wss = new ws_1.WebSocketServer({ server: bridgeServer });
170
170
  let overlaySocket = null;
@@ -179,7 +179,10 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
179
179
  ...readEnvFile(root, ".env.local"),
180
180
  ...readEnvFile(root, ".env.production"),
181
181
  }), {});
182
- const resolvedLassoKey = (0, project_1.resolveLassoApiKey)(fileEnv) ||
182
+ // Prioritize CLI auth credentials over env vars
183
+ const cliCredentials = (0, auth_1.loadCredentials)();
184
+ const resolvedLassoKey = cliCredentials?.apiKey ||
185
+ (0, project_1.resolveLassoApiKey)(fileEnv) ||
183
186
  process.env.LASSO_API_KEY ||
184
187
  fileEnv.LASSO_API_KEY ||
185
188
  process.env.VITE_LASSO_API_KEY ||
@@ -188,6 +191,19 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
188
191
  fileEnv.NEXT_LASSO_API_KEY ||
189
192
  "";
190
193
  const lassoKeyConfigured = Boolean(resolvedLassoKey);
194
+ // Fetch workspace configuration from server (dashboard-configured providers)
195
+ let workspaceConfig = null;
196
+ if (lassoKeyConfigured) {
197
+ workspaceConfig = await (0, project_1.resolveWorkspaceConfig)(fileEnv);
198
+ }
199
+ // Fetch available models from server
200
+ let dynamicModels = { anthropic: [], openai: [], google: [], nvidia: [] };
201
+ try {
202
+ dynamicModels = await (0, project_1.fetchModels)(fileEnv);
203
+ }
204
+ catch (error) {
205
+ console.warn('[Lasso] Failed to fetch dynamic models, using fallback');
206
+ }
191
207
  const localProviderKeys = {
192
208
  google: process.env.GOOGLE_GENERATIVE_AI_API_KEY || fileEnv.GOOGLE_GENERATIVE_AI_API_KEY || process.env.GEMINI_API_KEY || fileEnv.GEMINI_API_KEY,
193
209
  openai: process.env.OPENAI_API_KEY || fileEnv.OPENAI_API_KEY,
@@ -197,8 +213,9 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
197
213
  };
198
214
  let agentConfig = (() => {
199
215
  const provider = (process.env.LASSO_AGENT_PROVIDER || fileEnv.LASSO_AGENT_PROVIDER || process.env.AI_PROVIDER || fileEnv.AI_PROVIDER || "").toLowerCase();
200
- const selectedProvider = provider === "google" || provider === "gemini" ? "google" : provider === "openai" ? "openai" : provider === "ollama" ? "ollama" : provider === "anthropic" ? "anthropic" : provider === "nvidia" ? "nvidia" : localProviderKeys.ollama ? "ollama" : localProviderKeys.nvidia ? "nvidia" : localProviderKeys.google ? "google" : localProviderKeys.openai ? "openai" : localProviderKeys.anthropic ? "anthropic" : "google";
216
+ const selectedProvider = provider === "google" || provider === "gemini" ? "google" : provider === "openai" ? "openai" : provider === "ollama" ? "ollama" : provider === "anthropic" ? "anthropic" : provider === "nvidia" ? "nvidia" : localProviderKeys.ollama ? "ollama" : localProviderKeys.nvidia ? "nvidia" : localProviderKeys.google ? "google" : localProviderKeys.openai ? "openai" : localProviderKeys.anthropic ? "anthropic" : (cliCredentials ? "anthropic" : "google");
201
217
  const apiKey = selectedProvider === "ollama" ? "ollama" : localProviderKeys[selectedProvider];
218
+ // If user has CLI auth, use Lasso gateway by default
202
219
  if (!apiKey && !lassoKeyConfigured)
203
220
  return null;
204
221
  return {
@@ -301,7 +318,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
301
318
  return [];
302
319
  }
303
320
  }
304
- const availableModels = async (collabConfig = null) => {
321
+ const availableModels = async (collabConfig = null, workspaceConfig = null) => {
305
322
  localAgents = await (0, agent_1.detectLocalAgents)();
306
323
  const locals = await localModels();
307
324
  const discoveredOpenCodeModels = await openCodeModels();
@@ -310,37 +327,46 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
310
327
  const base = process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api";
311
328
  agentConfig = { provider: "ollama", apiKey: "ollama", model: process.env.OLLAMA_MODEL || fileEnv.OLLAMA_MODEL, baseUrl: `${base.replace(/\/api\/?$/, "")}/v1` };
312
329
  }
313
- const isPaidPlan = Boolean(collabConfig?.plan && collabConfig.plan.toLowerCase() !== "free");
314
- const configuredProviders = collabConfig?.configuredProviders || [];
330
+ // Check if user has CLI auth credentials (lasso auth login)
331
+ const cliCredentials = (0, auth_1.loadCredentials)();
332
+ const hasCliAuth = Boolean(cliCredentials?.apiKey);
333
+ // Prioritize workspace config (dashboard) over project config
334
+ // Workspace config is fetched independently of project config
335
+ const workspacePlan = (workspaceConfig?.plan || "free").toLowerCase();
336
+ const workspaceProviders = workspaceConfig?.configuredProviders || [];
337
+ const projectPlan = (collabConfig?.plan || "free").toLowerCase();
338
+ const projectProviders = collabConfig?.configuredProviders || [];
339
+ // Use the highest-tier plan available
340
+ const isPaidPlan = workspacePlan !== "free" || projectPlan !== "free";
341
+ // Use workspace providers if available, otherwise fall back to project providers
342
+ const configuredProviders = workspaceProviders.length > 0 ? workspaceProviders : projectProviders;
343
+ // Build model list from dynamic server fetch, falling back to hardcoded models
315
344
  const allModels = [
316
- // NVIDIA NIM models (confirmed working with server API key)
317
- { id: "meta/llama-3.2-11b-vision-instruct", label: "Llama 3.2 11B Vision", provider: "nvidia" },
318
- { id: "openai/gpt-oss-20b", label: "GPT-OSS 20B (NVIDIA)", provider: "nvidia" },
319
- { id: "deepseek-ai/deepseek-coder-6.7b-instruct", label: "DeepSeek Coder 6.7B", provider: "nvidia" },
320
- { id: "deepseek-ai/deepseek-v4.1-flash", label: "DeepSeek V4.1 Flash", provider: "nvidia" },
321
- { id: "ibm/granite-34b-code-instruct", label: "Granite 34B Code", provider: "nvidia" },
322
- { id: "ibm/granite-3.0-8b-instruct", label: "Granite 3.0 8B", provider: "nvidia" },
323
- { id: "mistralai/mistral-large-2-instruct", label: "Mistral Large 2", provider: "nvidia" },
324
- { id: "mistralai/codestral-22b-instruct-v0.1", label: "Codestral 22B", provider: "nvidia" },
325
- { id: "mistralai/mistral-large", label: "Mistral Large", provider: "nvidia" },
326
- { id: "nv-mistralai/mistral-nemo-12b-instruct", label: "Mistral Nemo 12B", provider: "nvidia" },
327
- { id: "nvidia/nemotron-3.5-lightning-30b-a3b", label: "Nemotron Lightning 30B", provider: "nvidia" },
328
- { id: "microsoft/phi-3.5-moe-instruct", label: "Phi-3.5 MoE", provider: "nvidia" },
329
- { id: "google/gemma-4-31b-it", label: "Gemma 4 31B", provider: "nvidia" },
330
- { id: "moonshotai/kimi-k2.6", label: "Kimi K2.6", provider: "nvidia" },
331
- { id: "poolside/laguna-xs-2.1", label: "Laguna XS 2.1", provider: "nvidia" },
332
- // Existing models
333
- { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
334
- { id: "claude-opus-4-1-20250805", label: "Claude Opus 4.1", provider: "anthropic" },
335
- { id: "gpt-4.1", label: "GPT-4.1", provider: "openai" },
336
- { id: "gpt-4.1-mini", label: "GPT-4.1 mini", provider: "openai" },
337
- { id: "gemini-3.8-flash", label: "Gemini 3.8 Flash", provider: "google" },
338
- { id: "gemini-3.7-flash", label: "Gemini 3.7 Flash", provider: "google" },
339
- { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro Preview", provider: "google" },
340
- { id: "gemini-3-flash-preview", label: "Gemini 3 Flash Preview", provider: "google" },
341
- { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "google" },
342
- { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google" },
343
- { id: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash-Lite", provider: "google" },
345
+ // Dynamic models from server API
346
+ ...dynamicModels.anthropic.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
347
+ ...dynamicModels.openai.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
348
+ ...dynamicModels.google.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
349
+ ...dynamicModels.nvidia.map(m => ({ id: m.id, label: m.name, provider: m.provider })),
350
+ // Fallback hardcoded models (only if dynamic fetch failed)
351
+ ...(dynamicModels.anthropic.length === 0 ? [
352
+ { id: "claude-sonnet-4-5-20250929", label: "Claude Sonnet 4.5", provider: "anthropic" },
353
+ { id: "claude-opus-4-1-20250805", label: "Claude Opus 4.1", provider: "anthropic" },
354
+ ] : []),
355
+ ...(dynamicModels.openai.length === 0 ? [
356
+ { id: "gpt-4.1", label: "GPT-4.1", provider: "openai" },
357
+ { id: "gpt-4.1-mini", label: "GPT-4.1 mini", provider: "openai" },
358
+ ] : []),
359
+ ...(dynamicModels.google.length === 0 ? [
360
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash", provider: "google" },
361
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro", provider: "google" },
362
+ ] : []),
363
+ ...(dynamicModels.nvidia.length === 0 ? [
364
+ { id: "meta/llama-3.3-70b-instruct", label: "Llama 3.3 70B Instruct", provider: "nvidia" },
365
+ { id: "meta/llama-3.2-11b-vision-instruct", label: "Llama 3.2 11B Vision", provider: "nvidia" },
366
+ { id: "deepseek-ai/deepseek-v4-flash-0731", label: "DeepSeek V4 Flash", provider: "nvidia" },
367
+ { id: "google/gemma-4-31b-it", label: "Gemma 4 31B", provider: "nvidia" },
368
+ ] : []),
369
+ // Local CLI agents (always included)
344
370
  ...(localAgents.has("claude-code") ? [
345
371
  { id: "claude-code:sonnet", label: "Claude Code · Sonnet", provider: "cli" },
346
372
  { id: "claude-code:opus", label: "Claude Code · Opus", provider: "cli" },
@@ -387,16 +413,33 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
387
413
  return model;
388
414
  }
389
415
  // Free plans: check if provider is configured
390
- const envKey = providerKeyMapping[model.provider];
391
- const isConfigured = configuredProviders.includes(model.provider) ||
392
- Boolean(process.env[envKey]) ||
393
- Boolean(fileEnv[envKey]) ||
394
- Boolean(localProviderKeys[model.provider]);
416
+ let isConfigured = false;
417
+ let lockedReason = "Configure your API key in the dashboard to use this model";
418
+ if (hasCliAuth) {
419
+ // User has CLI auth: check dashboard-configured providers first
420
+ isConfigured = configuredProviders.includes(model.provider);
421
+ lockedReason = "Configure this provider in your workspace dashboard to use this model";
422
+ }
423
+ // If not configured via dashboard, fall back to env-based detection
424
+ if (!isConfigured) {
425
+ const envKey = providerKeyMapping[model.provider];
426
+ isConfigured = Boolean(process.env[envKey]) ||
427
+ Boolean(fileEnv[envKey]) ||
428
+ Boolean(localProviderKeys[model.provider]);
429
+ if (!isConfigured) {
430
+ lockedReason = hasCliAuth
431
+ ? "Configure this provider in your workspace dashboard, or set the provider API key in your .env file"
432
+ : "Run `lasso auth login` to configure providers in your dashboard, or set the provider API key in your .env file";
433
+ }
434
+ else {
435
+ lockedReason = "";
436
+ }
437
+ }
395
438
  if (!isConfigured) {
396
439
  return {
397
440
  ...model,
398
441
  locked: true,
399
- lockedReason: "Configure your API key in the dashboard to use this model",
442
+ lockedReason: lockedReason || "Configure your API key to use this model",
400
443
  };
401
444
  }
402
445
  return model;
@@ -410,7 +453,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
410
453
  ? { name: creds.userName, email: creds.userEmail }
411
454
  : (process.env.USER ? { name: process.env.USER } : null);
412
455
  socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: lassoKeyConfigured || Boolean(agentConfig) || localAgents.size > 0, models: [], collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null, user: userInfo }));
413
- void availableModels(collabConfig).then((models) => {
456
+ void availableModels(collabConfig, workspaceConfig).then((models) => {
414
457
  if (socket.readyState === socket.OPEN)
415
458
  socket.send(JSON.stringify({ type: "config", apiKeyConfigured: lassoKeyConfigured, agentConfigured: lassoKeyConfigured || Boolean(agentConfig) || localAgents.size > 0, models, collab: collabConfig?.registered ? { ...collabConfig, plan: collabConfig.plan, configuredProviders: collabConfig.configuredProviders } : null, user: userInfo }));
416
459
  });
@@ -763,7 +806,14 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
763
806
  return;
764
807
  }
765
808
  const provider = (msg.provider || agentConfig?.provider || "google");
766
- const model = msg.model || agentConfig?.model || (provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : provider === "google" ? "gemini-2.5-flash" : "claude-3-7-sonnet");
809
+ const model = msg.model || agentConfig?.model;
810
+ if (!model) {
811
+ socket.send(JSON.stringify({
812
+ type: "error",
813
+ message: "No model selected. Please select a model in settings."
814
+ }));
815
+ return;
816
+ }
767
817
  const localKey = localProviderKeys[provider];
768
818
  const serverUrl = collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv);
769
819
  const targetApiUrl = (serverUrl.includes("collab.lasso.byorello.space") ? "https://api.lasso.byorello.space" : serverUrl).replace(/\/api\/v1\/?$/, "");
@@ -75,7 +75,7 @@ void (async () => {
75
75
  const nextBin = node_path_1.default.join(cwd, "node_modules", ".bin", "next");
76
76
  if (!node_fs_1.default.existsSync(nextBin))
77
77
  throw new Error("Next.js is not installed in this project.");
78
- const bridge = (0, bridge_1.startBridge)(cwd, null, bridgePort);
78
+ const bridge = await (0, bridge_1.startBridge)(cwd, null, bridgePort);
79
79
  const nextProcess = (0, node_child_process_1.spawn)(nextBin, ["dev", "--port", String(nextPort), "--hostname", "127.0.0.1"], {
80
80
  cwd,
81
81
  env: { ...process.env, PORT: String(nextPort) },
@@ -27,7 +27,7 @@ function lassoOverlayPlugin() {
27
27
  };
28
28
  }
29
29
  void (async () => {
30
- const bridge = (0, bridge_1.startBridge)(cwd, null, bridgePort);
30
+ const bridge = await (0, bridge_1.startBridge)(cwd, null, bridgePort);
31
31
  const require = (0, node_module_1.createRequire)(node_path_1.default.join(cwd, "package.json"));
32
32
  const viteEntry = require.resolve("vite");
33
33
  const { createServer } = await import((0, node_url_1.pathToFileURL)(viteEntry).href);
package/dist/cli/index.js CHANGED
@@ -94,6 +94,23 @@ program.command("create [name]")
94
94
  }
95
95
  });
96
96
  // prettier-ignore
97
+ program.command("whoami")
98
+ .description("Show the currently authenticated Lasso user")
99
+ .action(() => {
100
+ const credentials = (0, auth_1.loadCredentials)();
101
+ if (!credentials) {
102
+ console.log(chalk_1.default.dim("Not authenticated. Run ") + chalk_1.default.cyan("lasso auth login") + chalk_1.default.dim(" to sign in."));
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ const summary = (0, auth_1.credentialSummary)(credentials);
107
+ console.log(chalk_1.default.green("✓") + ` Authenticated as ${chalk_1.default.bold(summary.userEmail || "your account")}` + (summary.userName ? ` (${summary.userName})` : ""));
108
+ if (summary.workspaceName)
109
+ console.log(chalk_1.default.dim(`Workspace: ${summary.workspaceName} (${summary.workspaceId})`));
110
+ if (summary.masked)
111
+ console.log(chalk_1.default.dim("Credential: ") + summary.masked);
112
+ });
113
+ // prettier-ignore
97
114
  const daemon = program.command("daemon [action]")
98
115
  .description("Start/stop Lasso Host — the local .lasso domain server (start|status|stop|restart|install|uninstall)")
99
116
  .usage("[action]")
@@ -439,7 +456,7 @@ program.command("dev", { isDefault: true }).description("Start your dev server w
439
456
  else {
440
457
  console.log(chalk_1.default.yellow("!") + ` No ${chalk_1.default.bold(project_1.LASSO_CONFIG_FILE)} found. Run ${chalk_1.default.cyan("npx @lasso-ai/cli init")} to enable realtime collaboration.`);
441
458
  }
442
- (0, bridge_1.startBridge)(cwd, collabConfig);
459
+ await (0, bridge_1.startBridge)(cwd, collabConfig);
443
460
  switch (framework) {
444
461
  case "vite":
445
462
  console.log(chalk_1.default.green("✓") + " Detected: Vite");
@@ -31,5 +31,4 @@ export interface ConversationMessage {
31
31
  role: "user" | "assistant";
32
32
  content: string;
33
33
  }
34
- export declare function isConversationalPrompt(prompt: string): boolean;
35
34
  export declare function runOneShotAgent(cwd: string, prompt: string, scope: "project" | "component", config: AgentConfig, signal: AbortSignal, onProgress: ProgressCallback, onPrompt: PromptCallback, messages?: ConversationMessage[], serverUrl?: string, apiKey?: string, taskId?: string, pageContext?: any): Promise<OneShotResult>;
@@ -3,13 +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.isConversationalPrompt = isConversationalPrompt;
7
6
  exports.runOneShotAgent = runOneShotAgent;
8
7
  const node_fs_1 = __importDefault(require("node:fs"));
9
8
  const node_path_1 = __importDefault(require("node:path"));
10
9
  const node_child_process_1 = require("node:child_process");
11
10
  const node_util_1 = require("node:util");
12
11
  const agent_1 = require("./agent");
12
+ const auth_1 = require("./auth");
13
13
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
14
14
  const CLI_PROVIDERS = new Set(["claude-code", "codex", "opencode", "cursor"]);
15
15
  function isCliProvider(provider) {
@@ -40,28 +40,46 @@ function gatewayTargetForCli(provider, model) {
40
40
  return { provider: "google", model: GATEWAY_MODELS.google };
41
41
  return { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
42
42
  }
43
- function isConversationalPrompt(prompt) {
44
- const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
45
- const greetings = [
46
- "hi", "hello", "hey", "heya", "howdy", "sup", "yo",
47
- "good morning", "good afternoon", "good evening",
48
- "thanks", "thank you", "thx", "ty", "cool", "awesome", "great", "nice",
49
- "who are you", "what are you", "what can you do", "help", "help me",
50
- "what is lasso", "how do you work", "how does this work",
51
- ];
52
- if (greetings.includes(p))
53
- return true;
54
- if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
55
- const buildKeywords = /(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)/i;
56
- if (!buildKeywords.test(p))
57
- return true;
43
+ async function isConversationalPrompt(prompt, config, apiKey) {
44
+ // Use the user's selected model to intelligently detect if prompt is conversational vs. action-oriented
45
+ try {
46
+ // Use the gateway to classify - this respects the user's selected model
47
+ const response = await fetch(`${config.serverUrl || "https://api.lasso.byorello.space"}/api/v1/agent/generate`, {
48
+ method: "POST",
49
+ headers: {
50
+ "Content-Type": "application/json",
51
+ "Authorization": `Bearer ${apiKey || ""}`,
52
+ },
53
+ body: JSON.stringify({
54
+ model: {
55
+ id: config.model,
56
+ provider: config.provider,
57
+ },
58
+ prompt: `Classify this user prompt as either "conversational" or "action". Return ONLY the word "conversational" or "action" - nothing else.
59
+
60
+ Prompt: "${prompt}"`,
61
+ maxTokens: 10,
62
+ }),
63
+ });
64
+ if (response.ok) {
65
+ const data = await response.json();
66
+ const classification = data.reply?.toLowerCase().trim();
67
+ console.log('[Lasso] Classification result:', classification);
68
+ return classification === "conversational";
69
+ }
70
+ else {
71
+ console.log('[Lasso] Classification failed:', response.status);
72
+ }
58
73
  }
59
- const questionOnly = /^(what is|what's|how do i|why does|can you explain|tell me about)\b/i.test(p);
60
- const actionVerbs = /(build|create|add|make|fix|update|modify|change|refactor|implement|write code|code this|delete|remove|generate)/i;
61
- if (questionOnly && !actionVerbs.test(p)) {
62
- return true;
74
+ catch (err) {
75
+ console.log('[Lasso] Classification error:', err);
63
76
  }
64
- return false;
77
+ // Fallback: smarter heuristic
78
+ const p = prompt.trim().toLowerCase();
79
+ const actionKeywords = /(add|create|build|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code|replace|insert|append|generate)/i;
80
+ const isAction = actionKeywords.test(p);
81
+ console.log('[Lasso] Fallback classification:', isAction ? 'action' : 'conversational');
82
+ return !isAction;
65
83
  }
66
84
  function runtimeErrorObservation(pageContext) {
67
85
  const errors = Array.isArray(pageContext?.runtimeErrors) ? pageContext.runtimeErrors : [];
@@ -113,7 +131,10 @@ Do not output code changes, file patches, or JSON schemas — just talk to the d
113
131
  return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix. Run `lasso auth login` (or add a provider API key in settings) to unlock AI replies.";
114
132
  }
115
133
  try {
116
- 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" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
134
+ const model = config.model;
135
+ if (!model) {
136
+ return "No model selected. Please select a model in settings.";
137
+ }
117
138
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
118
139
  const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
119
140
  const res = await fetch(`${baseUrl}/chat/completions`, {
@@ -184,9 +205,10 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
184
205
  if (signal.aborted) {
185
206
  return { ok: false, error: "Operation was cancelled" };
186
207
  }
187
- const isConversational = isConversationalPrompt(prompt);
188
- // Browser inspection & snapshot steps
189
- if (pageContext) {
208
+ const isConversational = await isConversationalPrompt(prompt, config, apiKey);
209
+ console.log('[Lasso] Is conversational:', isConversational);
210
+ // Browser inspection & snapshot steps - skip for simple conversational queries
211
+ if (pageContext && !isConversational) {
190
212
  onProgress("thinking", "Inspecting browser page...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
191
213
  if (pageContext.viewport) {
192
214
  onProgress("thinking", "Capturing visual snapshot...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height} (Document height: ${pageContext.viewport.scrollHeight}px)`);
@@ -201,8 +223,9 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
201
223
  else {
202
224
  onProgress("thinking", isConversational ? "Thinking..." : "Analyzing project structure...", isConversational ? undefined : "Scanning files and dependencies");
203
225
  }
204
- const projectStructure = await analyzeProject(cwd);
205
- const framework = detectFramework(cwd);
226
+ // Skip project structure analysis for simple conversational queries
227
+ const projectStructure = isConversational ? null : await analyzeProject(cwd);
228
+ const framework = isConversational ? null : detectFramework(cwd);
206
229
  if (!isConversational && !pageContext) {
207
230
  onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
208
231
  }
@@ -216,15 +239,18 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
216
239
  }
217
240
  const targetServerUrl = rawServerUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
218
241
  const useCliAgent = isCliProvider(config.provider);
219
- const gatewayKey = apiKey || config.lassoKey || (config.apiKey && (config.apiKey.startsWith("lss_live_") || config.apiKey.startsWith("lss_")) ? config.apiKey : "") || process.env.LASSO_API_KEY || "";
242
+ // Prioritize CLI auth credentials over env vars
243
+ const cliCredentials = (0, auth_1.loadCredentials)();
244
+ const gatewayKey = cliCredentials?.apiKey || apiKey || config.lassoKey || (config.apiKey && (config.apiKey.startsWith("lss_live_") || config.apiKey.startsWith("lss_")) ? config.apiKey : "") || process.env.LASSO_API_KEY || "";
220
245
  const hasLocalProviderKey = Boolean(config.apiKey &&
221
246
  config.apiKey !== "ollama" &&
222
247
  !config.apiKey.startsWith("lss_live_") &&
223
248
  !config.apiKey.startsWith("lss_"));
249
+ // If the user has CLI auth, always route to server (unless using local agents)
224
250
  // If the user does NOT have a local provider key in .env, route to Lasso Agent Gateway on server.
225
251
  // When they DO have a local key in .env, it runs locally to save server resources!
226
252
  // Ollama always runs locally (the server cannot reach the user's localhost).
227
- const shouldRouteToServer = !useCliAgent && config.provider !== "ollama" && !hasLocalProviderKey && Boolean(targetServerUrl);
253
+ const shouldRouteToServer = !useCliAgent && config.provider !== "ollama" && (Boolean(cliCredentials) || !hasLocalProviderKey) && Boolean(targetServerUrl);
228
254
  const callGateway = async (target) => {
229
255
  try {
230
256
  if (!isConversational && !pageContext) {
@@ -241,10 +267,15 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
241
267
  const controller = new AbortController();
242
268
  const onAbort = () => controller.abort();
243
269
  signal.addEventListener("abort", onAbort, { once: true });
270
+ // Add a 5-minute timeout for gateway requests
271
+ const timeout = setTimeout(() => {
272
+ controller.abort();
273
+ }, 5 * 60 * 1000);
274
+ timeout.unref();
244
275
  // Build sourceHints: read relevant file contents to give the AI real code context.
245
276
  // This is what enables code changes instead of "please share the file" responses.
246
277
  const sourceHints = {};
247
- if (!isConversational) {
278
+ if (!isConversational && projectStructure) {
248
279
  const promptLower = prompt.toLowerCase();
249
280
  const allCssFiles = projectStructure.files.filter((f) => /\.(css|scss)$/i.test(f));
250
281
  const entryFiles = projectStructure.files.filter((f) => /(App|page|index|layout|main|hero|header|landing)\.(tsx|jsx|js|ts|html)$/i.test(f));
@@ -274,30 +305,43 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
274
305
  provider: target.provider,
275
306
  },
276
307
  context: {
277
- framework,
278
- hasTypeScript: projectStructure.hasTypeScript,
279
- hasTailwind: projectStructure.hasTailwind,
280
- hasReact: projectStructure.hasReact,
281
- hasNext: projectStructure.hasNext,
282
- hasVite: projectStructure.hasVite,
283
- dependencies: projectStructure.dependencies,
284
- devDependencies: projectStructure.devDependencies,
285
- files: projectStructure.files.slice(0, 100),
308
+ framework: framework || undefined,
309
+ hasTypeScript: projectStructure?.hasTypeScript,
310
+ hasTailwind: projectStructure?.hasTailwind,
311
+ hasReact: projectStructure?.hasReact,
312
+ hasNext: projectStructure?.hasNext,
313
+ hasVite: projectStructure?.hasVite,
314
+ dependencies: projectStructure?.dependencies,
315
+ devDependencies: projectStructure?.devDependencies,
316
+ files: projectStructure?.files.slice(0, 100) || [],
286
317
  sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined,
287
318
  currentRoute: pageContext?.route || "/",
288
319
  pageContext,
289
320
  },
290
321
  }),
291
322
  });
323
+ clearTimeout(timeout);
292
324
  signal.removeEventListener("abort", onAbort);
325
+ if (signal.aborted) {
326
+ return { ok: false, error: "Request timed out after 5 minutes. Your plan quota may be exhausted. Please upgrade your plan or add credits to continue using this service." };
327
+ }
293
328
  if (!response.ok) {
294
329
  const errData = (await response.json().catch(() => ({})));
295
330
  const errorMessage = errData.error || `Agent request failed (${response.status})`;
331
+ // Check for quota/plan exhaustion
332
+ 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)) {
333
+ return { ok: false, error: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service." };
334
+ }
296
335
  return { ok: false, error: errorMessage };
297
336
  }
298
337
  const data = (await response.json());
299
338
  if (data.status === "failed" || data.error) {
300
- return { ok: false, error: data.error || data.summary || "Agent session failed on server." };
339
+ const error = data.error || data.summary || "Agent session failed on server.";
340
+ // Check for quota/plan exhaustion
341
+ 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(error)) {
342
+ return { ok: false, error: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service." };
343
+ }
344
+ return { ok: false, error };
301
345
  }
302
346
  if (data) {
303
347
  const changes = Array.isArray(data.changes) ? data.changes : [];
@@ -422,7 +466,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
422
466
  }
423
467
  if (useCliAgent && isConversational) {
424
468
  try {
425
- const reply = await generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext, (message, detail) => onProgress("thinking", message, detail), taskId, (agentPrompt) => onPrompt({ question: agentPrompt.message, options: agentPrompt.options }));
469
+ const reply = await generateConversationalReply(cwd, prompt, framework || "", config, signal, messages, pageContext, (message, detail) => onProgress("thinking", message, detail), taskId, (agentPrompt) => onPrompt({ question: agentPrompt.message, options: agentPrompt.options }));
426
470
  return {
427
471
  ok: true,
428
472
  summary: reply,
@@ -443,7 +487,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
443
487
  }
444
488
  // If it's a conversational prompt and server was not used or failed, respond directly without modifying files!
445
489
  if (isConversational) {
446
- const reply = await generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext);
490
+ const reply = await generateConversationalReply(cwd, prompt, framework || "", config, signal, messages, pageContext);
447
491
  return {
448
492
  ok: true,
449
493
  summary: reply,
@@ -454,7 +498,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
454
498
  }
455
499
  // Phase 2: Local agent generation fallback for code changes
456
500
  onProgress("thinking", "Generating implementation plan...", `Planning components for ${prompt}`);
457
- const plan = await generatePlan(cwd, prompt, scope, projectStructure, framework, config, signal, messages, pageContext);
501
+ const plan = await generatePlan(cwd, prompt, scope, projectStructure || { framework: "", hasTypeScript: false, hasTailwind: false, hasReact: false, hasNext: false, hasVite: false, dependencies: [], devDependencies: [], files: [] }, framework || "", config, signal, messages, pageContext);
458
502
  if (signal.aborted) {
459
503
  return { ok: false, error: "Operation was cancelled during planning" };
460
504
  }
@@ -589,8 +633,8 @@ async function analyzeProject(cwd) {
589
633
  hasReact,
590
634
  hasNext,
591
635
  hasVite,
592
- dependencies,
593
- devDependencies,
636
+ dependencies: dependencies,
637
+ devDependencies: devDependencies,
594
638
  files,
595
639
  };
596
640
  }
@@ -711,7 +755,14 @@ Analyze the existing code structure, then respond in JSON:`;
711
755
  };
712
756
  }
713
757
  try {
714
- 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" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
758
+ const model = config.model;
759
+ if (!model) {
760
+ return {
761
+ steps: [],
762
+ isConversational: true,
763
+ reply: "No model selected. Please select a model in settings.",
764
+ };
765
+ }
715
766
  let text = "";
716
767
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
717
768
  const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
@@ -15,6 +15,24 @@ export interface CollabConfig {
15
15
  plan?: string;
16
16
  configuredProviders?: string[];
17
17
  }
18
+ export interface WorkspaceConfig {
19
+ workspaceId?: string;
20
+ workspaceName?: string;
21
+ plan?: string;
22
+ configuredProviders?: string[];
23
+ }
24
+ export interface ModelInfo {
25
+ id: string;
26
+ name: string;
27
+ provider: string;
28
+ capabilities?: string[];
29
+ }
30
+ export interface ModelsResponse {
31
+ anthropic: ModelInfo[];
32
+ openai: ModelInfo[];
33
+ google: ModelInfo[];
34
+ nvidia: ModelInfo[];
35
+ }
18
36
  export interface InitResult {
19
37
  ok: boolean;
20
38
  error?: string;
@@ -57,3 +75,13 @@ export declare function initProject(cwd: string, fileEnv: Record<string, string>
57
75
  * safely degraded (no collaboration, local editing unaffected) on any failure.
58
76
  */
59
77
  export declare function resolveCollabSession(cwd: string, fileEnv: Record<string, string>, projectId: string): Promise<CollabConfig>;
78
+ /**
79
+ * Fetch workspace configuration from the server using CLI auth credentials.
80
+ * This allows users to see their dashboard-configured providers even without a project config.
81
+ */
82
+ export declare function resolveWorkspaceConfig(fileEnv: Record<string, string>): Promise<WorkspaceConfig>;
83
+ /**
84
+ * Fetch available models from the server's models endpoint.
85
+ * This fetches from provider APIs dynamically and caches results.
86
+ */
87
+ export declare function fetchModels(fileEnv: Record<string, string>): Promise<ModelsResponse>;
@@ -10,6 +10,8 @@ exports.resolveLassoApiKey = resolveLassoApiKey;
10
10
  exports.readProjectConfig = readProjectConfig;
11
11
  exports.initProject = initProject;
12
12
  exports.resolveCollabSession = resolveCollabSession;
13
+ exports.resolveWorkspaceConfig = resolveWorkspaceConfig;
14
+ exports.fetchModels = fetchModels;
13
15
  const node_fs_1 = __importDefault(require("node:fs"));
14
16
  const node_path_1 = __importDefault(require("node:path"));
15
17
  const node_readline_1 = __importDefault(require("node:readline"));
@@ -351,3 +353,58 @@ async function resolveCollabSession(cwd, fileEnv, projectId) {
351
353
  return config;
352
354
  }
353
355
  }
356
+ /**
357
+ * Fetch workspace configuration from the server using CLI auth credentials.
358
+ * This allows users to see their dashboard-configured providers even without a project config.
359
+ */
360
+ async function resolveWorkspaceConfig(fileEnv) {
361
+ const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv).replace(/\/$/, "");
362
+ const apiUrl = `${serverUrl}/api/v1`;
363
+ const apiKey = resolveLassoApiKey(fileEnv);
364
+ if (!apiKey) {
365
+ return {};
366
+ }
367
+ try {
368
+ const response = await (0, auth_1.fetchWithTimeout)(`${apiUrl}/auth/workspace`, {
369
+ method: "GET",
370
+ headers: { authorization: `Bearer ${apiKey}` },
371
+ });
372
+ if (!response.ok) {
373
+ return {};
374
+ }
375
+ const body = (await response.json());
376
+ if (!body.success || !body.workspace) {
377
+ return {};
378
+ }
379
+ return {
380
+ workspaceId: body.workspace.id,
381
+ workspaceName: body.workspace.name,
382
+ plan: body.workspace.plan || "free",
383
+ configuredProviders: body.workspace.configuredProviders || [],
384
+ };
385
+ }
386
+ catch {
387
+ return {};
388
+ }
389
+ }
390
+ /**
391
+ * Fetch available models from the server's models endpoint.
392
+ * This fetches from provider APIs dynamically and caches results.
393
+ */
394
+ async function fetchModels(fileEnv) {
395
+ const serverUrl = (0, auth_1.serverUrlFrom)(fileEnv).replace(/\/$/, "");
396
+ const apiUrl = `${serverUrl}/api/v1`;
397
+ try {
398
+ const response = await (0, auth_1.fetchWithTimeout)(`${apiUrl}/models`, {
399
+ method: "GET",
400
+ });
401
+ if (!response.ok) {
402
+ return { anthropic: [], openai: [], google: [], nvidia: [] };
403
+ }
404
+ const body = await response.json();
405
+ return body;
406
+ }
407
+ catch {
408
+ return { anthropic: [], openai: [], google: [], nvidia: [] };
409
+ }
410
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lasso-ai/cli",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "Select any part of your running app, describe a change, and let AI edit the real source code.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli/index.js",