surf-cli 2.5.2 → 2.6.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.
@@ -0,0 +1,336 @@
1
+ /**
2
+ * AI Studio response extraction for surf-cli
3
+ *
4
+ * Two extraction strategies:
5
+ * 1. Network-first: intercept GenerateContent RPC responses (structured, reliable)
6
+ * 2. DOM fallback: walk the page DOM for rendered response text (when network fails)
7
+ */
8
+
9
+ const {
10
+ cleanAiStudioResponse,
11
+ delay,
12
+ parseAiStudioRpcError,
13
+ parseAiStudioGenerateContentText,
14
+ extractGenerateEntries,
15
+ doesGenerateEntryMatchPrompt,
16
+ } = require("./aistudio-parser.cjs");
17
+
18
+ async function evaluate(cdp, expression) {
19
+ const result = await cdp(expression);
20
+ if (result.exceptionDetails) {
21
+ const desc = result.exceptionDetails.exception?.description ||
22
+ result.exceptionDetails.text ||
23
+ "Evaluation failed";
24
+ throw new Error(desc);
25
+ }
26
+ if (result.error) {
27
+ throw new Error(result.error);
28
+ }
29
+ return result.result?.value;
30
+ }
31
+
32
+ async function waitForGenerateResponseFromNetwork(params) {
33
+ const {
34
+ tabId,
35
+ readNetworkEntries,
36
+ timeoutMs = 300000,
37
+ baselineEntryIds = new Set(),
38
+ prompt = '',
39
+ log = () => {},
40
+ } = params;
41
+
42
+ const deadline = Date.now() + timeoutMs;
43
+ let lastSeenCount = -1;
44
+ const parseErrorCounts = new Map();
45
+
46
+ while (Date.now() < deadline) {
47
+ const network = await readNetworkEntries(tabId);
48
+
49
+ if (network?.error) {
50
+ throw new Error(String(network.error));
51
+ }
52
+
53
+ const allEntries = Array.isArray(network?.entries)
54
+ ? network.entries
55
+ : Array.isArray(network?.requests)
56
+ ? network.requests
57
+ : [];
58
+
59
+ const generateEntries = extractGenerateEntries(allEntries);
60
+
61
+ if (generateEntries.length !== lastSeenCount) {
62
+ lastSeenCount = generateEntries.length;
63
+ log(`GenerateContent network entries seen: ${generateEntries.length}`);
64
+ }
65
+
66
+ const freshEntries = generateEntries.filter((entry) => !baselineEntryIds.has(entry.id));
67
+
68
+ for (const entry of freshEntries) {
69
+ if (!doesGenerateEntryMatchPrompt(entry, prompt)) {
70
+ log(`Skipping GenerateContent entry ${entry?.id || 'unknown'} (prompt mismatch)`);
71
+ continue;
72
+ }
73
+
74
+ const status = Number(entry?.status || 0);
75
+ const body = typeof entry?.responseBody === 'string' ? entry.responseBody : '';
76
+
77
+ if (status >= 400) {
78
+ const rpcError = parseAiStudioRpcError(body);
79
+ const msg = rpcError?.message || `HTTP ${status}`;
80
+ throw new Error(`AI Studio GenerateContent failed (${status}): ${msg}`);
81
+ }
82
+
83
+ if (status !== 200 || !body) {
84
+ continue;
85
+ }
86
+
87
+ let parsedText = '';
88
+ try {
89
+ parsedText = parseAiStudioGenerateContentText(body);
90
+ } catch (e) {
91
+ const requestId = entry.id || 'unknown';
92
+ const parseErrorCount = (parseErrorCounts.get(requestId) || 0) + 1;
93
+ parseErrorCounts.set(requestId, parseErrorCount);
94
+
95
+ log(`GenerateContent parse error (${requestId} #${parseErrorCount}): ${e.message || e}`);
96
+
97
+ if (parseErrorCount >= 3) {
98
+ throw new Error(`GenerateContent body for ${requestId} is not parseable; falling back to DOM`);
99
+ }
100
+
101
+ continue;
102
+ }
103
+
104
+ if (parsedText && parsedText.length > 0) {
105
+ return {
106
+ text: parsedText,
107
+ requestId: entry.id,
108
+ status,
109
+ };
110
+ }
111
+ }
112
+
113
+ await delay(350);
114
+ }
115
+
116
+ throw new Error('Timed out waiting for AI Studio GenerateContent network response');
117
+ }
118
+
119
+ async function waitForResponse(cdp, timeoutMs = 300000, userPrompt = '', log = () => {}) {
120
+ const deadline = Date.now() + timeoutMs;
121
+
122
+ await delay(1000);
123
+
124
+ let doneStreak = 0;
125
+
126
+ while (Date.now() < deadline) {
127
+ const status = await evaluate(cdp, `(function() {
128
+ var buttons = Array.from(document.querySelectorAll('button'));
129
+
130
+ var bodyText = (document.body && document.body.innerText ? document.body.innerText : '').toLowerCase();
131
+
132
+ var rateLimitMsg = null;
133
+ if (bodyText.indexOf("you've reached your rate limit") !== -1) {
134
+ rateLimitMsg = "You've reached your rate limit. Please try again later.";
135
+ } else if (bodyText.indexOf('failed to generate content: user has exceeded quota') !== -1) {
136
+ rateLimitMsg = "Failed to generate content: user has exceeded quota. Please try again later.";
137
+ }
138
+ var rateLimited = rateLimitMsg !== null;
139
+
140
+ var hasRatingBtns = buttons.some(function(b) {
141
+ return (b.getAttribute('aria-label') || b.textContent || '').toLowerCase().indexOf('good response') !== -1;
142
+ });
143
+
144
+ var hasStopBtn = buttons.some(function(b) {
145
+ var label = (b.getAttribute('aria-label') || '').toLowerCase();
146
+ var text = (b.textContent || '').toLowerCase();
147
+ return text.indexOf('stop') !== -1 || label.indexOf('stop') !== -1 || text.indexOf('running') !== -1;
148
+ });
149
+
150
+ return {
151
+ done: hasRatingBtns && !hasStopBtn,
152
+ hasStopBtn: hasStopBtn,
153
+ rateLimited: rateLimited,
154
+ rateLimitMsg: rateLimitMsg
155
+ };
156
+ })()`);
157
+
158
+ if (status && status.rateLimited) {
159
+ const msg = status.rateLimitMsg || "You've reached your rate limit. Please try again later.";
160
+ throw new Error(
161
+ `AI Studio rate limited: ${msg} ` +
162
+ "(Tip: use `surf gemini` / another provider as a fallback.)"
163
+ );
164
+ }
165
+
166
+ if (status && status.done) {
167
+ doneStreak++;
168
+ if (doneStreak === 1) {
169
+ log("Completion signal detected (waiting for stability...)");
170
+ }
171
+ if (doneStreak >= 3) {
172
+ log("Completion signal stable");
173
+ break;
174
+ }
175
+ } else {
176
+ doneStreak = 0;
177
+ }
178
+
179
+ await delay(500);
180
+ }
181
+
182
+ if (Date.now() >= deadline) {
183
+ throw new Error("Response timeout - AI Studio did not complete in time");
184
+ }
185
+
186
+ await delay(800);
187
+
188
+ const extractScript = `(function() {
189
+ function stripUi(text) {
190
+ if (!text) return '';
191
+
192
+ var removeLabels = ['Edit', 'Rerun this turn', 'Open options', 'Good response', 'Bad response'];
193
+ var removeSet = {};
194
+ for (var i = 0; i < removeLabels.length; i++) {
195
+ removeSet[String(removeLabels[i]).toLowerCase()] = true;
196
+ }
197
+
198
+ var lines = String(text).split(/\\r?\\n/);
199
+ var kept = [];
200
+
201
+ for (var j = 0; j < lines.length; j++) {
202
+ var line = lines[j];
203
+ var trimmed = (line || '').trim();
204
+ if (!trimmed) {
205
+ kept.push('');
206
+ continue;
207
+ }
208
+
209
+ if (removeSet[trimmed.toLowerCase()]) continue;
210
+ kept.push(line);
211
+ }
212
+
213
+ return kept.join('\\n').trim();
214
+ }
215
+
216
+ var buttons = Array.from(document.querySelectorAll('button'));
217
+ var goodBtn = buttons.find(function(b) {
218
+ return (b.getAttribute('aria-label') || b.textContent || '').toLowerCase().indexOf('good response') !== -1;
219
+ });
220
+
221
+ if (goodBtn) {
222
+ var container = goodBtn.parentElement;
223
+ while (container && container !== document.body) {
224
+ try {
225
+ var big = Array.from(container.querySelectorAll('[class*="very-large-text-container"]'));
226
+ if (big && big.length) {
227
+ var t = (big[big.length - 1].innerText || '').trim();
228
+ if (t && t.length > 10) {
229
+ return { text: stripUi(t), method: 'good-btn-large-container' };
230
+ }
231
+ }
232
+ } catch {}
233
+
234
+ var text = (container.innerText || '').trim();
235
+ if (text.length > 50) {
236
+ var cleaned = stripUi(text);
237
+ if (cleaned.length > 20) {
238
+ return { text: cleaned, method: 'good-btn-walk' };
239
+ }
240
+ }
241
+ container = container.parentElement;
242
+ }
243
+ }
244
+
245
+ var big2 = Array.from(document.querySelectorAll('[class*="very-large-text-container"]'));
246
+ if (big2 && big2.length) {
247
+ for (var j = big2.length - 1; j >= 0; j--) {
248
+ var tt = (big2[j].innerText || '').trim();
249
+ var ll = tt.toLowerCase();
250
+ if (tt.length > 50 && ll.indexOf('google ai studio uses cookies') === -1) {
251
+ return { text: stripUi(tt), method: 'very-large-text-container' };
252
+ }
253
+ }
254
+ }
255
+
256
+ var promptInput = document.querySelector('[role="textbox"][placeholder*="prompt" i], textarea[placeholder*="prompt" i]');
257
+ if (promptInput) {
258
+ var parent = promptInput.parentElement;
259
+ while (parent && parent !== document.body) {
260
+ var siblings = parent.parentElement ? Array.from(parent.parentElement.children) : [];
261
+ var myIdx = siblings.indexOf(parent);
262
+ for (var s = 0; s < myIdx; s++) {
263
+ var sibText = (siblings[s].innerText || '').trim();
264
+ if (sibText.length > 50) {
265
+ return { text: stripUi(sibText), method: 'sibling-walk' };
266
+ }
267
+ }
268
+ parent = parent.parentElement;
269
+ }
270
+ }
271
+
272
+ return { text: document.body.innerText || '', method: 'body-fallback' };
273
+ })()`;
274
+
275
+ let bestText = '';
276
+ let bestRaw = '';
277
+ let bestExtracted = null;
278
+ let lastText = null;
279
+ let stableCount = 0;
280
+
281
+ const extractDeadline = Math.min(deadline, Date.now() + 15000);
282
+
283
+ while (Date.now() < extractDeadline) {
284
+ const extracted = await evaluate(cdp, extractScript);
285
+ const responseTextRaw = extracted
286
+ ? String(extracted.text || '').trim()
287
+ : '';
288
+ const responseText = cleanAiStudioResponse(responseTextRaw, userPrompt);
289
+
290
+ if (responseText.length > bestText.length) {
291
+ bestText = responseText;
292
+ bestRaw = responseTextRaw;
293
+ bestExtracted = extracted;
294
+ }
295
+
296
+ if (lastText !== null && responseText === lastText && responseText.length > 5) {
297
+ stableCount++;
298
+ if (stableCount >= 2) {
299
+ log(
300
+ 'Extraction stabilized: method=' + (extracted ? extracted.method : 'none') +
301
+ ', raw length=' + responseTextRaw.length +
302
+ ', cleaned length=' + responseText.length
303
+ );
304
+ return {
305
+ text: responseText,
306
+ thinkingTime: null,
307
+ };
308
+ }
309
+ } else {
310
+ stableCount = 0;
311
+ lastText = responseText;
312
+ }
313
+
314
+ await delay(700);
315
+ }
316
+
317
+ log(
318
+ 'Extraction not stable before deadline; returning best length=' + bestText.length +
319
+ ', raw length=' + bestRaw.length +
320
+ ', method=' + (bestExtracted ? bestExtracted.method : 'none')
321
+ );
322
+
323
+ if (!bestText || bestText.length < 5) {
324
+ throw new Error('Could not extract response text from AI Studio');
325
+ }
326
+
327
+ return {
328
+ text: bestText,
329
+ thinkingTime: null,
330
+ };
331
+ }
332
+
333
+ module.exports = {
334
+ waitForGenerateResponseFromNetwork,
335
+ waitForResponse,
336
+ };
package/native/cli.cjs CHANGED
@@ -9,8 +9,12 @@ const networkFormatters = require("./formatters/network.cjs");
9
9
  const networkStore = require("./network-store.cjs");
