@lasso-ai/cli 1.2.5 → 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/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Fixed
11
+
12
+ - **One-shot generation no longer demands a provider API key when a local agent is
13
+ selected.** Picking Claude Code, Codex, OpenCode, or Cursor for a one-shot request
14
+ used to bail out with *"Please configure an AI provider API key or model in settings
15
+ to enable code generation."* Those agents authenticate themselves, so the request now
16
+ runs through the local CLI first. If the local CLI cannot run (not installed, not
17
+ signed in, out of credits) and a Lasso key is available, the request falls back to the
18
+ Lasso Agent Gateway with a hosted provider/model it understands (`claude-code`/`cursor`
19
+ → Anthropic, `codex` → OpenAI, `opencode` by model prefix). Without a key, the error
20
+ now names the CLI command and `lasso auth login` instead of pointing at settings.
21
+ - **Local agents no longer hang on stdin.** `codex exec` and `opencode run` read stdin
22
+ until EOF, so every local-agent request stalled until the 5-minute timeout. Stdin is
23
+ closed as soon as the process spawns (none of these command lines are interactive).
24
+ - **Ollama requests stay local.** One-shot requests for Ollama are no longer routed to
25
+ the gateway, which cannot reach the user's `localhost`.
26
+
10
27
  ### Changed
11
28
 
12
29
  - **`lasso init` registers against the Lasso API, not the realtime server.**
@@ -3,7 +3,7 @@ export type SourceChange = {
3
3
  oldString: string;
4
4
  newString: string;
5
5
  };
