@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.
Files changed (59) hide show
  1. package/dist/cli/agent.d.ts +10 -89
  2. package/dist/cli/agent.js +66 -1412
  3. package/dist/cli/backends.d.ts +53 -0
  4. package/dist/cli/backends.js +267 -0
  5. package/dist/cli/bridge-old.d.ts +259 -0
  6. package/dist/cli/bridge-old.js +1350 -0
  7. package/dist/cli/bridge.d.ts +22 -12
  8. package/dist/cli/bridge.js +824 -1174
  9. package/dist/cli/host/install.js +12 -1
  10. package/dist/cli/host/next-host-entry.js +0 -2
  11. package/dist/cli/host/vite-host-entry.js +1 -3
  12. package/dist/cli/local-runtime.d.ts +28 -0
  13. package/dist/cli/local-runtime.js +294 -0
  14. package/dist/cli/oneshot.js +177 -961
  15. package/dist/cli/runtime-client.d.ts +310 -0
  16. package/dist/cli/runtime-client.js +178 -0
  17. package/dist/cli/snapshot.d.ts +3 -1
  18. package/dist/cli/snapshot.js +8 -1
  19. package/dist/overlay.js +44 -28
  20. package/dist/runtime/__tests__/adapter-normalization.test.d.ts +1 -0
  21. package/dist/runtime/__tests__/adapter-normalization.test.js +392 -0
  22. package/dist/runtime/__tests__/state-machine.test.d.ts +1 -0
  23. package/dist/runtime/__tests__/state-machine.test.js +107 -0
  24. package/dist/runtime/__tests__/verification-engine.test.d.ts +1 -0
  25. package/dist/runtime/__tests__/verification-engine.test.js +91 -0
  26. package/dist/runtime/__tests__/workspace-guard.test.d.ts +1 -0
  27. package/dist/runtime/__tests__/workspace-guard.test.js +104 -0
  28. package/dist/runtime/adapters/claude-code-adapter.d.ts +46 -0
  29. package/dist/runtime/adapters/claude-code-adapter.js +230 -0
  30. package/dist/runtime/adapters/codex-cli-adapter.d.ts +44 -0
  31. package/dist/runtime/adapters/codex-cli-adapter.js +202 -0
  32. package/dist/runtime/adapters/cursor-adapter.d.ts +47 -0
  33. package/dist/runtime/adapters/cursor-adapter.js +214 -0
  34. package/dist/runtime/adapters/factory.d.ts +19 -0
  35. package/dist/runtime/adapters/factory.js +48 -0
  36. package/dist/runtime/adapters/local-agent-adapter.d.ts +143 -0
  37. package/dist/runtime/adapters/local-agent-adapter.js +408 -0
  38. package/dist/runtime/adapters/opencode-adapter.d.ts +43 -0
  39. package/dist/runtime/adapters/opencode-adapter.js +210 -0
  40. package/dist/runtime/agent-runtime.d.ts +286 -0
  41. package/dist/runtime/agent-runtime.js +8 -0
  42. package/dist/runtime/agent-state-machine.d.ts +67 -0
  43. package/dist/runtime/agent-state-machine.js +234 -0
  44. package/dist/runtime/backend-interface.d.ts +272 -0
  45. package/dist/runtime/backend-interface.js +38 -0
  46. package/dist/runtime/event-mapper.d.ts +11 -0
  47. package/dist/runtime/event-mapper.js +40 -0
  48. package/dist/runtime/index.d.ts +20 -0
  49. package/dist/runtime/index.js +43 -0
  50. package/dist/runtime/local-session-runner.d.ts +60 -0
  51. package/dist/runtime/local-session-runner.js +185 -0
  52. package/dist/runtime/review.d.ts +24 -0
  53. package/dist/runtime/review.js +30 -0
  54. package/dist/runtime/verification-engine.d.ts +71 -0
  55. package/dist/runtime/verification-engine.js +374 -0
  56. package/dist/runtime/workspace-guard.d.ts +70 -0
  57. package/dist/runtime/workspace-guard.js +321 -0
  58. package/docs/local-coding-agents.md +183 -0
  59. package/package.json +22 -3
@@ -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 node_child_process_1 = require("node:child_process");
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
- const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
15
- const CLI_PROVIDERS = new Set(["claude-code", "codex", "opencode", "cursor"]);
16
- function isCliProvider(provider) {
17
- return Boolean(provider && CLI_PROVIDERS.has(provider));
18
- }
19
- function cliLabel(provider) {
20
- return provider === "claude-code" ? "Claude Code" : provider === "opencode" ? "OpenCode" : provider === "cursor" ? "Cursor" : provider === "codex" ? "Codex" : "CLI agent";
21
- }
22
- function cliCommand(provider) {
23
- return provider === "claude-code" ? "claude" : provider || "the agent CLI";
24
- }
25
- // The Lasso Agent Gateway only understands hosted providers, so a CLI selection is
26
- // normalized to the closest hosted provider/model when we have to fall back to it.
27
- const GATEWAY_MODELS = {
28
- anthropic: "claude-sonnet-4-5-20250929",
29
- openai: "gpt-4.1",
30
- google: "gemini-2.5-flash",
31
- };
32
- function gatewayTargetForCli(provider, model) {
33
- const bare = model && model.includes(":") ? model.slice(model.indexOf(":") + 1) : model || "";
34
- if (provider === "codex")
35
- return { provider: "openai", model: GATEWAY_MODELS.openai };
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
- 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.";
34
+ catch { }
133
35
  }
