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
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
const CHATGPT_URL = "https://chatgpt.com/";
|
|
2
|
+
|
|
3
|
+
const SELECTORS = {
|
|
4
|
+
promptTextarea: '#prompt-textarea, [data-testid="composer-textarea"], textarea[name="prompt-textarea"], .ProseMirror, [contenteditable="true"][data-virtualkeyboard="true"]',
|
|
5
|
+
sendButton: 'button[data-testid="send-button"], button[data-testid*="composer-send"], form button[type="submit"]',
|
|
6
|
+
modelButton: '[data-testid="model-switcher-dropdown-button"]',
|
|
7
|
+
menuContainer: '[role="menu"], [data-radix-collection-root]',
|
|
8
|
+
menuItem: 'button, [role="menuitem"], [role="menuitemradio"], [data-testid*="model-switcher-"]',
|
|
9
|
+
assistantMessage: '[data-message-author-role="assistant"], [data-turn="assistant"]',
|
|
10
|
+
stopButton: '[data-testid="stop-button"]',
|
|
11
|
+
finishedActions: 'button[data-testid="copy-turn-action-button"], button[data-testid="good-response-turn-action-button"]',
|
|
12
|
+
conversationTurn: 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"]',
|
|
13
|
+
fileInput: 'input[type="file"]',
|
|
14
|
+
cloudflareScript: 'script[src*="/challenge-platform/"]',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function delay(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildClickDispatcher() {
|
|
22
|
+
return `function dispatchClickSequence(target){
|
|
23
|
+
if(!target || !(target instanceof EventTarget)) return false;
|
|
24
|
+
const types = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'];
|
|
25
|
+
for (const type of types) {
|
|
26
|
+
const common = { bubbles: true, cancelable: true, view: window };
|
|
27
|
+
let event;
|
|
28
|
+
if (type.startsWith('pointer') && 'PointerEvent' in window) {
|
|
29
|
+
event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' });
|
|
30
|
+
} else {
|
|
31
|
+
event = new MouseEvent(type, common);
|
|
32
|
+
}
|
|
33
|
+
target.dispatchEvent(event);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasRequiredCookies(cookies) {
|
|
40
|
+
if (!cookies || !Array.isArray(cookies)) return false;
|
|
41
|
+
const sessionCookie = cookies.find(
|
|
42
|
+
(c) => c.name === "__Secure-next-auth.session-token" && c.value
|
|
43
|
+
);
|
|
44
|
+
return Boolean(sessionCookie);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function evaluate(cdp, expression) {
|
|
48
|
+
const result = await cdp(expression);
|
|
49
|
+
if (result.exceptionDetails) {
|
|
50
|
+
const desc = result.exceptionDetails.exception?.description ||
|
|
51
|
+
result.exceptionDetails.text ||
|
|
52
|
+
"Evaluation failed";
|
|
53
|
+
throw new Error(desc);
|
|
54
|
+
}
|
|
55
|
+
if (result.error) {
|
|
56
|
+
throw new Error(result.error);
|
|
57
|
+
}
|
|
58
|
+
return result.result?.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function waitForPageLoad(cdp, timeoutMs = 45000) {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
const ready = await evaluate(cdp, "document.readyState");
|
|
65
|
+
if (ready === "complete" || ready === "interactive") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await delay(100);
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Page did not load in time");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function isCloudflareBlocked(cdp) {
|
|
74
|
+
const title = await evaluate(cdp, "document.title.toLowerCase()");
|
|
75
|
+
if (title && title.includes("just a moment")) return true;
|
|
76
|
+
const hasScript = await evaluate(
|
|
77
|
+
cdp,
|
|
78
|
+
`Boolean(document.querySelector('${SELECTORS.cloudflareScript}'))`
|
|
79
|
+
);
|
|
80
|
+
return hasScript;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function checkLoginStatus(cdp) {
|
|
84
|
+
const result = await evaluate(
|
|
85
|
+
cdp,
|
|
86
|
+
`(async () => {
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch('/backend-api/me', {
|
|
89
|
+
cache: 'no-store',
|
|
90
|
+
credentials: 'include'
|
|
91
|
+
});
|
|
92
|
+
const hasLoginCta = Array.from(document.querySelectorAll('a[href*="/auth/login"], button'))
|
|
93
|
+
.some(el => {
|
|
94
|
+
const text = (el.textContent || '').toLowerCase().trim();
|
|
95
|
+
return text.startsWith('log in') || text.startsWith('sign in');
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
status: response.status,
|
|
99
|
+
hasLoginCta,
|
|
100
|
+
url: location.href
|
|
101
|
+
};
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return { status: 0, error: e.message, url: location.href };
|
|
104
|
+
}
|
|
105
|
+
})()`
|
|
106
|
+
);
|
|
107
|
+
return result || { status: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function waitForPromptReady(cdp, timeoutMs = 30000) {
|
|
111
|
+
const deadline = Date.now() + timeoutMs;
|
|
112
|
+
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
113
|
+
while (Date.now() < deadline) {
|
|
114
|
+
const found = await evaluate(
|
|
115
|
+
cdp,
|
|
116
|
+
`(() => {
|
|
117
|
+
const selectors = ${selectors};
|
|
118
|
+
for (const selector of selectors) {
|
|
119
|
+
const node = document.querySelector(selector);
|
|
120
|
+
if (node && !node.hasAttribute('disabled')) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
})()`
|
|
126
|
+
);
|
|
127
|
+
if (found) return true;
|
|
128
|
+
await delay(200);
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
|
|
134
|
+
const modelButton = await evaluate(
|
|
135
|
+
cdp,
|
|
136
|
+
`(() => {
|
|
137
|
+
const btn = document.querySelector('${SELECTORS.modelButton}');
|
|
138
|
+
return btn ? true : false;
|
|
139
|
+
})()`
|
|
140
|
+
);
|
|
141
|
+
if (!modelButton) {
|
|
142
|
+
throw new Error("Model selector button not found");
|
|
143
|
+
}
|
|
144
|
+
await evaluate(
|
|
145
|
+
cdp,
|
|
146
|
+
`(() => {
|
|
147
|
+
${buildClickDispatcher()}
|
|
148
|
+
const btn = document.querySelector('${SELECTORS.modelButton}');
|
|
149
|
+
if (btn) dispatchClickSequence(btn);
|
|
150
|
+
})()`
|
|
151
|
+
);
|
|
152
|
+
await delay(300);
|
|
153
|
+
const normalizedModel = desiredModel.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
154
|
+
const result = await evaluate(
|
|
155
|
+
cdp,
|
|
156
|
+
`(async () => {
|
|
157
|
+
${buildClickDispatcher()}
|
|
158
|
+
const TIMEOUT_MS = ${timeoutMs};
|
|
159
|
+
const targetModel = ${JSON.stringify(normalizedModel)};
|
|
160
|
+
const menuSelector = '${SELECTORS.menuContainer}';
|
|
161
|
+
const itemSelector = '${SELECTORS.menuItem}';
|
|
162
|
+
const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
163
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
164
|
+
while (Date.now() < deadline) {
|
|
165
|
+
const menu = document.querySelector(menuSelector);
|
|
166
|
+
if (!menu) {
|
|
167
|
+
await new Promise(r => setTimeout(r, 100));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const items = Array.from(menu.querySelectorAll(itemSelector));
|
|
171
|
+
let bestMatch = null;
|
|
172
|
+
let bestScore = 0;
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
const text = normalize(item.textContent || '');
|
|
175
|
+
const testId = normalize(item.getAttribute('data-testid') || '');
|
|
176
|
+
let score = 0;
|
|
177
|
+
if (text.includes(targetModel) || testId.includes(targetModel)) score = 100;
|
|
178
|
+
else if (targetModel.includes(text) || targetModel.includes(testId)) score = 50;
|
|
179
|
+
if (score > bestScore) {
|
|
180
|
+
bestScore = score;
|
|
181
|
+
bestMatch = item;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (bestMatch) {
|
|
185
|
+
dispatchClickSequence(bestMatch);
|
|
186
|
+
await new Promise(r => setTimeout(r, 200));
|
|
187
|
+
return { success: true, label: bestMatch.textContent?.trim() };
|
|
188
|
+
}
|
|
189
|
+
await new Promise(r => setTimeout(r, 100));
|
|
190
|
+
}
|
|
191
|
+
return { success: false, error: 'Model option not found' };
|
|
192
|
+
})()`
|
|
193
|
+
);
|
|
194
|
+
if (!result || !result.success) {
|
|
195
|
+
throw new Error(`Model not found: ${desiredModel}`);
|
|
196
|
+
}
|
|
197
|
+
return result.label;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function typePrompt(cdp, inputCdp, prompt) {
|
|
201
|
+
const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", "));
|
|
202
|
+
const encodedPrompt = JSON.stringify(prompt);
|
|
203
|
+
const focused = await evaluate(
|
|
204
|
+
cdp,
|
|
205
|
+
`(() => {
|
|
206
|
+
${buildClickDispatcher()}
|
|
207
|
+
const selectors = ${selectors};
|
|
208
|
+
for (const selector of selectors) {
|
|
209
|
+
const node = document.querySelector(selector);
|
|
210
|
+
if (!node) continue;
|
|
211
|
+
dispatchClickSequence(node);
|
|
212
|
+
if (typeof node.focus === 'function') node.focus();
|
|
213
|
+
const doc = node.ownerDocument;
|
|
214
|
+
const selection = doc?.getSelection?.();
|
|
215
|
+
if (selection) {
|
|
216
|
+
const range = doc.createRange();
|
|
217
|
+
range.selectNodeContents(node);
|
|
218
|
+
range.collapse(false);
|
|
219
|
+
selection.removeAllRanges();
|
|
220
|
+
selection.addRange(range);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
})()`
|
|
226
|
+
);
|
|
227
|
+
if (!focused) {
|
|
228
|
+
throw new Error("Failed to focus prompt textarea");
|
|
229
|
+
}
|
|
230
|
+
await inputCdp("Input.insertText", { text: prompt });
|
|
231
|
+
await delay(300);
|
|
232
|
+
const verified = await evaluate(
|
|
233
|
+
cdp,
|
|
234
|
+
`(() => {
|
|
235
|
+
const selectors = ${selectors};
|
|
236
|
+
for (const selector of selectors) {
|
|
237
|
+
const node = document.querySelector(selector);
|
|
238
|
+
if (!node) continue;
|
|
239
|
+
const text = node.innerText || node.value || node.textContent || '';
|
|
240
|
+
if (text.trim().length > 0) return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
})()`
|
|
244
|
+
);
|
|
245
|
+
if (!verified) {
|
|
246
|
+
await evaluate(
|
|
247
|
+
cdp,
|
|
248
|
+
`(() => {
|
|
249
|
+
const editor = document.querySelector('#prompt-textarea');
|
|
250
|
+
const fallback = document.querySelector('textarea[name="prompt-textarea"]');
|
|
251
|
+
if (fallback) {
|
|
252
|
+
fallback.value = ${encodedPrompt};
|
|
253
|
+
fallback.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
254
|
+
}
|
|
255
|
+
if (editor) {
|
|
256
|
+
editor.textContent = ${encodedPrompt};
|
|
257
|
+
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: ${encodedPrompt}, inputType: 'insertFromPaste' }));
|
|
258
|
+
}
|
|
259
|
+
})()`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function clickSend(cdp, inputCdp) {
|
|
265
|
+
const selectors = SELECTORS.sendButton.split(", ");
|
|
266
|
+
const selectorsJson = JSON.stringify(selectors);
|
|
267
|
+
const deadline = Date.now() + 8000;
|
|
268
|
+
while (Date.now() < deadline) {
|
|
269
|
+
const result = await evaluate(
|
|
270
|
+
cdp,
|
|
271
|
+
`(() => {
|
|
272
|
+
${buildClickDispatcher()}
|
|
273
|
+
const selectors = ${selectorsJson};
|
|
274
|
+
let button = null;
|
|
275
|
+
for (const selector of selectors) {
|
|
276
|
+
button = document.querySelector(selector);
|
|
277
|
+
if (button) break;
|
|
278
|
+
}
|
|
279
|
+
if (!button) return 'missing';
|
|
280
|
+
const disabled = button.hasAttribute('disabled') ||
|
|
281
|
+
button.getAttribute('aria-disabled') === 'true' ||
|
|
282
|
+
button.getAttribute('data-disabled') === 'true';
|
|
283
|
+
if (disabled) return 'disabled';
|
|
284
|
+
dispatchClickSequence(button);
|
|
285
|
+
return 'clicked';
|
|
286
|
+
})()`
|
|
287
|
+
);
|
|
288
|
+
if (result === "clicked") return true;
|
|
289
|
+
if (result === "missing") break;
|
|
290
|
+
await delay(100);
|
|
291
|
+
}
|
|
292
|
+
await inputCdp("Input.dispatchKeyEvent", {
|
|
293
|
+
type: "keyDown",
|
|
294
|
+
key: "Enter",
|
|
295
|
+
code: "Enter",
|
|
296
|
+
windowsVirtualKeyCode: 13,
|
|
297
|
+
nativeVirtualKeyCode: 13,
|
|
298
|
+
text: "\r",
|
|
299
|
+
});
|
|
300
|
+
await inputCdp("Input.dispatchKeyEvent", {
|
|
301
|
+
type: "keyUp",
|
|
302
|
+
key: "Enter",
|
|
303
|
+
code: "Enter",
|
|
304
|
+
windowsVirtualKeyCode: 13,
|
|
305
|
+
nativeVirtualKeyCode: 13,
|
|
306
|
+
});
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function waitForResponse(cdp, timeoutMs = 2700000) {
|
|
311
|
+
const deadline = Date.now() + timeoutMs;
|
|
312
|
+
let previousLength = 0;
|
|
313
|
+
let stableCycles = 0;
|
|
314
|
+
const requiredStableCycles = 6;
|
|
315
|
+
const minStableMs = 1200;
|
|
316
|
+
let lastChangeAt = Date.now();
|
|
317
|
+
while (Date.now() < deadline) {
|
|
318
|
+
const snapshot = await evaluate(
|
|
319
|
+
cdp,
|
|
320
|
+
`(() => {
|
|
321
|
+
const CONVERSATION_SELECTOR = '${SELECTORS.conversationTurn}';
|
|
322
|
+
const ASSISTANT_SELECTOR = '${SELECTORS.assistantMessage}';
|
|
323
|
+
const STOP_SELECTOR = '${SELECTORS.stopButton}';
|
|
324
|
+
const FINISHED_SELECTOR = '${SELECTORS.finishedActions}';
|
|
325
|
+
const isAssistantTurn = (node) => {
|
|
326
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
327
|
+
const role = (node.getAttribute('data-message-author-role') || '').toLowerCase();
|
|
328
|
+
if (role === 'assistant') return true;
|
|
329
|
+
const turn = (node.getAttribute('data-turn') || '').toLowerCase();
|
|
330
|
+
if (turn === 'assistant') return true;
|
|
331
|
+
return Boolean(node.querySelector(ASSISTANT_SELECTOR));
|
|
332
|
+
};
|
|
333
|
+
const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
|
|
334
|
+
let lastAssistantTurn = null;
|
|
335
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
336
|
+
if (isAssistantTurn(turns[i])) {
|
|
337
|
+
lastAssistantTurn = turns[i];
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!lastAssistantTurn) {
|
|
342
|
+
return { text: '', stopVisible: Boolean(document.querySelector(STOP_SELECTOR)), finished: false };
|
|
343
|
+
}
|
|
344
|
+
const messageRoot = lastAssistantTurn.querySelector(ASSISTANT_SELECTOR) || lastAssistantTurn;
|
|
345
|
+
const contentRoot = messageRoot.querySelector('.markdown') ||
|
|
346
|
+
messageRoot.querySelector('[data-message-content]') ||
|
|
347
|
+
messageRoot.querySelector('.prose') ||
|
|
348
|
+
messageRoot;
|
|
349
|
+
const text = (contentRoot?.innerText || contentRoot?.textContent || '').trim();
|
|
350
|
+
const stopVisible = Boolean(document.querySelector(STOP_SELECTOR));
|
|
351
|
+
const finished = Boolean(lastAssistantTurn.querySelector(FINISHED_SELECTOR));
|
|
352
|
+
const messageId = messageRoot.getAttribute('data-message-id') || null;
|
|
353
|
+
return { text, stopVisible, finished, messageId, turnIndex: turns.length - 1 };
|
|
354
|
+
})()`
|
|
355
|
+
);
|
|
356
|
+
if (!snapshot) {
|
|
357
|
+
await delay(400);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const currentLength = (snapshot.text || "").length;
|
|
361
|
+
if (currentLength > previousLength) {
|
|
362
|
+
previousLength = currentLength;
|
|
363
|
+
stableCycles = 0;
|
|
364
|
+
lastChangeAt = Date.now();
|
|
365
|
+
} else {
|
|
366
|
+
stableCycles++;
|
|
367
|
+
}
|
|
368
|
+
const stableMs = Date.now() - lastChangeAt;
|
|
369
|
+
if (!snapshot.stopVisible) {
|
|
370
|
+
const stableEnough = stableCycles >= requiredStableCycles && stableMs >= minStableMs;
|
|
371
|
+
const finishedVisible = snapshot.finished;
|
|
372
|
+
if ((finishedVisible || stableEnough) && currentLength > 0) {
|
|
373
|
+
return {
|
|
374
|
+
text: snapshot.text,
|
|
375
|
+
messageId: snapshot.messageId,
|
|
376
|
+
turnIndex: snapshot.turnIndex,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await delay(400);
|
|
381
|
+
}
|
|
382
|
+
throw new Error("Response timeout");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function query(options) {
|
|
386
|
+
const {
|
|
387
|
+
prompt,
|
|
388
|
+
model,
|
|
389
|
+
file,
|
|
390
|
+
timeout = 2700000,
|
|
391
|
+
getCookies,
|
|
392
|
+
createTab,
|
|
393
|
+
closeTab,
|
|
394
|
+
cdpEvaluate,
|
|
395
|
+
cdpCommand,
|
|
396
|
+
log = () => {},
|
|
397
|
+
} = options;
|
|
398
|
+
const startTime = Date.now();
|
|
399
|
+
log("Starting ChatGPT query");
|
|
400
|
+
const { cookies } = await getCookies();
|
|
401
|
+
if (!hasRequiredCookies(cookies)) {
|
|
402
|
+
throw new Error("ChatGPT login required");
|
|
403
|
+
}
|
|
404
|
+
log(`Got ${cookies.length} cookies`);
|
|
405
|
+
const tabInfo = await createTab();
|
|
406
|
+
const { tabId } = tabInfo;
|
|
407
|
+
if (!tabId) {
|
|
408
|
+
throw new Error("Failed to create ChatGPT tab");
|
|
409
|
+
}
|
|
410
|
+
log(`Created tab ${tabId}`);
|
|
411
|
+
|
|
412
|
+
const cdp = (expr) => cdpEvaluate(tabId, expr);
|
|
413
|
+
const inputCdp = (method, params) => cdpCommand(tabId, method, params);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
await waitForPageLoad(cdp);
|
|
417
|
+
log("Page loaded");
|
|
418
|
+
if (await isCloudflareBlocked(cdp)) {
|
|
419
|
+
throw new Error("Cloudflare challenge detected - complete in browser");
|
|
420
|
+
}
|
|
421
|
+
const loginStatus = await checkLoginStatus(cdp);
|
|
422
|
+
if (loginStatus.status !== 200 || loginStatus.hasLoginCta) {
|
|
423
|
+
throw new Error("ChatGPT login required");
|
|
424
|
+
}
|
|
425
|
+
log("Login verified");
|
|
426
|
+
const promptReady = await waitForPromptReady(cdp);
|
|
427
|
+
if (!promptReady) {
|
|
428
|
+
throw new Error("Prompt textarea not ready");
|
|
429
|
+
}
|
|
430
|
+
log("Prompt ready");
|
|
431
|
+
if (model) {
|
|
432
|
+
const selectedLabel = await selectModel(cdp, model);
|
|
433
|
+
log(`Selected model: ${selectedLabel}`);
|
|
434
|
+
}
|
|
435
|
+
if (file) {
|
|
436
|
+
throw new Error("File upload not yet implemented");
|
|
437
|
+
}
|
|
438
|
+
await typePrompt(cdp, inputCdp, prompt);
|
|
439
|
+
log("Prompt typed");
|
|
440
|
+
await clickSend(cdp, inputCdp);
|
|
441
|
+
log("Prompt sent, waiting for response...");
|
|
442
|
+
const response = await waitForResponse(cdp, timeout);
|
|
443
|
+
log(`Response received (${response.text.length} chars)`);
|
|
444
|
+
return {
|
|
445
|
+
response: response.text,
|
|
446
|
+
model: model || "current",
|
|
447
|
+
messageId: response.messageId,
|
|
448
|
+
tookMs: Date.now() - startTime,
|
|
449
|
+
};
|
|
450
|
+
} finally {
|
|
451
|
+
await closeTab(tabId).catch(() => {});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = { query, hasRequiredCookies, CHATGPT_URL };
|