6
- type AgentInput = {
6
+ export type AgentInput = {
7
7
  taskId?: string;
8
8
  instruction: string;
9
9
  model: string;
@@ -97,6 +97,7 @@ export type AgentStepAction = {
97
97
  changes: SourceChange[];
98
98
  };
99
99
  export declare function parseAgentAction(text: string): AgentStepAction;
100
+ export declare function runLocalAgentPrompt(cwd: string, config: AgentConfig, prompt: string, signal?: AbortSignal, onProgress?: AgentProgress, taskId?: string, onPrompt?: AgentPromptHandler): Promise<string>;
100
101
  export declare function proposeChanges(cwd: string, input: AgentInput, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress, onPrompt?: AgentPromptHandler): Promise<{
101
102
  summary: string;
102
103
  packages?: string[];
@@ -104,4 +105,3 @@ export declare function proposeChanges(cwd: string, input: AgentInput, config: A
104
105
  }>;
105
106
  export declare function answerQuestion(cwd: string, input: AgentAnswer, config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress, onPrompt?: AgentPromptHandler): Promise<string>;
106
107
  export declare function generateCommitMessage(cwd: string, status: string[], config: AgentConfig, signal?: AbortSignal, onProgress?: AgentProgress): Promise<string>;
107
- export {};
package/dist/cli/agent.js CHANGED
@@ -10,6 +10,7 @@ exports.executeWebSearch = executeWebSearch;
10
10
  exports.executeAgentCommand = executeAgentCommand;
11
11
  exports.detectLocalAgents = detectLocalAgents;
12
12
  exports.parseAgentAction = parseAgentAction;
13
+ exports.runLocalAgentPrompt = runLocalAgentPrompt;
13
14
  exports.proposeChanges = proposeChanges;
14
15
  exports.answerQuestion = answerQuestion;
15
16
  exports.generateCommitMessage = generateCommitMessage;
@@ -17,6 +18,7 @@ const promises_1 = __importDefault(require("node:fs/promises"));
17
18
  const node_path_1 = __importDefault(require("node:path"));
18
19
  const node_child_process_1 = require("node:child_process");
19
20
  const node_util_1 = require("node:util");
21
+ const auth_1 = require("./auth");
20
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
21
23
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
22
24
  async function installPackages(cwd, packages, onProgress) {
@@ -306,8 +308,18 @@ function jsonObjectCandidates(text) {
306
308
  return candidates;
307
309
  }
308
310
  function jsonFrom(text) {
309
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
310
- 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)];
311
323
  for (const candidate of candidates) {
312
324
  try {
313
325
  const parsed = JSON.parse(candidate.trim());
@@ -583,6 +595,18 @@ const AGENT_FAILURE_HINTS = [
583
595
  pattern: /failed to fetch models\.dev/i,
584
596
  message: "The local agent could not reach models.dev to refresh its model list. Check the network or proxy it uses, then retry.",
585
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
+ },
586
610
  ];
587
611
  function agentFailureHint(line) {
588
612
  return AGENT_FAILURE_HINTS.find((hint) => hint.pattern.test(line))?.message;
@@ -731,6 +755,10 @@ function localAgentError(command, stderr, exitCode) {
731
755
  if (command === "codex" && output.includes("mcp.canva.com") && output.includes("invalid_token")) {
732
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.";
733
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
+ }
734
762
  return `${command} exited with code ${exitCode}${output ? `: ${output.slice(0, 500)}` : ""}`;
735
763
  }
736
764
  function localModel(provider, model) {
@@ -755,6 +783,12 @@ function localCommand(provider, model, prompt) {
755
783
  function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
756
784
  return new Promise((resolve, reject) => {
757
785
  const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
786
+ // Headless invocations: codex/opencode/cursor read stdin until EOF and otherwise hang
787
+ // until our timeout, so close stdin right away. Prompt answers can no longer be piped
788
+ // back in, which is fine — none of these command lines are interactive. Swallow the
789
+ // EPIPE that a late write would raise so it cannot crash the CLI.
790
+ child.stdin?.on("error", () => { });
791
+ child.stdin?.end();
758
792
  const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : provider === "cursor" ? "Cursor" : "Claude Code";
759
793
  let stdout = "";
760
794
  let stderr = "";
@@ -842,7 +876,14 @@ function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskI
842
876
  activeAgentInputs.delete(taskId);
843
877
  signal?.removeEventListener("abort", abort);
844
878
  if (timedOut) {
845
- 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
+ }
846
887
  return;
847
888
  }
848
889
  const stdoutTail = pendingStdout.trim();
@@ -882,6 +923,16 @@ async function proposeWithLocalAgent(cwd, instruction, context, config, signal,
882
923
  throw new Error(localAgentError(command, result.stderr, result.exitCode));
883
924
  return extractLocalAgentProposal(result.stdout, config.provider);
884
925
  }
926
+ // Runs a local coding-agent CLI (claude/codex/opencode/cursor) and returns its final text.
927
+ // The CLI authenticates itself (its own login), so no provider API key is required.
928
+ async function runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, taskId, onPrompt) {
929
+ const provider = config.provider;
930
+ const local = localCommand(provider, config.model, prompt);
931
+ const result = await runLocalCommand(cwd, local.command, local.args, provider, signal, onProgress, taskId, onPrompt);
932
+ if (result.exitCode !== 0)
933
+ throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
934
+ return extractLocalAgentText(result.stdout, provider);
935
+ }
885
936
  async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
886
937
  const context = await contextFor(cwd, input.element);
887
938
  const runtimeErrors = input.context?.runtimeErrors || [];
@@ -931,6 +982,9 @@ CRITICAL INSTRUCTIONS:
931
982
  }
932
983
  Do NOT return prose or markdown outside the JSON.`;
933
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;
934
988
  let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
935
989
  if (serverUrl.includes("collab.lasso.byorello.space")) {
936
990
  serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
@@ -947,7 +1001,7 @@ Do NOT return prose or markdown outside the JSON.`;
947
1001
  const response = await fetch(endpoint, {
948
1002
  method: "POST",
949
1003
  signal,
950
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1004
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
951
1005
  body: JSON.stringify({
952
1006
  sessionId: input.taskId,
953
1007
  prompt: instruction,
@@ -957,7 +1011,12 @@ Do NOT return prose or markdown outside the JSON.`;
957
1011
  });
958
1012
  if (!response.ok) {
959
1013
  const errData = await response.json().catch(() => ({}));
960
- 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);
961
1020
  }
962
1021
  const data = await response.json();
963
1022
  // If the server-side agent reported failure, surface it as a thrown error so the
@@ -990,7 +1049,12 @@ Do NOT return prose or markdown outside the JSON.`;
990
1049
  });
991
1050
  if (!response.ok) {
992
1051
  const errData = await response.json().catch(() => ({}));
993
- 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);
994
1058
  }
995
1059
  const data = await response.json();
996
1060
  if (!data.text?.trim())
@@ -1001,7 +1065,9 @@ Do NOT return prose or markdown outside the JSON.`;
1001
1065
  proposal = await proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
1002
1066
  }
1003
1067
  else {
1004
- 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_");
1005
1071
  const system = `You are Lasso, an autonomous agentic AI coding assistant.
1006
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.
1007
1073
 
@@ -1034,8 +1100,17 @@ RULES:
1034
1100
  - If you already have sufficient context to fulfill the user's request immediately, return "propose_changes" (or standard {"summary": "...", "packages": [...], "changes": [...]}) directly!
1035
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.
1036
1102
  - 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");
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
+ }
1039
1114
  const image = input.context?.screenshots?.element || input.context?.screenshots?.full;
1040
1115
  const imageData = image?.replace(/^data:image\/[^;]+;base64,/, "");
1041
1116
  const imageMime = image?.match(/^data:(image\/[^;]+);base64,/)?.[1] || "image/jpeg";
@@ -1051,7 +1126,8 @@ RULES:
1051
1126
  // If using a Lasso platform key, route this turn through the /generate endpoint
1052
1127
  // (full agentic loop via Lasso gateway instead of one-shot session endpoint)
1053
1128
  if (isLassoKey && config.provider !== "ollama") {
1054
- rawText = await callGatewayGenerate(config.apiKey, system, currentPrompt);
1129
+ const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1130
+ rawText = await callGatewayGenerate(apiKeyToUse, system, currentPrompt);
1055
1131
  }
1056
1132
  else {
1057
1133
  // Provider-direct inference path
@@ -1089,6 +1165,10 @@ RULES:
1089
1165
  });
1090
1166
  }
1091
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
+ }
1092
1172
  if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1093
1173
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1094
1174
  proposal = await callServerGateway(config.lassoKey);
@@ -1108,6 +1188,10 @@ RULES:
1108
1188
  }
1109
1189
  }
1110
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
+ }
1111
1195
  if (!isLassoKey && config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
1112
1196
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1113
1197
  proposal = await callServerGateway(config.lassoKey);
@@ -1176,13 +1260,12 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1176
1260
  const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
1177
1261
  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."}`;
1178
1262
  if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
1179
- const local = localCommand(config.provider, config.model, prompt);
1180
- const result = await runLocalCommand(cwd, local.command, local.args, config.provider, signal, onProgress, input.taskId, onPrompt);
1181
- if (result.exitCode !== 0)
1182
- throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
1183
- return extractLocalAgentText(result.stdout, config.provider).trim();
1263
+ return (await runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, input.taskId, onPrompt)).trim();
1184
1264
  }