10
10
  const { parseDoCommands } = require("./do-parser.cjs");
11
11
  const { executeDoSteps } = require("./do-executor.cjs");
12
+ const { version: VERSION } = require("../package.json");
12
13
 
13
- const SOCKET_PATH = "/tmp/surf.sock";
14
+ const IS_WIN = process.platform === "win32";
15
+ const SURF_TMP = IS_WIN ? path.join(os.tmpdir(), "surf") : "/tmp";
16
+ const SOCKET_PATH = IS_WIN ? "//./pipe/surf" : "/tmp/surf.sock";
17
+ if (IS_WIN) { try { fs.mkdirSync(SURF_TMP, { recursive: true }); } catch {} }
14
18
 
15
19
  // ============================================================================
16
20
  // Workflow Resolution and Management
@@ -268,12 +272,13 @@ function resizeImage(filePath, maxSize) {
268
272
  const height = parseInt(sizeInfo.match(/pixelHeight:\s*(\d+)/)?.[1] || "0", 10);
269
273
  return { success: true, width, height };
270
274
  } else {
271
- // Linux/other: use ImageMagick (try IM6 first, then IM7)
275
+ // Linux/Windows: use ImageMagick (try IM6 first, then IM7)
276
+ const resizeArg = IS_WIN ? `"${maxSize}x${maxSize}>"` : `${maxSize}x${maxSize}\\>`;
272
277
  try {
273
- execSync(`convert "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
278
+ execSync(`convert "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
274
279
  } catch {
275
280
  // IM7 uses 'magick' as main command
276
- execSync(`magick "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
281
+ execSync(`magick "${filePath}" -resize ${resizeArg} "${filePath}"`, { stdio: "pipe" });
277
282
  }
278
283
  // Get dimensions (IM7 may need 'magick identify' instead of just 'identify')
279
284
  let sizeInfo;
@@ -290,7 +295,6 @@ function resizeImage(filePath, maxSize) {
290
295
  }
291
296
  }
292
297
  const args = process.argv.slice(2);
293
- const VERSION = "2.5.2";
294
298
 
295
299
  const ALIASES = {
296
300
  snap: "screenshot",
@@ -414,6 +418,35 @@ const TOOLS = {
414
418
  { cmd: 'grok --validate --save-models', desc: "Save discovered models to settings" },
415
419
  ]
416
420
  },
421
+ "aistudio": {
422
+ desc: "Query via Google AI Studio (uses browser session)",
423
+ args: ["query"],
424
+ opts: {
425
+ "with-page": "Include current page context",
426
+ model: "Model (best-effort): pass an AI Studio model id like gemini-3.1-pro-preview, gemini-3-flash-preview, gemini-flash-lite-latest. If invalid, AI Studio uses the last-selected UI model",
427
+ timeout: "Timeout in seconds (default: 300)"
428
+ },
429
+ examples: [
430
+ { cmd: 'aistudio "explain quantum computing"', desc: "Basic query" },
431
+ { cmd: 'aistudio "redteam this" --with-page', desc: "With page context" },
432
+ { cmd: 'aistudio "quick answer" --model gemini-3-flash-preview', desc: "Model selection" },
433
+ ]
434
+ },
435
+ "aistudio.build": {
436
+ desc: "Build an app via Google AI Studio App Builder (uses browser session)",
437
+ args: ["query"],
438
+ opts: {
439
+ model: "Model override for Advanced Settings (e.g. gemini-3.1-pro-preview)",
440
+ output: "Directory to extract the downloaded zip",
441
+ timeout: "Build timeout in seconds (default: 600)",
442
+ "keep-open": "Keep the AI Studio tab open after completion",
443
+ },
444
+ examples: [
445
+ { cmd: 'aistudio.build "build a portfolio site"', desc: "Build with defaults" },
446
+ { cmd: 'aistudio.build "todo app with auth" --model gemini-3.1-pro-preview', desc: "Build with model override" },
447
+ { cmd: 'aistudio.build "crm dashboard" --output ./out', desc: "Build and extract to directory" },
448
+ ]
449
+ },
417
450
  "ai": {
418
451
  desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
419
452
  args: ["query"],
@@ -1752,8 +1785,7 @@ if (args[0] === "server") {
1752
1785
  }
1753
1786
 
1754
1787
  if (args[0] === "extension-path" || args[0] === "path") {
1755
- const path = require("path");
1756
- const distPath = path.resolve(__dirname, "../dist");
1788
+ const distPath = process.env.SURF_EXTENSION_PATH || path.resolve(__dirname, "../dist");
1757
1789
  console.log(distPath);
1758
1790
  process.exit(0);
1759
1791
  }
@@ -1775,7 +1807,7 @@ Arguments:
1775
1807
 
1776
1808
  Options:
1777
1809
  -b, --browser Browser(s) to install for (default: chrome)
1778
- Values: chrome, chromium, brave, edge, arc, all
1810
+ Values: chrome, chromium, brave, edge, arc, helium, all
1779
1811
  Multiple: --browser chrome,brave
1780
1812
 
1781
1813
  Examples:
@@ -1805,7 +1837,7 @@ Remove native messaging host configuration.
1805
1837
 
1806
1838
  Options:
1807
1839
  -b, --browser Browser(s) to uninstall from (default: chrome)
1808
- Values: chrome, chromium, brave, edge, arc, all
1840
+ Values: chrome, chromium, brave, edge, arc, helium, all
1809
1841
  -a, --all Uninstall from all browsers and remove wrapper
1810
1842
 
1811
1843
  Examples:
@@ -2449,7 +2481,7 @@ tool = ALIASES[tool] || tool;
2449
2481
  const config = loadConfig();
2450
2482
  const autoSaveEnabled = config.autoSaveScreenshots !== false && !options["no-save"];
2451
2483
  if (tool === "screenshot" && !options.output && !options.savePath && autoSaveEnabled) {
2452
- options.savePath = `/tmp/surf-snap-${Date.now()}.png`;
2484
+ options.savePath = path.join(SURF_TMP, `surf-snap-${Date.now()}.png`);
2453
2485
  }
2454
2486
 
2455
2487
  if (tool === "smoke") {
@@ -2485,6 +2517,8 @@ const PRIMARY_ARG_MAP = {
2485
2517
  chatgpt: "query",
2486
2518
  perplexity: "query",
2487
2519
  grok: "query",
2520
+ aistudio: "query",
2521
+ "aistudio.build": "query",
2488
2522
  navigate: "url",
2489
2523
  go: "url",
2490
2524
  js: "code",
@@ -2618,6 +2652,9 @@ if (!noScreenshot && AUTO_SCREENSHOT_TOOLS.includes(tool)) {
2618
2652
 
2619
2653
  const outputPath = toolArgs.output;
2620
2654
  delete toolArgs.output;
2655
+ if (tool === "aistudio.build" && outputPath) {
2656
+ toolArgs.output = path.resolve(outputPath);
2657
+ }
2621
2658
 
2622
2659
  if (tool === "screenshot" && outputPath) {
2623
2660
  if (typeof outputPath !== "string") {
@@ -2813,7 +2850,7 @@ const sendRequest = (toolName, toolArgs = {}) => {
2813
2850
 
2814
2851
  const performAutoCapture = async () => {
2815
2852
  const timestamp = Date.now();
2816
- const screenshotPath = `/tmp/surf-error-${timestamp}.png`;
2853
+ const screenshotPath = path.join(SURF_TMP, `surf-error-${timestamp}.png`);
2817
2854
 
2818
2855
  try {
2819
2856
  const [screenshotResp, consoleResp] = await Promise.all([
@@ -2851,8 +2888,12 @@ const socket = net.createConnection(SOCKET_PATH, () => {
2851
2888
  socket.write(JSON.stringify(request) + "\n");
2852
2889
  });
2853
2890
 
2854
- const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "grok", "ai"];
2855
- const requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000;
2891
+ const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "grok", "aistudio", "aistudio.build", "ai"];
2892
+ let requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000;
2893
+ if (tool === "aistudio.build") {
2894
+ const userTimeoutSec = parseInt(options.timeout || "600", 10);
2895
+ requestTimeout = (userTimeoutSec * 1000) + 60000;
2896
+ }
2856
2897
  const timeout = setTimeout(() => {
2857
2898
  console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
2858
2899
  socket.destroy();
@@ -2934,8 +2975,12 @@ async function handleResponse(response) {
2934
2975
  data = result || response.result;
2935
2976
  }
2936
2977
 
2978
+ if (tool === 'aistudio' && typeof data === 'string') {
2979
+ data = { response: data };
2980
+ }
2981
+
2937
2982
  if (wantJson) {
2938
- console.log(JSON.stringify(data, null, 2));
2983
+ console.log(JSON.stringify(data ?? null, null, 2));
2939
2984
  socket.end();
2940
2985
  process.exit(0);
2941
2986
  }
@@ -3105,6 +3150,30 @@ async function handleResponse(response) {
3105
3150
  console.log(`\nImage saved: ${data.imagePath}`);
3106
3151
  }
3107
3152
  console.error(`\n[${data.model || 'unknown'} | ${((data.tookMs || 0) / 1000).toFixed(1)}s]`);
3153
+ } else if (tool === "aistudio" && data?.response) {
3154
+ console.log(data.response);
3155
+
3156
+ const meta = [];
3157
+ if (data.model) meta.push(data.model);
3158
+ if (data.thinkingTime) meta.push(`thought ${data.thinkingTime}s`);
3159
+ if (Number.isFinite(data.tookMs)) meta.push(`${(data.tookMs / 1000).toFixed(1)}s`);
3160
+ if (meta.length > 0) {
3161
+ console.error(`\n[${meta.join(' | ')}]`);
3162
+ }
3163
+ } else if (tool === "aistudio.build" && data?.zipPath) {
3164
+ console.error(`Downloaded: ${data.zipPath}`);
3165
+ if (data.extractedPath) {
3166
+ console.error(`Extracted: ${data.extractedPath}`);
3167
+ console.error("");
3168
+ }
3169
+
3170
+ const meta = [];
3171
+ if (data.model) meta.push(data.model);
3172
+ if (Number.isFinite(data.buildDuration)) meta.push(`built ${data.buildDuration}s`);
3173
+ if (Number.isFinite(data.tookMs)) meta.push(`${(data.tookMs / 1000).toFixed(1)}s total`);
3174
+ if (meta.length > 0) {
3175
+ console.error(`[${meta.join(" | ")}]`);
3176
+ }
3108
3177
  } else if (tool === "perplexity" && data?.response) {
3109
3178
  console.log(data.response);
3110
3179
  const meta = [];
@@ -12,7 +12,7 @@
12
12
 
13
13
  const net = require("net");
14
14
 
15
- const SOCKET_PATH = "/tmp/surf.sock";
15
+ const SOCKET_PATH = process.platform === "win32" ? "//./pipe/surf" : "/tmp/surf.sock";
16
16
 
17
17
  // Maximum iterations for loops (safety cap)
18
18
  const MAX_LOOP_ITERATIONS = 100;
@@ -2,6 +2,10 @@ const fs = require("fs");
2
2
  const networkFormatters = require("./formatters/network.cjs");
3
3
  const networkStore = require("./network-store.cjs");
4
4
 
5
+ function normalizeModelString(model) {
6
+ return String(model || "").trim().toLowerCase();
7
+ }
8
+
5
9
  /**
6
10
  * Format tool result content for MCP response
7
11
  * @param {*} result - The result object from the extension
@@ -1070,6 +1074,31 @@ function mapToolToMessage(tool, args, tabId) {
1070
1074
  timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
1071
1075
  ...baseMsg
1072
1076
  };
1077
+ case "aistudio": {
1078
+ if (!a.query) throw new Error("query required");
1079
+
1080
+ return {
1081
+ type: "AISTUDIO_QUERY",
1082
+ query: a.query,
1083
+ model: a.model ? normalizeModelString(a.model) : undefined,
1084
+ withPage: a["with-page"],
1085
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
1086
+ ...baseMsg
1087
+ };
1088
+ }
1089
+ case "aistudio.build": {
1090
+ if (!a.query) throw new Error("query required");
1091
+
1092
+ return {
1093
+ type: "AISTUDIO_BUILD",
1094
+ query: a.query,
1095
+ model: a.model ? normalizeModelString(a.model) : undefined,
1096
+ output: a.output,
1097
+ keepOpen: Boolean(a["keep-open"] || a.keepOpen),
1098
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 600000,
1099
+ ...baseMsg,
1100
+ };
1101
+ }
1073
1102
  case "window.new":
1074
1103
  return {
1075
1104
  type: "WINDOW_NEW",