@youdie006/prodex 0.2.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -0
  3. package/dist/banner.d.ts +5 -0
  4. package/dist/banner.js +47 -0
  5. package/dist/banner.js.map +1 -0
  6. package/dist/bundle.d.ts +15 -0
  7. package/dist/bundle.js +30 -0
  8. package/dist/bundle.js.map +1 -0
  9. package/dist/chatgpt-browser.d.ts +119 -0
  10. package/dist/chatgpt-browser.js +857 -0
  11. package/dist/chatgpt-browser.js.map +1 -0
  12. package/dist/cli.d.ts +8 -0
  13. package/dist/cli.js +3502 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +58 -0
  16. package/dist/config.js +277 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/http-mcp.d.ts +20 -0
  19. package/dist/http-mcp.js +236 -0
  20. package/dist/http-mcp.js.map +1 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +9 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/mcp-tools.d.ts +395 -0
  25. package/dist/mcp-tools.js +167 -0
  26. package/dist/mcp-tools.js.map +1 -0
  27. package/dist/mcp.d.ts +37 -0
  28. package/dist/mcp.js +229 -0
  29. package/dist/mcp.js.map +1 -0
  30. package/dist/repo-write.d.ts +47 -0
  31. package/dist/repo-write.js +427 -0
  32. package/dist/repo-write.js.map +1 -0
  33. package/dist/repo.d.ts +27 -0
  34. package/dist/repo.js +386 -0
  35. package/dist/repo.js.map +1 -0
  36. package/dist/safe-file.d.ts +25 -0
  37. package/dist/safe-file.js +295 -0
  38. package/dist/safe-file.js.map +1 -0
  39. package/dist/schema.d.ts +402 -0
  40. package/dist/schema.js +109 -0
  41. package/dist/schema.js.map +1 -0
  42. package/dist/store.d.ts +157 -0
  43. package/dist/store.js +1402 -0
  44. package/dist/store.js.map +1 -0
  45. package/docs/claude.md +130 -0
  46. package/docs/clients.md +75 -0
  47. package/docs/http-mcp.md +223 -0
  48. package/package.json +67 -0
  49. package/scripts/release-check.mjs +436 -0
  50. package/scripts/release-pack.mjs +481 -0