1185
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;
1186
1269
  let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
1187
1270
  if (serverUrl.includes("collab.lasso.byorello.space")) {
1188
1271
  serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
@@ -1191,7 +1274,7 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1191
1274
  const response = await fetch(endpoint, {
1192
1275
  method: "POST",
1193
1276
  signal,
1194
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1277
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
1195
1278
  body: JSON.stringify({
1196
1279
  provider: config.provider,
1197
1280
  model: config.model,
@@ -1201,18 +1284,29 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1201
1284
  });
1202
1285
  if (!response.ok) {
1203
1286
  const errData = await response.json().catch(() => ({}));
1204
- 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);
1205
1293
  }
1206
1294
  const data = await response.json();
1207
1295
  if (!data.text?.trim())
1208
1296
  throw new Error("The agent returned an empty answer.");
1209
1297
  return data.text.trim();
1210
1298
  };
1211
- 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_");
1212
1302
  if (isLassoKey && config.provider !== "ollama") {
1213
- 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.";
1214
1309
  }
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
1310
  let response;
1217
1311
  try {
1218
1312
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
@@ -1235,6 +1329,10 @@ async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt)
1235
1329
  });
1236
1330
  }
1237
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
+ }
1238
1336
  if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1239
1337
  onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1240
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
  });
