@youdie006/prodex 0.25.1 → 0.26.1
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/dist/chatgpt-browser.js +137 -2
- package/dist/cli-pro.js +35 -8
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -363,6 +363,48 @@ export async function ensureVirtualDisplay(options = {}) {
|
|
|
363
363
|
}
|
|
364
364
|
throw new Error(`No free X display between :${first} and :${first + 9}. Set PRODEX_VIRTUAL_DISPLAY_NUM to a free number.`);
|
|
365
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* How should the dedicated browser be opened?
|
|
368
|
+
*
|
|
369
|
+
* An explicit flag or environment variable wins; otherwise reopen it the way it
|
|
370
|
+
* was last opened. Without the saved fallback, `pro browser login` - the exact
|
|
371
|
+
* command every `browser_unreachable` blocker tells people to run - put a
|
|
372
|
+
* VISIBLE window back on the desktop of someone who had set up a virtual
|
|
373
|
+
* display, which is the surprise window they went to that trouble to avoid.
|
|
374
|
+
*
|
|
375
|
+
* Choosing a mode explicitly replaces the saved one wholesale rather than
|
|
376
|
+
* merging with it: asking for headless must not also rejoin an old virtual
|
|
377
|
+
* display.
|
|
378
|
+
*/
|
|
379
|
+
export function resolveBrowserWindowMode(args) {
|
|
380
|
+
const env = args.env ?? process.env;
|
|
381
|
+
const flags = args.flags ?? {};
|
|
382
|
+
const fromEnv = (name) => {
|
|
383
|
+
const raw = (env[name] ?? "").trim().toLowerCase();
|
|
384
|
+
if (raw === "")
|
|
385
|
+
return undefined;
|
|
386
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
387
|
+
};
|
|
388
|
+
const explicit = {
|
|
389
|
+
headless: flags.headless ?? fromEnv("PRODEX_HEADLESS"),
|
|
390
|
+
virtualDisplay: flags.virtualDisplay ?? fromEnv("PRODEX_VIRTUAL_DISPLAY"),
|
|
391
|
+
minimized: flags.minimized ?? fromEnv("PRODEX_MINIMIZE_WINDOW")
|
|
392
|
+
};
|
|
393
|
+
const chosen = Object.values(explicit).some((value) => value === true);
|
|
394
|
+
if (chosen) {
|
|
395
|
+
return {
|
|
396
|
+
headless: explicit.headless === true,
|
|
397
|
+
virtualDisplay: explicit.virtualDisplay === true,
|
|
398
|
+
minimized: explicit.minimized === true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const saved = args.lastLogin;
|
|
402
|
+
return {
|
|
403
|
+
headless: saved?.headless === true,
|
|
404
|
+
virtualDisplay: saved?.virtual_display !== undefined,
|
|
405
|
+
minimized: saved?.minimized === true
|
|
406
|
+
};
|
|
407
|
+
}
|
|
366
408
|
export function resolveHeadlessPreference(explicit, env = process.env) {
|
|
367
409
|
if (typeof explicit === "boolean")
|
|
368
410
|
return explicit;
|
|
@@ -416,6 +458,11 @@ export function isUsableChatGptAnswer(answer) {
|
|
|
416
458
|
if (toolPanelOnly)
|
|
417
459
|
return false;
|
|
418
460
|
}
|
|
461
|
+
// ChatGPT's own interruption notice, which is what a thread shows after the
|
|
462
|
+
// browser dies mid-stream. Measured: recover saved "Pro thinking / Connection
|
|
463
|
+
// interrupted. Waiting for the complete answer" as a recovered answer.
|
|
464
|
+
if (/connection interrupted|연결이\s*중단/i.test(normalized) && normalized.length < 200)
|
|
465
|
+
return false;
|
|
419
466
|
return true;
|
|
420
467
|
}
|
|
421
468
|
/**
|
|
@@ -943,6 +990,31 @@ export function openChatGptBrowser(options = {}) {
|
|
|
943
990
|
}
|
|
944
991
|
};
|
|
945
992
|
}
|
|
993
|
+
/**
|
|
994
|
+
* Open a ChatGPT tab in the already-running dedicated Chrome.
|
|
995
|
+
*
|
|
996
|
+
* Reported from a live machine: Chrome was up but had no chatgpt.com tab, so
|
|
997
|
+
* login reused it ("no new window opened"), told the user to finish logging in
|
|
998
|
+
* "in the opened window", and then blocked forever on a tab that nobody was
|
|
999
|
+
* going to create. Opening it is the whole fix.
|
|
1000
|
+
*/
|
|
1001
|
+
export async function openChatGptTab(port = DEFAULT_CDP_PORT, url = "https://chatgpt.com/") {
|
|
1002
|
+
const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(url)}`;
|
|
1003
|
+
// Chrome moved /json/new from GET to PUT; try the current verb first and fall
|
|
1004
|
+
// back so this keeps working on either.
|
|
1005
|
+
for (const method of ["PUT", "GET"]) {
|
|
1006
|
+
try {
|
|
1007
|
+
const response = await fetch(endpoint, { method, signal: AbortSignal.timeout(5_000) });
|
|
1008
|
+
if (response.ok)
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
1012
|
+
// Try the next verb, then give up quietly: the caller keeps polling and
|
|
1013
|
+
// the user can still open the tab by hand.
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
946
1018
|
export async function getChatGptBrowserStatus(options = {}) {
|
|
947
1019
|
const port = resolveCdpPort(options.port);
|
|
948
1020
|
const page = await findChatGptPage(port, options.timeoutMs ?? 1500);
|
|
@@ -1745,6 +1817,26 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1745
1817
|
warnings: []
|
|
1746
1818
|
};
|
|
1747
1819
|
}
|
|
1820
|
+
// Not a research thread: read the ordinary answer from the transcript
|
|
1821
|
+
// too. Recover used to be page-only, so it inherited everything the
|
|
1822
|
+
// page loses - flattened markdown, dropped citation urls - and it once
|
|
1823
|
+
// saved ChatGPT's "Connection interrupted" notice as the answer.
|
|
1824
|
+
const transcript = await evaluateOnPage(page.page, transcriptAnswerExpression(conversationId), {
|
|
1825
|
+
timeoutMs: 60_000
|
|
1826
|
+
});
|
|
1827
|
+
if (transcript.ok && transcript.text.trim().length > 0) {
|
|
1828
|
+
const recovered = resolveTranscriptCitations(transcript.text, transcript.references).trim();
|
|
1829
|
+
if (recovered.length > 0) {
|
|
1830
|
+
return {
|
|
1831
|
+
url,
|
|
1832
|
+
title: "",
|
|
1833
|
+
answer: recovered,
|
|
1834
|
+
modelHints: [],
|
|
1835
|
+
...(transcript.modelSlug ? { modelSlug: transcript.modelSlug } : {}),
|
|
1836
|
+
warnings: []
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1748
1840
|
}
|
|
1749
1841
|
catch {
|
|
1750
1842
|
// Not a research thread, or the transcript API is unavailable: fall
|
|
@@ -2128,12 +2220,21 @@ export async function sendChatGptPrompt(options) {
|
|
|
2128
2220
|
if (!conversationId)
|
|
2129
2221
|
throw new ChatGptBrowserBlockerError(deepResearchUnreadableBlocker(pinnedThreadUrl ?? "https://chatgpt.com/"));
|
|
2130
2222
|
let lastState;
|
|
2223
|
+
let consecutiveResearchReadFailures = 0;
|
|
2131
2224
|
while (Date.now() - started < timeoutMs) {
|
|
2132
2225
|
try {
|
|
2133
2226
|
lastState = await evaluateOnPage(page, deepResearchReportExpression(conversationId), { timeoutMs: 60_000 });
|
|
2227
|
+
consecutiveResearchReadFailures = 0;
|
|
2134
2228
|
}
|
|
2135
2229
|
catch {
|
|
2136
|
-
//
|
|
2230
|
+
// A few failures in a row mean the browser is gone, not busy. Waiting
|
|
2231
|
+
// out a 30-minute budget on a dead browser helps nobody: the research
|
|
2232
|
+
// finishes on ChatGPT's side anyway, so hand back the thread and let
|
|
2233
|
+
// recover collect the report.
|
|
2234
|
+
consecutiveResearchReadFailures += 1;
|
|
2235
|
+
if (consecutiveResearchReadFailures >= CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP) {
|
|
2236
|
+
throw new ChatGptBrowserBlockerError(browserLostMidWaitBlocker(pinnedThreadUrl));
|
|
2237
|
+
}
|
|
2137
2238
|
await sleep(5_000);
|
|
2138
2239
|
continue;
|
|
2139
2240
|
}
|
|
@@ -2164,11 +2265,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
2164
2265
|
}
|
|
2165
2266
|
let recoveredNavigations = 0;
|
|
2166
2267
|
let lastTranscriptClassification;
|
|
2268
|
+
let consecutiveReadFailures = 0;
|
|
2167
2269
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
2168
2270
|
while (Date.now() - started < timeoutMs) {
|
|
2169
2271
|
await sleep(1000);
|
|
2170
2272
|
try {
|
|
2171
2273
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2274
|
+
consecutiveReadFailures = 0;
|
|
2172
2275
|
// First conversation id wins. Re-deriving it every poll would let a tab
|
|
2173
2276
|
// that wandered to another thread redirect the read to a stranger's
|
|
2174
2277
|
// conversation - and the prompt check below is the second line of defence,
|
|
@@ -2206,7 +2309,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
2206
2309
|
throw error;
|
|
2207
2310
|
// Transient CDP failure while the answer is streaming: retry. A throw here
|
|
2208
2311
|
// would discard an already-streamed partial answer and skip the salvage
|
|
2209
|
-
// path below, so keep the last good state and poll again
|
|
2312
|
+
// path below, so keep the last good state and poll again. But a run of
|
|
2313
|
+
// failures is a browser that went away rather than a busy one, and
|
|
2314
|
+
// sitting out the whole budget on it only delays the recovery.
|
|
2315
|
+
consecutiveReadFailures += 1;
|
|
2316
|
+
if (consecutiveReadFailures >= CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP) {
|
|
2317
|
+
throw new ChatGptBrowserBlockerError(browserLostMidWaitBlocker(finalState?.url ?? pinnedThreadUrl));
|
|
2318
|
+
}
|
|
2210
2319
|
continue;
|
|
2211
2320
|
}
|
|
2212
2321
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
@@ -3073,6 +3182,32 @@ export function chunkComposerText(text, size = COMPOSER_INSERT_CHUNK_CHARS) {
|
|
|
3073
3182
|
* so hand the run back rather than waiting on a page that never renders it -
|
|
3074
3183
|
* deep research draws into a widget iframe, leaving the thread DOM empty.
|
|
3075
3184
|
*/
|
|
3185
|
+
/**
|
|
3186
|
+
* The dedicated browser died while a send was waiting for its answer.
|
|
3187
|
+
*
|
|
3188
|
+
* Observed live: Chrome went away mid deep-research run, every poll threw, the
|
|
3189
|
+
* loop swallowed each failure, and the send sat silent for the rest of a
|
|
3190
|
+
* 30-minute budget - while the research finished on ChatGPT's side. Nothing is
|
|
3191
|
+
* lost except the ability to read it, so fail fast and hand back the thread.
|
|
3192
|
+
*/
|
|
3193
|
+
// Reading the page can fail for a moment (a navigation, a busy renderer). Five
|
|
3194
|
+
// failures in a row is not a moment - it is a browser that went away.
|
|
3195
|
+
const CONSECUTIVE_READ_FAILURES_BEFORE_GIVING_UP = 5;
|
|
3196
|
+
export function browserLostMidWaitBlocker(threadUrl) {
|
|
3197
|
+
// Killing the browser aborts a streaming answer too, so promise nothing
|
|
3198
|
+
// about the answer itself - only say where to look. Deep research is the one
|
|
3199
|
+
// case that genuinely keeps going without us.
|
|
3200
|
+
const where = threadUrl ? ` The consult landed in ${threadUrl}` : "";
|
|
3201
|
+
return {
|
|
3202
|
+
code: "browser_unreachable",
|
|
3203
|
+
message: `The dedicated ChatGPT browser stopped responding while this consult was waiting for its answer.${where}`,
|
|
3204
|
+
retryable: true,
|
|
3205
|
+
next_step: threadUrl
|
|
3206
|
+
? `Run \`prodex pro browser login\` to reopen the browser, then collect the answer with \`prodex pro browser recover --target-url ${threadUrl}\` (MCP: pro_recover with thread ${threadUrl}).`
|
|
3207
|
+
: "Run `prodex pro browser login` to reopen the browser, then retry.",
|
|
3208
|
+
...(threadUrl ? { thread: threadUrl } : {})
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3076
3211
|
export function deepResearchUnreadableBlocker(threadUrl) {
|
|
3077
3212
|
return {
|
|
3078
3213
|
code: "deep_research_not_readable",
|
package/dist/cli-pro.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDryRunBundle } from "./bundle.js";
|
|
5
|
-
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
6
6
|
import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
|
|
7
7
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
8
8
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
@@ -222,14 +222,30 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
222
222
|
// again: Chrome's singleton would just open ANOTHER window (the recurring
|
|
223
223
|
// "extra windows" problem, which then blocks sends as
|
|
224
224
|
// ambiguous_chatgpt_tabs). Reuse the running instance instead.
|
|
225
|
-
|
|
226
|
-
// A real browser on a virtual X display: no window anywhere, and
|
|
227
|
-
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
228
|
-
const wantsVirtualDisplay = resolveVirtualDisplayPreference(browserArgs.includes("--virtual-display") ? true : undefined);
|
|
229
|
-
if (wantsVirtualDisplay && headless) {
|
|
225
|
+
if (browserArgs.includes("--virtual-display") && browserArgs.includes("--headless")) {
|
|
230
226
|
throw new Error("pro browser login cannot combine --headless and --virtual-display (a virtual display already hides the window).");
|
|
231
227
|
}
|
|
232
|
-
|
|
228
|
+
// Reopen the browser the way it was last opened unless a flag or the
|
|
229
|
+
// environment says otherwise. A virtual-display user who follows the
|
|
230
|
+
// `browser_unreachable` advice used to get a visible window back.
|
|
231
|
+
const savedLaunch = await readLastBrowserLoginLaunch();
|
|
232
|
+
const windowMode = resolveBrowserWindowMode({
|
|
233
|
+
flags: {
|
|
234
|
+
...(browserArgs.includes("--headless") ? { headless: true } : {}),
|
|
235
|
+
...(browserArgs.includes("--virtual-display") ? { virtualDisplay: true } : {}),
|
|
236
|
+
...(browserArgs.includes("--minimized") ? { minimized: true } : {})
|
|
237
|
+
},
|
|
238
|
+
...(savedLaunch ? { lastLogin: savedLaunch } : {})
|
|
239
|
+
});
|
|
240
|
+
const headless = windowMode.headless;
|
|
241
|
+
// A real browser on a virtual X display: no window anywhere, and
|
|
242
|
+
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
243
|
+
const wantsVirtualDisplay = windowMode.virtualDisplay;
|
|
244
|
+
const virtualDisplay = wantsVirtualDisplay
|
|
245
|
+
? await ensureVirtualDisplay(savedLaunch?.virtual_display !== undefined && !browserArgs.includes("--virtual-display")
|
|
246
|
+
? { displayNumber: savedLaunch.virtual_display }
|
|
247
|
+
: {})
|
|
248
|
+
: undefined;
|
|
233
249
|
const alreadyRunning = (await getChatGptBrowserStatus({ port })).reachable;
|
|
234
250
|
if (alreadyRunning) {
|
|
235
251
|
// One Chrome profile cannot serve a headed and a headless instance at
|
|
@@ -1286,13 +1302,24 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
1286
1302
|
const statusFn = deps.statusFn ?? getChatGptBrowserStatus;
|
|
1287
1303
|
const sleepFn = deps.sleepFn ?? sleep;
|
|
1288
1304
|
const now = deps.now ?? Date.now;
|
|
1305
|
+
const openTabFn = deps.openTabFn ?? openChatGptTab;
|
|
1289
1306
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
1290
1307
|
const pollMs = options.pollMs ?? 2_000;
|
|
1291
1308
|
const startedAt = now();
|
|
1292
|
-
stderr("login: waiting for a logged-in ChatGPT tab (finish login in the
|
|
1309
|
+
stderr("login: waiting for a logged-in ChatGPT tab (finish login in the dedicated Chrome window; Ctrl+C stops waiting)...");
|
|
1293
1310
|
let lastState = "";
|
|
1311
|
+
let openedMissingTab = false;
|
|
1294
1312
|
while (now() - startedAt < timeoutMs) {
|
|
1295
1313
|
const status = await statusFn({ port: options.port, timeoutMs: 1_500 });
|
|
1314
|
+
// A running Chrome with no chatgpt.com tab leaves the user nothing to log
|
|
1315
|
+
// into. Open the tab once rather than waiting for one to appear.
|
|
1316
|
+
if (status.reachable && status.blocker?.code === "chatgpt_page_missing" && !openedMissingTab) {
|
|
1317
|
+
openedMissingTab = true;
|
|
1318
|
+
stderr("login: the running Chrome had no ChatGPT tab - opening a ChatGPT tab in it...");
|
|
1319
|
+
await openTabFn(options.port);
|
|
1320
|
+
await sleepFn(pollMs);
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1296
1323
|
const state = !status.reachable
|
|
1297
1324
|
? "login: browser starting..."
|
|
1298
1325
|
: status.blocker
|