surf-cli 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/manifest.json +70 -0
- package/dist/options/options.html +30 -0
- package/dist/options/options.js +30 -0
- package/dist/options/options.js.map +1 -0
- package/dist/service-worker/index.js +156 -0
- package/dist/service-worker/index.js.map +1 -0
- package/dist/service-worker-loader.js +1 -0
- package/native/CHANGELOG.md +136 -0
- package/native/README.md +141 -0
- package/native/chatgpt-client.cjs +455 -0
- package/native/cli.cjs +2424 -0
- package/native/config.cjs +87 -0
- package/native/device-presets.cjs +211 -0
- package/native/formatters/network.cjs +402 -0
- package/native/gemini-client.cjs +637 -0
- package/native/host-helpers.cjs +989 -0
- package/native/host-wrapper.py +15 -0
- package/native/host.cjs +1271 -0
- package/native/host.sh +2 -0
- package/native/mcp-server.cjs +511 -0
- package/native/network-store.cjs +851 -0
- package/native/perplexity-client.cjs +561 -0
- package/native/protocol.cjs +27 -0
- package/native/test-host.py +41 -0
- package/native/tests/cli-tests.sh +115 -0
- package/package.json +70 -0
- package/scripts/install-native-host.cjs +308 -0
- package/scripts/uninstall-native-host.cjs +194 -0
package/native/host.cjs
ADDED
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const net = require("net");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
|
9
|
+
const chatgptClient = require("./chatgpt-client.cjs");
|
|
10
|
+
const geminiClient = require("./gemini-client.cjs");
|
|
11
|
+
const perplexityClient = require("./perplexity-client.cjs");
|
|
12
|
+
const { mapToolToMessage, mapComputerAction, formatToolContent } = require("./host-helpers.cjs");
|
|
13
|
+
|
|
14
|
+
const SOCKET_PATH = "/tmp/surf.sock";
|
|
15
|
+
|
|
16
|
+
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
17
|
+
function resizeImage(filePath, maxSize) {
|
|
18
|
+
const platform = process.platform;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (platform === "darwin") {
|
|
22
|
+
// macOS: use sips
|
|
23
|
+
execSync(`sips --resampleHeightWidthMax ${maxSize} "${filePath}" --out "${filePath}" 2>/dev/null`, { stdio: "pipe" });
|
|
24
|
+
const sizeInfo = execSync(`sips -g pixelWidth -g pixelHeight "${filePath}" 2>/dev/null`, { encoding: "utf8" });
|
|
25
|
+
const width = parseInt(sizeInfo.match(/pixelWidth:\s*(\d+)/)?.[1] || "0", 10);
|
|
26
|
+
const height = parseInt(sizeInfo.match(/pixelHeight:\s*(\d+)/)?.[1] || "0", 10);
|
|
27
|
+
return { success: true, width, height };
|
|
28
|
+
} else {
|
|
29
|
+
// Linux/other: use ImageMagick (try IM6 first, then IM7)
|
|
30
|
+
try {
|
|
31
|
+
execSync(`convert "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
|
|
32
|
+
} catch {
|
|
33
|
+
// IM7 uses 'magick' as main command
|
|
34
|
+
execSync(`magick "${filePath}" -resize ${maxSize}x${maxSize}\\> "${filePath}"`, { stdio: "pipe" });
|
|
35
|
+
}
|
|
36
|
+
// Get dimensions (IM7 may need 'magick identify' instead of just 'identify')
|
|
37
|
+
let sizeInfo;
|
|
38
|
+
try {
|
|
39
|
+
sizeInfo = execSync(`identify -format "%w %h" "${filePath}"`, { encoding: "utf8" });
|
|
40
|
+
} catch {
|
|
41
|
+
sizeInfo = execSync(`magick identify -format "%w %h" "${filePath}"`, { encoding: "utf8" });
|
|
42
|
+
}
|
|
43
|
+
const [width, height] = sizeInfo.trim().split(" ").map(Number);
|
|
44
|
+
return { success: true, width, height };
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return { success: false, error: e.message };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const aiRequestQueue = [];
|
|
52
|
+
let aiRequestInProgress = false;
|
|
53
|
+
|
|
54
|
+
function queueAiRequest(handler) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
aiRequestQueue.push({ handler, resolve, reject });
|
|
57
|
+
processAiQueue();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function processAiQueue() {
|
|
62
|
+
if (aiRequestInProgress || aiRequestQueue.length === 0) return;
|
|
63
|
+
aiRequestInProgress = true;
|
|
64
|
+
const { handler, resolve, reject } = aiRequestQueue.shift();
|
|
65
|
+
try {
|
|
66
|
+
const result = await handler();
|
|
67
|
+
resolve(result);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
reject(err);
|
|
70
|
+
} finally {
|
|
71
|
+
aiRequestInProgress = false;
|
|
72
|
+
setTimeout(processAiQueue, 2000);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const LOG_FILE = "/tmp/surf-host.log";
|
|
76
|
+
const AUTH_FILE = path.join(os.homedir(), ".pi", "agent", "auth.json");
|
|
77
|
+
|
|
78
|
+
const DEFAULT_RETRY_OPTIONS = {
|
|
79
|
+
maxRetries: 3,
|
|
80
|
+
initialDelayMs: 1000,
|
|
81
|
+
maxDelayMs: 10000,
|
|
82
|
+
backoffFactor: 2,
|
|
83
|
+
retryableStatusCodes: [429, 500, 502, 503, 504]
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
async function withRetry(fn, retryOptions = DEFAULT_RETRY_OPTIONS, retryCount = 0) {
|
|
87
|
+
try {
|
|
88
|
+
return await fn();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (retryCount >= retryOptions.maxRetries) {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let isRetryable = false;
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
const statusCodeMatch = error.message.match(/status code (\d+)/i);
|
|
97
|
+
if (statusCodeMatch) {
|
|
98
|
+
const statusCode = parseInt(statusCodeMatch[1], 10);
|
|
99
|
+
isRetryable = retryOptions.retryableStatusCodes.includes(statusCode);
|
|
100
|
+
} else {
|
|
101
|
+
const isNetworkError = error.message.includes('network') ||
|
|
102
|
+
error.message.includes('timeout') ||
|
|
103
|
+
error.message.includes('connection');
|
|
104
|
+
const isContentError = error.message.includes('exceeds maximum') ||
|
|
105
|
+
error.message.includes('too large') ||
|
|
106
|
+
error.message.includes('token limit');
|
|
107
|
+
isRetryable = isNetworkError && !isContentError;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!isRetryable) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const delay = Math.min(
|
|
116
|
+
retryOptions.initialDelayMs * Math.pow(retryOptions.backoffFactor, retryCount),
|
|
117
|
+
retryOptions.maxDelayMs
|
|
118
|
+
);
|
|
119
|
+
const jitter = 0.8 + Math.random() * 0.4;
|
|
120
|
+
const delayWithJitter = Math.floor(delay * jitter);
|
|
121
|
+
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, delayWithJitter));
|
|
123
|
+
return withRetry(fn, retryOptions, retryCount + 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const AI_PROMPTS = {
|
|
128
|
+
find: (query, pageContext) => `You are analyzing a web page's accessibility tree. Find the element matching the user's description.
|
|
129
|
+
|
|
130
|
+
Page Context:
|
|
131
|
+
${pageContext}
|
|
132
|
+
|
|
133
|
+
User Query: "${query}"
|
|
134
|
+
|
|
135
|
+
Respond with ONLY the element ref (e.g., "e5") or "NOT_FOUND" if no match.`,
|
|
136
|
+
|
|
137
|
+
summary: (query, pageContext) => `Summarize this web page based on its accessibility tree.
|
|
138
|
+
|
|
139
|
+
Page Context:
|
|
140
|
+
${pageContext}
|
|
141
|
+
|
|
142
|
+
${query ? `Focus on: ${query}` : ""}
|
|
143
|
+
|
|
144
|
+
Keep the summary under 300 characters. Focus on the page's purpose and main content.`,
|
|
145
|
+
|
|
146
|
+
extract: (query, pageContext) => `Extract structured data from this web page based on the user's request.
|
|
147
|
+
|
|
148
|
+
Page Context:
|
|
149
|
+
${pageContext}
|
|
150
|
+
|
|
151
|
+
User Request: "${query}"
|
|
152
|
+
|
|
153
|
+
Respond with valid JSON only.`
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function detectQueryMode(query) {
|
|
157
|
+
const q = query.toLowerCase();
|
|
158
|
+
if (q.includes("find") || q.includes("where is") || q.includes("locate") ||
|
|
159
|
+
q.includes("click") || q.includes("button") || q.includes("link") ||
|
|
160
|
+
q.includes("input") || q.includes("field")) {
|
|
161
|
+
return "find";
|
|
162
|
+
}
|
|
163
|
+
if (q.includes("summarize") || q.includes("summary") || q.includes("what is this") ||
|
|
164
|
+
q.includes("about") || q.includes("describe") || q.includes("overview")) {
|
|
165
|
+
return "summary";
|
|
166
|
+
}
|
|
167
|
+
if (q.includes("list") || q.includes("extract") || q.includes("all the") ||
|
|
168
|
+
q.includes("get all") || q.includes("show all") || q.includes("json")) {
|
|
169
|
+
return "extract";
|
|
170
|
+
}
|
|
171
|
+
return "summary";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let geminiClientCache = null;
|
|
175
|
+
|
|
176
|
+
function getGeminiClient(apiKey) {
|
|
177
|
+
if (!geminiClientCache || geminiClientCache.apiKey !== apiKey) {
|
|
178
|
+
geminiClientCache = { client: new GeminiClient(apiKey), apiKey };
|
|
179
|
+
}
|
|
180
|
+
return geminiClientCache.client;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
class GeminiClient {
|
|
184
|
+
constructor(apiKey) {
|
|
185
|
+
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
186
|
+
this.model = this.genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async analyze(query, pageContext, options = {}) {
|
|
190
|
+
const mode = options.mode || detectQueryMode(query);
|
|
191
|
+
const promptFn = AI_PROMPTS[mode];
|
|
192
|
+
const prompt = promptFn(query, pageContext);
|
|
193
|
+
|
|
194
|
+
const result = await withRetry(async () => {
|
|
195
|
+
const response = await this.model.generateContent(prompt);
|
|
196
|
+
return response.response.text();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let content = result.trim();
|
|
200
|
+
|
|
201
|
+
if (mode === "extract") {
|
|
202
|
+
content = content.replace(/^```(?:json)?\n?|\n?```$/g, '').trim();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { mode, content };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async function handleApiRequest(msg, sendResponse) {
|
|
212
|
+
const { url, method, headers, body, streamId } = msg;
|
|
213
|
+
|
|
214
|
+
log(`API_REQUEST: ${method} ${url} streamId=${streamId}`);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const urlObj = new URL(url);
|
|
218
|
+
const options = {
|
|
219
|
+
hostname: urlObj.hostname,
|
|
220
|
+
port: urlObj.port || 443,
|
|
221
|
+
path: urlObj.pathname + urlObj.search,
|
|
222
|
+
method: method || "POST",
|
|
223
|
+
headers: headers || {},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const req = https.request(options, (res) => {
|
|
227
|
+
log(`API response status: ${res.statusCode}`);
|
|
228
|
+
|
|
229
|
+
sendResponse({
|
|
230
|
+
type: "API_RESPONSE_START",
|
|
231
|
+
streamId,
|
|
232
|
+
status: res.statusCode,
|
|
233
|
+
headers: res.headers,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
res.on("data", (chunk) => {
|
|
237
|
+
sendResponse({
|
|
238
|
+
type: "API_RESPONSE_CHUNK",
|
|
239
|
+
streamId,
|
|
240
|
+
chunk: chunk.toString("utf8"),
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
res.on("end", () => {
|
|
245
|
+
sendResponse({
|
|
246
|
+
type: "API_RESPONSE_END",
|
|
247
|
+
streamId,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
res.on("error", (err) => {
|
|
252
|
+
log(`API response error: ${err.message}`);
|
|
253
|
+
sendResponse({
|
|
254
|
+
type: "API_RESPONSE_ERROR",
|
|
255
|
+
streamId,
|
|
256
|
+
error: err.message,
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
req.on("error", (err) => {
|
|
262
|
+
log(`API request error: ${err.message}`);
|
|
263
|
+
sendResponse({
|
|
264
|
+
type: "API_RESPONSE_ERROR",
|
|
265
|
+
streamId,
|
|
266
|
+
error: err.message,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (body) {
|
|
271
|
+
req.write(typeof body === "string" ? body : JSON.stringify(body));
|
|
272
|
+
}
|
|
273
|
+
req.end();
|
|
274
|
+
} catch (err) {
|
|
275
|
+
log(`API_REQUEST error: ${err.message}`);
|
|
276
|
+
sendResponse({
|
|
277
|
+
type: "API_RESPONSE_ERROR",
|
|
278
|
+
streamId,
|
|
279
|
+
error: err.message,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const log = (msg) => {
|
|
285
|
+
fs.appendFileSync(LOG_FILE, `${new Date().toISOString()} ${msg}\n`);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
log("Host starting...");
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
fs.unlinkSync(SOCKET_PATH);
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
const pendingRequests = new Map();
|
|
295
|
+
const pendingToolRequests = new Map();
|
|
296
|
+
const activeStreams = new Map();
|
|
297
|
+
let requestCounter = 0;
|
|
298
|
+
|
|
299
|
+
function sendToolResponse(socket, id, result, error) {
|
|
300
|
+
const response = { type: "tool_response", id };
|
|
301
|
+
|
|
302
|
+
if (error) {
|
|
303
|
+
response.error = { content: [{ type: "text", text: error }] };
|
|
304
|
+
} else {
|
|
305
|
+
response.result = { content: formatToolContent(result, log) };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
socket.write(JSON.stringify(response) + "\n");
|
|
310
|
+
} catch (e) {
|
|
311
|
+
log(`Error sending tool_response: ${e.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleStreamRequest(msg, socket) {
|
|
316
|
+
const { streamType, options, id: originalId } = msg;
|
|
317
|
+
const tabId = msg.tabId;
|
|
318
|
+
const streamId = ++requestCounter;
|
|
319
|
+
|
|
320
|
+
activeStreams.set(streamId, {
|
|
321
|
+
socket,
|
|
322
|
+
originalId,
|
|
323
|
+
streamType,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
writeMessage({
|
|
327
|
+
type: streamType,
|
|
328
|
+
streamId,
|
|
329
|
+
options: options || {},
|
|
330
|
+
tabId,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
socket.write(JSON.stringify({ type: "stream_started", streamId }) + "\n");
|
|
335
|
+
} catch (e) {
|
|
336
|
+
log(`Error sending stream_started: ${e.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function handleToolRequest(msg, socket) {
|
|
341
|
+
const { method, params } = msg;
|
|
342
|
+
const originalId = msg.id || null;
|
|
343
|
+
|
|
344
|
+
if (method !== "execute_tool") {
|
|
345
|
+
sendToolResponse(socket, originalId, null, `Unknown method: ${method}`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const { tool, args } = params || {};
|
|
350
|
+
const rawTabId = msg.tabId || params?.tabId || args?.tabId;
|
|
351
|
+
const tabId = rawTabId !== undefined ? parseInt(rawTabId, 10) : undefined;
|
|
352
|
+
const rawWindowId = msg.windowId || params?.windowId || args?.windowId;
|
|
353
|
+
const windowId = rawWindowId !== undefined ? parseInt(rawWindowId, 10) : undefined;
|
|
354
|
+
|
|
355
|
+
// Validate parsed IDs
|
|
356
|
+
if (tabId !== undefined && isNaN(tabId)) {
|
|
357
|
+
sendToolResponse(socket, originalId, null, "tabId must be a number");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (windowId !== undefined && isNaN(windowId)) {
|
|
361
|
+
sendToolResponse(socket, originalId, null, "windowId must be a number");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!tool) {
|
|
366
|
+
sendToolResponse(socket, originalId, null, "No tool specified");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const extensionMsg = mapToolToMessage(tool, args, tabId);
|
|
371
|
+
if (!extensionMsg) {
|
|
372
|
+
sendToolResponse(socket, originalId, null, `Unknown tool: ${tool}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (extensionMsg.type === "UNSUPPORTED_ACTION") {
|
|
377
|
+
sendToolResponse(socket, originalId, null, extensionMsg.message);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
382
|
+
setTimeout(() => {
|
|
383
|
+
sendToolResponse(socket, originalId, { success: true }, null);
|
|
384
|
+
}, extensionMsg.seconds * 1000);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (extensionMsg.type === "BATCH_EXECUTE") {
|
|
389
|
+
executeBatch(extensionMsg.actions, extensionMsg.tabId, socket, originalId);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (extensionMsg.type === "AI_ANALYZE") {
|
|
394
|
+
if (!extensionMsg.query || !extensionMsg.query.trim()) {
|
|
395
|
+
sendToolResponse(socket, originalId, null, "Query is required for AI analysis");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
400
|
+
if (!apiKey) {
|
|
401
|
+
sendToolResponse(socket, originalId, null, "GOOGLE_API_KEY environment variable not set. Export it with: export GOOGLE_API_KEY='your-key'");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const pageRequestId = ++requestCounter;
|
|
406
|
+
pendingToolRequests.set(pageRequestId, {
|
|
407
|
+
socket: null,
|
|
408
|
+
originalId: null,
|
|
409
|
+
tool: "read_page",
|
|
410
|
+
onComplete: async (pageResult) => {
|
|
411
|
+
if (pageResult.error) {
|
|
412
|
+
sendToolResponse(socket, originalId, null, `Failed to read page: ${pageResult.error}`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const pageContent = pageResult.pageContent || "";
|
|
417
|
+
if (!pageContent) {
|
|
418
|
+
sendToolResponse(socket, originalId, null, "No page content available");
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const gemini = getGeminiClient(apiKey);
|
|
424
|
+
const result = await gemini.analyze(extensionMsg.query, pageContent, { mode: extensionMsg.mode });
|
|
425
|
+
|
|
426
|
+
if (result.mode === "find") {
|
|
427
|
+
sendToolResponse(socket, originalId, {
|
|
428
|
+
ref: result.content === "NOT_FOUND" ? null : result.content,
|
|
429
|
+
mode: result.mode,
|
|
430
|
+
aiResult: true
|
|
431
|
+
}, null);
|
|
432
|
+
} else {
|
|
433
|
+
sendToolResponse(socket, originalId, {
|
|
434
|
+
content: result.content,
|
|
435
|
+
mode: result.mode,
|
|
436
|
+
aiResult: true
|
|
437
|
+
}, null);
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
sendToolResponse(socket, originalId, null, `AI analysis failed: ${err.message}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
writeMessage({ type: "READ_PAGE", options: { filter: "interactive" }, tabId: extensionMsg.tabId, id: pageRequestId });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (extensionMsg.type === "CHATGPT_QUERY") {
|
|
449
|
+
const { query, model, withPage, file, timeout } = extensionMsg;
|
|
450
|
+
|
|
451
|
+
queueAiRequest(async () => {
|
|
452
|
+
let pageContext = null;
|
|
453
|
+
if (withPage) {
|
|
454
|
+
const pageResult = await new Promise((resolve) => {
|
|
455
|
+
const pageId = ++requestCounter;
|
|
456
|
+
pendingToolRequests.set(pageId, {
|
|
457
|
+
socket: null,
|
|
458
|
+
originalId: null,
|
|
459
|
+
tool: "read_page",
|
|
460
|
+
onComplete: resolve
|
|
461
|
+
});
|
|
462
|
+
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
463
|
+
});
|
|
464
|
+
if (pageResult && !pageResult.error) {
|
|
465
|
+
pageContext = {
|
|
466
|
+
url: pageResult.url,
|
|
467
|
+
text: pageResult.text || pageResult.pageContent || ""
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
let fullPrompt = query;
|
|
473
|
+
if (pageContext) {
|
|
474
|
+
fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${query}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const result = await chatgptClient.query({
|
|
478
|
+
prompt: fullPrompt,
|
|
479
|
+
model,
|
|
480
|
+
file,
|
|
481
|
+
timeout,
|
|
482
|
+
getCookies: () => new Promise((resolve) => {
|
|
483
|
+
const cookieId = ++requestCounter;
|
|
484
|
+
pendingToolRequests.set(cookieId, {
|
|
485
|
+
socket: null,
|
|
486
|
+
originalId: null,
|
|
487
|
+
tool: "get_cookies",
|
|
488
|
+
onComplete: (r) => resolve(r)
|
|
489
|
+
});
|
|
490
|
+
writeMessage({ type: "GET_CHATGPT_COOKIES", id: cookieId });
|
|
491
|
+
}),
|
|
492
|
+
createTab: () => new Promise((resolve) => {
|
|
493
|
+
const tabCreateId = ++requestCounter;
|
|
494
|
+
pendingToolRequests.set(tabCreateId, {
|
|
495
|
+
socket: null,
|
|
496
|
+
originalId: null,
|
|
497
|
+
tool: "create_tab",
|
|
498
|
+
onComplete: (r) => resolve(r)
|
|
499
|
+
});
|
|
500
|
+
writeMessage({ type: "CHATGPT_NEW_TAB", id: tabCreateId });
|
|
501
|
+
}),
|
|
502
|
+
closeTab: (tabIdToClose) => new Promise((resolve) => {
|
|
503
|
+
const tabCloseId = ++requestCounter;
|
|
504
|
+
pendingToolRequests.set(tabCloseId, {
|
|
505
|
+
socket: null,
|
|
506
|
+
originalId: null,
|
|
507
|
+
tool: "close_tab",
|
|
508
|
+
onComplete: (r) => resolve(r)
|
|
509
|
+
});
|
|
510
|
+
writeMessage({ type: "CHATGPT_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
511
|
+
}),
|
|
512
|
+
cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
|
|
513
|
+
const evalId = ++requestCounter;
|
|
514
|
+
pendingToolRequests.set(evalId, {
|
|
515
|
+
socket: null,
|
|
516
|
+
originalId: null,
|
|
517
|
+
tool: "cdp_evaluate",
|
|
518
|
+
onComplete: (r) => resolve(r)
|
|
519
|
+
});
|
|
520
|
+
writeMessage({ type: "CHATGPT_EVALUATE", tabId, expression, id: evalId });
|
|
521
|
+
}),
|
|
522
|
+
cdpCommand: (tabId, method, params) => new Promise((resolve) => {
|
|
523
|
+
const cmdId = ++requestCounter;
|
|
524
|
+
pendingToolRequests.set(cmdId, {
|
|
525
|
+
socket: null,
|
|
526
|
+
originalId: null,
|
|
527
|
+
tool: "cdp_command",
|
|
528
|
+
onComplete: (r) => resolve(r)
|
|
529
|
+
});
|
|
530
|
+
writeMessage({ type: "CHATGPT_CDP_COMMAND", tabId, method, params, id: cmdId });
|
|
531
|
+
}),
|
|
532
|
+
log: (msg) => log(`[chatgpt] ${msg}`)
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
return result;
|
|
536
|
+
}).then((result) => {
|
|
537
|
+
sendToolResponse(socket, originalId, {
|
|
538
|
+
response: result.response,
|
|
539
|
+
model: result.model,
|
|
540
|
+
tookMs: result.tookMs
|
|
541
|
+
}, null);
|
|
542
|
+
}).catch((err) => {
|
|
543
|
+
sendToolResponse(socket, originalId, null, err.message);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (extensionMsg.type === "PERPLEXITY_QUERY") {
|
|
550
|
+
const { query, mode, model, withPage, timeout } = extensionMsg;
|
|
551
|
+
|
|
552
|
+
queueAiRequest(async () => {
|
|
553
|
+
let pageContext = null;
|
|
554
|
+
if (withPage) {
|
|
555
|
+
const pageResult = await new Promise((resolve) => {
|
|
556
|
+
const pageId = ++requestCounter;
|
|
557
|
+
pendingToolRequests.set(pageId, {
|
|
558
|
+
socket: null,
|
|
559
|
+
originalId: null,
|
|
560
|
+
tool: "read_page",
|
|
561
|
+
onComplete: resolve
|
|
562
|
+
});
|
|
563
|
+
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
564
|
+
});
|
|
565
|
+
if (pageResult && !pageResult.error) {
|
|
566
|
+
pageContext = {
|
|
567
|
+
url: pageResult.url,
|
|
568
|
+
text: pageResult.text || pageResult.pageContent || ""
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let fullPrompt = query;
|
|
574
|
+
if (pageContext) {
|
|
575
|
+
fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${query}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const result = await perplexityClient.query({
|
|
579
|
+
prompt: fullPrompt,
|
|
580
|
+
mode: mode || 'search',
|
|
581
|
+
model,
|
|
582
|
+
timeout: timeout || 120000,
|
|
583
|
+
createTab: () => new Promise((resolve) => {
|
|
584
|
+
const tabCreateId = ++requestCounter;
|
|
585
|
+
pendingToolRequests.set(tabCreateId, {
|
|
586
|
+
socket: null,
|
|
587
|
+
originalId: null,
|
|
588
|
+
tool: "create_tab",
|
|
589
|
+
onComplete: (r) => resolve(r)
|
|
590
|
+
});
|
|
591
|
+
writeMessage({ type: "PERPLEXITY_NEW_TAB", id: tabCreateId });
|
|
592
|
+
}),
|
|
593
|
+
closeTab: (tabIdToClose) => new Promise((resolve) => {
|
|
594
|
+
const tabCloseId = ++requestCounter;
|
|
595
|
+
pendingToolRequests.set(tabCloseId, {
|
|
596
|
+
socket: null,
|
|
597
|
+
originalId: null,
|
|
598
|
+
tool: "close_tab",
|
|
599
|
+
onComplete: (r) => resolve(r)
|
|
600
|
+
});
|
|
601
|
+
writeMessage({ type: "PERPLEXITY_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
|
|
602
|
+
}),
|
|
603
|
+
cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
|
|
604
|
+
const evalId = ++requestCounter;
|
|
605
|
+
pendingToolRequests.set(evalId, {
|
|
606
|
+
socket: null,
|
|
607
|
+
originalId: null,
|
|
608
|
+
tool: "cdp_evaluate",
|
|
609
|
+
onComplete: (r) => resolve(r)
|
|
610
|
+
});
|
|
611
|
+
writeMessage({ type: "PERPLEXITY_EVALUATE", tabId, expression, id: evalId });
|
|
612
|
+
}),
|
|
613
|
+
cdpCommand: (tabId, method, params) => new Promise((resolve) => {
|
|
614
|
+
const cmdId = ++requestCounter;
|
|
615
|
+
pendingToolRequests.set(cmdId, {
|
|
616
|
+
socket: null,
|
|
617
|
+
originalId: null,
|
|
618
|
+
tool: "cdp_command",
|
|
619
|
+
onComplete: (r) => resolve(r)
|
|
620
|
+
});
|
|
621
|
+
writeMessage({ type: "PERPLEXITY_CDP_COMMAND", tabId, method, params, id: cmdId });
|
|
622
|
+
}),
|
|
623
|
+
log: (msg) => log(`[perplexity] ${msg}`)
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return result;
|
|
627
|
+
}).then((result) => {
|
|
628
|
+
sendToolResponse(socket, originalId, {
|
|
629
|
+
response: result.response,
|
|
630
|
+
sources: result.sources,
|
|
631
|
+
url: result.url,
|
|
632
|
+
mode: result.mode,
|
|
633
|
+
model: result.model,
|
|
634
|
+
tookMs: result.tookMs
|
|
635
|
+
}, null);
|
|
636
|
+
}).catch((err) => {
|
|
637
|
+
sendToolResponse(socket, originalId, null, err.message);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (extensionMsg.type === "GEMINI_QUERY") {
|
|
644
|
+
const { query, model, withPage, file, generateImage, editImage, output, youtube, aspectRatio, timeout } = extensionMsg;
|
|
645
|
+
|
|
646
|
+
queueAiRequest(async () => {
|
|
647
|
+
// 1. Get page context if requested
|
|
648
|
+
let pageContext = null;
|
|
649
|
+
if (withPage) {
|
|
650
|
+
const pageResult = await new Promise((resolve) => {
|
|
651
|
+
const pageId = ++requestCounter;
|
|
652
|
+
pendingToolRequests.set(pageId, {
|
|
653
|
+
socket: null,
|
|
654
|
+
originalId: null,
|
|
655
|
+
tool: "get_page_text",
|
|
656
|
+
onComplete: resolve
|
|
657
|
+
});
|
|
658
|
+
writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
|
|
659
|
+
});
|
|
660
|
+
if (pageResult && !pageResult.error) {
|
|
661
|
+
pageContext = {
|
|
662
|
+
url: pageResult.url,
|
|
663
|
+
text: pageResult.text || pageResult.pageContent || ""
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 2. Build full prompt
|
|
669
|
+
let fullPrompt = query || "";
|
|
670
|
+
if (pageContext) {
|
|
671
|
+
fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${fullPrompt}`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// 3. Call Gemini client
|
|
675
|
+
const result = await geminiClient.query({
|
|
676
|
+
prompt: fullPrompt,
|
|
677
|
+
model: model || "gemini-3-pro",
|
|
678
|
+
file,
|
|
679
|
+
generateImage,
|
|
680
|
+
editImage,
|
|
681
|
+
output,
|
|
682
|
+
youtube,
|
|
683
|
+
aspectRatio,
|
|
684
|
+
timeout: timeout || 300000,
|
|
685
|
+
getCookies: () => new Promise((resolve) => {
|
|
686
|
+
const cookieId = ++requestCounter;
|
|
687
|
+
pendingToolRequests.set(cookieId, {
|
|
688
|
+
socket: null,
|
|
689
|
+
originalId: null,
|
|
690
|
+
tool: "get_cookies",
|
|
691
|
+
onComplete: (r) => resolve(r)
|
|
692
|
+
});
|
|
693
|
+
writeMessage({ type: "GET_GOOGLE_COOKIES", id: cookieId });
|
|
694
|
+
}),
|
|
695
|
+
log: (msg) => log(`[gemini] ${msg}`)
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
return result;
|
|
699
|
+
}).then((result) => {
|
|
700
|
+
const response = {
|
|
701
|
+
response: result.response,
|
|
702
|
+
model: result.model,
|
|
703
|
+
tookMs: result.tookMs
|
|
704
|
+
};
|
|
705
|
+
if (result.imagePath) {
|
|
706
|
+
response.imagePath = result.imagePath;
|
|
707
|
+
}
|
|
708
|
+
sendToolResponse(socket, originalId, response, null);
|
|
709
|
+
}).catch((err) => {
|
|
710
|
+
sendToolResponse(socket, originalId, null, err.message);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (extensionMsg.type === "EXECUTE_KEY_REPEAT") {
|
|
717
|
+
const { key, repeat, tabId: tid } = extensionMsg;
|
|
718
|
+
let completed = 0;
|
|
719
|
+
let lastError = null;
|
|
720
|
+
|
|
721
|
+
const sendNextKey = () => {
|
|
722
|
+
if (completed >= repeat) {
|
|
723
|
+
if (lastError) {
|
|
724
|
+
sendToolResponse(socket, originalId, null, `Key repeat failed: ${lastError}`);
|
|
725
|
+
} else {
|
|
726
|
+
sendToolResponse(socket, originalId, { success: true }, null);
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const id = ++requestCounter;
|
|
731
|
+
pendingToolRequests.set(id, {
|
|
732
|
+
socket: null,
|
|
733
|
+
originalId: null,
|
|
734
|
+
tool,
|
|
735
|
+
onComplete: (result) => {
|
|
736
|
+
if (result.error) lastError = result.error;
|
|
737
|
+
completed++;
|
|
738
|
+
setTimeout(sendNextKey, 50);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
writeMessage({ type: "EXECUTE_KEY", key, tabId: tid, id });
|
|
742
|
+
};
|
|
743
|
+
sendNextKey();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (extensionMsg.type === "NAMED_TAB_SWITCH" || extensionMsg.type === "NAMED_TAB_CLOSE") {
|
|
748
|
+
const { name, type: opType } = extensionMsg;
|
|
749
|
+
const lookupId = ++requestCounter;
|
|
750
|
+
pendingToolRequests.set(lookupId, {
|
|
751
|
+
socket: null,
|
|
752
|
+
originalId: null,
|
|
753
|
+
tool: "tabs_get_by_name",
|
|
754
|
+
onComplete: (result) => {
|
|
755
|
+
if (result.error || !result.tabId) {
|
|
756
|
+
sendToolResponse(socket, originalId, null, result.error || `No tab found with name "${name}"`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const actionId = ++requestCounter;
|
|
760
|
+
const actionType = opType === "NAMED_TAB_SWITCH" ? "SWITCH_TAB" : "CLOSE_TAB";
|
|
761
|
+
pendingToolRequests.set(actionId, { socket, originalId, tool, tabId: result.tabId });
|
|
762
|
+
writeMessage({ type: actionType, tabId: result.tabId, id: actionId });
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
writeMessage({ type: "TABS_GET_BY_NAME", name, id: lookupId });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const id = ++requestCounter;
|
|
770
|
+
const pendingData = {
|
|
771
|
+
socket,
|
|
772
|
+
originalId,
|
|
773
|
+
tool,
|
|
774
|
+
savePath: extensionMsg.savePath || args?.savePath,
|
|
775
|
+
autoScreenshot: args?.autoScreenshot,
|
|
776
|
+
fullRes: extensionMsg.fullRes || args?.fullRes,
|
|
777
|
+
maxSize: extensionMsg.maxSize || args?.maxSize,
|
|
778
|
+
tabId: extensionMsg.tabId || tabId
|
|
779
|
+
};
|
|
780
|
+
pendingToolRequests.set(id, pendingData);
|
|
781
|
+
|
|
782
|
+
// Include windowId for tab resolution scoping
|
|
783
|
+
const finalMsg = { ...extensionMsg, id };
|
|
784
|
+
if (windowId) finalMsg.windowId = windowId;
|
|
785
|
+
writeMessage(finalMsg);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function executeBatch(actions, tabId, socket, originalId) {
|
|
789
|
+
const results = [];
|
|
790
|
+
const DELAY_MS = 100;
|
|
791
|
+
let currentIndex = 0;
|
|
792
|
+
|
|
793
|
+
function executeNextAction() {
|
|
794
|
+
if (currentIndex >= actions.length) {
|
|
795
|
+
sendToolResponse(socket, originalId, {
|
|
796
|
+
success: true,
|
|
797
|
+
completedActions: actions.length,
|
|
798
|
+
totalActions: actions.length,
|
|
799
|
+
results,
|
|
800
|
+
}, null);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const action = actions[currentIndex];
|
|
805
|
+
const toolName = mapBatchActionToTool(action);
|
|
806
|
+
const toolArgs = mapBatchActionToArgs(action);
|
|
807
|
+
|
|
808
|
+
const extensionMsg = mapToolToMessage(toolName, toolArgs, tabId);
|
|
809
|
+
if (!extensionMsg || extensionMsg.type === "UNSUPPORTED_ACTION") {
|
|
810
|
+
results.push({ index: currentIndex, type: action.type, success: false, error: "Unsupported action" });
|
|
811
|
+
sendToolResponse(socket, originalId, {
|
|
812
|
+
success: false,
|
|
813
|
+
completedActions: currentIndex,
|
|
814
|
+
totalActions: actions.length,
|
|
815
|
+
results,
|
|
816
|
+
error: `Action ${currentIndex} failed: Unsupported action type "${action.type}"`,
|
|
817
|
+
}, null);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (extensionMsg.type === "LOCAL_WAIT") {
|
|
822
|
+
results.push({ index: currentIndex, type: action.type, success: true });
|
|
823
|
+
currentIndex++;
|
|
824
|
+
setTimeout(executeNextAction, extensionMsg.seconds * 1000);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const id = ++requestCounter;
|
|
829
|
+
pendingToolRequests.set(id, {
|
|
830
|
+
socket: null,
|
|
831
|
+
originalId: null,
|
|
832
|
+
tool: toolName,
|
|
833
|
+
onComplete: (result) => {
|
|
834
|
+
if (result.error) {
|
|
835
|
+
results.push({ index: currentIndex, type: action.type, success: false, error: result.error });
|
|
836
|
+
sendToolResponse(socket, originalId, {
|
|
837
|
+
success: false,
|
|
838
|
+
completedActions: currentIndex,
|
|
839
|
+
totalActions: actions.length,
|
|
840
|
+
results,
|
|
841
|
+
error: `Action ${currentIndex} failed: ${result.error}`,
|
|
842
|
+
}, null);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
results.push({ index: currentIndex, type: action.type, success: true });
|
|
847
|
+
currentIndex++;
|
|
848
|
+
|
|
849
|
+
setTimeout(executeNextAction, DELAY_MS);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
writeMessage({ ...extensionMsg, id });
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
executeNextAction();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function mapBatchActionToTool(action) {
|
|
860
|
+
const map = {
|
|
861
|
+
click: "left_click",
|
|
862
|
+
type: "type",
|
|
863
|
+
key: "key",
|
|
864
|
+
wait: "wait",
|
|
865
|
+
scroll: "scroll",
|
|
866
|
+
screenshot: "screenshot",
|
|
867
|
+
navigate: "navigate",
|
|
868
|
+
};
|
|
869
|
+
return map[action.type] || action.type;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function mapBatchActionToArgs(action) {
|
|
873
|
+
switch (action.type) {
|
|
874
|
+
case "click":
|
|
875
|
+
return { ref: action.ref, selector: action.selector, x: action.x, y: action.y };
|
|
876
|
+
case "type":
|
|
877
|
+
return { text: action.text };
|
|
878
|
+
case "key":
|
|
879
|
+
return { key: action.key };
|
|
880
|
+
case "wait":
|
|
881
|
+
return { duration: (action.ms || 1000) / 1000 };
|
|
882
|
+
case "scroll":
|
|
883
|
+
return { scroll_direction: action.direction };
|
|
884
|
+
case "screenshot":
|
|
885
|
+
return { savePath: action.output };
|
|
886
|
+
case "navigate":
|
|
887
|
+
return { url: action.url };
|
|
888
|
+
default:
|
|
889
|
+
return action;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function writeMessage(msg) {
|
|
894
|
+
const json = JSON.stringify(msg);
|
|
895
|
+
const len = Buffer.byteLength(json);
|
|
896
|
+
const buf = Buffer.alloc(4 + len);
|
|
897
|
+
buf.writeUInt32LE(len, 0);
|
|
898
|
+
buf.write(json, 4);
|
|
899
|
+
process.stdout.write(buf);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let inputBuffer = Buffer.alloc(0);
|
|
903
|
+
|
|
904
|
+
function processInput() {
|
|
905
|
+
while (inputBuffer.length >= 4) {
|
|
906
|
+
const msgLen = inputBuffer.readUInt32LE(0);
|
|
907
|
+
if (inputBuffer.length < 4 + msgLen) break;
|
|
908
|
+
|
|
909
|
+
const jsonStr = inputBuffer.slice(4, 4 + msgLen).toString("utf8");
|
|
910
|
+
inputBuffer = inputBuffer.slice(4 + msgLen);
|
|
911
|
+
|
|
912
|
+
try {
|
|
913
|
+
const msg = JSON.parse(jsonStr);
|
|
914
|
+
log(`Received from extension: ${JSON.stringify(msg)}`);
|
|
915
|
+
|
|
916
|
+
if (msg.type === "GET_AUTH") {
|
|
917
|
+
log("Handling GET_AUTH from extension");
|
|
918
|
+
try {
|
|
919
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
920
|
+
const authData = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
921
|
+
writeMessage({ id: msg.id, auth: authData, hint: null });
|
|
922
|
+
} else {
|
|
923
|
+
writeMessage({
|
|
924
|
+
id: msg.id,
|
|
925
|
+
auth: null,
|
|
926
|
+
hint: "No OAuth credentials found. Run 'pi --login anthropic' in terminal to authenticate with Claude Max."
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
} catch (e) {
|
|
930
|
+
log(`Error reading auth file: ${e.message}`);
|
|
931
|
+
writeMessage({
|
|
932
|
+
id: msg.id,
|
|
933
|
+
auth: null,
|
|
934
|
+
hint: "Failed to read auth credentials. Run 'pi --login anthropic' in terminal to authenticate."
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (msg.type === "API_REQUEST") {
|
|
941
|
+
handleApiRequest(msg, writeMessage);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (msg.type === "STREAM_EVENT") {
|
|
946
|
+
const stream = activeStreams.get(msg.streamId);
|
|
947
|
+
if (stream) {
|
|
948
|
+
try {
|
|
949
|
+
stream.socket.write(JSON.stringify(msg.event) + "\n");
|
|
950
|
+
} catch (e) {
|
|
951
|
+
log(`Error forwarding stream event: ${e.message}`);
|
|
952
|
+
activeStreams.delete(msg.streamId);
|
|
953
|
+
writeMessage({ type: "STREAM_STOP", streamId: msg.streamId });
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (msg.type === "STREAM_ERROR") {
|
|
960
|
+
const stream = activeStreams.get(msg.streamId);
|
|
961
|
+
if (stream) {
|
|
962
|
+
try {
|
|
963
|
+
stream.socket.write(JSON.stringify({ error: msg.error }) + "\n");
|
|
964
|
+
} catch (e) {}
|
|
965
|
+
activeStreams.delete(msg.streamId);
|
|
966
|
+
}
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
if (msg.id && pendingToolRequests.has(msg.id)) {
|
|
972
|
+
const pending = pendingToolRequests.get(msg.id);
|
|
973
|
+
pendingToolRequests.delete(msg.id);
|
|
974
|
+
|
|
975
|
+
if (pending.onComplete) {
|
|
976
|
+
pending.onComplete(msg);
|
|
977
|
+
} else {
|
|
978
|
+
const { socket, originalId, savePath, autoScreenshot, tabId: storedTabId } = pending;
|
|
979
|
+
const tabId = storedTabId || msg._resolvedTabId;
|
|
980
|
+
|
|
981
|
+
if (savePath && msg.base64) {
|
|
982
|
+
try {
|
|
983
|
+
const dir = path.dirname(savePath);
|
|
984
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
985
|
+
fs.writeFileSync(savePath, Buffer.from(msg.base64, "base64"));
|
|
986
|
+
const origWidth = msg.width || 0;
|
|
987
|
+
const origHeight = msg.height || 0;
|
|
988
|
+
const maxSize = pending.maxSize || 1200;
|
|
989
|
+
const skipResize = pending.fullRes;
|
|
990
|
+
|
|
991
|
+
let finalDims = origWidth && origHeight ? `${origWidth}x${origHeight}` : "";
|
|
992
|
+
if (!skipResize && (origWidth > maxSize || origHeight > maxSize)) {
|
|
993
|
+
const result = resizeImage(savePath, maxSize);
|
|
994
|
+
if (result.success) {
|
|
995
|
+
finalDims = `${result.width}x${result.height}, from ${origWidth}x${origHeight}`;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
sendToolResponse(socket, originalId, {
|
|
999
|
+
message: `Saved to ${savePath} (${finalDims})`
|
|
1000
|
+
}, null);
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
sendToolResponse(socket, originalId, null, `Failed to save: ${e.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
} else if (autoScreenshot && tabId && !msg.error && !msg.base64) {
|
|
1005
|
+
|
|
1006
|
+
const screenshotId = ++requestCounter;
|
|
1007
|
+
const screenshotPath = `/tmp/pi-auto-${Date.now()}.png`;
|
|
1008
|
+
|
|
1009
|
+
const autoFiles = fs.readdirSync("/tmp")
|
|
1010
|
+
.filter(f => f.startsWith("pi-auto-") && f.endsWith(".png"))
|
|
1011
|
+
.map(f => ({ name: f, time: parseInt(f.match(/pi-auto-(\d+)\.png/)?.[1] || "0", 10) }))
|
|
1012
|
+
.sort((a, b) => b.time - a.time);
|
|
1013
|
+
if (autoFiles.length >= 10) {
|
|
1014
|
+
autoFiles.slice(9).forEach(f => {
|
|
1015
|
+
try { fs.unlinkSync(path.join("/tmp", f.name)); } catch (e) {}
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
pendingToolRequests.set(screenshotId, {
|
|
1019
|
+
socket: null,
|
|
1020
|
+
originalId: null,
|
|
1021
|
+
tool: "screenshot",
|
|
1022
|
+
onComplete: (screenshotMsg) => {
|
|
1023
|
+
if (screenshotMsg.base64) {
|
|
1024
|
+
try {
|
|
1025
|
+
fs.writeFileSync(screenshotPath, Buffer.from(screenshotMsg.base64, "base64"));
|
|
1026
|
+
const origW = screenshotMsg.width || 0;
|
|
1027
|
+
const origH = screenshotMsg.height || 0;
|
|
1028
|
+
let finalW = origW, finalH = origH;
|
|
1029
|
+
const maxSize = 1200;
|
|
1030
|
+
if (origW > maxSize || origH > maxSize) {
|
|
1031
|
+
const result = resizeImage(screenshotPath, maxSize);
|
|
1032
|
+
if (result.success) {
|
|
1033
|
+
finalW = result.width;
|
|
1034
|
+
finalH = result.height;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
sendToolResponse(socket, originalId, {
|
|
1038
|
+
...msg,
|
|
1039
|
+
autoScreenshot: { path: screenshotPath, width: finalW, height: finalH, originalWidth: origW, originalHeight: origH }
|
|
1040
|
+
}, null);
|
|
1041
|
+
} catch (e) {
|
|
1042
|
+
sendToolResponse(socket, originalId, { ...msg, autoScreenshotError: e.message }, null);
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
const errMsg = screenshotMsg.error || "Failed to capture";
|
|
1046
|
+
sendToolResponse(socket, originalId, { ...msg, autoScreenshotError: errMsg }, null);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
setTimeout(() => writeMessage({ type: "EXECUTE_SCREENSHOT", tabId, id: screenshotId }), 500);
|
|
1051
|
+
return;
|
|
1052
|
+
} else if (msg.results && msg.savePath) {
|
|
1053
|
+
try {
|
|
1054
|
+
const dir = msg.savePath;
|
|
1055
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1056
|
+
|
|
1057
|
+
for (const result of msg.results) {
|
|
1058
|
+
if (result.screenshotBase64 && result.hostname) {
|
|
1059
|
+
const ssPath = path.join(dir, `${result.hostname}.png`);
|
|
1060
|
+
fs.writeFileSync(ssPath, Buffer.from(result.screenshotBase64, "base64"));
|
|
1061
|
+
result.screenshot = ssPath;
|
|
1062
|
+
delete result.screenshotBase64;
|
|
1063
|
+
delete result.hostname;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
delete msg.savePath;
|
|
1067
|
+
sendToolResponse(socket, originalId, msg, null);
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
sendToolResponse(socket, originalId, null, `Failed to save screenshots: ${e.message}`);
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
const isPureError = msg.error && !msg.success && !msg.base64 &&
|
|
1073
|
+
!msg.pageContent && !msg.tabs && !msg.text &&
|
|
1074
|
+
!msg.output && !msg.messages && !msg.requests;
|
|
1075
|
+
|
|
1076
|
+
if (isPureError) {
|
|
1077
|
+
sendToolResponse(socket, originalId, null, msg.error);
|
|
1078
|
+
} else {
|
|
1079
|
+
sendToolResponse(socket, originalId, msg, null);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
} else if (msg.id && pendingRequests.has(msg.id)) {
|
|
1084
|
+
const { socket } = pendingRequests.get(msg.id);
|
|
1085
|
+
try {
|
|
1086
|
+
socket.write(JSON.stringify(msg) + "\n");
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
log(`Error writing to CLI socket: ${e.message}`);
|
|
1089
|
+
}
|
|
1090
|
+
pendingRequests.delete(msg.id);
|
|
1091
|
+
}
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
log(`Error parsing message: ${e.message}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
process.stdin.on("readable", () => {
|
|
1099
|
+
let chunk;
|
|
1100
|
+
while ((chunk = process.stdin.read()) !== null) {
|
|
1101
|
+
inputBuffer = Buffer.concat([inputBuffer, chunk]);
|
|
1102
|
+
processInput();
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// Track connected CLI sockets for disconnect notification
|
|
1107
|
+
const connectedSockets = new Set();
|
|
1108
|
+
|
|
1109
|
+
process.stdin.on("end", () => {
|
|
1110
|
+
log("stdin ended (extension disconnected), notifying clients");
|
|
1111
|
+
for (const socket of Array.from(connectedSockets)) {
|
|
1112
|
+
try {
|
|
1113
|
+
socket.write(JSON.stringify({
|
|
1114
|
+
type: "extension_disconnected",
|
|
1115
|
+
message: "Surf extension was reloaded. Restart your command."
|
|
1116
|
+
}) + "\n");
|
|
1117
|
+
socket.end();
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
// Socket may already be closed
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
process.exit(0);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
process.stdin.on("error", (err) => {
|
|
1126
|
+
log(`stdin error: ${err.message}`);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
process.stdout.on("error", (err) => {
|
|
1130
|
+
log(`stdout error: ${err.message}`);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
const server = net.createServer((socket) => {
|
|
1134
|
+
log("CLI client connected");
|
|
1135
|
+
connectedSockets.add(socket);
|
|
1136
|
+
socket.on("close", () => connectedSockets.delete(socket));
|
|
1137
|
+
|
|
1138
|
+
let dataBuffer = "";
|
|
1139
|
+
|
|
1140
|
+
socket.on("data", (data) => {
|
|
1141
|
+
dataBuffer += data.toString();
|
|
1142
|
+
const lines = dataBuffer.split("\n");
|
|
1143
|
+
dataBuffer = lines.pop() || "";
|
|
1144
|
+
|
|
1145
|
+
for (const line of lines) {
|
|
1146
|
+
if (!line.trim()) continue;
|
|
1147
|
+
try {
|
|
1148
|
+
const msg = JSON.parse(line);
|
|
1149
|
+
|
|
1150
|
+
if (msg.type === "GET_AUTH") {
|
|
1151
|
+
log("Handling GET_AUTH locally");
|
|
1152
|
+
try {
|
|
1153
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
1154
|
+
const authData = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
|
|
1155
|
+
socket.write(JSON.stringify({
|
|
1156
|
+
id: msg.id || 0,
|
|
1157
|
+
auth: authData,
|
|
1158
|
+
hint: null
|
|
1159
|
+
}) + "\n");
|
|
1160
|
+
} else {
|
|
1161
|
+
socket.write(JSON.stringify({
|
|
1162
|
+
id: msg.id || 0,
|
|
1163
|
+
auth: null,
|
|
1164
|
+
hint: "No OAuth credentials found. Run 'pi --login anthropic' in terminal to authenticate with Claude Max."
|
|
1165
|
+
}) + "\n");
|
|
1166
|
+
}
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
log(`Error reading auth file: ${e.message}`);
|
|
1169
|
+
socket.write(JSON.stringify({
|
|
1170
|
+
id: msg.id || 0,
|
|
1171
|
+
auth: null,
|
|
1172
|
+
hint: "Failed to read auth credentials. Run 'pi --login anthropic' in terminal to authenticate."
|
|
1173
|
+
}) + "\n");
|
|
1174
|
+
}
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (msg.type === "tool_request") {
|
|
1179
|
+
log("Handling tool_request: " + msg.method + " " + (msg.params?.tool || ""));
|
|
1180
|
+
try {
|
|
1181
|
+
handleToolRequest(msg, socket);
|
|
1182
|
+
} catch (e) {
|
|
1183
|
+
socket.write(JSON.stringify({ error: e.message || "Request failed" }) + "\n");
|
|
1184
|
+
}
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (msg.type === "stream_request") {
|
|
1189
|
+
log("Handling stream_request: " + msg.streamType);
|
|
1190
|
+
handleStreamRequest(msg, socket);
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (msg.type === "stream_stop") {
|
|
1195
|
+
log("Handling stream_stop");
|
|
1196
|
+
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1197
|
+
if (stream.socket === socket) {
|
|
1198
|
+
writeMessage({ type: "STREAM_STOP", streamId });
|
|
1199
|
+
activeStreams.delete(streamId);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const id = ++requestCounter;
|
|
1206
|
+
log(`Forwarding to extension: id=${id} type=${msg.type}`);
|
|
1207
|
+
pendingRequests.set(id, { socket });
|
|
1208
|
+
writeMessage({ ...msg, id });
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
log(`Error parsing CLI request: ${e.message}`);
|
|
1211
|
+
socket.write(JSON.stringify({ error: "Invalid request" }) + "\n");
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
socket.on("error", (err) => {
|
|
1217
|
+
log(`CLI socket error: ${err.message}`);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
socket.on("close", () => {
|
|
1221
|
+
log("CLI client disconnected");
|
|
1222
|
+
for (const [id, pending] of pendingRequests.entries()) {
|
|
1223
|
+
if (pending.socket === socket) {
|
|
1224
|
+
pendingRequests.delete(id);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
for (const [id, pending] of pendingToolRequests.entries()) {
|
|
1228
|
+
if (pending.socket === socket && !pending.autoScreenshot) {
|
|
1229
|
+
pendingToolRequests.delete(id);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
for (const [streamId, stream] of activeStreams.entries()) {
|
|
1233
|
+
if (stream.socket === socket) {
|
|
1234
|
+
writeMessage({ type: "STREAM_STOP", streamId });
|
|
1235
|
+
activeStreams.delete(streamId);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
server.listen(SOCKET_PATH, () => {
|
|
1242
|
+
log("Socket server listening on " + SOCKET_PATH);
|
|
1243
|
+
fs.chmodSync(SOCKET_PATH, 0o600);
|
|
1244
|
+
writeMessage({ type: "HOST_READY" });
|
|
1245
|
+
log("Sent HOST_READY to extension");
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
server.on("error", (err) => {
|
|
1249
|
+
log(`Server error: ${err.message}`);
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
process.on("SIGTERM", () => {
|
|
1253
|
+
log("SIGTERM received");
|
|
1254
|
+
server.close();
|
|
1255
|
+
try { fs.unlinkSync(SOCKET_PATH); } catch {}
|
|
1256
|
+
process.exit(0);
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
process.on("SIGINT", () => {
|
|
1260
|
+
log("SIGINT received");
|
|
1261
|
+
server.close();
|
|
1262
|
+
try { fs.unlinkSync(SOCKET_PATH); } catch {}
|
|
1263
|
+
process.exit(0);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
process.on("uncaughtException", (err) => {
|
|
1267
|
+
log(`Uncaught exception: ${err.message}\n${err.stack}`);
|
|
1268
|
+
process.exit(1);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
log("Host initialization complete, waiting for connections...");
|