@@ -556,7 +599,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
556
599
  taskControllers.set(msg.taskId, controller);
557
600
  let selectedConfig;
558
601
  if (localProvider) {
559
- selectedConfig = { provider: cliProvider, model: msg.model };
602
+ selectedConfig = { provider: cliProvider, model: msg.model, lassoKey: resolvedLassoKey, serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv) };
560
603
  }
561
604
  else {
562
605
  const requestedProvider = (msg.provider || agentConfig.provider);
@@ -608,7 +651,7 @@ function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort = expo
608
651
  }
609
652
  let selectedConfig;
610
653
  if (localProvider) {
611
- selectedConfig = { provider: cliProvider, model: msg.model };
654
+ selectedConfig = { provider: cliProvider, model: msg.model, lassoKey: resolvedLassoKey, serverUrl: collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv) };
612
655
  }
613
656
  else {
614
657
  const requestedProvider = (msg.provider || agentConfig.provider);
@@ -763,23 +806,32 @@ 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];
818
+ const serverUrl = collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv);
819
+ const targetApiUrl = (serverUrl.includes("collab.lasso.byorello.space") ? "https://api.lasso.byorello.space" : serverUrl).replace(/\/api\/v1\/?$/, "");
768
820
  const selectedConfig = localProvider
769
- ? { provider: cliProvider, model: msg.model || "claude-code:sonnet" }
821
+ ? { provider: cliProvider, model: msg.model || "claude-code:sonnet", lassoKey: resolvedLassoKey, serverUrl: targetApiUrl }
770
822
  : {
771
823
  ...(agentConfig || {}),
772
824
  provider,
773
825
  model,
774
826
  apiKey: localKey || apiKey,
827
+ lassoKey: resolvedLassoKey,
828
+ serverUrl: targetApiUrl,
775
829
  baseUrl: provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : provider === "ollama" ? `${(process.env.OLLAMA_BASE_URL || fileEnv.OLLAMA_BASE_URL || "http://localhost:11434/api").replace(/\/api\/?$/, "")}/v1` : undefined,
776
830
  };
777
831
  const controller = new AbortController();
778
832
  taskControllers.set(taskId, controller);
779
833
  socket.send(JSON.stringify({ type: "agent_status", taskId, status: "thinking", message: "Planning your request..." }));
780
834
  const { runOneShotAgent } = await import("./oneshot.js");
