@lasso-ai/cli 1.4.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/agent.d.ts +10 -89
- package/dist/cli/agent.js +66 -1412
- package/dist/cli/backends.d.ts +53 -0
- package/dist/cli/backends.js +267 -0
- package/dist/cli/bridge-old.d.ts +259 -0
- package/dist/cli/bridge-old.js +1350 -0
- package/dist/cli/bridge.d.ts +22 -12
- package/dist/cli/bridge.js +824 -1174
- package/dist/cli/host/install.js +12 -1
- package/dist/cli/host/next-host-entry.js +0 -2
- package/dist/cli/host/vite-host-entry.js +1 -3
- package/dist/cli/local-runtime.d.ts +28 -0
- package/dist/cli/local-runtime.js +294 -0
- package/dist/cli/oneshot.js +177 -961
- package/dist/cli/runtime-client.d.ts +310 -0
- package/dist/cli/runtime-client.js +178 -0
- package/dist/cli/snapshot.d.ts +3 -1
- package/dist/cli/snapshot.js +8 -1
- package/dist/overlay.js +44 -28
- package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
- package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
- package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
- package/dist/runtime/__tests__/state-machine.test.js +107 -0
- package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
- package/dist/runtime/__tests__/verification-engine.test.js +91 -0
- package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
- package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
- package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
- package/dist/runtime/adapters/claude-code-adapter.js +230 -0
- package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
- package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
- package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
- package/dist/runtime/adapters/cursor-adapter.js +214 -0
- package/dist/runtime/adapters/factory.d.ts +19 -0
- package/dist/runtime/adapters/factory.js +48 -0
- package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
- package/dist/runtime/adapters/local-agent-adapter.js +408 -0
- package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
- package/dist/runtime/adapters/opencode-adapter.js +210 -0
- package/dist/runtime/agent-runtime.d.ts +286 -0
- package/dist/runtime/agent-runtime.js +8 -0
- package/dist/runtime/agent-state-machine.d.ts +67 -0
- package/dist/runtime/agent-state-machine.js +234 -0
- package/dist/runtime/backend-interface.d.ts +272 -0
- package/dist/runtime/backend-interface.js +38 -0
- package/dist/runtime/event-mapper.d.ts +11 -0
- package/dist/runtime/event-mapper.js +40 -0
- package/dist/runtime/index.d.ts +20 -0
- package/dist/runtime/index.js +43 -0
- package/dist/runtime/local-session-runner.d.ts +60 -0
- package/dist/runtime/local-session-runner.js +185 -0
- package/dist/runtime/review.d.ts +24 -0
- package/dist/runtime/review.js +30 -0
- package/dist/runtime/verification-engine.d.ts +71 -0
- package/dist/runtime/verification-engine.js +374 -0
- package/dist/runtime/workspace-guard.d.ts +70 -0
- package/dist/runtime/workspace-guard.js +321 -0
- package/docs/local-coding-agents.md +183 -0
- package/package.json +22 -3
package/dist/cli/oneshot.js
CHANGED
|
@@ -6,199 +6,65 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runOneShotAgent = runOneShotAgent;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
-
const
|
|
10
|
-
const node_util_1 = require("node:util");
|
|
11
|
-
const agent_1 = require("./agent");
|
|
12
|
-
const auth_1 = require("./auth");
|
|
9
|
+
const runtime_client_1 = require("./runtime-client");
|
|
13
10
|
const framework_1 = require("./utils/framework");
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (provider === "cursor")
|
|
37
|
-
return bare.includes("gpt") ? { provider: "openai", model: GATEWAY_MODELS.openai } : { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
|
|
38
|
-
if (bare.startsWith("openai/") || /^gpt/i.test(bare))
|
|
39
|
-
return { provider: "openai", model: GATEWAY_MODELS.openai };
|
|
40
|
-
if (bare.startsWith("google/") || /^gemini/i.test(bare))
|
|
41
|
-
return { provider: "google", model: GATEWAY_MODELS.google };
|
|
42
|
-
return { provider: "anthropic", model: GATEWAY_MODELS.anthropic };
|
|
43
|
-
}
|
|
44
|
-
async function isConversationalPrompt(prompt, config, apiKey) {
|
|
45
|
-
// Use the user's selected model to intelligently detect if prompt is conversational vs. action-oriented
|
|
46
|
-
try {
|
|
47
|
-
// Use the gateway to classify - this respects the user's selected model
|
|
48
|
-
const response = await fetch(`${config.serverUrl || "https://api.lasso.byorello.space"}/api/v1/agent/generate`, {
|
|
49
|
-
method: "POST",
|
|
50
|
-
headers: {
|
|
51
|
-
"Content-Type": "application/json",
|
|
52
|
-
"Authorization": `Bearer ${apiKey || ""}`,
|
|
53
|
-
},
|
|
54
|
-
body: JSON.stringify({
|
|
55
|
-
model: {
|
|
56
|
-
id: config.model,
|
|
57
|
-
provider: config.provider,
|
|
58
|
-
},
|
|
59
|
-
prompt: `Classify this user prompt as either "conversational" or "action". Return ONLY the word "conversational" or "action" - nothing else.
|
|
60
|
-
|
|
61
|
-
Prompt: "${prompt}"`,
|
|
62
|
-
maxTokens: 10,
|
|
63
|
-
}),
|
|
64
|
-
});
|
|
65
|
-
if (response.ok) {
|
|
66
|
-
const data = await response.json();
|
|
67
|
-
const classification = data.reply?.toLowerCase().trim();
|
|
68
|
-
console.log('[Lasso] Classification result:', classification);
|
|
69
|
-
return classification === "conversational";
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
console.log('[Lasso] Classification failed:', response.status);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch (err) {
|
|
76
|
-
console.log('[Lasso] Classification error:', err);
|
|
77
|
-
}
|
|
78
|
-
// Fallback: smarter heuristic
|
|
79
|
-
const p = prompt.trim().toLowerCase();
|
|
80
|
-
const actionKeywords = /(add|create|build|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code|replace|insert|append|generate)/i;
|
|
81
|
-
const isAction = actionKeywords.test(p);
|
|
82
|
-
console.log('[Lasso] Fallback classification:', isAction ? 'action' : 'conversational');
|
|
83
|
-
return !isAction;
|
|
84
|
-
}
|
|
85
|
-
function runtimeErrorObservation(pageContext) {
|
|
86
|
-
const errors = Array.isArray(pageContext?.runtimeErrors) ? pageContext.runtimeErrors : [];
|
|
87
|
-
if (!errors.length)
|
|
88
|
-
return "- Console/Runtime Errors: None";
|
|
89
|
-
return `- Console/Runtime Errors (the page is currently throwing these):\n${errors.map((e) => ` ! ${e}`).join("\n")}`;
|
|
90
|
-
}
|
|
91
|
-
async function generateConversationalReply(cwd, prompt, framework, config, signal, messages, pageContext, onProgress, taskId, onPrompt) {
|
|
92
|
-
const history = messages?.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n") || "";
|
|
93
|
-
let pageObservation = "";
|
|
94
|
-
if (pageContext) {
|
|
95
|
-
const headings = (pageContext.domSummary?.headings || []).map((h) => `H${h.level}: "${h.text}"`).join("\n ");
|
|
96
|
-
const sections = (pageContext.domSummary?.sections || []).map((s) => `- [${s.name}]: ${s.textPreview}`).join("\n ");
|
|
97
|
-
const buttons = (pageContext.domSummary?.buttons || []).join(", ");
|
|
98
|
-
const links = (pageContext.domSummary?.links || []).join(", ");
|
|
99
|
-
pageObservation = `
|
|
100
|
-
Live Browser Page Context (what the user is currently viewing in their live app):
|
|
101
|
-
- Route: ${pageContext.route || "/"}
|
|
102
|
-
- Page Title: "${pageContext.title || "Untitled"}"
|
|
103
|
-
- Viewport: ${pageContext.viewport?.width}x${pageContext.viewport?.height} (Document height: ${pageContext.viewport?.scrollHeight}px, scrolled: ${pageContext.viewport?.scrollY}px)
|
|
104
|
-
- Rendered Headings:
|
|
105
|
-
${headings || "None"}
|
|
106
|
-
- Rendered Page Sections & Landmarks:
|
|
107
|
-
${sections || "None"}
|
|
108
|
-
- Call-to-Action Buttons: ${buttons || "None"}
|
|
109
|
-
- Navigation Links: ${links || "None"}
|
|
110
|
-
- Visible Text Preview: "${pageContext.domSummary?.visibleTextSnippet?.slice(0, 1000) || "Empty"}"
|
|
111
|
-
${runtimeErrorObservation(pageContext)}
|
|
112
|
-
${pageContext.selectedElement ? `- Selected Element: <${pageContext.selectedElement.tag}> "${pageContext.selectedElement.text || ""}"` : ""}
|
|
113
|
-
`;
|
|
114
|
-
}
|
|
115
|
-
const systemPrompt = `You are Lasso, a fast and helpful AI coding assistant embedded in a live web application (${framework}).
|
|
116
|
-
Respond conversationally, concisely, and helpfully.
|
|
117
|
-
${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." : ""}
|
|
118
|
-
Do not output code changes, file patches, or JSON schemas — just talk to the developer naturally and offer assistance.`;
|
|
119
|
-
const userPrompt = `${history ? `Conversation History:\n${history}\n\n` : ""}${pageObservation}\nUser message: ${prompt}`;
|
|
120
|
-
// CLI agents (Claude Code, Codex, OpenCode, Cursor) authenticate themselves, so no
|
|
121
|
-
// provider API key is needed. Failures are thrown so the caller can fall back.
|
|
122
|
-
if (isCliProvider(config.provider)) {
|
|
123
|
-
const text = await (0, agent_1.runLocalAgentPrompt)(cwd, config, `${systemPrompt}\n\n${userPrompt}`, signal, onProgress, taskId, onPrompt);
|
|
124
|
-
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.";
|
|
125
|
-
}
|
|
126
|
-
if (!config.apiKey && config.provider !== "ollama") {
|
|
127
|
-
if (pageContext && (prompt.toLowerCase().includes("landing page") || prompt.toLowerCase().includes("page"))) {
|
|
128
|
-
const headings = (pageContext.domSummary?.headings || []).map((h) => `"${h.text}"`).join(", ");
|
|
129
|
-
const sections = (pageContext.domSummary?.sections || []).map((s) => s.name).join(", ");
|
|
130
|
-
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!`;
|
|
11
|
+
function detectProjectStructure(cwd) {
|
|
12
|
+
const pkgPath = node_path_1.default.join(cwd, "package.json");
|
|
13
|
+
let hasTypeScript = false;
|
|
14
|
+
let hasReact = false;
|
|
15
|
+
let hasNext = false;
|
|
16
|
+
let hasVite = false;
|
|
17
|
+
let hasTailwind = false;
|
|
18
|
+
const files = [];
|
|
19
|
+
const directories = [];
|
|
20
|
+
const dependencies = [];
|
|
21
|
+
const devDependencies = [];
|
|
22
|
+
if (node_fs_1.default.existsSync(pkgPath)) {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
|
|
25
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
26
|
+
hasTypeScript = Boolean(deps.typescript);
|
|
27
|
+
hasReact = Boolean(deps.react);
|
|
28
|
+
hasNext = Boolean(deps.next);
|
|
29
|
+
hasVite = Boolean(deps.vite);
|
|
30
|
+
hasTailwind = Boolean(deps.tailwindcss);
|
|
31
|
+
dependencies.push(...Object.keys(pkg.dependencies || {}));
|
|
32
|
+
devDependencies.push(...Object.keys(pkg.devDependencies || {}));
|
|
131
33
|
}
|
|
132
|
-
|
|
34
|
+
catch { }
|
|
133
35
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (!
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
temperature: 0.7,
|
|
148
|
-
messages: [
|
|
149
|
-
{ role: "system", content: systemPrompt },
|
|
150
|
-
{ role: "user", content: userPrompt },
|
|
151
|
-
],
|
|
152
|
-
}),
|
|
153
|
-
});
|
|
154
|
-
if (res.ok) {
|
|
155
|
-
const json = (await res.json());
|
|
156
|
-
const text = json.choices?.[0]?.message?.content?.trim();
|
|
157
|
-
if (text)
|
|
158
|
-
return text;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else if (config.provider === "google") {
|
|
162
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
signal,
|
|
165
|
-
headers: { "content-type": "application/json" },
|
|
166
|
-
body: JSON.stringify({
|
|
167
|
-
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
168
|
-
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
|
|
169
|
-
}),
|
|
170
|
-
});
|
|
171
|
-
if (res.ok) {
|
|
172
|
-
const json = (await res.json());
|
|
173
|
-
const text = json.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("")?.trim();
|
|
174
|
-
if (text)
|
|
175
|
-
return text;
|
|
36
|
+
// Scan for files and directories
|
|
37
|
+
function scanDir(dir, base = "") {
|
|
38
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
39
|
+
return;
|
|
40
|
+
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const fullPath = node_path_1.default.join(dir, entry.name);
|
|
43
|
+
const relativePath = base ? node_path_1.default.join(base, entry.name) : entry.name;
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
46
|
+
directories.push(relativePath);
|
|
47
|
+
scanDir(fullPath, relativePath);
|
|
48
|
+
}
|
|
176
49
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
180
|
-
method: "POST",
|
|
181
|
-
signal,
|
|
182
|
-
headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
|
|
183
|
-
body: JSON.stringify({
|
|
184
|
-
model,
|
|
185
|
-
max_tokens: 1024,
|
|
186
|
-
system: systemPrompt,
|
|
187
|
-
messages: [{ role: "user", content: userPrompt }],
|
|
188
|
-
}),
|
|
189
|
-
});
|
|
190
|
-
if (res.ok) {
|
|
191
|
-
const json = (await res.json());
|
|
192
|
-
const text = json.content?.find((item) => item.type === "text")?.text?.trim();
|
|
193
|
-
if (text)
|
|
194
|
-
return text;
|
|
50
|
+
else {
|
|
51
|
+
files.push(relativePath);
|
|
195
52
|
}
|
|
196
53
|
}
|
|
197
54
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
55
|
+
scanDir(cwd);
|
|
56
|
+
return {
|
|
57
|
+
hasTypeScript,
|
|
58
|
+
hasReact,
|
|
59
|
+
hasNext,
|
|
60
|
+
hasVite,
|
|
61
|
+
hasTailwind,
|
|
62
|
+
files: files.slice(0, 200),
|
|
63
|
+
directories: directories.slice(0, 50),
|
|
64
|
+
dependencies,
|
|
65
|
+
devDependencies,
|
|
66
|
+
sourceRoot: node_fs_1.default.existsSync(node_path_1.default.join(cwd, "src")) ? "src" : ".",
|
|
67
|
+
};
|
|
202
68
|
}
|
|
203
69
|
async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, onPrompt, messages, serverUrl, apiKey, taskId, pageContext) {
|
|
204
70
|
const startTime = Date.now();
|
|
@@ -206,798 +72,148 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
206
72
|
if (signal.aborted) {
|
|
207
73
|
return { ok: false, error: "Operation was cancelled" };
|
|
208
74
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
config.apiKey !== "ollama" &&
|
|
245
|
-
!config.apiKey.startsWith("lss_live_") &&
|
|
246
|
-
!config.apiKey.startsWith("lss_"));
|
|
247
|
-
// If the user has CLI auth, always route to server (unless using local agents)
|
|
248
|
-
// If the user does NOT have a local provider key in .env, route to Lasso Agent Gateway on server.
|
|
249
|
-
// When they DO have a local key in .env, it runs locally to save server resources!
|
|
250
|
-
// Ollama always runs locally (the server cannot reach the user's localhost).
|
|
251
|
-
const shouldRouteToServer = !useCliAgent && config.provider !== "ollama" && (Boolean(cliCredentials) || !hasLocalProviderKey) && Boolean(targetServerUrl);
|
|
252
|
-
const callGateway = async (target) => {
|
|
253
|
-
try {
|
|
254
|
-
if (!isConversational && !pageContext) {
|
|
255
|
-
onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${target.model || "claude-3-7-sonnet"}`);
|
|
256
|
-
}
|
|
257
|
-
const endpoint = `${targetServerUrl.replace(/\/$/, "")}/api/v1/agent/session`;
|
|
258
|
-
const headers = {
|
|
259
|
-
"content-type": "application/json",
|
|
260
|
-
};
|
|
261
|
-
const resolvedApiKey = apiKey || config.apiKey || process.env.LASSO_API_KEY;
|
|
262
|
-
if (resolvedApiKey) {
|
|
263
|
-
headers["authorization"] = `Bearer ${resolvedApiKey}`;
|
|
264
|
-
}
|
|
265
|
-
const controller = new AbortController();
|
|
266
|
-
const onAbort = () => controller.abort();
|
|
267
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
268
|
-
// Add a 5-minute timeout for gateway requests
|
|
269
|
-
const timeout = setTimeout(() => {
|
|
270
|
-
controller.abort();
|
|
271
|
-
}, 5 * 60 * 1000);
|
|
272
|
-
timeout.unref();
|
|
273
|
-
// Build sourceHints: read relevant file contents to give the AI real code context.
|
|
274
|
-
// This is what enables code changes instead of "please share the file" responses.
|
|
275
|
-
const sourceHints = {};
|
|
276
|
-
if (!isConversational && projectStructure) {
|
|
277
|
-
const promptLower = prompt.toLowerCase();
|
|
278
|
-
const allCssFiles = projectStructure.files.filter((f) => /\.(css|scss)$/i.test(f));
|
|
279
|
-
const entryFiles = projectStructure.files.filter((f) => /(App|page|index|layout|main|hero|header|landing)\.(tsx|jsx|js|ts|html)$/i.test(f));
|
|
280
|
-
const mentionedFiles = projectStructure.files.filter((f) => {
|
|
281
|
-
const base = node_path_1.default.basename(f, node_path_1.default.extname(f)).toLowerCase();
|
|
282
|
-
return base.length > 2 && promptLower.includes(base);
|
|
283
|
-
});
|
|
284
|
-
const targetFiles = [...new Set([...allCssFiles, ...entryFiles, ...mentionedFiles])].slice(0, 8);
|
|
285
|
-
for (const file of targetFiles) {
|
|
286
|
-
try {
|
|
287
|
-
const content = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, file), "utf8");
|
|
288
|
-
sourceHints[file] = content.slice(0, 8000);
|
|
289
|
-
}
|
|
290
|
-
catch { /* skip unreadable files */ }
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
onProgress("thinking", "Contacting AI agent...", `Provider: ${target.provider}, Model: ${target.model || "default"}`);
|
|
294
|
-
// Use streaming request for real-time progress updates
|
|
295
|
-
const response = await fetch(endpoint, {
|
|
296
|
-
method: "POST",
|
|
297
|
-
headers: {
|
|
298
|
-
...headers,
|
|
299
|
-
"Accept": "text/event-stream",
|
|
300
|
-
},
|
|
301
|
-
signal: controller.signal,
|
|
302
|
-
body: JSON.stringify({
|
|
303
|
-
sessionId: taskId,
|
|
304
|
-
prompt,
|
|
305
|
-
messages,
|
|
306
|
-
model: {
|
|
307
|
-
id: target.model,
|
|
308
|
-
provider: target.provider,
|
|
309
|
-
},
|
|
310
|
-
context: {
|
|
311
|
-
framework: framework || undefined,
|
|
312
|
-
hasTypeScript: projectStructure?.hasTypeScript,
|
|
313
|
-
hasTailwind: projectStructure?.hasTailwind,
|
|
314
|
-
hasReact: projectStructure?.hasReact,
|
|
315
|
-
hasNext: projectStructure?.hasNext,
|
|
316
|
-
hasVite: projectStructure?.hasVite,
|
|
317
|
-
dependencies: projectStructure?.dependencies,
|
|
318
|
-
devDependencies: projectStructure?.devDependencies,
|
|
319
|
-
files: projectStructure?.files.slice(0, 100) || [],
|
|
320
|
-
directories: projectStructure?.directories.slice(0, 50) || [],
|
|
321
|
-
sourceRoot: projectStructure?.sourceRoot,
|
|
322
|
-
appRouter: projectStructure?.appRouter,
|
|
323
|
-
pagesRouter: projectStructure?.pagesRouter,
|
|
324
|
-
multipleAppDirs: projectStructure?.multipleAppDirs,
|
|
325
|
-
sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined,
|
|
326
|
-
currentRoute: pageContext?.route || "/",
|
|
327
|
-
pageContext,
|
|
328
|
-
},
|
|
329
|
-
}),
|
|
330
|
-
});
|
|
331
|
-
signal.removeEventListener("abort", onAbort);
|
|
332
|
-
if (signal.aborted) {
|
|
333
|
-
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." };
|
|
334
|
-
}
|
|
335
|
-
if (!response.ok) {
|
|
336
|
-
const errText = await response.text();
|
|
337
|
-
console.error(`[Lasso] Gateway error ${response.status}:`, errText);
|
|
338
|
-
const errData = (await response.json().catch(() => ({})));
|
|
339
|
-
const errorMessage = errData.error || `Agent request failed (${response.status}): ${errText}`;
|
|
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(errorMessage)) {
|
|
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: errorMessage };
|
|
345
|
-
}
|
|
346
|
-
// Process streaming response
|
|
347
|
-
const reader = response.body?.getReader();
|
|
348
|
-
if (!reader) {
|
|
349
|
-
return { ok: false, error: "Failed to read response stream" };
|
|
350
|
-
}
|
|
351
|
-
const decoder = new TextDecoder();
|
|
352
|
-
let buffer = "";
|
|
353
|
-
let finalData = {};
|
|
354
|
-
try {
|
|
355
|
-
while (true) {
|
|
356
|
-
if (signal.aborted) {
|
|
357
|
-
reader.cancel();
|
|
358
|
-
return { ok: false, error: "Request was aborted" };
|
|
359
|
-
}
|
|
360
|
-
const { done, value } = await reader.read();
|
|
361
|
-
if (done)
|
|
362
|
-
break;
|
|
363
|
-
buffer += decoder.decode(value, { stream: true });
|
|
364
|
-
const lines = buffer.split("\n");
|
|
365
|
-
buffer = lines.pop() || "";
|
|
366
|
-
for (const line of lines) {
|
|
367
|
-
if (line.startsWith("data: ")) {
|
|
368
|
-
const data = line.slice(6);
|
|
369
|
-
if (data === "[DONE]")
|
|
370
|
-
continue;
|
|
371
|
-
try {
|
|
372
|
-
const event = JSON.parse(data);
|
|
373
|
-
console.log(`[Lasso] Stream event:`, JSON.stringify({ type: event.type || "unknown" }));
|
|
374
|
-
// Handle thinking steps
|
|
375
|
-
if (event.type === "thinking" && event.step) {
|
|
376
|
-
onProgress("thinking", event.step.title, event.step.detail);
|
|
377
|
-
}
|
|
378
|
-
// Handle tool calls
|
|
379
|
-
if (event.type === "tool_call" && event.tool) {
|
|
380
|
-
onProgress("working", `Using ${event.tool.name}...`, event.tool.input?.path || event.tool.input?.query || "");
|
|
381
|
-
}
|
|
382
|
-
// Handle progress messages
|
|
383
|
-
if (event.type === "progress" && event.message) {
|
|
384
|
-
onProgress("thinking", event.message, event.detail);
|
|
385
|
-
}
|
|
386
|
-
// Accumulate final data
|
|
387
|
-
if (event.type === "done" || event.changes || event.summary) {
|
|
388
|
-
finalData = { ...finalData, ...event };
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
catch (e) {
|
|
392
|
-
console.warn(`[Lasso] Failed to parse stream event:`, data, e);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
finally {
|
|
399
|
-
reader.releaseLock();
|
|
400
|
-
}
|
|
401
|
-
const data = finalData;
|
|
402
|
-
console.log(`[Lasso] Gateway response:`, JSON.stringify({ status: data.status, hasChanges: !!data.changes, changesCount: data.changes?.length, hasReply: !!data.reply, hasError: !!data.error }));
|
|
403
|
-
if (data.status === "failed" || data.error) {
|
|
404
|
-
const error = data.error || data.summary || "Agent session failed on server.";
|
|
405
|
-
// Check for quota/plan exhaustion
|
|
406
|
-
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)) {
|
|
407
|
-
return { ok: false, error: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service." };
|
|
408
|
-
}
|
|
409
|
-
return { ok: false, error };
|
|
410
|
-
}
|
|
411
|
-
if (data) {
|
|
412
|
-
const changes = Array.isArray(data.changes) ? data.changes : [];
|
|
413
|
-
const isChatReply = isConversational ||
|
|
414
|
-
(typeof data.reply === "string" && data.reply.trim().length > 0 && changes.length === 0) ||
|
|
415
|
-
(changes.length === 0 && Boolean(data.summary));
|
|
416
|
-
if (isChatReply) {
|
|
417
|
-
// Conversational reply — no file changes needed
|
|
418
|
-
return {
|
|
419
|
-
ok: true,
|
|
420
|
-
summary: data.reply?.trim() || data.summary || "Hello! How can I help you with your project today?",
|
|
421
|
-
changes: [],
|
|
422
|
-
thinking: [],
|
|
423
|
-
totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
// Replay thinking steps progress for implementation (fallback if not streamed)
|
|
427
|
-
if (data.thinking && data.thinking.length) {
|
|
428
|
-
for (const step of data.thinking) {
|
|
429
|
-
if (signal.aborted)
|
|
430
|
-
break;
|
|
431
|
-
onProgress("thinking", step.title, step.detail);
|
|
432
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
if (changes.length > 0) {
|
|
436
|
-
onProgress("working", "Preparing proposed changes...", `${changes.length} files modified`);
|
|
437
|
-
return {
|
|
438
|
-
ok: true,
|
|
439
|
-
summary: data.summary || `Implemented changes for: ${prompt}`,
|
|
440
|
-
todo: Array.isArray(data.todo) ? data.todo : undefined,
|
|
441
|
-
changes,
|
|
442
|
-
thinking: data.thinking || [
|
|
443
|
-
{ title: "Project analysis", detail: `Framework: ${framework}` },
|
|
444
|
-
{ title: "Component planning", detail: "Prepared changes" },
|
|
445
|
-
],
|
|
446
|
-
totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
// Server returned 200 but no changes and no reply — surface a clear error
|
|
450
|
-
return {
|
|
451
|
-
ok: false,
|
|
452
|
-
error: "The agent couldn't generate changes for that request. Try rephrasing or check your AI provider key in the dashboard.",
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
catch (serverErr) {
|
|
457
|
-
return {
|
|
458
|
-
ok: false,
|
|
459
|
-
error: serverErr instanceof Error ? serverErr.message : "Failed to connect to Lasso Agent Gateway.",
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
return { ok: false, error: "The agent session returned no result." };
|
|
463
|
-
};
|
|
464
|
-
const gatewayFallback = async (reason) => {
|
|
465
|
-
if (!gatewayKey || !targetServerUrl) {
|
|
466
|
-
return {
|
|
467
|
-
ok: false,
|
|
468
|
-
error: `${reason} — run \`${cliCommand(config.provider)}\` once to authenticate it, or run \`lasso auth login\` to use the Lasso gateway.`,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
onProgress("thinking", `${cliLabel(config.provider)} is unavailable; using the Lasso Agent Gateway...`, reason);
|
|
472
|
-
const target = gatewayTargetForCli(config.provider, config.model);
|
|
473
|
-
const result = await callGateway({ provider: target.provider, model: target.model });
|
|
474
|
-
if (!result.ok && result.error) {
|
|
475
|
-
return { ok: false, error: `${reason} — gateway fallback failed: ${result.error}` };
|
|
476
|
-
}
|
|
477
|
-
return result;
|
|
75
|
+
onProgress("thinking", "Analyzing project structure...", "Scanning files and dependencies");
|
|
76
|
+
const projectStructure = detectProjectStructure(cwd);
|
|
77
|
+
const framework = (0, framework_1.detectFramework)(cwd);
|
|
78
|
+
onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
|
|
79
|
+
const targetServerUrl = serverUrl || config.serverUrl || "https://api.lasso.byorello.space";
|
|
80
|
+
const resolvedApiKey = apiKey || config.apiKey || config.lassoKey || "";
|
|
81
|
+
const runtimeClient = new runtime_client_1.RuntimeClient(targetServerUrl, resolvedApiKey || undefined);
|
|
82
|
+
onProgress("thinking", "Creating runtime session...", `Model: ${config.model || "claude-3-5-sonnet-20241022"}`);
|
|
83
|
+
const sessionInput = {
|
|
84
|
+
userPrompt: prompt,
|
|
85
|
+
project: {
|
|
86
|
+
projectId: "", // TODO: Get from workspace config
|
|
87
|
+
framework,
|
|
88
|
+
hasTypeScript: projectStructure.hasTypeScript,
|
|
89
|
+
hasReact: projectStructure.hasReact,
|
|
90
|
+
hasNext: projectStructure.hasNext,
|
|
91
|
+
hasVite: projectStructure.hasVite,
|
|
92
|
+
files: projectStructure.files.slice(0, 100),
|
|
93
|
+
currentRoute: pageContext?.route || "/",
|
|
94
|
+
},
|
|
95
|
+
model: {
|
|
96
|
+
provider: config.provider,
|
|
97
|
+
id: config.model || "claude-3-5-sonnet-20241022",
|
|
98
|
+
},
|
|
99
|
+
selection: pageContext ? {
|
|
100
|
+
element: pageContext.selectedElement ? {
|
|
101
|
+
tag: pageContext.selectedElement.tag,
|
|
102
|
+
text: pageContext.selectedElement.text,
|
|
103
|
+
source: pageContext.selectedElement.source,
|
|
104
|
+
attributes: pageContext.selectedElement.attributes,
|
|
105
|
+
} : undefined,
|
|
106
|
+
viewport: pageContext.viewport,
|
|
107
|
+
route: pageContext.route,
|
|
108
|
+
screenshot: pageContext.screenshot,
|
|
109
|
+
} : undefined,
|
|
478
110
|
};
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
onProgress(
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
catch (err) {
|
|
526
|
-
if (signal.aborted) {
|
|
527
|
-
return { ok: false, error: "Operation was cancelled" };
|
|
528
|
-
}
|
|
529
|
-
return await gatewayFallback(err instanceof Error ? err.message : `${cliLabel(config.provider)} failed`);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
if (useCliAgent && isConversational) {
|
|
533
|
-
try {
|
|
534
|
-
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 }));
|
|
535
|
-
return {
|
|
536
|
-
ok: true,
|
|
537
|
-
summary: reply,
|
|
538
|
-
changes: [],
|
|
539
|
-
thinking: [],
|
|
540
|
-
totalThinkingTimeMs: Date.now() - startTime,
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
catch (err) {
|
|
544
|
-
if (signal.aborted) {
|
|
545
|
-
return { ok: false, error: "Operation was cancelled" };
|
|
546
|
-
}
|
|
547
|
-
return await gatewayFallback(err instanceof Error ? err.message : `${cliLabel(config.provider)} failed`);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
if (shouldRouteToServer) {
|
|
551
|
-
return await callGateway({ provider: config.provider, model: config.model });
|
|
552
|
-
}
|
|
553
|
-
// If it's a conversational prompt and server was not used or failed, respond directly without modifying files!
|
|
554
|
-
if (isConversational) {
|
|
555
|
-
const reply = await generateConversationalReply(cwd, prompt, framework || "", config, signal, messages, pageContext);
|
|
556
|
-
return {
|
|
557
|
-
ok: true,
|
|
558
|
-
summary: reply,
|
|
559
|
-
changes: [],
|
|
560
|
-
thinking: [],
|
|
561
|
-
totalThinkingTimeMs: Date.now() - startTime,
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
// Phase 2: Local agent generation fallback for code changes
|
|
565
|
-
onProgress("thinking", "Generating implementation plan...", `Planning components for ${prompt}`);
|
|
566
|
-
const plan = await generatePlan(cwd, prompt, scope, projectStructure || { framework: "", hasTypeScript: false, hasTailwind: false, hasReact: false, hasNext: false, hasVite: false, dependencies: [], devDependencies: [], files: [], directories: [], sourceRoot: ".", appRouter: false, pagesRouter: false, multipleAppDirs: [] }, framework || "", config, signal, messages, pageContext);
|
|
567
|
-
if (signal.aborted) {
|
|
568
|
-
return { ok: false, error: "Operation was cancelled during planning" };
|
|
569
|
-
}
|
|
570
|
-
if (plan.isConversational || (plan.reply && (!plan.steps || plan.steps.length === 0))) {
|
|
571
|
-
return {
|
|
572
|
-
ok: true,
|
|
573
|
-
summary: plan.reply || "How can I help you with your project?",
|
|
574
|
-
changes: [],
|
|
575
|
-
thinking: [],
|
|
576
|
-
totalThinkingTimeMs: Date.now() - startTime,
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
if (!plan.steps.length) {
|
|
580
|
-
return {
|
|
581
|
-
ok: false,
|
|
582
|
-
error: "Could not generate file modifications. Check your AI provider configuration or prompt clarity.",
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
onProgress("thinking", "Execution plan ready", `${plan.steps.length} steps identified`);
|
|
586
|
-
// Phase 3: Execute plan step by step
|
|
587
|
-
const allChanges = [];
|
|
588
|
-
const thinkingSteps = [
|
|
589
|
-
{ title: "Analyzed project structure", detail: `Detected ${framework} framework` },
|
|
590
|
-
{ title: "Formulated execution steps", detail: `${plan.steps.length} steps planned` },
|
|
591
|
-
];
|
|
592
|
-
for (let i = 0; i < plan.steps.length; i++) {
|
|
593
|
-
if (signal.aborted) {
|
|
594
|
-
return { ok: false, error: "Operation was cancelled during execution" };
|
|
595
|
-
}
|
|
596
|
-
const step = plan.steps[i];
|
|
597
|
-
onProgress("working", `Step ${i + 1}/${plan.steps.length}: ${step.description}`, step.detail || "");
|
|
598
|
-
thinkingSteps.push({ title: step.description, detail: step.detail });
|
|
599
|
-
const stepResult = await executeStep(cwd, step, config, signal, onProgress, onPrompt);
|
|
600
|
-
if (!stepResult.ok) {
|
|
601
|
-
return { ok: false, error: `Failed at step ${i + 1}: ${stepResult.error}` };
|
|
602
|
-
}
|
|
603
|
-
if (stepResult.changes) {
|
|
604
|
-
allChanges.push(...stepResult.changes);
|
|
605
|
-
}
|
|
606
|
-
onProgress("working", `Step ${i + 1}/${plan.steps.length} complete`, step.description);
|
|
607
|
-
}
|
|
608
|
-
// Phase 4: Validate
|
|
609
|
-
onProgress("working", "Validating changes...", "Checking syntax and structure");
|
|
610
|
-
thinkingSteps.push({ title: "Validating changes", detail: "Checked syntax and file structure" });
|
|
111
|
+
const session = await runtimeClient.createSession(sessionInput);
|
|
112
|
+
const thinkingSteps = [];
|
|
113
|
+
let changes = [];
|
|
114
|
+
await runtimeClient.runSession(session.id, cwd, (event) => {
|
|
115
|
+
if (signal.aborted)
|
|
116
|
+
return;
|
|
117
|
+
switch (event.type) {
|
|
118
|
+
case "agent_started":
|
|
119
|
+
onProgress("thinking", "Agent started", "Initializing runtime");
|
|
120
|
+
break;
|
|
121
|
+
case "agent_reasoning":
|
|
122
|
+
const summary = event.data.summary || "Thinking...";
|
|
123
|
+
thinkingSteps.push({ title: summary, durationMs: 0 });
|
|
124
|
+
onProgress("thinking", summary);
|
|
125
|
+
break;
|
|
126
|
+
case "tool_started":
|
|
127
|
+
const tool = event.data.tool || "tool";
|
|
128
|
+
onProgress("working", `Running ${tool}...`);
|
|
129
|
+
break;
|
|
130
|
+
case "tool_completed":
|
|
131
|
+
onProgress("working", "Tool completed");
|
|
132
|
+
break;
|
|
133
|
+
case "phase_changed":
|
|
134
|
+
const phase = event.data.phase;
|
|
135
|
+
onProgress("working", `Phase: ${phase}`);
|
|
136
|
+
break;
|
|
137
|
+
case "verification_started":
|
|
138
|
+
onProgress("review", "Verifying changes...");
|
|
139
|
+
break;
|
|
140
|
+
case "verification_completed":
|
|
141
|
+
const passed = event.data.success ?? event.data.passed;
|
|
142
|
+
onProgress(passed ? "working" : "error", passed ? "Verification passed" : "Verification failed");
|
|
143
|
+
break;
|
|
144
|
+
case "waiting_for_user":
|
|
145
|
+
const question = event.data?.question || "Input required";
|
|
146
|
+
onPrompt({ question });
|
|
147
|
+
break;
|
|
148
|
+
case "error":
|
|
149
|
+
onProgress("error", event.data.error || "Unknown error");
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}, signal);
|
|
153
|
+
// Get final session state to extract changes
|
|
154
|
+
const finalSession = await runtimeClient.getSession(session.id);
|
|
155
|
+
changes = extractChangesFromSession(finalSession);
|
|
611
156
|
const totalThinkingTimeMs = Date.now() - startTime;
|
|
612
|
-
onProgress("review", "Implementation complete", `${allChanges.length} files modified`);
|
|
613
157
|
return {
|
|
614
158
|
ok: true,
|
|
615
|
-
summary:
|
|
616
|
-
|
|
617
|
-
changes: allChanges,
|
|
159
|
+
summary: "Task completed",
|
|
160
|
+
changes,
|
|
618
161
|
thinking: thinkingSteps,
|
|
619
162
|
totalThinkingTimeMs,
|
|
620
163
|
};
|
|
621
164
|
}
|
|
622
165
|
catch (error) {
|
|
623
|
-
|
|
624
|
-
return { ok: false, error: `One-shot agent failed: ${reason}` };
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
function extractJson(text) {
|
|
628
|
-
try {
|
|
629
|
-
return JSON.parse(text);
|
|
630
|
-
}
|
|
631
|
-
catch { }
|
|
632
|
-
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
633
|
-
if (match) {
|
|
634
|
-
try {
|
|
635
|
-
return JSON.parse(match[1]);
|
|
636
|
-
}
|
|
637
|
-
catch { }
|
|
638
|
-
}
|
|
639
|
-
const firstBrace = text.indexOf("{");
|
|
640
|
-
const lastBrace = text.lastIndexOf("}");
|
|
641
|
-
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
642
|
-
try {
|
|
643
|
-
return JSON.parse(text.slice(firstBrace, lastBrace + 1));
|
|
644
|
-
}
|
|
645
|
-
catch { }
|
|
646
|
-
}
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
async function analyzeProject(cwd) {
|
|
650
|
-
const pkgPath = node_path_1.default.join(cwd, "package.json");
|
|
651
|
-
const pkg = node_fs_1.default.existsSync(pkgPath) ? JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8")) : {};
|
|
652
|
-
const dependencies = Object.keys(pkg.dependencies || {});
|
|
653
|
-
const devDependencies = Object.keys(pkg.devDependencies || {});
|
|
654
|
-
const hasTypeScript = dependencies.includes("typescript") ||
|
|
655
|
-
devDependencies.includes("typescript") ||
|
|
656
|
-
node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"));
|
|
657
|
-
const hasTailwind = dependencies.includes("tailwindcss") || devDependencies.includes("tailwindcss");
|
|
658
|
-
const hasReact = dependencies.includes("react") || devDependencies.includes("react");
|
|
659
|
-
const hasNext = dependencies.includes("next") || devDependencies.includes("next");
|
|
660
|
-
const hasVite = dependencies.includes("vite") || devDependencies.includes("vite");
|
|
661
|
-
// FAST CONTEXT: Only scan top-level directories, not full recursive scan
|
|
662
|
-
// This is the key optimization - let the server-side agent handle file discovery
|
|
663
|
-
const files = [];
|
|
664
|
-
const directories = [];
|
|
665
|
-
const appDirs = [];
|
|
666
|
-
const pagesDirs = [];
|
|
667
|
-
const roots = ["src", "app", "pages", "components", "lib"];
|
|
668
|
-
for (const root of roots) {
|
|
669
|
-
const rootPath = node_path_1.default.join(cwd, root);
|
|
670
|
-
if (!node_fs_1.default.existsSync(rootPath))
|
|
671
|
-
continue;
|
|
672
|
-
// Only scan top-level (depth 0-1), not recursive
|
|
673
|
-
try {
|
|
674
|
-
const entries = node_fs_1.default.readdirSync(rootPath, { withFileTypes: true });
|
|
675
|
-
for (const entry of entries) {
|
|
676
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
const rel = entry.name;
|
|
680
|
-
if (entry.isDirectory()) {
|
|
681
|
-
directories.push(rel);
|
|
682
|
-
// Track app and pages directories at top level
|
|
683
|
-
if (entry.name === "app")
|
|
684
|
-
appDirs.push(rel);
|
|
685
|
-
if (entry.name === "pages")
|
|
686
|
-
pagesDirs.push(rel);
|
|
687
|
-
// Only scan one level deep for key directories
|
|
688
|
-
if (["components", "lib", "hooks", "utils", "styles"].includes(entry.name)) {
|
|
689
|
-
try {
|
|
690
|
-
const subEntries = node_fs_1.default.readdirSync(node_path_1.default.join(rootPath, entry.name), { withFileTypes: true });
|
|
691
|
-
for (const subEntry of subEntries) {
|
|
692
|
-
if (!subEntry.name.startsWith(".") && !subEntry.isDirectory() && /\.(tsx?|jsx?|vue|svelte|css)$/i.test(subEntry.name)) {
|
|
693
|
-
files.push(node_path_1.default.join(rel, subEntry.name));
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
catch {
|
|
698
|
-
// ignore scan errors
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
|
|
703
|
-
files.push(rel);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
708
|
-
// ignore scan errors
|
|
709
|
-
}
|
|
710
|
-
if (files.length > 20)
|
|
711
|
-
break; // Limit to 20 files for fast context
|
|
712
|
-
}
|
|
713
|
-
// Also check root directory for key files
|
|
714
|
-
try {
|
|
715
|
-
const rootEntries = node_fs_1.default.readdirSync(cwd, { withFileTypes: true });
|
|
716
|
-
for (const entry of rootEntries) {
|
|
717
|
-
if (!entry.isDirectory() && /\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
|
|
718
|
-
files.push(entry.name);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
catch {
|
|
723
|
-
// ignore scan errors
|
|
724
|
-
}
|
|
725
|
-
// Determine source root
|
|
726
|
-
let sourceRoot = ".";
|
|
727
|
-
if (directories.includes("src/app"))
|
|
728
|
-
sourceRoot = "src";
|
|
729
|
-
else if (directories.includes("app"))
|
|
730
|
-
sourceRoot = ".";
|
|
731
|
-
else if (directories.includes("src"))
|
|
732
|
-
sourceRoot = "src";
|
|
733
|
-
// Detect App Router vs Pages Router (lightweight check)
|
|
734
|
-
const appRouter = appDirs.length > 0 && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "app", "layout.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "app", "page.tsx"));
|
|
735
|
-
const pagesRouter = pagesDirs.length > 0 && (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_app.tsx")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pages", "_document.tsx")));
|
|
736
|
-
return {
|
|
737
|
-
framework: (0, framework_1.detectFramework)(cwd),
|
|
738
|
-
hasTypeScript,
|
|
739
|
-
hasTailwind,
|
|
740
|
-
hasReact,
|
|
741
|
-
hasNext,
|
|
742
|
-
hasVite,
|
|
743
|
-
dependencies: dependencies,
|
|
744
|
-
devDependencies: devDependencies,
|
|
745
|
-
files,
|
|
746
|
-
directories,
|
|
747
|
-
sourceRoot,
|
|
748
|
-
appRouter,
|
|
749
|
-
pagesRouter,
|
|
750
|
-
multipleAppDirs: appDirs,
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
async function generatePlan(cwd, prompt, _scope, structure, framework, config, signal, _messages, pageContext) {
|
|
754
|
-
const steps = [];
|
|
755
|
-
// Identify relevant files to supply as context.
|
|
756
|
-
// Priority 1: Any CSS / SCSS files (global styles are the most common source of regressions)
|
|
757
|
-
// Priority 2: Core app entry points
|
|
758
|
-
// Priority 3: Any component mentioned by keyword in the prompt
|
|
759
|
-
const promptLower = prompt.toLowerCase();
|
|
760
|
-
const allCssFiles = structure.files.filter((f) => /\.(css|scss)$/i.test(f));
|
|
761
|
-
const entryFiles = structure.files.filter((f) => /(App|page|index|layout|main)\.(tsx|jsx|js|ts|html)$/i.test(f));
|
|
762
|
-
// Pull component files whose filename appears in the user prompt
|
|
763
|
-
const mentionedFiles = structure.files.filter((f) => {
|
|
764
|
-
const base = node_path_1.default.basename(f, node_path_1.default.extname(f)).toLowerCase();
|
|
765
|
-
return base.length > 2 && promptLower.includes(base);
|
|
766
|
-
});
|
|
767
|
-
const targetFiles = [...new Set([...allCssFiles, ...entryFiles, ...mentionedFiles])].slice(0, 8);
|
|
768
|
-
const fileSnippets = targetFiles.map((f) => {
|
|
769
|
-
try {
|
|
770
|
-
const content = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, f), "utf8");
|
|
771
|
-
return `File: ${f}\n\`\`\`\n${content.slice(0, 8000)}\n\`\`\``;
|
|
772
|
-
}
|
|
773
|
-
catch {
|
|
774
|
-
return "";
|
|
775
|
-
}
|
|
776
|
-
}).filter(Boolean).join("\n\n");
|
|
777
|
-
let pageObservation = "";
|
|
778
|
-
if (pageContext) {
|
|
779
|
-
const headings = (pageContext.domSummary?.headings || []).map((h) => `H${h.level}: "${h.text}"`).join("\n ");
|
|
780
|
-
const sections = (pageContext.domSummary?.sections || []).map((s) => `- [${s.name}]: ${s.textPreview}`).join("\n ");
|
|
781
|
-
const buttons = (pageContext.domSummary?.buttons || []).join(", ");
|
|
782
|
-
pageObservation = `
|
|
783
|
-
Live Browser Page Context (currently rendered):
|
|
784
|
-
- Route: ${pageContext.route || "/"}
|
|
785
|
-
- Title: "${pageContext.title || "Untitled"}"
|
|
786
|
-
- Viewport: ${pageContext.viewport?.width}x${pageContext.viewport?.height} (Height: ${pageContext.viewport?.scrollHeight}px)
|
|
787
|
-
- Rendered Headings: ${headings || "None"}
|
|
788
|
-
- Rendered Sections: ${sections || "None"}
|
|
789
|
-
- Action Buttons: ${buttons || "None"}
|
|
790
|
-
${runtimeErrorObservation(pageContext)}
|
|
791
|
-
`;
|
|
792
|
-
}
|
|
793
|
-
const systemPrompt = `You are Lasso's Agentic Coding Agent embedded in a live ${framework} web app.
|
|
794
|
-
All project files: ${structure.files.slice(0, 50).join(", ")}
|
|
795
|
-
|
|
796
|
-
## CRITICAL RULES — read before generating anything:
|
|
797
|
-
1. You will receive the full content of ALL CSS/style files. Treat them as ground truth for the design system. Do NOT rewrite, restructure, or remove existing CSS classes — only add new rules or make targeted changes the user explicitly asked for.
|
|
798
|
-
2. Before writing any code, declare your TASK SCOPE: the exact list of files you will touch. You must NOT modify any file outside this scope.
|
|
799
|
-
3. When modifying an existing file, output the COMPLETE updated file content — never partial snippets.
|
|
800
|
-
4. If the user's request only affects one component, do not touch global stylesheets or unrelated files.
|
|
801
|
-
5. If the Live Browser Page Context lists console/runtime errors, treat fixing them as part of this task (a broken import or undefined export means the page is dead) and never propose code that would produce new ones.
|
|
802
|
-
|
|
803
|
-
Analyze the user's request.
|
|
804
|
-
If the request is a general question, explanation, greeting, or does not require file modifications, return strictly valid JSON:
|
|
805
|
-
{
|
|
806
|
-
"isConversational": true,
|
|
807
|
-
"reply": "Clear, friendly, conversational answer or explanation"
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
If the request asks to build, modify, create, fix, style, or refactor code, return strictly valid JSON:
|
|
811
|
-
{
|
|
812
|
-
"isConversational": false,
|
|
813
|
-
"summary": "Clear, concise user-facing description of what was changed",
|
|
814
|
-
"todo": [
|
|
815
|
-
{
|
|
816
|
-
"file": "relative/file/path.tsx",
|
|
817
|
-
"action": "modify" | "create" | "delete",
|
|
818
|
-
"reason": "One sentence explaining exactly what will change and why"
|
|
819
|
-
}
|
|
820
|
-
],
|
|
821
|
-
"steps": [
|
|
822
|
-
{
|
|
823
|
-
"type": "install" | "modify" | "create",
|
|
824
|
-
"target": "package name (e.g. '@iconify/react @hugeicons/react' for install) or relative/file/path.tsx",
|
|
825
|
-
"description": "Short action title",
|
|
826
|
-
"detail": "Brief detail",
|
|
827
|
-
"content": "The full complete new or updated file content (required for modify/create, omit for install)"
|
|
828
|
-
}
|
|
829
|
-
]
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
If any third-party npm libraries or packages are needed, add an "install" step first before modifying or creating components.`;
|
|
833
|
-
const userPrompt = `User Request: ${prompt}
|
|
834
|
-
${pageObservation}
|
|
835
|
-
Existing code context (study these carefully before writing — especially the CSS files):
|
|
836
|
-
${fileSnippets || "No existing components found. Create appropriate files in src/."}
|
|
837
|
-
|
|
838
|
-
Analyze the existing code structure, then respond in JSON:`;
|
|
839
|
-
if (!config.apiKey && config.provider !== "ollama" && !isCliProvider(config.provider)) {
|
|
166
|
+
console.error("[Lasso] OneShot error:", error);
|
|
840
167
|
return {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
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.",
|
|
168
|
+
ok: false,
|
|
169
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
844
170
|
};
|
|
845
171
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
172
|
+
}
|
|
173
|
+
function extractChangesFromSession(session) {
|
|
174
|
+
if (!session)
|
|
175
|
+
return [];
|
|
176
|
+
const pending = session.metadata?.pendingChanges;
|
|
177
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
178
|
+
return pending.map((change) => ({
|
|
179
|
+
filePath: change.filePath ?? change.path,
|
|
180
|
+
oldString: change.oldString ?? "",
|
|
181
|
+
newString: change.newString ?? "",
|
|
182
|
+
operation: change.operation ||
|
|
183
|
+
(change.oldString === "" && change.newString === "" ? "deleted" : change.oldString === "" ? "created" : "modified"),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
const changes = [];
|
|
187
|
+
for (const action of session.actions || []) {
|
|
188
|
+
if (!action.data)
|
|
189
|
+
continue;
|
|
190
|
+
const filePath = action.data.filePath ?? action.data.path;
|
|
191
|
+
if (!filePath)
|
|
192
|
+
continue;
|
|
193
|
+
if (action.type === "edit_file") {
|
|
194
|
+
changes.push({
|
|
195
|
+
filePath,
|
|
196
|
+
oldString: action.data.oldString ?? "",
|
|
197
|
+
newString: action.data.newString ?? "",
|
|
198
|
+
operation: "modified",
|
|
870
199
|
});
|
|
871
|
-
if (res.ok) {
|
|
872
|
-
const json = await res.json();
|
|
873
|
-
text = json.choices?.[0]?.message?.content || "";
|
|
874
|
-
}
|
|
875
200
|
}
|
|
876
|
-
else if (
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
883
|
-
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
|
|
884
|
-
}),
|
|
201
|
+
else if (action.type === "create_file") {
|
|
202
|
+
changes.push({
|
|
203
|
+
filePath,
|
|
204
|
+
oldString: "",
|
|
205
|
+
newString: action.data.content || "",
|
|
206
|
+
operation: "created",
|
|
885
207
|
});
|
|
886
|
-
if (res.ok) {
|
|
887
|
-
const json = await res.json();
|
|
888
|
-
text = json.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "";
|
|
889
|
-
}
|
|
890
208
|
}
|
|
891
|
-
else {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
model,
|
|
898
|
-
max_tokens: 4096,
|
|
899
|
-
system: systemPrompt,
|
|
900
|
-
messages: [{ role: "user", content: userPrompt }],
|
|
901
|
-
}),
|
|
209
|
+
else if (action.type === "delete_file") {
|
|
210
|
+
changes.push({
|
|
211
|
+
filePath,
|
|
212
|
+
oldString: action.data.oldContent ?? "(deleted)",
|
|
213
|
+
newString: "",
|
|
214
|
+
operation: "deleted",
|
|
902
215
|
});
|
|
903
|
-
if (res.ok) {
|
|
904
|
-
const json = await res.json();
|
|
905
|
-
text = json.content?.find((item) => item.type === "text")?.text || "";
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
const parsed = extractJson(text);
|
|
909
|
-
if (parsed) {
|
|
910
|
-
if (parsed.isConversational || (typeof parsed.reply === "string" && (!parsed.steps || parsed.steps.length === 0))) {
|
|
911
|
-
return {
|
|
912
|
-
steps: [],
|
|
913
|
-
isConversational: true,
|
|
914
|
-
reply: parsed.reply || text.trim(),
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
if (Array.isArray(parsed.steps) && parsed.steps.length > 0) {
|
|
918
|
-
return {
|
|
919
|
-
steps: parsed.steps,
|
|
920
|
-
summary: parsed.summary,
|
|
921
|
-
todo: Array.isArray(parsed.todo) ? parsed.todo : undefined,
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
if (text.trim().length > 0) {
|
|
926
|
-
return {
|
|
927
|
-
steps: [],
|
|
928
|
-
isConversational: true,
|
|
929
|
-
reply: text.trim(),
|
|
930
|
-
};
|
|
931
216
|
}
|
|
932
217
|
}
|
|
933
|
-
|
|
934
|
-
console.warn("[lasso] Local agent generation failed:", err);
|
|
935
|
-
}
|
|
936
|
-
return { steps };
|
|
937
|
-
}
|
|
938
|
-
async function executeStep(cwd, step, _config, signal, _onProgress, _onPrompt) {
|
|
939
|
-
if (signal.aborted) {
|
|
940
|
-
return { ok: false, error: "Cancelled" };
|
|
941
|
-
}
|
|
942
|
-
switch (step.type) {
|
|
943
|
-
case "install":
|
|
944
|
-
if (step.target) {
|
|
945
|
-
let cmd = "npm install";
|
|
946
|
-
try {
|
|
947
|
-
if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "bun.lockb")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "bun.lock")))
|
|
948
|
-
cmd = "bun add";
|
|
949
|
-
else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pnpm-lock.yaml")))
|
|
950
|
-
cmd = "pnpm add";
|
|
951
|
-
else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "yarn.lock")))
|
|
952
|
-
cmd = "yarn add";
|
|
953
|
-
}
|
|
954
|
-
catch { }
|
|
955
|
-
const commandStr = `${cmd} ${step.target}`;
|
|
956
|
-
_onProgress("working", `Installing ${step.target}…`, commandStr);
|
|
957
|
-
await execAsync(commandStr, { cwd, timeout: 120000 });
|
|
958
|
-
}
|
|
959
|
-
return { ok: true };
|
|
960
|
-
case "create":
|
|
961
|
-
if (step.target && step.content) {
|
|
962
|
-
const filePath = node_path_1.default.join(cwd, step.target);
|
|
963
|
-
const dir = node_path_1.default.dirname(filePath);
|
|
964
|
-
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
965
|
-
return {
|
|
966
|
-
ok: true,
|
|
967
|
-
changes: [
|
|
968
|
-
{
|
|
969
|
-
filePath: step.target,
|
|
970
|
-
oldString: "",
|
|
971
|
-
newString: step.content,
|
|
972
|
-
},
|
|
973
|
-
],
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
return { ok: false, error: "Missing target or content for create step" };
|
|
977
|
-
case "modify":
|
|
978
|
-
if (step.target && step.content) {
|
|
979
|
-
const filePath = node_path_1.default.join(cwd, step.target);
|
|
980
|
-
if (node_fs_1.default.existsSync(filePath)) {
|
|
981
|
-
const oldContent = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
982
|
-
return {
|
|
983
|
-
ok: true,
|
|
984
|
-
changes: [
|
|
985
|
-
{
|
|
986
|
-
filePath: step.target,
|
|
987
|
-
oldString: oldContent,
|
|
988
|
-
newString: step.content,
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
return { ok: false, error: `File not found: ${step.target}` };
|
|
994
|
-
}
|
|
995
|
-
return { ok: false, error: "Missing target or content for modify step" };
|
|
996
|
-
case "delete":
|
|
997
|
-
return { ok: true };
|
|
998
|
-
case "command":
|
|
999
|
-
return { ok: true };
|
|
1000
|
-
default:
|
|
1001
|
-
return { ok: true };
|
|
1002
|
-
}
|
|
218
|
+
return changes;
|
|
1003
219
|
}
|