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.
- package/README.md +43 -2
- package/dist/manifest.json +1 -1
- package/dist/service-worker/index.js +5 -5
- package/dist/service-worker/index.js.map +1 -1
- package/native/aistudio-build.cjs +562 -0
- package/native/aistudio-client.cjs +502 -0
- package/native/aistudio-model.cjs +225 -0
- package/native/aistudio-parser.cjs +424 -0
- package/native/aistudio-response.cjs +336 -0
- package/native/cli.cjs +83 -14
- package/native/do-executor.cjs +1 -1
- package/native/host-helpers.cjs +29 -0
- package/native/host.cjs +210 -14
- package/native/mcp-server.cjs +1 -1
- package/native/network-store.cjs +3 -1
- package/package.json +3 -2
- package/scripts/install-native-host.cjs +14 -2
- package/scripts/uninstall-native-host.cjs +7 -1
- package/skills/README.md +21 -0
- package/skills/surf/SKILL.md +545 -0
|
@@ -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
|
|
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/
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 = [];
|
package/native/do-executor.cjs
CHANGED
package/native/host-helpers.cjs
CHANGED
|
@@ -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",
|