781
- const serverUrl = collabConfig?.serverUrl || (0, auth_1.serverUrlFrom)(fileEnv);
782
- const targetApiUrl = (serverUrl.includes("collab.lasso.byorello.space") ? "https://api.lasso.byorello.space" : serverUrl).replace(/\/api\/v1\/?$/, "");
783
835
  runOneShotAgent(cwd, msg.prompt, msg.scope || "project", selectedConfig, controller.signal, (status, message, detail) => {
784
836
  if (!controller.signal.aborted && socket.readyState === socket.OPEN) {
785
837
  socket.send(JSON.stringify({ type: "agent_status", taskId, status, message, detail }));
@@ -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,35 +3,83 @@ 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");
11
+ const agent_1 = require("./agent");
12
+ const auth_1 = require("./auth");
12
13
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
13
- function isConversationalPrompt(prompt) {
14
- const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
15
- const greetings = [
16
- "hi", "hello", "hey", "heya", "howdy", "sup", "yo",
17
- "good morning", "good afternoon", "good evening",
18
- "thanks", "thank you", "thx", "ty", "cool", "awesome", "great", "nice",
19
- "who are you", "what are you", "what can you do", "help", "help me",
20
- "what is lasso", "how do you work", "how does this work",
21
- ];
22
- if (greetings.includes(p))
23
- return true;
24
- if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
25
- const buildKeywords = /(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)/i;
26
- if (!buildKeywords.test(p))
27
- return true;
14
+ const CLI_PROVIDERS = new Set(["claude-code", "codex", "opencode", "cursor"]);
15
+ function isCliProvider(provider) {
16
+ return Boolean(provider && CLI_PROVIDERS.has(provider));
17
+ }
18
+ function cliLabel(provider) {
19
+ return provider === "claude-code" ? "Claude Code" : provider === "opencode" ? "OpenCode" : provider === "cursor" ? "Cursor" : provider === "codex" ? "Codex" : "CLI agent";
20
+ }
21
+ function cliCommand(provider) {
22
+ return provider === "claude-code" ? "claude" : provider || "the agent CLI";
23
+ }
24
+ // The Lasso Agent Gateway only understands hosted providers, so a CLI selection is
25
+ // normalized to the closest hosted provider/model when we have to fall back to it.
26
+ const GATEWAY_MODELS = {
27
+ anthropic: "claude-sonnet-4-5-20250929",
28
+ openai: "gpt-4.1",
29
+ google: "gemini-2.5-flash",
30
+ };
31
+ function gatewayTargetForCli(provider, model) {
32
+ const bare = model && model.includes(":") ? model.slice(model.indexOf(":") + 1) : model || "";
33
+ if (provider === "codex")
34
+ return { provider: "openai", model: GATEWAY_MODELS.openai };
35
+ if (provider === "cursor")
36
+ return bare.includes("gpt") ? { provider: "openai", model: GATEWAY_MODELS.openai } : { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
37
+ if (bare.startsWith("openai/") || /^gpt/i.test(bare))
38
+ return { provider: "openai", model: GATEWAY_MODELS.openai };
39
+ if (bare.startsWith("google/") || /^gemini/i.test(bare))
40
+ return { provider: "google", model: GATEWAY_MODELS.google };
41
+ return { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
42
+ }
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
+ }
28
73
  }
29
- const questionOnly = /^(what is|what's|how do i|why does|can you explain|tell me about)\b/i.test(p);
30
- const actionVerbs = /(build|create|add|make|fix|update|modify|change|refactor|implement|write code|code this|delete|remove|generate)/i;
31
- if (questionOnly && !actionVerbs.test(p)) {
32
- return true;
74
+ catch (err) {
75
+ console.log('[Lasso] Classification error:', err);
33
76
  }
34
- 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;
35
83
  }
36
84
  function runtimeErrorObservation(pageContext) {
37
85
  const errors = Array.isArray(pageContext?.runtimeErrors) ? pageContext.runtimeErrors : [];
@@ -39,7 +87,7 @@ function runtimeErrorObservation(pageContext) {
39
87
  return "- Console/Runtime Errors: None";
40
88
  return `- Console/Runtime Errors (the page is currently throwing these):\n${errors.map((e) => ` ! ${e}`).join("\n")}`;
41
89
  }
42
- async function generateConversationalReply(prompt, framework, config, signal, messages, pageContext) {
90
+ async function generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext, onProgress, taskId, onPrompt) {
43
91
  const history = messages?.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n") || "";
44
92
  let pageObservation = "";
45
93
  if (pageContext) {
@@ -68,16 +116,25 @@ Respond conversationally, concisely, and helpfully.
68
116
  ${pageContext ? "You have live access to the browser page context. When the user asks about the page, design, layout, or components, reference the actual headings, sections, buttons, and text rendered on screen. Provide concrete, expert design and UX feedback. Never claim the page has no content or components when rendered headings and sections are present." : ""}
69
117
  Do not output code changes, file patches, or JSON schemas — just talk to the developer naturally and offer assistance.`;
70
118
  const userPrompt = `${history ? `Conversation History:\n${history}\n\n` : ""}${pageObservation}\nUser message: ${prompt}`;
119
+ // CLI agents (Claude Code, Codex, OpenCode, Cursor) authenticate themselves, so no
120
+ // provider API key is needed. Failures are thrown so the caller can fall back.
121
+ if (isCliProvider(config.provider)) {
122
+ const text = await (0, agent_1.runLocalAgentPrompt)(cwd, config, `${systemPrompt}\n\n${userPrompt}`, signal, onProgress, taskId, onPrompt);
123
+ return text.trim() || "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.";
124
+ }
71
125
  if (!config.apiKey && config.provider !== "ollama") {
72
126
  if (pageContext && (prompt.toLowerCase().includes("landing page") || prompt.toLowerCase().includes("page"))) {
73
127
  const headings = (pageContext.domSummary?.headings || []).map((h) => `"${h.text}"`).join(", ");
74
128
  const sections = (pageContext.domSummary?.sections || []).map((s) => s.name).join(", ");
75
- return `Looking at your current page ("${pageContext.title}" on ${pageContext.route}):\n\n- **Rendered Sections:** ${sections || "Standard container"}\n- **Key Headings:** ${headings || "None"}\n- **CTAs:** ${(pageContext.domSummary?.buttons || []).join(", ") || "None"}\n\nTo build or refine components, configure your AI API key in settings or ask me what to modify!`;
129
+ return `Looking at your current page ("${pageContext.title}" on ${pageContext.route}):\n\n- **Rendered Sections:** ${sections || "Standard container"}\n- **Key Headings:** ${headings || "None"}\n- **CTAs:** ${(pageContext.domSummary?.buttons || []).join(", ") || "None"}\n\nTo build or refine components, run \`lasso auth login\` or add an AI provider API key in settings, then ask me what to modify!`;
76
130
  }
77
- 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.";
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.";
78
132
  }
79
133
  try {
80
- 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
+ }
81
138
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
82
139
  const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
83
140
  const res = await fetch(`${baseUrl}/chat/completions`, {
@@ -148,9 +205,10 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
148
205
  if (signal.aborted) {
149
206
  return { ok: false, error: "Operation was cancelled" };
150
207
  }
151
- const isConversational = isConversationalPrompt(prompt);
152
- // Browser inspection & snapshot steps
153
- 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) {
154
212
  onProgress("thinking", "Inspecting browser page...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
155
213
  if (pageContext.viewport) {
156
214
  onProgress("thinking", "Capturing visual snapshot...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height} (Document height: ${pageContext.viewport.scrollHeight}px)`);
@@ -165,8 +223,9 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
165
223
  else {
166
224
  onProgress("thinking", isConversational ? "Thinking..." : "Analyzing project structure...", isConversational ? undefined : "Scanning files and dependencies");
167
225
  }
168
- const projectStructure = await analyzeProject(cwd);
169
- 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);
170
229
  if (!isConversational && !pageContext) {
171
230
  onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
172
231
  }
@@ -179,21 +238,23 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
179
238
  rawServerUrl = rawServerUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
180
239
  }
181
240
  const targetServerUrl = rawServerUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
182
- const isCliProvider = config.provider === "claude-code" ||
183
- config.provider === "codex" ||
184
- config.provider === "opencode" ||
185
- config.provider === "cursor";
241
+ const useCliAgent = isCliProvider(config.provider);
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 || "";
186
245
  const hasLocalProviderKey = Boolean(config.apiKey &&
187
246
  config.apiKey !== "ollama" &&
188
247
  !config.apiKey.startsWith("lss_live_") &&
189
248
  !config.apiKey.startsWith("lss_"));
249
+ // If the user has CLI auth, always route to server (unless using local agents)
190
250
  // If the user does NOT have a local provider key in .env, route to Lasso Agent Gateway on server.
191
251
  // When they DO have a local key in .env, it runs locally to save server resources!
192
- const shouldRouteToServer = !isCliProvider && !hasLocalProviderKey && Boolean(targetServerUrl);
193
- if (shouldRouteToServer) {
252
+ // Ollama always runs locally (the server cannot reach the user's localhost).
253
+ const shouldRouteToServer = !useCliAgent && config.provider !== "ollama" && (Boolean(cliCredentials) || !hasLocalProviderKey) && Boolean(targetServerUrl);
254
+ const callGateway = async (target) => {
194
255
  try {
195
256
  if (!isConversational && !pageContext) {
196
- onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${config.model || "claude-3-7-sonnet"}`);
257
+ onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${target.model || "claude-3-7-sonnet"}`);
197
258
  }
198
259
  const endpoint = `${targetServerUrl.replace(/\/$/, "")}/api/v1/agent/session`;
199
260
  const headers = {
@@ -206,10 +267,15 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
206
267
  const controller = new AbortController();
207
268
  const onAbort = () => controller.abort();
208
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();
209
275
  // Build sourceHints: read relevant file contents to give the AI real code context.
210
276
  // This is what enables code changes instead of "please share the file" responses.
211
277
  const sourceHints = {};
212
- if (!isConversational) {
278
+ if (!isConversational && projectStructure) {
213
279
  const promptLower = prompt.toLowerCase();
214
280
  const allCssFiles = projectStructure.files.filter((f) => /\.(css|scss)$/i.test(f));
215
281
  const entryFiles = projectStructure.files.filter((f) => /(App|page|index|layout|main|hero|header|landing)\.(tsx|jsx|js|ts|html)$/i.test(f));
@@ -235,34 +301,47 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
235
301
  prompt,
236
302
  messages,
237
303
  model: {
238
- id: config.model,
239
- provider: config.provider,
304
+ id: target.model,
305
+ provider: target.provider,
240
306
  },
241
307
  context: {
242
- framework,
243
- hasTypeScript: projectStructure.hasTypeScript,
244
- hasTailwind: projectStructure.hasTailwind,
245
- hasReact: projectStructure.hasReact,
246
- hasNext: projectStructure.hasNext,
247
- hasVite: projectStructure.hasVite,
248
- dependencies: projectStructure.dependencies,
249
- devDependencies: projectStructure.devDependencies,
250
- 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) || [],
251
317
  sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined,
252
318
  currentRoute: pageContext?.route || "/",
253
319
  pageContext,
254
320
  },
255
321
  }),
256
322
  });
323
+ clearTimeout(timeout);
257
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
+ }
258
328
  if (!response.ok) {
259
329
  const errData = (await response.json().catch(() => ({})));
260
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
+ }
261
335
  return { ok: false, error: errorMessage };
262
336
  }
263
337
  const data = (await response.json());
264
338
  if (data.status === "failed" || data.error) {
265
- 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 };
266
345
  }
267
346
  if (data) {
268
347
  const changes = Array.isArray(data.changes) ? data.changes : [];
@@ -315,10 +394,100 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
315
394
  error: serverErr instanceof Error ? serverErr.message : "Failed to connect to Lasso Agent Gateway.",
316
395
  };
317
396
  }
397
+ return { ok: false, error: "The agent session returned no result." };
398
+ };
399
+ const gatewayFallback = async (reason) => {
400
+ if (!gatewayKey || !targetServerUrl) {
401
+ return {
402
+ ok: false,
403
+ error: `${reason} — run \`${cliCommand(config.provider)}\` once to authenticate it, or run \`lasso auth login\` to use the Lasso gateway.`,
404
+ };
405
+ }
406
+ onProgress("thinking", `${cliLabel(config.provider)} is unavailable; using the Lasso Agent Gateway...`, reason);
407
+ const target = gatewayTargetForCli(config.provider, config.model);
408
+ const result = await callGateway({ provider: target.provider, model: target.model });
409
+ if (!result.ok && result.error) {
410
+ return { ok: false, error: `${reason} — gateway fallback failed: ${result.error}` };
411
+ }
412
+ return result;
413
+ };
414
+ // CLI agents (Claude Code, Codex, OpenCode, Cursor) authenticate themselves, so they run
415
+ // locally first instead of demanding a provider API key from settings.
416
+ if (useCliAgent && !isConversational) {
417
+ onProgress("thinking", `Running ${cliLabel(config.provider)}...`, `Local CLI agent · ${config.model || ""}`);
418
+ try {
419
+ const selectedElement = pageContext?.selectedElement;
420
+ const input = {
421
+ taskId,
422
+ instruction: prompt,
423
+ model: config.model || "",
424
+ messages,
425
+ context: {
426
+ runtimeErrors: pageContext?.runtimeErrors,
427
+ screenshots: pageContext?.screenshot ? { full: pageContext.screenshot } : undefined,
428
+ viewport: pageContext?.viewport,
429
+ },
430
+ element: {
431
+ tag: selectedElement?.tag || "project",
432
+ group: scope === "component" ? "component" : "workspace",
433
+ label: selectedElement?.text || "project",
434
+ html: selectedElement ? `<${selectedElement.tag}> ${selectedElement.text || ""}` : undefined,
435
+ },
436
+ };
437
+ const proposal = await (0, agent_1.proposeChanges)(cwd, input, config, signal, (message, detail, level) => {
438
+ onProgress(level === "error" ? "error" : "working", message, detail);
439
+ }, (agentPrompt) => {
440
+ onPrompt({ question: agentPrompt.message, options: agentPrompt.options });
441
+ });
442
+ if (signal.aborted) {
443
+ return { ok: false, error: "Operation was cancelled" };
444
+ }
445
+ if (!proposal.changes.length) {
446
+ return { ok: true, summary: proposal.summary, changes: [], thinking: [], totalThinkingTimeMs: Date.now() - startTime };
447
+ }
448
+ onProgress("review", "Implementation complete", `${proposal.changes.length} files modified`);
449
+ return {
450
+ ok: true,
451
+ summary: proposal.summary,
452
+ changes: proposal.changes,
453
+ thinking: [
454
+ { title: "Analyzed project structure", detail: `Detected ${framework} framework` },
455
+ { title: `${cliLabel(config.provider)} generated a proposal`, detail: `${proposal.changes.length} file(s)` },
456
+ ],
457
+ totalThinkingTimeMs: Date.now() - startTime,
458
+ };
459
+ }
460
+ catch (err) {
461
+ if (signal.aborted) {
462
+ return { ok: false, error: "Operation was cancelled" };
463
+ }
464
+ return await gatewayFallback(err instanceof Error ? err.message : `${cliLabel(config.provider)} failed`);
465
+ }
466
+ }
467
+ if (useCliAgent && isConversational) {
468
+ try {
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 }));
470
+ return {
471
+ ok: true,
472
+ summary: reply,
473
+ changes: [],
474
+ thinking: [],
475
+ totalThinkingTimeMs: Date.now() - startTime,
476
+ };
477
+ }
478
+ catch (err) {
479
+ if (signal.aborted) {
480
+ return { ok: false, error: "Operation was cancelled" };
481
+ }
482
+ return await gatewayFallback(err instanceof Error ? err.message : `${cliLabel(config.provider)} failed`);
483
+ }
484
+ }
485
+ if (shouldRouteToServer) {
486
+ return await callGateway({ provider: config.provider, model: config.model });
318
487
  }
319
488
  // If it's a conversational prompt and server was not used or failed, respond directly without modifying files!
320
489
  if (isConversational) {
321
- const reply = await generateConversationalReply(prompt, framework, config, signal, messages, pageContext);
490
+ const reply = await generateConversationalReply(cwd, prompt, framework || "", config, signal, messages, pageContext);
322
491
  return {
323
492
  ok: true,
324
493
  summary: reply,
@@ -329,7 +498,7 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
329
498
  }
330
499
  // Phase 2: Local agent generation fallback for code changes
331
500
  onProgress("thinking", "Generating implementation plan...", `Planning components for ${prompt}`);
332
- 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);
333
502
  if (signal.aborted) {
334
503
  return { ok: false, error: "Operation was cancelled during planning" };
335
504
  }
@@ -464,8 +633,8 @@ async function analyzeProject(cwd) {
464
633
  hasReact,
465
634
  hasNext,
466
635
  hasVite,
467
- dependencies,
468
- devDependencies,
636
+ dependencies: dependencies,
637
+ devDependencies: devDependencies,
469
638
  files,
470
639
  };
471
640
  }
@@ -578,15 +747,22 @@ Existing code context (study these carefully before writing — especially the C
578
747
  ${fileSnippets || "No existing components found. Create appropriate files in src/."}
579
748
 
580
749
  Analyze the existing code structure, then respond in JSON:`;
581
- if (!config.apiKey && config.provider !== "ollama") {
750
+ if (!config.apiKey && config.provider !== "ollama" && !isCliProvider(config.provider)) {
582
751
  return {
583
752
  steps: [],
584
753
  isConversational: true,
585
- reply: "Please configure an AI provider API key or model in settings to enable code generation.",
754
+ reply: "I couldn't generate code because no AI provider is available. Run `lasso auth login` to use your Lasso account, or add an AI provider API key in settings, then try again.",
586
755
  };
587
756
  }
588
757
  try {
589
- 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
+ }
590
766
  let text = "";
591
767
  if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
592
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.5",
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",