@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 +17 -0
- package/dist/cli/agent.d.ts +2 -2
- package/dist/cli/agent.js +118 -20
- package/dist/cli/bridge.d.ts +4 -2
- package/dist/cli/bridge.js +99 -47
- package/dist/cli/host/next-host-entry.js +1 -1
- package/dist/cli/host/vite-host-entry.js +1 -1
- package/dist/cli/index.js +18 -1
- package/dist/cli/oneshot.d.ts +0 -1
- package/dist/cli/oneshot.js +233 -57
- package/dist/cli/project.d.ts +28 -0
- package/dist/cli/project.js +57 -0
- package/package.json +1 -1
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.**
|
package/dist/cli/agent.d.ts
CHANGED
|
@@ -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
|
-
|
|
310
|
-
const
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1038
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/cli/bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
314
|
-
const
|
|
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
|
-
//
|
|
317
|
-
{ id:
|
|
318
|
-
{ id:
|
|
319
|
-
{ id:
|
|
320
|
-
{ id:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
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
|
|
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");
|
package/dist/cli/oneshot.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli/oneshot.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
if (questionOnly && !actionVerbs.test(p)) {
|
|
32
|
-
return true;
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.log('[Lasso] Classification error:', err);
|
|
33
76
|
}
|
|
34
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
169
|
-
const
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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: ${
|
|
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:
|
|
239
|
-
provider:
|
|
304
|
+
id: target.model,
|
|
305
|
+
provider: target.provider,
|
|
240
306
|
},
|
|
241
307
|
context: {
|
|
242
|
-
framework,
|
|
243
|
-
hasTypeScript: projectStructure
|
|
244
|
-
hasTailwind: projectStructure
|
|
245
|
-
hasReact: projectStructure
|
|
246
|
-
hasNext: projectStructure
|
|
247
|
-
hasVite: projectStructure
|
|
248
|
-
dependencies: projectStructure
|
|
249
|
-
devDependencies: projectStructure
|
|
250
|
-
files: projectStructure
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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");
|
package/dist/cli/project.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli/project.js
CHANGED
|
@@ -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