@@ -0,0 +1,857 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { accessSync, constants, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ export class ChatGptBrowserBlockerError extends Error {
6
+ blocker;
7
+ constructor(blocker) {
8
+ super(formatBlockerError(blocker) ?? blocker.message);
9
+ this.name = "ChatGptBrowserBlockerError";
10
+ this.blocker = blocker;
11
+ }
12
+ }
13
+ export const CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS = '[data-message-author-role],script,style,noscript,[aria-hidden="true"],div[role="textbox"],textarea,[contenteditable="true"]';
14
+ export const CHATGPT_COMPOSER_CANDIDATE_EXCLUDED_ANCESTORS = '[data-message-author-role],script,style,noscript,[aria-hidden="true"]';
15
+ export const PRODEX_ACTIVE_COMPOSER_ATTRIBUTE = "data-prodex-active-composer";
16
+ const MAX_PAGE_DISCOVERY_TIMEOUT_MS = 5_000;
17
+ const PAGE_VISIBILITY_PROBE_TIMEOUT_MS = 1_000;
18
+ const CHATGPT_GENERATING_CONTROL_PATTERN = /\bstop\s+(?:generating|responding|response)\b|응답\s*중지|생성\s*중지/i;
19
+ export function defaultChatGptProfileDir() {
20
+ return path.join(os.homedir(), ".local", "share", "prodex", "chrome-chatgpt-pro");
21
+ }
22
+ export function buildChromeLaunchArgs(options) {
23
+ return [
24
+ "--remote-debugging-address=127.0.0.1",
25
+ `--remote-debugging-port=${options.port}`,
26
+ `--user-data-dir=${options.profileDir}`,
27
+ "--no-first-run",
28
+ "--no-default-browser-check",
29
+ "--new-window",
30
+ options.url
31
+ ];
32
+ }
33
+ export function inferLoggedInLikely(text, visibleButtonLabels = []) {
34
+ // Only sign-up prompts and explicit login/sign-up buttons count as logged-out signals. Bare
35
+ // "Log in"/"로그인" substrings appear in the menus and footers of a logged-in page, so matching
36
+ // them against the full page text falsely reported logged-in Pro users as logged out.
37
+ const hasLoginPrompt = text.includes("Sign up for free") ||
38
+ text.includes("무료로 가입") ||
39
+ visibleButtonLabels.some((label) => /^(log in|sign up|로그인|회원가입)$/i.test(label.trim()));
40
+ const hasNewChat = text.includes("New chat") || text.includes("새 채팅");
41
+ const hasProjectNav = text.includes("Projects") || text.includes("프로젝트");
42
+ const hasProfileButton = visibleButtonLabels.some((label) => /profile|account|프로필|계정/i.test(label));
43
+ const hasPlanHint = /\bPro\b|Plus|Team|Enterprise|매우 높음|Extra High/i.test(text);
44
+ return !hasLoginPrompt && hasNewChat && (hasProfileButton || hasProjectNav || hasPlanHint);
45
+ }
46
+ export function isUsableChatGptAnswer(answer) {
47
+ const normalized = answer.trim();
48
+ if (!normalized)
49
+ return false;
50
+ if (/^(생각 중|thinking|thought for|thought about)/i.test(normalized.replace(/\.+$/, ""))) {
51
+ return normalized.split(/\r?\n/).filter(Boolean).length > 1;
52
+ }
53
+ return true;
54
+ }
55
+ export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
56
+ return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer) && !state.generating;
57
+ }
58
+ export function hasChatGptPromptAcceptance(previous, state) {
59
+ return state.userMessageCount > previous.userMessageCount || state.assistantMessageCount > previous.assistantMessageCount;
60
+ }
61
+ export function chatGptBusyBlocker(generating) {
62
+ if (!generating)
63
+ return undefined;
64
+ return {
65
+ code: "response_in_progress",
66
+ message: "ChatGPT is still generating a previous response.",
67
+ retryable: true,
68
+ next_step: "Wait for the visible response to finish, or stop it manually in the browser, then retry."
69
+ };
70
+ }
71
+ export function isLikelyChatGptSubmitButton(label, dataTestId) {
72
+ const normalized = label.trim().toLowerCase();
73
+ return dataTestId === "send-button" || /\b(send|submit)\b|보내기|전송/.test(normalized);
74
+ }
75
+ export function isLikelyChatGptGeneratingControl(label) {
76
+ return CHATGPT_GENERATING_CONTROL_PATTERN.test(label.trim());
77
+ }
78
+ export function detectChatGptBlocker(text, visibleButtonLabels = []) {
79
+ const haystack = `${text}\n${visibleButtonLabels.join("\n")}`.toLowerCase();
80
+ if (/just a moment|checking if the site connection is secure|verify you are human|잠시만 기다려|연결이 안전한지/i.test(haystack)) {
81
+ return {
82
+ code: "cloudflare_check",
83
+ message: "ChatGPT is showing a Cloudflare or human-verification interstitial.",
84
+ retryable: true,
85
+ next_step: "Complete the visible browser check manually, then retry."
86
+ };
87
+ }
88
+ if (hasLikelyChatGptLoginPrompt(haystack)) {
89
+ return {
90
+ code: "login_required",
91
+ message: "ChatGPT is asking you to log in.",
92
+ retryable: true,
93
+ next_step: "Log in manually in the visible browser, then retry."
94
+ };
95
+ }
96
+ // Match real captcha / human-verification phrasing only. Bare words like "robot"/"로봇"/"자동화"
97
+ // appear in ordinary chat titles and sidebar history, so matching them on the full page text
98
+ // wrongly flagged logged-in users as captcha-blocked.
99
+ if (/captcha|보안문자|i'?m not a robot|verify you are (?:not a robot|human)|로봇이 아닙니다|사람인지 확인|자동화된 트래픽/i.test(haystack)) {
100
+ return {
101
+ code: "captcha_required",
102
+ message: "ChatGPT is asking for captcha or human verification.",
103
+ retryable: true,
104
+ next_step: "Solve it manually in the visible browser, then retry."
105
+ };
106
+ }
107
+ if (/message limit|usage limit|model limit|rate limit|you.?ve reached|try again later|limit resets|사용 한도|메시지 한도|모델 한도|요금 제한|나중에 다시/i.test(haystack)) {
108
+ return {
109
+ code: "usage_limit",
110
+ message: "ChatGPT is reporting a usage, message, model, or rate limit.",
111
+ retryable: true,
112
+ next_step: "Wait for the limit to reset or choose an available model in the browser."
113
+ };
114
+ }
115
+ if (/additional verification|required verification|verify your account|permission required|account verification|권한|추가 인증|계정 인증|인증 필요/i.test(haystack)) {
116
+ return {
117
+ code: "permission_required",
118
+ message: "ChatGPT requires account verification or permission handling.",
119
+ retryable: true,
120
+ next_step: "Complete the visible account or permission prompt manually, then retry."
121
+ };
122
+ }
123
+ return undefined;
124
+ }
125
+ export function detectChatGptPageBlocker(state) {
126
+ return detectChatGptBlocker(state.blockerTextSample ?? state.textSample, state.visibleButtonLabels);
127
+ }
128
+ export function inferChatGptPageLoggedInLikely(state) {
129
+ return inferLoggedInLikely(state.blockerTextSample ?? state.textSample, state.visibleButtonLabels);
130
+ }
131
+ function hasLikelyChatGptLoginPrompt(haystack) {
132
+ const hasSpecificSignup = /sign up for free|무료로 가입/i.test(haystack);
133
+ const hasLogin = /\blog in\b|로그인/i.test(haystack);
134
+ const hasSignup = /\bsign up\b|가입/i.test(haystack);
135
+ const hasSessionPrompt = /log in to|login to|sign in to|session expired|logged out|다시 로그인|로그인이 필요/i.test(haystack);
136
+ return hasSpecificSignup || (hasLogin && hasSignup) || hasSessionPrompt;
137
+ }
138
+ export function chatGptBlockerErrorFromAnswerState(state) {
139
+ return formatBlockerError(chatGptBlockerFromAnswerState(state));
140
+ }
141
+ export function chatGptBlockerFromAnswerState(state) {
142
+ return detectChatGptPageBlocker(state);
143
+ }
144
+ export function computePromptAcceptanceDeadline(timeoutMs, startedAt) {
145
+ return startedAt + Math.max(1, timeoutMs);
146
+ }
147
+ export function computePageDiscoveryTimeout(timeoutMs) {
148
+ return Math.max(1, Math.min(timeoutMs, MAX_PAGE_DISCOVERY_TIMEOUT_MS));
149
+ }
150
+ export function normalizeChatGptTargetUrl(value) {
151
+ let url;
152
+ try {
153
+ url = new URL(value);
154
+ }
155
+ catch {
156
+ throw new Error("Target URL must be a ChatGPT web URL.");
157
+ }
158
+ if (url.protocol !== "https:" || url.hostname !== "chatgpt.com") {
159
+ throw new Error("Target URL must be a ChatGPT web URL.");
160
+ }
161
+ url.hash = "";
162
+ url.search = "";
163
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
164
+ return url.toString();
165
+ }
166
+ export function chatGptUrlsReferToSameTarget(currentUrl, expectedUrl) {
167
+ try {
168
+ return normalizeChatGptTargetUrl(currentUrl) === normalizeChatGptTargetUrl(expectedUrl);
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ }
174
+ export function selectChatGptPage(pages, targetUrl, visibilityByPage = new Map()) {
175
+ const chatGptPages = pages.filter((page) => page.type === "page" && isChatGptPageUrl(page.url));
176
+ if (chatGptPageSelectionBlocker(pages, targetUrl, visibilityByPage))
177
+ return undefined;
178
+ if (!targetUrl) {
179
+ return (chatGptPages.find((page) => visibilityByPage.get(page.webSocketDebuggerUrl) === "visible") ??
180
+ chatGptPages.find((page) => isVisibilityUnknown(page, visibilityByPage)) ??
181
+ chatGptPages[0]);
182
+ }
183
+ const targetMatches = chatGptPages.filter((page) => chatGptUrlsReferToSameTarget(page.url, targetUrl));
184
+ return (targetMatches.find((page) => visibilityByPage.get(page.webSocketDebuggerUrl) === "visible") ??
185
+ targetMatches.find((page) => isVisibilityUnknown(page, visibilityByPage)) ??
186
+ targetMatches[0]);
187
+ }
188
+ export function chatGptPageSelectionBlocker(pages, targetUrl, visibilityByPage = new Map()) {
189
+ if (targetUrl)
190
+ return undefined;
191
+ const possiblyVisibleChatGptPages = pages.filter((page) => page.type === "page" && isChatGptPageUrl(page.url) && isChatGptPagePossiblyVisible(page, visibilityByPage));
192
+ if (possiblyVisibleChatGptPages.length <= 1)
193
+ return undefined;
194
+ const urls = possiblyVisibleChatGptPages
195
+ .map((page) => `${page.url} (${visibilityByPage.get(page.webSocketDebuggerUrl) ?? "unknown"})`)
196
+ .slice(0, 5)
197
+ .join(", ");
198
+ return {
199
+ code: "ambiguous_chatgpt_tabs",
200
+ message: `Multiple visible or unverified ChatGPT tabs or windows are available: ${urls}.`,
201
+ retryable: true,
202
+ next_step: "Close extra ChatGPT windows, leave only the intended tab visible, or pass --target-url with --confirm-target."
203
+ };
204
+ }
205
+ function isChatGptPagePossiblyVisible(page, visibilityByPage) {
206
+ const visibility = visibilityByPage.get(page.webSocketDebuggerUrl);
207
+ return visibility === "visible" || visibility === undefined;
208
+ }
209
+ function isVisibilityUnknown(page, visibilityByPage) {
210
+ return visibilityByPage.get(page.webSocketDebuggerUrl) === undefined;
211
+ }
212
+ export function assertVisibleChatGptTab(visibilityState, url, targetUrl) {
213
+ if (visibilityState === "visible")
214
+ return;
215
+ const blocker = chatGptVisibilityBlocker(visibilityState, targetUrl ?? url);
216
+ if (blocker)
217
+ throw new ChatGptBrowserBlockerError(blocker);
218
+ }
219
+ export function assertChatGptTargetUrlMatches(currentUrl, targetUrl) {
220
+ if (chatGptUrlsReferToSameTarget(currentUrl, targetUrl))
221
+ return;
222
+ throw new ChatGptBrowserBlockerError({
223
+ code: "target_url_mismatch",
224
+ message: "ChatGPT tab is not at the confirmed target URL.",
225
+ retryable: true,
226
+ next_step: `Open ${targetUrl} in the visible browser and retry. Current: ${currentUrl}`
227
+ });
228
+ }
229
+ export function assertChatGptTargetTabAvailable(targetUrl) {
230
+ throw new ChatGptBrowserBlockerError({
231
+ code: "target_tab_missing",
232
+ message: "No open ChatGPT tab matches the confirmed target URL.",
233
+ retryable: true,
234
+ next_step: `Open ${targetUrl} in the dedicated browser and retry.`
235
+ });
236
+ }
237
+ export function assertChatGptPageAvailable() {
238
+ throw new ChatGptBrowserBlockerError(chatGptPageMissingBlocker());
239
+ }
240
+ function chatGptPageMissingBlocker() {
241
+ return {
242
+ code: "chatgpt_page_missing",
243
+ message: "Chrome debug port is reachable, but no chatgpt.com tab is open.",
244
+ retryable: true,
245
+ next_step: "Open https://chatgpt.com/ in the dedicated Chrome profile, or run `prodex pro browser login` to reopen it."
246
+ };
247
+ }
248
+ export function assertChatGptReadyForPrompt(loggedInLikely, hasComposer) {
249
+ if (loggedInLikely && hasComposer)
250
+ return;
251
+ const missing = [
252
+ loggedInLikely ? undefined : "a clear logged-in ChatGPT session",
253
+ hasComposer ? undefined : "a visible prompt composer"
254
+ ].filter(Boolean);
255
+ throw new ChatGptBrowserBlockerError({
256
+ code: "chatgpt_not_ready",
257
+ message: `ChatGPT browser is reachable, but it is missing ${missing.join(" and ")}.`,
258
+ retryable: true,
259
+ next_step: "Log in manually and open a normal chat or Project thread with the prompt composer visible, then retry."
260
+ });
261
+ }
262
+ export function chatGptVisibilityBlocker(visibilityState, url) {
263
+ if (visibilityState === "visible")
264
+ return undefined;
265
+ const visibility = visibilityState ?? "unknown";
266
+ return {
267
+ code: "tab_not_visible",
268
+ message: `Selected ChatGPT tab is ${visibility}, not the active visible tab.`,
269
+ retryable: true,
270
+ next_step: `Select ${url ?? "the ChatGPT tab"} in the dedicated browser, then retry.`
271
+ };
272
+ }
273
+ export function openChatGptBrowser(options = {}) {
274
+ const command = resolveChromeCommand();
275
+ const port = options.port ?? 9333;
276
+ const profileDir = options.profileDir ?? defaultChatGptProfileDir();
277
+ const args = buildChromeLaunchArgs({
278
+ port,
279
+ profileDir,
280
+ url: options.url ?? "https://chatgpt.com/"
281
+ });
282
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
283
+ let earlyExit;
284
+ const earlyExitWaiters = new Set();
285
+ const recordEarlyExit = (exit) => {
286
+ if (earlyExit)
287
+ return;
288
+ earlyExit = exit;
289
+ for (const waiter of earlyExitWaiters)
290
+ waiter(exit);
291
+ earlyExitWaiters.clear();
292
+ };
293
+ child.once("exit", (code, signal) => recordEarlyExit({ code, signal }));
294
+ child.once("error", (error) => recordEarlyExit({ error: error.message }));
295
+ child.unref();
296
+ return {
297
+ command,
298
+ args,
299
+ profileDir,
300
+ port,
301
+ waitForEarlyExit: (timeoutMs = 1000) => {
302
+ if (earlyExit)
303
+ return Promise.resolve(earlyExit);
304
+ return new Promise((resolve) => {
305
+ const resolveExit = (exit) => {
306
+ clearTimeout(timer);
307
+ resolve(exit);
308
+ };
309
+ const timer = setTimeout(() => {
310
+ earlyExitWaiters.delete(resolveExit);
311
+ resolve(undefined);
312
+ }, timeoutMs);
313
+ earlyExitWaiters.add(resolveExit);
314
+ });
315
+ }
316
+ };
317
+ }
318
+ export async function getChatGptBrowserStatus(options = {}) {
319
+ const port = options.port ?? 9333;
320
+ const page = await findChatGptPage(port, options.timeoutMs ?? 1500);
321
+ if (!page.ok) {
322
+ return {
323
+ reachable: false,
324
+ loggedInLikely: false,
325
+ hasComposer: false,
326
+ modelHints: [],
327
+ blocker: page.blocker
328
+ };
329
+ }
330
+ if (!page.page) {
331
+ if (page.blocker) {
332
+ return {
333
+ reachable: true,
334
+ loggedInLikely: false,
335
+ hasComposer: false,
336
+ modelHints: [],
337
+ blocker: page.blocker
338
+ };
339
+ }
340
+ return {
341
+ reachable: true,
342
+ loggedInLikely: false,
343
+ hasComposer: false,
344
+ modelHints: [],
345
+ blocker: chatGptPageMissingBlocker()
346
+ };
347
+ }
348
+ const state = await evaluateOnPage(page.page, statusExpression());
349
+ const loggedInLikely = inferChatGptPageLoggedInLikely(state);
350
+ const blocker = chatGptVisibilityBlocker(state.visibilityState, state.url) ?? detectChatGptPageBlocker(state) ?? chatGptBusyBlocker(state.generating);
351
+ return {
352
+ reachable: true,
353
+ loggedInLikely,
354
+ hasComposer: state.hasComposer,
355
+ visibilityState: state.visibilityState,
356
+ url: state.url,
357
+ title: state.title,
358
+ modelHints: state.modelHints,
359
+ blocker
360
+ };
361
+ }
362
+ export async function sendChatGptPrompt(options) {
363
+ const port = options.port ?? 9333;
364
+ const timeoutMs = options.timeoutMs ?? 90_000;
365
+ const normalizedTargetUrl = options.targetUrl ? normalizeChatGptTargetUrl(options.targetUrl) : undefined;
366
+ const pageResult = await findChatGptPage(port, computePageDiscoveryTimeout(timeoutMs), normalizedTargetUrl);
367
+ if (!pageResult.ok) {
368
+ throwBlockerOrError(pageResult.blocker, "ChatGPT browser page is not available");
369
+ }
370
+ if (!pageResult.page) {
371
+ if (pageResult.blocker) {
372
+ throw new ChatGptBrowserBlockerError(pageResult.blocker);
373
+ }
374
+ if (normalizedTargetUrl) {
375
+ assertChatGptTargetTabAvailable(normalizedTargetUrl);
376
+ }
377
+ assertChatGptPageAvailable();
378
+ }
379
+ const page = pageResult.page;
380
+ const status = await evaluateOnPage(page, statusExpression());
381
+ const blocker = detectChatGptPageBlocker(status);
382
+ if (blocker) {
383
+ throw new ChatGptBrowserBlockerError(blocker);
384
+ }
385
+ assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer);
386
+ if (normalizedTargetUrl)
387
+ assertChatGptTargetUrlMatches(status.url, normalizedTargetUrl);
388
+ assertVisibleChatGptTab(status.visibilityState, status.url, normalizedTargetUrl);
389
+ const busyBlocker = chatGptBusyBlocker(status.generating);
390
+ if (busyBlocker) {
391
+ throw new ChatGptBrowserBlockerError(busyBlocker);
392
+ }
393
+ const beforeSubmit = await evaluateOnPage(page, answerExpression());
394
+ const cdp = await connectCdp(page.webSocketDebuggerUrl);
395
+ try {
396
+ await cdp.send("Runtime.enable");
397
+ const inserted = await cdp.evaluate(setComposerTextExpression(options.prompt));
398
+ if (!inserted.ok)
399
+ throw new Error(inserted.reason ?? "Could not insert text into ChatGPT composer");
400
+ await sleep(300);
401
+ const submitted = await cdp.evaluate(submitExpression());
402
+ if (!submitted.ok) {
403
+ await cdp.send("Input.dispatchKeyEvent", enterKeyEvent("keyDown"));
404
+ await cdp.send("Input.dispatchKeyEvent", enterKeyEvent("keyUp"));
405
+ }
406
+ }
407
+ finally {
408
+ cdp.close();
409
+ }
410
+ const started = Date.now();
411
+ const acceptDeadline = computePromptAcceptanceDeadline(timeoutMs, started);
412
+ let accepted = false;
413
+ let finalState;
414
+ while (Date.now() < acceptDeadline) {
415
+ await sleep(500);
416
+ finalState = await evaluateOnPage(page, answerExpression());
417
+ const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
418
+ if (runtimeBlocker)
419
+ throw new ChatGptBrowserBlockerError(runtimeBlocker);
420
+ if (hasChatGptPromptAcceptance(beforeSubmit, finalState)) {
421
+ accepted = true;
422
+ break;
423
+ }
424
+ }
425
+ if (!accepted) {
426
+ throw new Error("Timed out waiting for ChatGPT to accept the prompt.");
427
+ }
428
+ let stableAnswer;
429
+ let stableConfirmations = 0;
430
+ while (Date.now() - started < timeoutMs) {
431
+ await sleep(1000);
432
+ finalState = await evaluateOnPage(page, answerExpression());
433
+ const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
434
+ if (runtimeBlocker)
435
+ throw new ChatGptBrowserBlockerError(runtimeBlocker);
436
+ if (!hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, finalState))
437
+ continue;
438
+ // A "fresh" answer must also be stable: ChatGPT can momentarily look done mid-stream, so accept
439
+ // it only once the text stops changing across polls. Otherwise trailing tokens (e.g. the final
440
+ // "_OK" of the smoke token) get dropped.
441
+ if (finalState.answer === stableAnswer) {
442
+ stableConfirmations += 1;
443
+ if (stableConfirmations >= 1)
444
+ break;
445
+ }
446
+ else {
447
+ stableAnswer = finalState.answer;
448
+ stableConfirmations = 0;
449
+ }
450
+ }
451
+ const completed = finalState;
452
+ if (!completed || !hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
453
+ throw new Error("Timed out waiting for ChatGPT response.");
454
+ }
455
+ return {
456
+ url: completed.url,
457
+ title: completed.title,
458
+ answer: completed.answer.trim(),
459
+ modelHints: completed.modelHints,
460
+ warnings: []
461
+ };
462
+ }
463
+ async function findChatGptPage(port, timeoutMs, targetUrl) {
464
+ try {
465
+ const response = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(timeoutMs) });
466
+ if (!response.ok)
467
+ throw new Error(`HTTP ${response.status}`);
468
+ const pages = (await response.json());
469
+ const visibilityByPage = await getChatGptPageVisibility(pages);
470
+ const blocker = chatGptPageSelectionBlocker(pages, targetUrl, visibilityByPage);
471
+ if (blocker)
472
+ return { ok: true, blocker };
473
+ return { ok: true, page: selectChatGptPage(pages, targetUrl, visibilityByPage) };
474
+ }
475
+ catch (error) {
476
+ return {
477
+ ok: false,
478
+ blocker: {
479
+ code: "browser_unreachable",
480
+ message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
481
+ retryable: true,
482
+ next_step: "Run `prodex pro browser login`, log in, then retry.",
483
+ ...(error instanceof Error ? { detail: error.message } : {})
484
+ }
485
+ };
486
+ }
487
+ }
488
+ async function getChatGptPageVisibility(pages) {
489
+ const visibilityByPage = new Map();
490
+ await Promise.all(pages
491
+ .filter((page) => page.type === "page" && isChatGptPageUrl(page.url))
492
+ .map(async (page) => {
493
+ try {
494
+ visibilityByPage.set(page.webSocketDebuggerUrl, await evaluateOnPage(page, "document.visibilityState", { timeoutMs: PAGE_VISIBILITY_PROBE_TIMEOUT_MS }));
495
+ }
496
+ catch {
497
+ // Leave visibility unknown; untargeted sends treat unknown ChatGPT pages conservatively.
498
+ }
499
+ }));
500
+ return visibilityByPage;
501
+ }
502
+ function isChatGptPageUrl(value) {
503
+ try {
504
+ const url = new URL(value);
505
+ return url.protocol === "https:" && url.hostname === "chatgpt.com";
506
+ }
507
+ catch {
508
+ return false;
509
+ }
510
+ }
511
+ function formatBlockerError(blocker) {
512
+ if (!blocker)
513
+ return undefined;
514
+ return `${blocker.message}${blocker.next_step ? ` Next: ${blocker.next_step}` : ""}`;
515
+ }
516
+ function throwBlockerOrError(blocker, fallback) {
517
+ if (blocker)
518
+ throw new ChatGptBrowserBlockerError(blocker);
519
+ throw new Error(fallback);
520
+ }
521
+ async function evaluateOnPage(page, expression, options = {}) {
522
+ const cdp = await connectCdp(page.webSocketDebuggerUrl, options.timeoutMs);
523
+ try {
524
+ await cdp.send("Runtime.enable");
525
+ return await cdp.evaluate(expression);
526
+ }
527
+ finally {
528
+ cdp.close();
529
+ }
530
+ }
531
+ async function connectCdp(webSocketUrl, timeoutMs) {
532
+ const ws = new WebSocket(webSocketUrl);
533
+ let id = 0;
534
+ const pending = new Map();
535
+ ws.addEventListener("message", (event) => {
536
+ const data = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf8");
537
+ const message = JSON.parse(data);
538
+ if (message.id && pending.has(message.id)) {
539
+ const waiter = pending.get(message.id);
540
+ if (waiter.timer)
541
+ clearTimeout(waiter.timer);
542
+ waiter.resolve(message);
543
+ pending.delete(message.id);
544
+ }
545
+ });
546
+ ws.addEventListener("close", () => {
547
+ for (const [messageId, waiter] of pending) {
548
+ if (waiter.timer)
549
+ clearTimeout(waiter.timer);
550
+ waiter.reject(new Error("Chrome DevTools websocket closed"));
551
+ pending.delete(messageId);
552
+ }
553
+ });
554
+ await new Promise((resolve, reject) => {
555
+ const timer = timeoutMs
556
+ ? setTimeout(() => {
557
+ ws.close();
558
+ reject(new Error("Chrome DevTools websocket timed out"));
559
+ }, Math.max(1, timeoutMs))
560
+ : undefined;
561
+ ws.addEventListener("open", () => {
562
+ if (timer)
563
+ clearTimeout(timer);
564
+ resolve();
565
+ }, { once: true });
566
+ ws.addEventListener("error", () => {
567
+ if (timer)
568
+ clearTimeout(timer);
569
+ reject(new Error("Chrome DevTools websocket failed"));
570
+ }, { once: true });
571
+ });
572
+ const send = (method, params = {}) => {
573
+ const messageId = ++id;
574
+ return new Promise((resolve, reject) => {
575
+ const timer = timeoutMs
576
+ ? setTimeout(() => {
577
+ pending.delete(messageId);
578
+ ws.close();
579
+ reject(new Error(`Chrome DevTools command timed out: ${method}`));
580
+ }, Math.max(1, timeoutMs))
581
+ : undefined;
582
+ pending.set(messageId, { resolve, reject, timer });
583
+ ws.send(JSON.stringify({ id: messageId, method, params }));
584
+ });
585
+ };
586
+ const evaluate = async (expression) => {
587
+ const response = await send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
588
+ if (response.error?.message)
589
+ throw new Error(response.error.message);
590
+ if (response.result?.exceptionDetails)
591
+ throw new Error("Runtime.evaluate failed");
592
+ return response.result?.result?.value;
593
+ };
594
+ return { send, evaluate, close: () => ws.close() };
595
+ }
596
+ export function statusExpression() {
597
+ const excludedTextSelector = JSON.stringify(CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS);
598
+ const generatingControlPattern = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.source);
599
+ const generatingControlFlags = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.flags);
600
+ return `(() => {
601
+ ${composerExpressionHelpers()}
602
+ const text = document.body?.innerText || "";
603
+ const runtimeExcludedTextSelector = ${excludedTextSelector};
604
+ const generatingControlPattern = new RegExp(${generatingControlPattern}, ${generatingControlFlags});
605
+ const visibleTextOutsideMessages = () => {
606
+ if (!document.body) return "";
607
+ const parts = [];
608
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
609
+ while (walker.nextNode()) {
610
+ const node = walker.currentNode;
611
+ const parent = node.parentElement;
612
+ const value = node.nodeValue?.trim();
613
+ if (!parent || !value) continue;
614
+ if (parent.closest(runtimeExcludedTextSelector)) continue;
615
+ const style = window.getComputedStyle(parent);
616
+ if (style.display === "none" || style.visibility === "hidden") continue;
617
+ if (!(parent.offsetWidth || parent.offsetHeight || parent.getClientRects().length)) continue;
618
+ parts.push(value);
619
+ }
620
+ return parts.join(String.fromCharCode(10));
621
+ };
622
+ const blockerText = visibleTextOutsideMessages();
623
+ const lines = text.split(String.fromCharCode(10)).map((line) => line.trim()).filter(Boolean);
624
+ const visibleButtonLabels = [...document.querySelectorAll('button,a,[role="button"]')]
625
+ .filter((el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length))
626
+ .filter((el) => !el.closest(runtimeExcludedTextSelector))
627
+ .map((el) => (el.innerText || el.getAttribute("aria-label") || el.getAttribute("data-testid") || "").trim())
628
+ .filter(Boolean);
629
+ const messages = [...document.querySelectorAll('[data-message-author-role]')].map((node) => ({
630
+ role: node.getAttribute('data-message-author-role'),
631
+ text: node.innerText || ""
632
+ }));
633
+ const assistant = messages.filter((message) => message.role === "assistant").at(-1);
634
+ const answer = assistant?.text || "";
635
+ const placeholder = /^(생각 중|thinking|thought for|thought about)/i.test(answer.trim().replace(/\\.+$/, ""));
636
+ const hasComposer = Boolean(findChatGptComposerCandidate());
637
+ return {
638
+ title: document.title,
639
+ url: location.href,
640
+ visibilityState: document.visibilityState,
641
+ textSample: text.slice(0, 12000),
642
+ blockerTextSample: blockerText.slice(0, 12000),
643
+ visibleButtonLabels,
644
+ hasComposer,
645
+ generating: placeholder || visibleButtonLabels.some((label) => generatingControlPattern.test(label)),
646
+ modelHints: lines.filter((line) => /GPT|Pro|Thinking|ChatGPT|Extra High|Auto/i.test(line)).slice(0, 30)
647
+ };
648
+ })()`;
649
+ }
650
+ export function setComposerTextExpression(text) {
651
+ const serializedText = JSON.stringify(text);
652
+ return `(() => {
653
+ ${composerExpressionHelpers()}
654
+ const el = findChatGptComposerCandidate();
655
+ if (!el) return { ok: false, reason: "No visible composer" };
656
+ const root = findChatGptComposerRoot(el);
657
+ if (root) markChatGptComposerRoot(root);
658
+ el.focus();
659
+ const text = ${serializedText};
660
+ const dispatchInput = () => {
661
+ el.dispatchEvent(new InputEvent("input", { inputType: "insertText", data: text, bubbles: true, composed: true }));
662
+ el.dispatchEvent(new Event("change", { bubbles: true }));
663
+ };
664
+ if ("value" in el) {
665
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
666
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
667
+ if (setter) setter.call(el, text);
668
+ else el.value = text;
669
+ dispatchInput();
670
+ } else {
671
+ const selection = window.getSelection();
672
+ const range = document.createRange();
673
+ range.selectNodeContents(el);
674
+ range.deleteContents();
675
+ range.collapse(true);
676
+ selection?.removeAllRanges();
677
+ selection?.addRange(range);
678
+ document.execCommand("insertText", false, text);
679
+ const current = el.innerText || el.textContent || "";
680
+ if (!current.trim()) {
681
+ el.textContent = text;
682
+ dispatchInput();
683
+ }
684
+ }
685
+ const actualText = ("value" in el ? el.value : el.innerText || el.textContent || "").trim();
686
+ return actualText ? { ok: true, actualText: actualText.slice(0, 120) } : { ok: false, reason: "Composer stayed empty after text insertion" };
687
+ })()`;
688
+ }
689
+ export function submitExpression() {
690
+ return `(() => {
691
+ ${composerExpressionHelpers()}
692
+ const markedRoot = findMarkedChatGptComposerRoot();
693
+ const composer = markedRoot ? undefined : findChatGptComposerCandidate();
694
+ const root = markedRoot || (composer ? findChatGptComposerRoot(composer) : undefined);
695
+ const button = root ? findChatGptSubmitButton(root) : undefined;
696
+ if (!button) return { ok: false, reason: "No enabled submit button" };
697
+ button.click();
698
+ return { ok: true };
699
+ })()`;
700
+ }
701
+ function composerExpressionHelpers() {
702
+ const excludedTextSelector = JSON.stringify(CHATGPT_COMPOSER_CANDIDATE_EXCLUDED_ANCESTORS);
703
+ const activeComposerAttribute = JSON.stringify(PRODEX_ACTIVE_COMPOSER_ATTRIBUTE);
704
+ return `
705
+ const excludedTextSelector = ${excludedTextSelector};
706
+ const activeComposerAttribute = ${activeComposerAttribute};
707
+ const isVisible = (node) => !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length);
708
+ const isEditableComposer = (node) => isVisible(node) && !(node.parentElement && node.parentElement.closest(excludedTextSelector));
709
+ const findChatGptComposerRoot = (node) =>
710
+ node.closest('form') ||
711
+ node.closest('[data-testid*="composer"],[data-testid*="prompt"],[class*="composer"]') ||
712
+ node.parentElement;
713
+ const isChatGptSubmitLikeButton = (node) => {
714
+ const label = (node.innerText || node.getAttribute("aria-label") || node.getAttribute("data-testid") || "").toLowerCase();
715
+ const dataTestId = node.getAttribute("data-testid");
716
+ return isVisible(node) && (dataTestId === "send-button" || /\\b(send|submit)\\b|보내기|전송/.test(label));
717
+ };
718
+ const isEnabledButton = (node) => !node.disabled && node.getAttribute("aria-disabled") !== "true";
719
+ const findChatGptSubmitButton = (root, requireEnabled = true) =>
720
+ [...root.querySelectorAll('button')].find((node) => isChatGptSubmitLikeButton(node) && (!requireEnabled || isEnabledButton(node)));
721
+ const markChatGptComposerRoot = (root) => {
722
+ document.querySelectorAll('[' + activeComposerAttribute + '="true"]').forEach((node) => node.removeAttribute(activeComposerAttribute));
723
+ root.setAttribute(activeComposerAttribute, "true");
724
+ };
725
+ const findMarkedChatGptComposerRoot = () => document.querySelector('[' + activeComposerAttribute + '="true"]');
726
+ const isChatGptComposerRootEl = (root) =>
727
+ root.tagName === 'FORM' || (root.matches && root.matches('[data-testid*="composer"],[data-testid*="prompt"],[class*="composer"]'));
728
+ const findChatGptComposerCandidate = () => {
729
+ const candidates = [...document.querySelectorAll('textarea[data-testid="prompt-textarea"], div[role="textbox"], textarea, [contenteditable="true"]')]
730
+ .filter(isEditableComposer);
731
+ // Prefer a composer whose root still shows a submit button.
732
+ const withSubmit = candidates.find((node) => {
733
+ const root = findChatGptComposerRoot(node);
734
+ return root && findChatGptSubmitButton(root, false);
735
+ });
736
+ if (withSubmit) return withSubmit;
737
+ // ChatGPT hides the send button until text is entered, so also accept an editable inside a
738
+ // composer form/container even when no submit button is visible on an empty composer.
739
+ return candidates.find((node) => {
740
+ const root = findChatGptComposerRoot(node);
741
+ return root && isChatGptComposerRootEl(root);
742
+ });
743
+ };
744
+ `;
745
+ }
746
+ function answerExpression() {
747
+ const excludedTextSelector = JSON.stringify(CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS);
748
+ const generatingControlPattern = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.source);
749
+ const generatingControlFlags = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.flags);
750
+ return `(() => {
751
+ const text = document.body?.innerText || "";
752
+ const excludedTextSelector = ${excludedTextSelector};
753
+ const generatingControlPattern = new RegExp(${generatingControlPattern}, ${generatingControlFlags});
754
+ const visibleTextOutsideMessages = () => {
755
+ if (!document.body) return "";
756
+ const parts = [];
757
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
758
+ while (walker.nextNode()) {
759
+ const node = walker.currentNode;
760
+ const parent = node.parentElement;
761
+ const value = node.nodeValue?.trim();
762
+ if (!parent || !value) continue;
763
+ if (parent.closest(excludedTextSelector)) continue;
764
+ const style = window.getComputedStyle(parent);
765
+ if (style.display === "none" || style.visibility === "hidden") continue;
766
+ if (!(parent.offsetWidth || parent.offsetHeight || parent.getClientRects().length)) continue;
767
+ parts.push(value);
768
+ }
769
+ return parts.join(String.fromCharCode(10));
770
+ };
771
+ const lines = text.split(String.fromCharCode(10)).map((line) => line.trim()).filter(Boolean);
772
+ const messages = [...document.querySelectorAll('[data-message-author-role]')].map((node) => ({
773
+ role: node.getAttribute('data-message-author-role'),
774
+ text: node.innerText || ""
775
+ }));
776
+ const assistantMessages = messages.filter((message) => message.role === "assistant");
777
+ const userMessages = messages.filter((message) => message.role === "user");
778
+ const assistant = assistantMessages.at(-1);
779
+ const buttons = [...document.querySelectorAll('button,[role="button"]')]
780
+ .filter((node) => !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length))
781
+ .filter((node) => !node.closest(excludedTextSelector))
782
+ .map((node) => (node.innerText || node.getAttribute("aria-label") || node.getAttribute("data-testid") || "").trim())
783
+ .filter(Boolean);
784
+ const answer = assistant?.text || "";
785
+ const placeholder = /^(생각 중|thinking|thought for|thought about)/i.test(answer.trim().replace(/\\.+$/, ""));
786
+ return {
787
+ title: document.title,
788
+ url: location.href,
789
+ answer: answer || text.slice(-4000),
790
+ textSample: text.slice(0, 12000),
791
+ blockerTextSample: visibleTextOutsideMessages().slice(0, 12000),
792
+ visibleButtonLabels: buttons,
793
+ generating: placeholder || buttons.some((label) => generatingControlPattern.test(label)),
794
+ assistantMessageCount: assistantMessages.length,
795
+ userMessageCount: userMessages.length,
796
+ modelHints: lines.filter((line) => /GPT|Pro|Thinking|ChatGPT|Extra High|Auto/i.test(line)).slice(0, 30)
797
+ };
798
+ })()`;
799
+ }
800
+ function enterKeyEvent(type) {
801
+ return { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 };
802
+ }
803
+ function resolveChromeCommand() {
804
+ const fromEnv = process.env.PRODEX_CHROME;
805
+ if (fromEnv) {
806
+ assertChromeCommandAvailable(fromEnv, "PRODEX_CHROME");
807
+ return fromEnv;
808
+ }
809
+ for (const command of ["google-chrome", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"]) {
810
+ if (isCommandOnPath(command) && hasChromeLikeVersion(command))
811
+ return command;
812
+ }
813
+ throw new Error("Could not find Chrome/Chromium. Set PRODEX_CHROME to the browser executable.");
814
+ }
815
+ function assertChromeCommandAvailable(command, label) {
816
+ if (isPathLikeCommand(command)) {
817
+ try {
818
+ if (!statSync(command).isFile()) {
819
+ throw new Error("not a file");
820
+ }
821
+ accessSync(command, constants.X_OK);
822
+ }
823
+ catch {
824
+ throw new Error(`${label} does not point to an executable browser: ${command}`);
825
+ }
826
+ }
827
+ else if (!isCommandOnPath(command)) {
828
+ throw new Error(`${label} browser command was not found on PATH: ${command}`);
829
+ }
830
+ assertChromeLikeVersion(command, label);
831
+ }
832
+ function isPathLikeCommand(command) {
833
+ return path.isAbsolute(command) || command.includes("/") || command.includes("\\");
834
+ }
835
+ function isCommandOnPath(command) {
836
+ const lookup = process.platform === "win32" ? "where" : "which";
837
+ const result = spawnSync(lookup, [command], { stdio: "ignore" });
838
+ return result.status === 0;
839
+ }
840
+ function assertChromeLikeVersion(command, label) {
841
+ if (!hasChromeLikeVersion(command)) {
842
+ throw new Error(`${label} must point to a Chrome/Chromium-compatible browser executable: ${command}`);
843
+ }
844
+ }
845
+ function hasChromeLikeVersion(command) {
846
+ const result = spawnSync(command, ["--version"], {
847
+ encoding: "utf8",
848
+ timeout: 3000,
849
+ maxBuffer: 1024 * 1024
850
+ });
851
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
852
+ return !result.error && result.status === 0 && /Chrome|Chromium|Brave|Microsoft Edge/i.test(output);
853
+ }
854
+ function sleep(ms) {
855
+ return new Promise((resolve) => setTimeout(resolve, ms));
856
+ }
857
+ //# sourceMappingURL=chatgpt-browser.js.map