134
- try {
135
- const model = config.model;
136
- if (!model) {
137
- return "No model selected. Please select a model in settings.";
138
- }
139
- if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
140
- const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
141
- const res = await fetch(`${baseUrl}/chat/completions`, {
142
- method: "POST",
143
- signal,
144
- headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
145
- body: JSON.stringify({
146
- model,
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
- else {
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
- catch (err) {
199
- console.warn("[lasso] Local conversational reply generation error:", err);
200
- }
201
- 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.";
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
- const isConversational = await isConversationalPrompt(prompt, config, apiKey);
210
- console.log('[Lasso] Is conversational:', isConversational);
211
- // Browser inspection & snapshot steps - skip for simple conversational queries
212
- // FAST CONTEXT: Skip detailed DOM analysis if pageContext is already provided
213
- if (pageContext && !isConversational) {
214
- onProgress("thinking", "Analyzing page context...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
215
- // Skip the slow "Scrolling & analyzing" steps - the pageContext already has the data
216
- // Only show viewport info if available
217
- if (pageContext.viewport) {
218
- onProgress("thinking", "Ready to proceed...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height}`);
219
- }
220
- }
221
- else {
222
- onProgress("thinking", isConversational ? "Thinking..." : "Analyzing project structure...", isConversational ? undefined : "Scanning files and dependencies");
223
- }
224
- // Skip project structure analysis for simple conversational queries
225
- const projectStructure = isConversational ? null : await analyzeProject(cwd);
226
- const framework = isConversational ? null : (0, framework_1.detectFramework)(cwd);
227
- if (!isConversational && !pageContext) {
228
- onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
229
- }
230
- let rawServerUrl = serverUrl ||
231
- process.env.LASSO_SERVER_URL ||
232
- process.env.NEXT_PUBLIC_LASSO_SERVER_URL ||
233
- process.env.LASSO_API_URL ||
234
- "https://api.lasso.byorello.space";
235
- if (rawServerUrl.includes("collab.lasso.byorello.space")) {
236
- rawServerUrl = rawServerUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
237
- }
238
- const targetServerUrl = rawServerUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
239
- const useCliAgent = isCliProvider(config.provider);
240
- // Prioritize CLI auth credentials over env vars
241
- const cliCredentials = (0, auth_1.loadCredentials)();
242
- 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 || "";
243
- const hasLocalProviderKey = Boolean(config.apiKey &&
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
- // CLI agents (Claude Code, Codex, OpenCode, Cursor) authenticate themselves, so they run
480
- // locally first instead of demanding a provider API key from settings.
481
- if (useCliAgent && !isConversational) {
482
- onProgress("thinking", `Running ${cliLabel(config.provider)}...`, `Local CLI agent · ${config.model || ""}`);
483
- try {
484
- const selectedElement = pageContext?.selectedElement;
485
- const input = {
486
- taskId,
487
- instruction: prompt,
488
- model: config.model || "",
489
- messages,
490
- context: {
491
- runtimeErrors: pageContext?.runtimeErrors,
492
- screenshots: pageContext?.screenshot ? { full: pageContext.screenshot } : undefined,
493
- viewport: pageContext?.viewport,
494
- },
495
- element: {
496
- tag: selectedElement?.tag || "project",
497
- group: scope === "component" ? "component" : "workspace",
498
- label: selectedElement?.text || "project",
499
- html: selectedElement ? `<${selectedElement.tag}> ${selectedElement.text || ""}` : undefined,
500
- },
501
- };
502
- const proposal = await (0, agent_1.proposeChanges)(cwd, input, config, signal, (message, detail, level) => {
503
- onProgress(level === "error" ? "error" : "working", message, detail);
504
- }, (agentPrompt) => {
505
- onPrompt({ question: agentPrompt.message, options: agentPrompt.options });
506
- });
507
- if (signal.aborted) {
508
- return { ok: false, error: "Operation was cancelled" };
509
- }
510
- if (!proposal.changes.length) {
511
- return { ok: true, summary: proposal.summary, changes: [], thinking: [], totalThinkingTimeMs: Date.now() - startTime };
512
- }
513
- onProgress("review", "Implementation complete", `${proposal.changes.length} files modified`);
514
- return {
515
- ok: true,
516
- summary: proposal.summary,
517
- changes: proposal.changes,
518
- thinking: [
519
- { title: "Analyzed project structure", detail: `Detected ${framework} framework` },
520
- { title: `${cliLabel(config.provider)} generated a proposal`, detail: `${proposal.changes.length} file(s)` },
521
- ],
522
- totalThinkingTimeMs: Date.now() - startTime,
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: plan.summary || `Implemented changes for: ${prompt}`,
616
- todo: plan.todo,
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
- const reason = error instanceof Error ? error.message : "Unknown error";
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
- steps: [],
842
- isConversational: true,
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
- try {
847
- const model = config.model;
848
- if (!model) {
849
- return {
850
- steps: [],
851
- isConversational: true,
852
- reply: "No model selected. Please select a model in settings.",
853
- };
854
- }
855
- let text = "";
856
- if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
857
- const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
858
- const res = await fetch(`${baseUrl}/chat/completions`, {
859
- method: "POST",
860
- signal,
861
- headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
862
- body: JSON.stringify({
863
- model,
864
- temperature: 0.1,
865
- messages: [
866
- { role: "system", content: systemPrompt },
867
- { role: "user", content: userPrompt },
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 (config.provider === "google") {
877
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
878
- method: "POST",
879
- signal,
880
- headers: { "content-type": "application/json" },
881
- body: JSON.stringify({
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
- const res = await fetch("https://api.anthropic.com/v1/messages", {
893
- method: "POST",
894
- signal,
895
- headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
896
- body: JSON.stringify({
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
- catch (err) {
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
  }