@lasso-ai/cli 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/agent.js CHANGED
@@ -3,1450 +3,104 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.installPackages = installPackages;
7
6
  exports.respondToAgentPrompt = respondToAgentPrompt;
8
7
  exports.waitForAgentPrompt = waitForAgentPrompt;
8
+ exports.installPackages = installPackages;
9
9
  exports.executeWebSearch = executeWebSearch;
10
10
  exports.executeAgentCommand = executeAgentCommand;
11
- exports.detectLocalAgents = detectLocalAgents;
12
- exports.parseAgentAction = parseAgentAction;
13
- exports.runLocalAgentPrompt = runLocalAgentPrompt;
14
- exports.proposeChanges = proposeChanges;
15
- exports.answerQuestion = answerQuestion;
16
- exports.generateCommitMessage = generateCommitMessage;
17
11
  const node_fs_1 = __importDefault(require("node:fs"));
18
12
  const node_path_1 = __importDefault(require("node:path"));
19
13
  const node_child_process_1 = require("node:child_process");
20
14
  const node_util_1 = require("node:util");
21
- const auth_1 = require("./auth");
22
- // Optional layout-intelligence support (local dev only)
23
- let searchLayouts = null;
24
- try {
25
- const layoutModule = require("@lasso-ai/layout-intelligence");
26
- searchLayouts = layoutModule.searchLayouts;
27
- }
28
- catch {
29
- // layout-intelligence not available in production builds
30
- }
31
15
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
32
16
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
33
- function detectProjectStructure(cwd) {
34
- const pkgPath = node_path_1.default.join(cwd, "package.json");
35
- let framework = "unknown";
36
- const directories = [];
37
- const appDirs = [];
38
- if (node_fs_1.default.existsSync(pkgPath)) {
39
- try {
40
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
41
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
42
- if (deps.next)
43
- framework = "Next.js";
44
- else if (deps.vite)
45
- framework = "Vite";
46
- else if (deps.react)
47
- framework = "React";
48
- }
49
- catch {
50
- // Ignore errors
51
- }
52
- }
53
- // Scan for important directories
54
- const scanDirs = ["src", "app", "pages", "web", "lib", "components"];
55
- for (const dir of scanDirs) {
56
- if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, dir))) {
57
- directories.push(dir);
58
- if (dir === "app")
59
- appDirs.push(dir);
60
- // Check for src/app structure
61
- if (dir === "src" && node_fs_1.default.existsSync(node_path_1.default.join(cwd, "src", "app"))) {
62
- directories.push("src/app");
63
- appDirs.push("src/app");
64
- }
65
- }
66
- }
67
- // Determine source root
68
- let sourceRoot = ".";
69
- if (directories.includes("src/app"))
70
- sourceRoot = "src";
71
- else if (directories.includes("app"))
72
- sourceRoot = ".";
73
- else if (directories.includes("src"))
74
- sourceRoot = "src";
75
- return { framework, sourceRoot, directories, multipleAppDirs: appDirs };
76
- }
77
- async function installPackages(cwd, packages, onProgress) {
78
- const validPkgs = [...new Set(packages
79
- .map((p) => p.trim().replace(/^['"`]|['"`]$/g, ""))
80
- .filter((p) => /^(@[a-z0-9\-~][a-z0-9\-_.]*\/)?[a-z0-9\-~][a-z0-9\-_.]*$/i.test(p)))];
81
- if (validPkgs.length === 0)
82
- return [];
83
- // Check package.json to see what is already installed
84
- let existingDeps = {};
85
- try {
86
- const pkgJsonPath = node_path_1.default.join(cwd, "package.json");
87
- const raw = node_fs_1.default.readFileSync(pkgJsonPath, "utf8");
88
- const parsed = JSON.parse(raw);
89
- existingDeps = { ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) };
90
- }
91
- catch { }
92
- const needed = validPkgs.filter((pkg) => !existingDeps[pkg]);
93
- if (needed.length === 0)
94
- return validPkgs;
95
- // Detect package manager
96
- let cmd = "npm install";
97
- try {
98
- const rootFiles = node_fs_1.default.readdirSync(cwd);
99
- if (rootFiles.includes("bun.lockb") || rootFiles.includes("bun.lock"))
100
- cmd = "bun add";
101
- else if (rootFiles.includes("pnpm-lock.yaml"))
102
- cmd = "pnpm add";
103
- else if (rootFiles.includes("yarn.lock"))
104
- cmd = "yarn add";
105
- }
106
- catch { }
107
- const commandStr = `${cmd} ${needed.join(" ")}`;
108
- onProgress?.(`Installing dependencies (${needed.join(", ")})…`, commandStr, "working");
109
- try {
110
- await execAsync(commandStr, { cwd, timeout: 120000 });
111
- onProgress?.(`Installed ${needed.join(", ")}`, undefined, "working");
112
- }
113
- catch (err) {
114
- onProgress?.(`Package installation failed: ${err.message}`, undefined, "error");
115
- }
116
- return validPkgs;
117
- }
118
- const activeAgentInputs = new Map();
17
+ // Prompt handlers for runtime agent prompts
18
+ const agentPromptHandlers = new Map();
119
19
  function respondToAgentPrompt(taskId, value) {
120
- const respond = activeAgentInputs.get(taskId);
121
- if (!respond)
122
- return false;
123
- respond(value);
124
- activeAgentInputs.delete(taskId);
125
- return true;
20
+ const handler = agentPromptHandlers.get(taskId);
21
+ if (handler) {
22
+ handler(value);
23
+ agentPromptHandlers.delete(taskId);
24
+ return true;
25
+ }
26
+ return false;
126
27
  }
127
28
  function waitForAgentPrompt(taskId, timeoutMs = 120000) {
128
- return new Promise((resolve) => {
129
- const timer = setTimeout(() => {
130
- activeAgentInputs.delete(taskId);
131
- resolve("Skip");
29
+ return new Promise((resolve, reject) => {
30
+ const timeout = setTimeout(() => {
31
+ agentPromptHandlers.delete(taskId);
32
+ reject(new Error("Agent prompt timeout"));
132
33
  }, timeoutMs);
133
- activeAgentInputs.set(taskId, (value) => {
134
- clearTimeout(timer);
135
- activeAgentInputs.delete(taskId);
34
+ agentPromptHandlers.set(taskId, (value) => {
35
+ clearTimeout(timeout);
136
36
  resolve(value);
137
37
  });
138
38
  });
139
39
  }
140
- async function executeWebSearch(query, maxResults = 5) {
141
- try {
142
- const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
143
- const res = await fetch(url, {
144
- headers: {
145
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
146
- "Accept": "text/html",
147
- },
148
- signal: AbortSignal.timeout(10000),
149
- });
150
- if (!res.ok)
151
- return [];
152
- const html = await res.text();
153
- const results = [];
154
- const resultRegex = /<a class="result__snippet[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
155
- let match;
156
- while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
157
- const rawHref = match[1];
158
- const uddgMatch = rawHref.match(/uddg=([^&]+)/);
159
- const targetUrl = uddgMatch ? decodeURIComponent(uddgMatch[1]) : rawHref;
160
- const cleanSnippet = match[2]
161
- .replace(/<[^>]*>/g, "")
162
- .replace(/&amp;/g, "&")
163
- .replace(/&quot;/g, '"')
164
- .replace(/&#39;/g, "'")
165
- .replace(/&lt;/g, "<")
166
- .replace(/&gt;/g, ">")
167
- .replace(/\s+/g, " ")
168
- .trim();
169
- if (cleanSnippet && targetUrl.startsWith("http")) {
170
- results.push({
171
- title: cleanSnippet.slice(0, 80),
172
- url: targetUrl,
173
- snippet: cleanSnippet,
174
- });
175
- }
176
- }
177
- return results;
178
- }
179
- catch {
40
+ async function installPackages(cwd, packages, onProgress) {
41
+ const validPkgs = [...new Set(packages
42
+ .map((p) => p.trim().replace(/^['"`]|['"`]$/g, ""))
43
+ .filter((p) => /^(@[a-z0-9\-~][a-z0-9\-_.]*\/)?[a-z0-9\-~][a-z0-9\-_.]*$/i.test(p)))];
44
+ if (validPkgs.length === 0)
180
45
  return [];
181
- }
182
- }
183
- const BLOCKED_COMMANDS = /\b(rm\s+-rf\s+\/|shutdown|reboot|sudo|mkfs|:\(\)\{\|:&\}|chmod\s+-R\s+777\s+\/)\b/i;
184
- async function executeAgentCommand(cwd, command, timeout = 30000) {
185
- if (BLOCKED_COMMANDS.test(command)) {
186
- return { stdout: "", stderr: "Command rejected for safety reasons.", exitCode: 1 };
187
- }
46
+ if (onProgress)
47
+ onProgress("working", `Installing ${validPkgs.length} package(s)...`);
48
+ const pkgPath = node_path_1.default.join(cwd, "package.json");
49
+ let pkgManager = "npm";
50
+ if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pnpm-lock.yaml")))
51
+ pkgManager = "pnpm";
52
+ else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "yarn.lock")))
53
+ pkgManager = "yarn";
54
+ const installCmd = pkgManager === "pnpm" ? "add" : pkgManager === "yarn" ? "add" : "install";
55
+ const args = [installCmd, ...validPkgs, "--silent"];
188
56
  try {
189
- const { stdout, stderr } = await execAsync(command, { cwd, timeout });
190
- return {
191
- stdout: stdout.slice(0, 8000),
192
- stderr: stderr.slice(0, 2000),
193
- exitCode: 0,
194
- };
195
- }
196
- catch (err) {
197
- return {
198
- stdout: (err.stdout || "").slice(0, 8000),
199
- stderr: (err.stderr || err.message || "").slice(0, 2000),
200
- exitCode: err.code || 1,
201
- };
202
- }
203
- }
204
- async function detectLocalAgents() {
205
- const found = new Set();
206
- for (const [name, command] of [["claude-code", "claude"], ["codex", "codex"], ["opencode", "opencode"], ["cursor", "cursor"]]) {
207
- try {
208
- await execFileAsync(process.platform === "win32" ? "where.exe" : "which", [command]);
209
- found.add(name);
210
- }
211
- catch {
212
- // Local coding CLIs are optional.
213
- }
214
- }
215
- return found;
216
- }
217
- const ignored = new Set(["node_modules", ".git", ".next", "dist", "build", ".turbo"]);
218
- const sourceExtensions = /\.(tsx?|jsx?|vue|svelte|css|scss|html)$/i;
219
- const layoutFiles = /^(layout|_layout|_app|app|_document|document|template|_template)\.(tsx?|jsx?)$/i;
220
- const sourceFileCache = new Map();
221
- function sourceFiles(directory, budget = { remaining: 25 }) {
222
- if (budget.remaining <= 0)
223
- return [];
224
- const cached = sourceFileCache.get(directory);
225
- if (cached && cached.expiresAt > Date.now()) {
226
- const files = cached.files.slice(0, budget.remaining);
227
- budget.remaining -= files.length;
228
- return files;
229
- }
230
- const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true });
231
- const files = [];
232
- const layoutFilesList = [];
233
- const otherFiles = [];
234
- for (const entry of entries) {
235
- if (ignored.has(entry.name) || entry.name.startsWith("."))
236
- continue;
237
- const fullPath = node_path_1.default.join(directory, entry.name);
238
- if (budget.remaining <= 0)
239
- break;
240
- if (entry.isDirectory()) {
241
- const nested = sourceFiles(fullPath, budget);
242
- files.push(...nested);
243
- }
244
- else if (sourceExtensions.test(entry.name)) {
245
- if (layoutFiles.test(entry.name)) {
246
- layoutFilesList.push(fullPath);
247
- }
248
- else {
249
- otherFiles.push(fullPath);
250
- }
251
- }
252
- }
253
- // Prioritize layout files first, then other files
254
- files.push(...layoutFilesList, ...otherFiles);
255
- budget.remaining -= files.length;
256
- sourceFileCache.set(directory, { expiresAt: Date.now() + 30000, files });
257
- return files;
258
- }
259
- function contextFor(cwd, element) {
260
- const needle = element.label.replace(/^[^.#]+[.#]?/, "");
261
- const hintedPath = element.sourceHint?.split(":")[0];
262
- const sourceFile = hintedPath ? node_path_1.default.basename(hintedPath) : "";
263
- // Add project structure information at the top
264
- const structure = detectProjectStructure(cwd);
265
- let projectStructureInfo = `PROJECT STRUCTURE:
266
- - Framework: ${structure.framework}
267
- - Source root: ${structure.sourceRoot}
268
- - Directories found: ${structure.directories.join(", ") || "root"}
269
- - Multiple app directories: ${structure.multipleAppDirs.length > 0 ? structure.multipleAppDirs.join(", ") : "none"}
270
- - IMPORTANT: When creating files, use the existing directory structure. Do not create duplicate directories (e.g., if "src/app/" exists, use it instead of creating a new "app/").
271
- - Path aliases: Check tsconfig.json or jsconfig.json for path aliases like "@/components"
272
-
273
- `;
274
- // Extract unique class names and attributes from the element for better matching
275
- const classMatches = element.html?.match(/class="([^"]+)"/);
276
- const classNames = classMatches ? classMatches[1].split(/\s+/).filter((c) => c.length > 3) : [];
277
- const hrefMatch = element.html?.match(/href="([^"]+)"/);
278
- const href = hrefMatch ? hrefMatch[1] : "";
279
- if (hintedPath) {
280
- const candidates = [
281
- node_path_1.default.isAbsolute(hintedPath) ? hintedPath : node_path_1.default.resolve(cwd, hintedPath),
282
- ];
283
- const normalizedHint = hintedPath.replace(/\\/g, "/");
284
- const srcMarker = "/src/";
285
- const srcIndex = normalizedHint.lastIndexOf(srcMarker);
286
- if (srcIndex >= 0)
287
- candidates.push(node_path_1.default.join(cwd, normalizedHint.slice(srcIndex + 1)));
288
- for (const candidate of candidates) {
289
- try {
290
- const content = node_fs_1.default.readFileSync(candidate, "utf8");
291
- return `${projectStructureInfo}FILE: ${node_path_1.default.relative(cwd, candidate)}\n${content.slice(0, 16000)}`;
292
- }
293
- catch {
294
- // The runtime source hint can point to a different checkout.
295
- }
296
- }
297
- }
298
- const files = sourceFiles(cwd);
299
- const snippets = [];
300
- const scoredFiles = [];
301
- const results = files.map((file) => {
302
- try {
303
- const content = node_fs_1.default.readFileSync(file, "utf8");
304
- let score = 0;
305
- // Score files based on relevance
306
- if (sourceFile && file.endsWith(sourceFile))
307
- score += 10;
308
- if (content.includes(needle))
309
- score += 5;
310
- if (content.includes(element.label))
311
- score += 5;
312
- if (href && content.includes(href))
313
- score += 8;
314
- // Check for class name matches
315
- for (const className of classNames) {
316
- if (content.includes(className))
317
- score += 3;
318
- }
319
- // Boost score for component files
320
- if (file.includes("component") || file.includes("nav") || file.includes("header") || file.includes("layout")) {
321
- score += 2;
322
- }
323
- if (score > 0) {
324
- return { file, score, content };
325
- }
326
- }
327
- catch {
328
- // A file can disappear while a dev server is rebuilding; skip it.
329
- }
330
- return null;
331
- }).filter((r) => r !== null);
332
- // Sort by score and take top files
333
- const sortedResults = results.filter((r) => r !== null).sort((a, b) => b.score - a.score);
334
- for (const result of sortedResults) {
335
- if (snippets.length < 8) {
336
- snippets.push(`FILE: ${node_path_1.default.relative(cwd, result.file)}\n${result.content.slice(0, 6000)}`);
337
- }
338
- }
339
- return projectStructureInfo + snippets.join("\n\n---\n\n");
340
- }
341
- function jsonObjectCandidates(text) {
342
- const candidates = [];
343
- for (let start = 0; start < text.length; start += 1) {
344
- if (text[start] !== "{")
345
- continue;
346
- let depth = 0;
347
- let inString = false;
348
- let escaped = false;
349
- for (let index = start; index < text.length; index += 1) {
350
- const character = text[index];
351
- if (inString) {
352
- if (escaped)
353
- escaped = false;
354
- else if (character === "\\")
355
- escaped = true;
356
- else if (character === '"')
357
- inString = false;
358
- continue;
359
- }
360
- if (character === '"') {
361
- inString = true;
362
- }
363
- else if (character === "{") {
364
- depth += 1;
365
- }
366
- else if (character === "}" && --depth === 0) {
367
- candidates.push(text.slice(start, index + 1));
368
- break;
369
- }
370
- }
371
- }
372
- return candidates;
373
- }
374
- function jsonFrom(text) {
375
- // Remove common progress prefixes and tool output noise
376
- const cleaned = text
377
- .replace(/^(Thinking|Working|Planning|Analyzing|Executing|Searching|Scanning)\.\.\.?$/gm, '')
378
- .replace(/^›/gm, '')
379
- .replace(/^>/gm, '')
380
- .replace(/^(grep|find|cat|ls|cd|npm|pnpm|yarn|git).*$/gm, '')
381
- .replace(/^src\/.*:\d+:.*$/gm, '')
382
- .replace(/^(The agent|Error|Warning|Info|Note):.*$/gm, '')
383
- .replace(/^(Progress|Activity|Output|Result):.*$/gm, '')
384
- .trim();
385
- const fenced = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
386
- const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(cleaned)];
387
- for (const candidate of candidates) {
388
- try {
389
- const parsed = JSON.parse(candidate.trim());
390
- if (!Array.isArray(parsed.changes))
391
- continue;
392
- for (const change of parsed.changes) {
393
- if (!change.filePath || typeof change.oldString !== "string" || typeof change.newString !== "string") {
394
- throw new Error("The agent returned an invalid file change.");
395
- }
396
- }
397
- const packages = Array.isArray(parsed.packages)
398
- ? parsed.packages.map((p) => String(p).trim()).filter(Boolean)
399
- : [];
400
- return {
401
- summary: parsed.summary || (packages.length > 0 ? `Installed ${packages.join(", ")} and updated components.` : "The proposed source changes are ready for review."),
402
- packages,
403
- changes: parsed.changes,
404
- };
405
- }
406
- catch (error) {
407
- if (error instanceof Error && error.message === "The agent returned an invalid file change.")
408
- throw error;
409
- }
410
- }
411
- throw new Error("The agent returned no valid reviewable changes. Progress output may have been mixed with the final JSON.");
412
- }
413
- function parseAgentAction(text) {
414
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
415
- const candidates = [...(fenced ? [fenced] : []), ...jsonObjectCandidates(text)];
416
- for (const candidate of candidates) {
417
- try {
418
- const parsed = JSON.parse(candidate.trim());
419
- if (parsed.action === "run_command" && typeof parsed.command === "string" && parsed.command.trim()) {
420
- return { type: "run_command", command: parsed.command.trim(), thought: parsed.thought };
421
- }
422
- if (parsed.action === "web_search" && typeof parsed.query === "string" && parsed.query.trim()) {
423
- return { type: "web_search", query: parsed.query.trim(), thought: parsed.thought };
424
- }
425
- if (parsed.action === "ask_user" && typeof parsed.question === "string" && parsed.question.trim()) {
426
- return {
427
- type: "ask_user",
428
- question: parsed.question.trim(),
429
- kind: parsed.kind === "multiselect" ? "multiselect" : parsed.kind === "input" ? "input" : "permission",
430
- options: Array.isArray(parsed.options) ? parsed.options.map(String) : undefined,
431
- thought: parsed.thought,
432
- };
433
- }
434
- if (Array.isArray(parsed.changes)) {
435
- for (const change of parsed.changes) {
436
- if (!change.filePath || typeof change.oldString !== "string" || typeof change.newString !== "string") {
437
- throw new Error("The agent returned an invalid file change.");
438
- }
439
- }
440
- const packages = Array.isArray(parsed.packages) ? parsed.packages.map((p) => String(p).trim()).filter(Boolean) : [];
441
- return {
442
- type: "proposal",
443
- summary: parsed.summary || (packages.length > 0 ? `Installed ${packages.join(", ")} and updated components.` : "The proposed source changes are ready for review."),
444
- packages,
445
- changes: parsed.changes,
446
- };
447
- }
448
- }
449
- catch (error) {
450
- if (error instanceof Error && error.message === "The agent returned an invalid file change.")
451
- throw error;
452
- }
453
- }
454
- const fallback = jsonFrom(text);
455
- return { type: "proposal", ...fallback };
456
- }
457
- function extractLocalAgentText(raw, provider) {
458
- if (provider === "claude-code") {
459
- try {
460
- const payload = JSON.parse(raw);
461
- if (payload.result)
462
- return payload.result;
463
- }
464
- catch {
465
- // Stream-json output is handled below.
466
- }
467
- const results = [];
468
- for (const line of raw.split(/\r?\n/)) {
469
- try {
470
- const event = JSON.parse(line);
471
- if (typeof event.result === "string")
472
- results.push(event.result);
473
- const message = event.message;
474
- for (const part of message?.content || [])
475
- if (part.type === "text" && part.text)
476
- results.push(part.text);
477
- }
478
- catch {
479
- // Ignore progress lines that are not JSON.
480
- }
481
- }
482
- return results.at(-1) || raw;
483
- }
484
- if (provider === "cursor") {
485
- // Cursor uses similar output format to claude-code
486
- try {
487
- const payload = JSON.parse(raw);
488
- if (payload.result)
489
- return payload.result;
490
- }
491
- catch {
492
- // Stream-json output is handled below.
493
- }
494
- const results = [];
495
- for (const line of raw.split(/\r?\n/)) {
496
- try {
497
- const event = JSON.parse(line);
498
- if (typeof event.result === "string")
499
- results.push(event.result);
500
- const message = event.message;
501
- for (const part of message?.content || [])
502
- if (part.type === "text" && part.text)
503
- results.push(part.text);
504
- }
505
- catch {
506
- // Ignore progress lines that are not JSON.
507
- }
508
- }
509
- return results.at(-1) || raw;
510
- }
511
- const texts = [];
512
- for (const line of raw.split(/\r?\n/)) {
513
- try {
514
- const event = JSON.parse(line);
515
- const item = event.item;
516
- const part = event.part;
517
- const candidates = provider === "opencode"
518
- ? [part?.type === "text" ? part.text : undefined, event.text, event.output_text]
519
- : [event.text, event.output_text, item?.text, item?.output_text, item?.message];
520
- for (const candidate of candidates) {
521
- if (typeof candidate === "string" && candidate.trim())
522
- texts.push(candidate);
523
- }
524
- }
525
- catch {
526
- // Ignore non-JSON progress lines.
527
- }
57
+ await execFileAsync(pkgManager, args, { cwd, maxBuffer: 1024 * 1024 });
58
+ return validPkgs;
528
59
  }
529
- return texts.at(-1) || raw;
530
- }
531
- function collectTextFields(value, fields = new Set(["text", "output_text", "result", "content", "message", "output", "value"]), depth = 0) {
532
- if (depth > 8 || value == null)
533
- return [];
534
- if (typeof value === "string")
535
- return [value];
536
- if (Array.isArray(value))
537
- return value.flatMap((entry) => collectTextFields(entry, fields, depth + 1));
538
- if (typeof value !== "object")
539
- return [];
540
- const output = [];
541
- for (const [key, entry] of Object.entries(value)) {
542
- if (fields.has(key) && typeof entry === "string")
543
- output.push(entry);
544
- if (typeof entry === "object" && entry !== null)
545
- output.push(...collectTextFields(entry, fields, depth + 1));
60
+ catch (error) {
61
+ if (onProgress)
62
+ onProgress("error", `Failed to install packages: ${error instanceof Error ? error.message : "Unknown error"}`);
63
+ throw error;
546
64
  }
547
- return output;
548
65
  }
549
- function findStructuredProposals(value, depth = 0) {
550
- if (depth > 8 || value == null || typeof value !== "object")
551
- return [];
552
- if (!Array.isArray(value)) {
553
- const record = value;
554
- if (Array.isArray(record.changes))
555
- return [JSON.stringify(record)];
556
- return Object.values(record).flatMap((entry) => findStructuredProposals(entry, depth + 1));
66
+ async function executeWebSearch(query, apiKey, signal) {
67
+ if (!apiKey) {
68
+ throw new Error("API key required for web search");
557
69
  }
558
- return value.flatMap((entry) => findStructuredProposals(entry, depth + 1));
559
- }
560
- function extractLocalAgentProposal(raw, provider) {
561
- const outputs = [];
562
- const lines = raw.split(/\r?\n/).filter((line) => line.trim());
563
- const streamObjects = jsonObjectCandidates(raw).flatMap((candidate) => {
564
- try {
565
- return [JSON.parse(candidate)];
566
- }
567
- catch {
568
- return [];
569
- }
70
+ const response = await fetch("https://api.jina.ai/v1/search", {
71
+ method: "POST",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ "Authorization": `Bearer ${apiKey}`,
75
+ },
76
+ body: JSON.stringify({ q: query, limit: 5 }),
77
+ signal,
570
78
  });
571
- for (const line of lines) {
572
- try {
573
- const event = JSON.parse(line);
574
- const item = event.item;
575
- const part = event.part;
576
- const values = provider === "claude-code" || provider === "cursor"
577
- ? [event.result, ...(event.message?.content || []).map((entry) => entry.text)]
578
- : provider === "opencode"
579
- ? [...collectTextFields(event), part?.type === "text" ? part.text : undefined]
580
- : [event.text, event.output_text, item?.text, item?.output_text, item?.message];
581
- for (const value of values)
582
- if (typeof value === "string" && value.trim())
583
- outputs.push(value);
584
- }
585
- catch {
586
- if (line.trim())
587
- outputs.push(line);
588
- }
589
- }
590
- // OpenCode can emit multiple JSON records on one physical line. The
591
- // line-based pass above cannot see those records, so inspect each balanced
592
- // JSON object as well and collect text nested in its final response event.
593
- for (const event of streamObjects) {
594
- outputs.push(...collectTextFields(event));
595
- const part = event.part;
596
- if (part?.type === "text" && typeof part.text === "string")
597
- outputs.push(part.text);
598
- }
599
- for (const proposal of streamObjects.flatMap((event) => findStructuredProposals(event)).reverse()) {
600
- try {
601
- return jsonFrom(proposal);
602
- }
603
- catch {
604
- // Continue through text candidates below.
605
- }
606
- }
607
- // Tool events are interleaved with the final answer. Try text events in
608
- // reverse order, then the complete stream as a final fallback.
609
- for (const output of [...outputs.reverse(), raw]) {
610
- try {
611
- return jsonFrom(output);
612
- }
613
- catch {
614
- // Keep looking; this output may only be a progress or tool event.
615
- }
616
- }
617
- throw new Error(`${provider === "opencode" ? "OpenCode" : provider === "cursor" ? "Cursor" : "The agent"} did not return a reviewable proposal. It produced progress/tool events but no final JSON containing file changes.`);
618
- }
619
- function snippet(value, max = 80) {
620
- const s = String(value ?? "").trim().replace(/\s+/g, " ");
621
- return s.length > max ? `${s.slice(0, max)}…` : s;
622
- }
623
- const ANSI_PATTERN = /\u001B\[[0-9;?]*[ -/]*[@-~]/g;
624
- const LOG_FIELD_PATTERN = /([A-Za-z_][\w.-]*)=("(?:[^"\\]|\\.)*"|[^\s]*)/g;
625
- function cleanAgentText(value) {
626
- return value.replace(ANSI_PATTERN, "").replace(/\s+/g, " ").trim();
627
- }
628
- /**
629
- * Local agents (notably OpenCode) print structured logs such as
630
- * `timestamp=… level=ERROR run=… message="Failed to fetch models.dev" cause="…"`.
631
- * Those lines are machine diagnostics, never user-facing copy, so they are parsed
632
- * into their fields instead of being shown verbatim.
633
- */
634
- function parseAgentLogFields(line) {
635
- if (!/^\s*(?:timestamp|time|ts)=\S/.test(line))
636
- return null;
637
- const fields = {};
638
- for (const match of line.matchAll(LOG_FIELD_PATTERN)) {
639
- const key = match[1];
640
- const raw = match[2] ?? "";
641
- fields[key] = raw.startsWith('"')
642
- ? raw.slice(1, -1).replace(/\\(["\\])/g, "$1")
643
- : raw;
644
- }
645
- return fields;
646
- }
647
- // Filesystem failures that a permission request can never resolve.
648
- const AGENT_FAILURE_MARKERS = /\b(?:EACCES|EPERM|ENOENT|EEXIST|EISDIR|ENOTDIR|ENOSPC|EMFILE|ENOTEMPTY)\b|\bCause\(\[Die\(/i;
649
- const AGENT_FAILURE_HINTS = [
650
- {
651
- pattern: /EACCES: permission denied[\s\S]*(?:locks?|state|storage|\.config|config\/)/i,
652
- message: "The local agent cannot write to its own state folder. Fix that folder's ownership (for example `sudo chown -R $(whoami) ~/.local/state/opencode`) and retry.",
653
- },
654
- {
655
- pattern: /EACCES: permission denied/i,
656
- message: "The local agent was denied access to a file or folder it needs. Check the permissions on its state directory, then retry.",
657
- },
658
- {
659
- pattern: /failed to fetch models\.dev/i,
660
- message: "The local agent could not reach models.dev to refresh its model list. Check the network or proxy it uses, then retry.",
661
- },
662
- {
663
- pattern: /quota exceeded|quota limit|out of quota|no quota|quota.*limit|usage.*quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted/i,
664
- message: "Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.",
665
- },
666
- {
667
- pattern: /payment.*required|subscription.*required|upgrade.*required|upgrade.*plan|billing.*required/i,
668
- message: "Your subscription or plan requires an upgrade. Please check your billing settings and upgrade to continue.",
669
- },
670
- {
671
- pattern: /api.*key.*invalid|api.*key.*expired|invalid.*api.*key|expired.*api.*key|authentication.*failed|unauthorized|forbidden|401|403/i,
672
- message: "Authentication failed. Check your API key or sign in to your account, then retry.",
673
- },
674
- ];
675
- function agentFailureHint(line) {
676
- return AGENT_FAILURE_HINTS.find((hint) => hint.pattern.test(line))?.message;
677
- }
678
- const PERMISSION_PROMPT_PATTERN = /permission\s+(?:is\s+)?(?:required|requested|needed|denied)|needs?\s+(?:your\s+)?permission|permission\s+to\s+(?:run|edit|write|read|execute|create|delete|modify|access|apply)|approve\s+(?:this|the|tool|action|command)|authori[sz]e\s+(?:this|the|tool|action|command)|authentication\s+(?:is\s+)?required|(?:please\s+)?log\s?in|sign\s?in\s+(?:required|to\s+continue)|press\s+.{1,24}?to\s+(?:continue|approve|confirm)|waiting for (?:your )?(?:permission|approval|input|response)/i;
679
- /**
680
- * Returns human-readable copy for a real approval/authentication request, or
681
- * `null` when the line is anything else. A structured error log never becomes a
682
- * permission prompt: Allow/Deny cannot fix a filesystem failure, and hiding the
683
- * error behind those buttons is what made the prompt unreadable.
684
- */
685
- function permissionPromptMessage(raw) {
686
- const line = cleanAgentText(raw);
687
- if (!line)
688
- return null;
689
- if (parseAgentLogFields(line) || AGENT_FAILURE_MARKERS.test(line))
690
- return null;
691
- if (!PERMISSION_PROMPT_PATTERN.test(line))
692
- return null;
693
- const detail = line
694
- .replace(/^[A-Za-z0-9_.-]+:\s*/, "")
695
- .replace(/^["'\s]+|["'\s]+$/g, "")
696
- .trim();
697
- return snippet(detail || line, 240);
698
- }
699
- function progressEvent(message, detail) {
700
- const value = detail == null ? "" : String(detail).trim();
701
- return value ? { message, detail: value } : { message };
702
- }
703
- function progressFromLine(raw, provider) {
704
- try {
705
- const event = JSON.parse(raw);
706
- const item = event.item;
707
- if (provider === "claude-code" || provider === "cursor") {
708
- const agentName = provider === "cursor" ? "Cursor" : "Claude Code";
709
- if (event.type === "system")
710
- return progressEvent(`${agentName} connected`);
711
- // tool_use blocks inside assistant messages
712
- const toolBlock = event.message?.content?.find?.((p) => p.type === "tool_use");
713
- if (toolBlock) {
714
- const toolName = toolBlock.name || "tool";
715
- const inp = toolBlock.input;
716
- const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
717
- return progressEvent(detail
718
- ? `${agentName} · ${toolName} ${snippet(detail)}`
719
- : `${agentName} · ${toolName}`, detail);
720
- }
721
- // thinking blocks inside assistant messages
722
- const thinkBlock = event.message?.content?.find?.((p) => p.type === "thinking");
723
- if (thinkBlock?.thinking) {
724
- return progressEvent(`${agentName} · ${snippet(thinkBlock.thinking)}`, thinkBlock.thinking);
725
- }
726
- // top-level tool event fields (stream-json verbose format)
727
- const tool = event.tool_name || event.name || event.tool?.name;
728
- if (tool) {
729
- const inp = event.tool_input;
730
- const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
731
- return progressEvent(detail
732
- ? `${agentName} · ${tool} ${snippet(detail)}`
733
- : `${agentName} · ${tool}`, detail);
734
- }
735
- if (event.type === "result" || event.result) {
736
- return progressEvent(`${agentName} · preparing the proposal`);
737
- }
738
- if (event.type === "assistant")
739
- return progressEvent(`${agentName} · reasoning about the change`);
740
- }
741
- else if (provider === "codex") {
742
- const type = item?.type || event.type || "";
743
- if (type === "command_execution" || type === "command_execution_output") {
744
- const cmd = item?.command || event.command || "";
745
- return progressEvent(cmd ? `Codex · Bash ${snippet(cmd)}` : "Codex · running a command", cmd);
746
- }
747
- if (type === "file_read" || type === "read_file") {
748
- const fp = item?.path || event.path || "";
749
- return progressEvent(fp ? `Codex · Read ${snippet(fp)}` : "Codex · reading a file", fp);
750
- }
751
- if (type === "agent_message" || type === "message") {
752
- return progressEvent("Codex · drafting the proposal");
753
- }
754
- if (type === "reasoning") {
755
- const text = item?.content || event.content || "";
756
- return progressEvent(text ? `Codex · ${snippet(text)}` : "Codex · reasoning", text);
757
- }
758
- if (type === "turn.started" || type === "turn_start") {
759
- return progressEvent("Codex · starting a turn");
760
- }
761
- if (type === "turn.completed" || type === "turn_complete") {
762
- return progressEvent("Codex · preparing the proposal");
763
- }
764
- }
765
- else if (provider === "opencode") {
766
- const part = event.part;
767
- if (event.type === "step-start")
768
- return progressEvent("OpenCode · starting a step");
769
- if (part?.type === "tool") {
770
- const toolName = part.tool || "tool";
771
- const inp = part.input;
772
- const detail = inp?.file_path ?? inp?.path ?? inp?.command ?? inp?.query ?? inp?.url ?? "";
773
- return progressEvent(detail
774
- ? `OpenCode · ${toolName} ${snippet(detail)}`
775
- : `OpenCode · ${toolName}`, detail);
776
- }
777
- if (part?.type === "text") {
778
- const text = part.text || "";
779
- return progressEvent(text ? `OpenCode · ${snippet(text)}` : "OpenCode · drafting the response", text);
780
- }
781
- if (event.type === "step-finish")
782
- return progressEvent("OpenCode · finalizing the response");
783
- }
784
- }
785
- catch {
786
- // Progress output is best-effort; the final parser reports malformed output.
787
- }
788
- return null;
789
- }
790
- function progressEventsFromRaw(raw, provider) {
791
- const records = (() => {
792
- try {
793
- JSON.parse(raw);
794
- return [raw];
795
- }
796
- catch {
797
- return jsonObjectCandidates(raw);
798
- }
799
- })();
800
- return records.flatMap((record) => {
801
- const progress = progressFromLine(record, provider);
802
- return progress ? [progress] : [];
803
- });
804
- }
805
- function localAgentError(command, stderr, exitCode) {
806
- const output = stderr.trim();
807
- const hint = agentFailureHint(output);
808
- if (hint)
809
- return `${command === "opencode" ? "OpenCode" : command === "cursor" ? "Cursor" : command} could not finish: ${hint}`;
810
- if (command === "opencode" && /waiting for permission or authentication/i.test(output)) {
811
- return "OpenCode needs permission or authentication. Run OpenCode once in a terminal, approve the requested access or sign in, then retry in Lasso.";
812
- }
813
- if (command === "cursor" && /waiting for permission or authentication/i.test(output)) {
814
- return "Cursor needs permission or authentication. Run Cursor once in a terminal, approve the requested access or sign in, then retry in Lasso.";
815
- }
816
- if (command === "codex" && output.includes("missing field `base_instructions`")) {
817
- return "Codex could not read its models cache because it uses an older cache format. Update Codex, then retry. Lasso already bypasses the repository trust check.";
818
- }
819
- if (command === "codex" && output.includes("mcp.canva.com") && output.includes("invalid_token")) {
820
- return "Codex started, but the Canva MCP connection has an expired OAuth token. Re-authenticate or remove the Canva MCP server from Codex, then retry.";
821
- }
822
- // Check for quota/plan exhaustion in the output
823
- if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted/i.test(output)) {
824
- return `${command === "opencode" ? "OpenCode" : command === "cursor" ? "Cursor" : command} request failed: Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.`;
825
- }
826
- return `${command} exited with code ${exitCode}${output ? `: ${output.slice(0, 500)}` : ""}`;
827
- }
828
- function localModel(provider, model) {
829
- if (!model)
830
- return undefined;
831
- const prefix = `${provider}:`;
832
- return model.startsWith(prefix) ? model.slice(prefix.length) : model;
833
- }
834
- function localCommand(provider, model, prompt) {
835
- const selectedModel = localModel(provider, model);
836
- if (provider === "claude-code") {
837
- return { command: "claude", args: ["-p", prompt || "", "--output-format", "stream-json", "--verbose", "--permission-mode", "plan", "--max-turns", "3", ...(selectedModel ? ["--model", selectedModel] : [])] };
838
- }
839
- if (provider === "cursor") {
840
- return { command: "cursor", args: ["agent", "run", "--non-interactive", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
841
- }
842
- if (provider === "opencode") {
843
- return { command: "opencode", args: ["run", "--format", "json", "--print-logs", "--log-level", "INFO", "--agent", "plan", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
79
+ if (!response.ok) {
80
+ throw new Error(`Web search failed: ${response.statusText}`);
844
81
  }
845
- return { command: "codex", args: ["exec", "--json", "--sandbox", "read-only", "--skip-git-repo-check", ...(selectedModel ? ["--model", selectedModel] : []), prompt || ""] };
82
+ const data = await response.json();
83
+ return data.results?.map(r => `[${r.title}](${r.url})\n${r.content}`).join("\n\n") || "No results found";
846
84
  }
847
- function runLocalCommand(cwd, command, args, provider, signal, onProgress, taskId, onPrompt) {
85
+ async function executeAgentCommand(cwd, command, signal, onProgress) {
86
+ if (onProgress)
87
+ onProgress("working", `Running: ${command}`);
848
88
  return new Promise((resolve, reject) => {
849
- const child = (0, node_child_process_1.spawn)(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
850
- // Headless invocations: codex/opencode/cursor read stdin until EOF and otherwise hang
851
- // until our timeout, so close stdin right away. Prompt answers can no longer be piped
852
- // back in, which is fine — none of these command lines are interactive. Swallow the
853
- // EPIPE that a late write would raise so it cannot crash the CLI.
854
- child.stdin?.on("error", () => { });
855
- child.stdin?.end();
856
- const label = provider === "opencode" ? "OpenCode" : provider === "codex" ? "Codex" : provider === "cursor" ? "Cursor" : "Claude Code";
89
+ const [cmd, ...args] = command.split(/\s+/);
90
+ const proc = (0, node_child_process_1.spawn)(cmd, args, { cwd, signal });
857
91
  let stdout = "";
858
92
  let stderr = "";
859
- let pendingStdout = "";
860
- let pendingStderr = "";
861
- let settled = false;
862
- let timedOut = false;
863
- let promptShown = false;
864
- const sendPrompt = (message, kind = "permission") => {
865
- if (!taskId || promptShown)
866
- return;
867
- promptShown = true;
868
- activeAgentInputs.set(taskId, (value) => {
869
- promptShown = false;
870
- activeAgentInputs.delete(taskId);
871
- child.stdin.write(`${value}\n`);
872
- });
873
- onPrompt?.({ message, kind, options: kind === "permission" ? ["Allow", "Deny"] : undefined });
874
- };
875
- const timeout = setTimeout(() => {
876
- timedOut = true;
877
- child.kill("SIGTERM");
878
- setTimeout(() => child.kill("SIGKILL"), 3000).unref();
879
- }, 5 * 60 * 1000);
880
- timeout.unref();
881
- const consume = (chunk, stream) => {
882
- const value = String(chunk);
883
- if (stream === "stdout") {
884
- stdout += value;
885
- pendingStdout += value;
886
- }
887
- else {
888
- stderr += value;
889
- pendingStderr += value;
890
- }
891
- const pending = stream === "stdout" ? pendingStdout : pendingStderr;
892
- const lines = pending.split(/\r?\n/);
893
- const remainder = lines.pop() || "";
894
- if (stream === "stdout")
895
- pendingStdout = remainder;
896
- else
897
- pendingStderr = remainder;
898
- for (const line of lines) {
899
- for (const progress of progressEventsFromRaw(line, provider)) {
900
- onProgress?.(progress.message, progress.detail);
901
- }
902
- const promptMessage = permissionPromptMessage(line);
903
- if (promptMessage) {
904
- sendPrompt(promptMessage);
905
- }
906
- else if (stream === "stderr" && line.trim()) {
907
- const hint = agentFailureHint(line);
908
- if (hint)
909
- onProgress?.(`${label} · ${hint}`, undefined, "error");
910
- if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(line)) {
911
- onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
912
- child.kill("SIGTERM");
913
- }
914
- }
915
- }
916
- };
917
- child.stdout.on("data", (chunk) => consume(chunk, "stdout"));
918
- child.stderr.on("data", (chunk) => consume(chunk, "stderr"));
919
- const abort = () => child.kill("SIGTERM");
920
- if (signal?.aborted)
921
- abort();
922
- else
923
- signal?.addEventListener("abort", abort, { once: true });
924
- child.once("error", (error) => {
925
- if (settled)
926
- return;
927
- settled = true;
928
- clearTimeout(timeout);
929
- if (taskId)
930
- activeAgentInputs.delete(taskId);
931
- signal?.removeEventListener("abort", abort);
932
- reject(error);
93
+ proc.stdout?.on("data", (data) => {
94
+ stdout += data.toString();
933
95
  });
934
- child.once("close", (code) => {
935
- if (settled)
936
- return;
937
- settled = true;
938
- clearTimeout(timeout);
939
- if (taskId)
940
- activeAgentInputs.delete(taskId);
941
- signal?.removeEventListener("abort", abort);
942
- if (timedOut) {
943
- const stderrTail = pendingStderr.trim();
944
- // Check if timeout was due to quota exhaustion
945
- if (/quota exceeded|quota limit|out of quota|no quota|plan.*limit|plan.*exhausted|credit.*exhausted|billing.*quota|rate.*limit|429|too many requests|request.*limit|insufficient.*credits|no.*credits|credits.*exhausted/i.test(stderrTail)) {
946
- reject(new Error(`${label} did not finish within 5 minutes: Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.`));
947
- }
948
- else {
949
- reject(new Error(`${label} did not finish within 5 minutes. Check its login or approval prompt, then retry.`));
950
- }
951
- return;
952
- }
953
- const stdoutTail = pendingStdout.trim();
954
- const stderrTail = pendingStderr.trim();
955
- if (stdoutTail) {
956
- stdout += pendingStdout;
957
- for (const progress of progressEventsFromRaw(stdoutTail, provider)) {
958
- onProgress?.(progress.message, progress.detail);
959
- }
960
- }
961
- if (stderrTail) {
962
- for (const progress of progressEventsFromRaw(stderrTail, provider)) {
963
- onProgress?.(progress.message, progress.detail);
964
- }
965
- const hint = agentFailureHint(stderrTail);
966
- if (hint)
967
- onProgress?.(`${label} · ${hint}`, undefined, "error");
968
- if (provider === "opencode" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
969
- onProgress?.("OpenCode · provider rejected the request; stopping this task…", undefined, "error");
970
- }
971
- if (provider === "cursor" && /quota exceeded|authentication failed|invalid api key|unauthorized|forbidden/i.test(stderrTail)) {
972
- onProgress?.("Cursor · provider rejected the request; stopping this task…", undefined, "error");
973
- }
974
- }
975
- resolve({ stdout, stderr, exitCode: code ?? 1 });
96
+ proc.stderr?.on("data", (data) => {
97
+ stderr += data.toString();
976
98
  });
977
- });
978
- }
979
- async function proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, taskId, onPrompt) {
980
- const outputContract = `CRITICAL: You MUST return ONLY valid JSON with actual file changes. NEVER explain, describe, or analyze code without proposing edits. If third-party npm packages are needed, list them in "packages". ALWAYS return JSON with at least one change when given a modification instruction. Format: {"summary":"brief action taken","packages":["optional-package-name"],"changes":[{"filePath":"relative/path","oldString":"exact existing text","newString":"replacement text"}]}. Treat the supplied source context as read-only. Before returning, verify every oldString against that context. Use project-relative paths only. Do not edit files, run write commands, commit, or produce markdown fences. If you cannot find the exact text to change, search the provided context more carefully - do not give up and explain instead.`;
981
- const prompt = `${instruction}\n\n${outputContract}\n\nLasso has already assembled this source context:\n${context || "No matching source context was found."}`;
982
- const local = localCommand(config.provider, config.model, prompt);
983
- const command = local.command;
984
- const args = local.args;
985
- const result = await runLocalCommand(cwd, command, args, config.provider, signal, onProgress, taskId, onPrompt);
986
- if (result.exitCode !== 0)
987
- throw new Error(localAgentError(command, result.stderr, result.exitCode));
988
- return extractLocalAgentProposal(result.stdout, config.provider);
989
- }
990
- // Runs a local coding-agent CLI (claude/codex/opencode/cursor) and returns its final text.
991
- // The CLI authenticates itself (its own login), so no provider API key is required.
992
- async function runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, taskId, onPrompt) {
993
- const provider = config.provider;
994
- const local = localCommand(provider, config.model, prompt);
995
- const result = await runLocalCommand(cwd, local.command, local.args, provider, signal, onProgress, taskId, onPrompt);
996
- if (result.exitCode !== 0)
997
- throw new Error(localAgentError(local.command, result.stderr, result.exitCode));
998
- return extractLocalAgentText(result.stdout, provider);
999
- }
1000
- async function proposeChanges(cwd, input, config, signal, onProgress, onPrompt) {
1001
- const context = contextFor(cwd, input.element);
1002
- const runtimeErrors = input.context?.runtimeErrors || [];
1003
- const visualContext = input.context ? { ...input.context, screenshots: undefined, runtimeErrors: undefined } : undefined;
1004
- const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || input.instruction;
1005
- const priorChanges = input.changesHistory?.length ? JSON.stringify(input.changesHistory, null, 2) : "None";
1006
- const dragHint = input.context?.drag
1007
- ? `\n\nDRAG REPOSITIONING TASK:\nThe user dragged this element by dx: ${input.context.drag.delta?.dx ?? 0}px, dy: ${input.context.drag.delta?.dy ?? 0}px to target coordinates (left: ${input.context.drag.targetRect?.left ?? 0}px, top: ${input.context.drag.targetRect?.top ?? 0}px). Modify the source code (CSS classes, Tailwind classes, flex/grid alignment, margin offsets, or positioning properties) so the element is accurately rendered at this target position.`
1008
- : "";
1009
- const runtimeErrorHint = runtimeErrors.length
1010
- ? `\n\nLIVE CONSOLE/RUNTIME ERRORS (reported by the user's browser — the page is currently broken by these):\n${runtimeErrors.map((error) => `! ${error}`).join("\n")}`
1011
- : "";
1012
- // Detect project structure for instruction
1013
- const structure = detectProjectStructure(cwd);
1014
- // Query layout intelligence for relevant patterns (if available)
1015
- let layoutContext = "";
1016
- if (searchLayouts) {
1017
- try {
1018
- const layoutQuery = input.instruction;
1019
- const relevantLayouts = await searchLayouts(layoutQuery);
1020
- if (relevantLayouts.length > 0) {
1021
- layoutContext = `\n\nLAYOUT INTELLIGENCE:
1022
- Based on your request, here are relevant layout patterns you should consider:
1023
- ${relevantLayouts.slice(0, 3).map((result) => `- ${result.pattern.name}: ${result.pattern.description}`).join("\n")}
1024
- Use these patterns as inspiration for your implementation. Focus on matching the layout structure, spacing, and composition principles.`;
1025
- }
1026
- }
1027
- catch (error) {
1028
- // Layout intelligence is optional; continue without it if it fails
1029
- console.warn("Layout intelligence query failed:", error);
1030
- }
1031
- }
1032
- const instruction = `Selection context:
1033
- ${JSON.stringify(input.element, null, 2)}
1034
- ${JSON.stringify(visualContext, null, 2)}
1035
-
1036
- User instruction:
1037
- ${input.instruction}
1038
-
1039
- Conversation history:
1040
- ${history}
1041
-
1042
- Previous change history for this selection:
1043
- ${priorChanges}
1044
-
1045
- Relevant source context:
1046
- ${context || "No matching source context was found. Ask for a more specific selection rather than inventing a file."}${dragHint}${runtimeErrorHint}${layoutContext}
1047
-
1048
- CRITICAL INSTRUCTIONS:
1049
- 0. PROJECT STRUCTURE AWARENESS: This project has specific directory structure. Before creating or moving files, verify the actual structure. Do not assume standard locations.
1050
- ${structure.multipleAppDirs.length > 0 ? ` WARNING: Multiple app directories detected: ${structure.multipleAppDirs.join(", ")}. Use the correct one for this project.\n` : ""}
1051
- 1. You are an autonomous agent with the ability to install npm packages and edit source code.
1052
- 2. If the user's request requires or asks for third-party libraries, icon packs, animation tools, or utility packages (e.g. icon libraries, motion, charts, UI primitives, etc.), determine the best npm package for this project and declare them in the "packages" array (e.g. ["@iconify/react", "@hugeicons/react"]).
1053
- 3. In the "changes" array, propose concrete, minimal edits to the source code to implement the request. You may freely import and use the packages you specified in "packages".
1054
- 4. If no new packages are required, "packages" should be empty [].
1055
- 5. If LIVE CONSOLE/RUNTIME ERRORS are listed, fixing them is part of this task — the user's page is currently broken by them.
1056
- 6. Never guess a package's export names. Before importing a named export (an icon, a component, a hook), verify it actually exists in the installed package (run_command grepping node_modules, or the package docs) — importing a non-existent export throws a SyntaxError and kills the whole page.
1057
- 7. You MUST return ONLY valid JSON in this exact structure:
1058
- {
1059
- "summary": "Brief explanation of what was done",
1060
- "packages": ["package-name-1", "package-name-2"],
1061
- "changes": [
1062
- {
1063
- "filePath": "relative/path/to/file.tsx",
1064
- "oldString": "exact existing text",
1065
- "newString": "replacement text"
1066
- }
1067
- ]
1068
- }
1069
- Do NOT return prose or markdown outside the JSON.`;
1070
- const callServerGateway = async (gatewayKey) => {
1071
- // Use CLI auth key if available, otherwise use the provided gateway key
1072
- const cliCredentials = (0, auth_1.loadCredentials)();
1073
- const apiKeyToUse = cliCredentials?.apiKey || gatewayKey;
1074
- let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
1075
- if (serverUrl.includes("collab.lasso.byorello.space")) {
1076
- serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
1077
- }
1078
- const endpoint = `${serverUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, "")}/api/v1/agent/session`;
1079
- const sourceHints = {};
1080
- const fileMatch = context?.match(/^FILE:\s*([^\r\n]+)\r?\n([\s\S]*)$/);
1081
- if (fileMatch) {
1082
- sourceHints[fileMatch[1].trim()] = fileMatch[2].slice(0, 16000);
1083
- }
1084
- else if (context) {
1085
- sourceHints["selection-context"] = context.slice(0, 16000);
1086
- }
1087
- const response = await fetch(endpoint, {
1088
- method: "POST",
1089
- signal,
1090
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
1091
- body: JSON.stringify({
1092
- sessionId: input.taskId,
1093
- prompt: instruction,
1094
- model: { id: config.model, provider: config.provider },
1095
- context: { sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined },
1096
- }),
99
+ proc.on("close", (exitCode) => {
100
+ resolve({ stdout, stderr, exitCode });
1097
101
  });
1098
- if (!response.ok) {
1099
- const errData = await response.json().catch(() => ({}));
1100
- const errorMessage = errData.error || `Lasso Agent Gateway request failed (${response.status})`;
1101
- // Check for quota/plan exhaustion
1102
- 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)) {
1103
- throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1104
- }
1105
- throw new Error(errorMessage);
1106
- }
1107
- const data = await response.json();
1108
- // If the server-side agent reported failure, surface it as a thrown error so the
1109
- // bridge catches it and shows status="error" / "Retry" button, not status="review".
1110
- if (data.status === "failed") {
1111
- throw new Error(data.error || data.summary || "Agent encountered an issue while generating changes.");
1112
- }
1113
- return { summary: data.summary || "Done", packages: data.packages || [], changes: data.changes || [] };
1114
- };
1115
- let proposal = null;
1116
- // Helper: call /api/v1/agent/generate for one turn of the agentic loop using the Lasso gateway.
1117
- // This allows paid/hosted model users to run the full agentic loop (web_search, run_command,
1118
- // ask_user, propose_changes) rather than being shunted to the one-shot /session endpoint.
1119
- const callGatewayGenerate = async (gatewayKey, systemPrompt, turnPrompt) => {
1120
- let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
1121
- if (serverUrl.includes("collab.lasso.byorello.space")) {
1122
- serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
1123
- }
1124
- const endpoint = `${serverUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, "")}/api/v1/agent/generate`;
1125
- const response = await fetch(endpoint, {
1126
- method: "POST",
1127
- signal,
1128
- headers: { "content-type": "application/json", authorization: `Bearer ${gatewayKey}` },
1129
- body: JSON.stringify({
1130
- provider: config.provider,
1131
- model: config.model,
1132
- system: systemPrompt,
1133
- prompt: turnPrompt,
1134
- }),
1135
- });
1136
- if (!response.ok) {
1137
- const errData = await response.json().catch(() => ({}));
1138
- const errorMessage = errData.error || `Lasso Gateway generate failed (${response.status})`;
1139
- // Check for quota/plan exhaustion
1140
- 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)) {
1141
- throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1142
- }
1143
- throw new Error(errorMessage);
1144
- }
1145
- const data = await response.json();
1146
- if (!data.text?.trim())
1147
- throw new Error("The agent returned an empty response.");
1148
- return data.text.trim();
1149
- };
1150
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
1151
- proposal = await proposeWithLocalAgent(cwd, instruction, context, config, signal, onProgress, input.taskId, onPrompt);
1152
- }
1153
- else {
1154
- // Check if using Lasso platform key (from CLI auth or env)
1155
- const cliCredentials = (0, auth_1.loadCredentials)();
1156
- const isLassoKey = cliCredentials?.apiKey || config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1157
- const system = `You are Lasso, an autonomous agentic AI coding assistant.
1158
- You have the ability to run inspection commands, search the live web for documentation or libraries, ask the user for input or single/multi-selection choices, install npm packages, and edit source code.
1159
-
1160
- AVAILABLE ACTIONS:
1161
- 1. "run_command": Run safe inspection commands in the project (e.g. grep, find, ls, git status, cat) to investigate files or exports.
1162
- Format: {"action": "run_command", "command": "grep -rn 'search_term' src/", "thought": "Why you need to run this command"}
1163
-
1164
- 2. "web_search": Search the live web to find real-time documentation, package details, or API signatures rather than assuming.
1165
- Format: {"action": "web_search", "query": "lucide react icons documentation", "thought": "Why you need to search"}
1166
-
1167
- 3. "ask_user": Ask the user for clarification, single-choice, or multi-selection input when their intent is ambiguous.
1168
- Format: {"action": "ask_user", "question": "Which icon library would you prefer?", "kind": "multiselect" | "single_select" | "input", "options": ["Option 1", "Option 2"], "thought": "Why you need user choice"}
1169
-
1170
- 4. "propose_changes": Finalize and propose the exact file edits and required npm packages.
1171
- Format:
1172
- {
1173
- "action": "propose_changes",
1174
- "summary": "Brief explanation of what was done",
1175
- "packages": ["package-name-1", "package-name-2"],
1176
- "changes": [
1177
- {
1178
- "filePath": "relative/path/to/file.tsx",
1179
- "oldString": "exact existing text",
1180
- "newString": "replacement text"
1181
- }
1182
- ]
1183
- }
1184
-
1185
- RULES:
1186
- - If you already have sufficient context to fulfill the user's request immediately, return "propose_changes" (or standard {"summary": "...", "packages": [...], "changes": [...]}) directly!
1187
- - If live console/runtime errors are provided, resolving them is part of the task. Use run_command to verify package exports and file contents before proposing changes instead of guessing.
1188
- - Each oldString must match the existing file context exactly.
1189
- - CRITICAL: Return ONLY valid JSON with no markdown formatting, no code blocks, no prose, no thinking tags, and no progress output. Just the JSON object.
1190
- - Do NOT include phrases like "Thinking...", "Working...", or any progress indicators in your response.
1191
- - Do NOT wrap the JSON in code blocks. Return raw JSON only.`;
1192
- const model = config.model;
1193
- if (!model) {
1194
- return {
1195
- summary: "No model selected. Please select a model in settings.",
1196
- changes: [],
1197
- packages: [],
1198
- };
1199
- }
1200
- const image = input.context?.screenshots?.element || input.context?.screenshots?.full;
1201
- const imageData = image?.replace(/^data:image\/[^;]+;base64,/, "");
1202
- const imageMime = image?.match(/^data:(image\/[^;]+);base64,/)?.[1] || "image/jpeg";
1203
- const MAX_TURNS = 5;
1204
- let turn = 0;
1205
- const toolLogs = [];
1206
- while (turn < MAX_TURNS) {
1207
- turn++;
1208
- const toolContext = toolLogs.length > 0 ? `\n\nTOOL EXECUTION & USER INTERACTION HISTORY:\n${toolLogs.join("\n\n")}` : "";
1209
- const currentPrompt = `${instruction}${toolContext}`;
1210
- let rawText = "";
1211
- try {
1212
- // If using a Lasso platform key, route this turn through the /generate endpoint
1213
- // (full agentic loop via Lasso gateway instead of one-shot session endpoint)
1214
- if (isLassoKey && config.provider !== "ollama") {
1215
- const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1216
- rawText = await callGatewayGenerate(apiKeyToUse, system, currentPrompt);
1217
- }
1218
- else {
1219
- // Provider-direct inference path
1220
- let response;
1221
- if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
1222
- const content = image && turn === 1 ? [{ type: "text", text: currentPrompt }, { type: "image_url", image_url: { url: image } }] : currentPrompt;
1223
- const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
1224
- response = await fetch(`${baseUrl}/chat/completions`, {
1225
- method: "POST",
1226
- signal,
1227
- headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
1228
- body: JSON.stringify({ model, temperature: 0.1, messages: [{ role: "system", content: system }, { role: "user", content }] }),
1229
- });
1230
- }
1231
- else if (config.provider === "google") {
1232
- const parts = [{ text: currentPrompt }];
1233
- if (imageData && turn === 1)
1234
- parts.push({ inlineData: { mimeType: imageMime, data: imageData } });
1235
- response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
1236
- method: "POST",
1237
- signal,
1238
- headers: { "content-type": "application/json" },
1239
- body: JSON.stringify({ systemInstruction: { parts: [{ text: system }] }, contents: [{ role: "user", parts }] }),
1240
- });
1241
- }
1242
- else {
1243
- const content = [{ type: "text", text: currentPrompt }];
1244
- if (imageData && turn === 1)
1245
- content.push({ type: "image", source: { type: "base64", media_type: imageMime, data: imageData } });
1246
- response = await fetch("https://api.anthropic.com/v1/messages", {
1247
- method: "POST",
1248
- signal,
1249
- headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
1250
- body: JSON.stringify({ model, max_tokens: 4096, system, messages: [{ role: "user", content }] }),
1251
- });
1252
- }
1253
- if (!response.ok) {
1254
- // Check for quota/plan exhaustion before fallback
1255
- if (response.status === 429 || response.status === 402 || response.status === 403) {
1256
- throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1257
- }
1258
- if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1259
- onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1260
- proposal = await callServerGateway(config.lassoKey);
1261
- break;
1262
- }
1263
- throw new Error(`Agent request failed (${response.status}).`);
1264
- }
1265
- const payload = await response.json();
1266
- const contentCandidate = (config.provider === "openai" || config.provider === "nvidia" || config.provider === "ollama")
1267
- ? payload.choices?.[0]?.message?.content
1268
- : config.provider === "google"
1269
- ? payload.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("")
1270
- : payload.content?.find((item) => item.type === "text")?.text;
1271
- rawText = contentCandidate || "";
1272
- if (!rawText)
1273
- throw new Error("The agent returned an empty response.");
1274
- }
1275
- }
1276
- catch (err) {
1277
- // Check for quota/plan exhaustion before fallback
1278
- if (err.message?.includes("429") || err.message?.includes("402") || err.message?.includes("403")) {
1279
- throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1280
- }
1281
- if (!isLassoKey && config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
1282
- onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1283
- proposal = await callServerGateway(config.lassoKey);
1284
- break;
1285
- }
1286
- throw err;
1287
- }
1288
- const action = parseAgentAction(rawText);
1289
- if (action.type === "run_command") {
1290
- onProgress?.("Running command…", `$ ${action.command}`, "working");
1291
- const res = await executeAgentCommand(cwd, action.command);
1292
- const outputSnippet = res.stdout || res.stderr || "(no output)";
1293
- onProgress?.(`Command completed: ${action.command}`, outputSnippet.slice(0, 150), "working");
1294
- toolLogs.push(`Turn ${turn} Command executed: ${action.command}\nExit code: ${res.exitCode}\nOutput:\n${outputSnippet.slice(0, 3000)}`);
1295
- continue;
1296
- }
1297
- if (action.type === "web_search") {
1298
- onProgress?.("Searching the web…", `🔍 ${action.query}`, "working");
1299
- const results = await executeWebSearch(action.query);
1300
- const summary = results.length > 0
1301
- ? results.map((r, i) => `[${i + 1}] ${r.title}\nURL: ${r.url}\n${r.snippet}`).join("\n\n")
1302
- : "No web results found.";
1303
- onProgress?.(`Web search complete: ${action.query}`, `${results.length} results found`, "working");
1304
- toolLogs.push(`Turn ${turn} Web search for "${action.query}":\n${summary.slice(0, 3000)}`);
1305
- continue;
1306
- }
1307
- if (action.type === "ask_user") {
1308
- if (onPrompt && input.taskId) {
1309
- onProgress?.("Waiting for your response…", action.question, "working");
1310
- onPrompt({
1311
- message: action.question,
1312
- kind: action.kind || "permission",
1313
- options: action.options,
1314
- });
1315
- const answer = await waitForAgentPrompt(input.taskId);
1316
- onProgress?.(`Answer received: ${answer}`, undefined, "working");
1317
- toolLogs.push(`Turn ${turn} Asked user: "${action.question}"\nUser response: ${answer}`);
1318
- continue;
1319
- }
1320
- }
1321
- if (action.type === "proposal") {
1322
- proposal = {
1323
- summary: action.summary,
1324
- packages: action.packages,
1325
- changes: action.changes,
1326
- };
1327
- }
1328
- else {
1329
- proposal = { summary: "Waiting for user input.", changes: [] };
1330
- }
1331
- break;
1332
- }
1333
- if (!proposal) {
1334
- proposal = { summary: "Completed agent task.", changes: [] };
1335
- }
1336
- }
1337
- const finalProposal = proposal || { summary: "Completed agent task.", changes: [] };
1338
- // Agentic execution: If the AI decided packages are needed, install them now
1339
- if (finalProposal.packages && finalProposal.packages.length > 0) {
1340
- await installPackages(cwd, finalProposal.packages, onProgress);
1341
- }
1342
- return finalProposal;
1343
- }
1344
- async function answerQuestion(cwd, input, config, signal, onProgress, onPrompt) {
1345
- const context = contextFor(cwd, input.element);
1346
- const history = input.messages?.map((message) => `${message.role}: ${message.content}`).join("\n") || "None";
1347
- const prompt = `Answer the user's question conversationally and directly. Do not propose file changes and do not return JSON. If the question is about the selected UI, use the selection and source context below.\n\nUser question:\n${input.question}\n\nSelected element:\n${JSON.stringify(input.element, null, 2)}\n\nVisual context:\n${JSON.stringify({ ...input.context, screenshots: undefined }, null, 2)}\n\nConversation:\n${history}\n\nRelevant source context:\n${context || "No matching source context was found."}`;
1348
- if (config.provider === "claude-code" || config.provider === "codex" || config.provider === "opencode" || config.provider === "cursor") {
1349
- return (await runLocalAgentPrompt(cwd, config, prompt, signal, onProgress, input.taskId, onPrompt)).trim();
1350
- }
1351
- const callServerGatewayAnswer = async (gatewayKey) => {
1352
- // Use CLI auth key if available, otherwise use the provided gateway key
1353
- const cliCredentials = (0, auth_1.loadCredentials)();
1354
- const apiKeyToUse = cliCredentials?.apiKey || gatewayKey;
1355
- let serverUrl = config.serverUrl || process.env.LASSO_SERVER_URL || process.env.NEXT_PUBLIC_LASSO_SERVER_URL || "https://api.lasso.byorello.space";
1356
- if (serverUrl.includes("collab.lasso.byorello.space")) {
1357
- serverUrl = serverUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
1358
- }
1359
- const endpoint = `${serverUrl.replace(/\/+$/, "").replace(/\/api\/v1$/, "")}/api/v1/agent/generate`;
1360
- const response = await fetch(endpoint, {
1361
- method: "POST",
1362
- signal,
1363
- headers: { "content-type": "application/json", authorization: `Bearer ${apiKeyToUse}` },
1364
- body: JSON.stringify({
1365
- provider: config.provider,
1366
- model: config.model,
1367
- system: "Answer conversationally and directly. Do not edit files or return JSON.",
1368
- prompt,
1369
- }),
102
+ proc.on("error", (error) => {
103
+ reject(error);
1370
104
  });
1371
- if (!response.ok) {
1372
- const errData = await response.json().catch(() => ({}));
1373
- const errorMessage = errData.error || `Lasso Agent Gateway request failed (${response.status})`;
1374
- // Check for quota/plan exhaustion
1375
- 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)) {
1376
- throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1377
- }
1378
- throw new Error(errorMessage);
1379
- }
1380
- const data = await response.json();
1381
- if (!data.text?.trim())
1382
- throw new Error("The agent returned an empty answer.");
1383
- return data.text.trim();
1384
- };
1385
- // Check if using Lasso platform key (from CLI auth or env)
1386
- const cliCredentials = (0, auth_1.loadCredentials)();
1387
- const isLassoKey = cliCredentials?.apiKey || config.apiKey?.startsWith("lss_live_") || config.apiKey?.startsWith("lss_");
1388
- if (isLassoKey && config.provider !== "ollama") {
1389
- const apiKeyToUse = cliCredentials?.apiKey || config.apiKey;
1390
- return await callServerGatewayAnswer(apiKeyToUse);
1391
- }
1392
- const model = config.model;
1393
- if (!model) {
1394
- return "No model selected. Please select a model in settings.";
1395
- }
1396
- let response;
1397
- try {
1398
- if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
1399
- const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
1400
- response = await fetch(`${baseUrl}/chat/completions`, {
1401
- method: "POST", signal, headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
1402
- body: JSON.stringify({ model, temperature: 0.2, messages: [{ role: "system", content: "Answer conversationally. Do not edit files or return JSON." }, { role: "user", content: prompt }] }),
1403
- });
1404
- }
1405
- else if (config.provider === "google") {
1406
- response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
1407
- method: "POST", signal, headers: { "content-type": "application/json" },
1408
- body: JSON.stringify({ systemInstruction: { parts: [{ text: "Answer conversationally. Do not edit files or return JSON." }] }, contents: [{ role: "user", parts: [{ text: prompt }] }] }),
1409
- });
1410
- }
1411
- else {
1412
- response = await fetch("https://api.anthropic.com/v1/messages", {
1413
- method: "POST", signal, headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
1414
- body: JSON.stringify({ model, max_tokens: 2048, system: "Answer conversationally. Do not edit files or return JSON.", messages: [{ role: "user", content: prompt }] }),
1415
- });
1416
- }
1417
- if (!response.ok) {
1418
- // Check for quota/plan exhaustion before fallback
1419
- if (response.status === 429 || response.status === 402 || response.status === 403) {
1420
- throw new Error("Your plan quota has been exhausted. Please upgrade your plan or add credits to continue using this service.");
1421
- }
1422
- if (config.lassoKey && (response.status === 429 || response.status === 401)) {
1423
- onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1424
- return await callServerGatewayAnswer(config.lassoKey);
1425
- }
1426
- throw new Error(`Agent request failed (${response.status}).`);
1427
- }
1428
- const payload = await response.json();
1429
- const answer = config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia"
1430
- ? payload.choices?.[0]?.message?.content
1431
- : config.provider === "google"
1432
- ? payload.candidates?.[0]?.content?.parts?.map((part) => part.text || "").join("")
1433
- : payload.content?.find((item) => item.type === "text")?.text;
1434
- if (!answer?.trim())
1435
- throw new Error("The agent returned an empty answer.");
1436
- return answer.trim();
1437
- }
1438
- catch (err) {
1439
- if (config.lassoKey && (err.message?.includes("429") || err.message?.includes("401"))) {
1440
- onProgress?.("Local provider key limit reached; routing via Lasso Gateway…", undefined, "working");
1441
- return await callServerGatewayAnswer(config.lassoKey);
1442
- }
1443
- throw err;
1444
- }
1445
- }
1446
- async function generateCommitMessage(cwd, status, config, signal, onProgress) {
1447
- const answer = await answerQuestion(cwd, {
1448
- question: `Generate exactly one concise Conventional Commit message for these changes. Return only the commit subject line, no quotes, markdown, explanation, or body. Keep it under 100 characters.\n\nChanged files:\n${status.join("\n") || "No changed files listed."}`,
1449
- element: { tag: "git", group: "workspace", label: "Git working tree" },
1450
- }, config, signal, onProgress);
1451
- return answer.split(/\r?\n/).map((line) => line.replace(/^[-*]\s*/, "").replace(/^['"`]|['"`]$/g, "").trim()).find(Boolean)?.slice(0, 120) || "Update project files";
105
+ });
1452
106
  }