@youdie006/prodex 0.16.6 → 0.16.7
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 +51 -20
- package/dist/cli-pro.js +54 -44
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -476,6 +476,12 @@ async function readSettledChatGptPageStatus(page) {
|
|
|
476
476
|
// "not ready". Escape dismisses such dialogs; try it (bounded) only when the
|
|
477
477
|
// composer is actually blocked and a dialog is open.
|
|
478
478
|
for (let attempt = 0; attempt < 2 && !status.hasComposer && (status.openDialogText ?? "").length > 0; attempt += 1) {
|
|
479
|
+
// Never Escape a dialog that IS the evidence of a real blocker (usage
|
|
480
|
+
// limit, verification, captcha): dismissing it would destroy the precise
|
|
481
|
+
// diagnosis and let the send proceed into a generic failure. The blocker
|
|
482
|
+
// scan sample includes the dialog text, so detection sees it.
|
|
483
|
+
if (detectChatGptPageBlocker(status))
|
|
484
|
+
break;
|
|
479
485
|
await dismissOpenDialogViaEscape(page);
|
|
480
486
|
await sleep(500);
|
|
481
487
|
status = await evaluateOnPage(page, statusExpression());
|
|
@@ -483,17 +489,20 @@ async function readSettledChatGptPageStatus(page) {
|
|
|
483
489
|
return status;
|
|
484
490
|
}
|
|
485
491
|
async function dismissOpenDialogViaEscape(page) {
|
|
486
|
-
|
|
492
|
+
// Best effort throughout - including the connect itself: a browser that quit
|
|
493
|
+
// between the status read and this dismiss must not abort the settle loop
|
|
494
|
+
// with a raw websocket error (the loop just reports the not-ready state).
|
|
495
|
+
let cdp;
|
|
487
496
|
try {
|
|
497
|
+
cdp = await connectCdp(page.webSocketDebuggerUrl);
|
|
488
498
|
await cdp.send("Runtime.enable");
|
|
489
499
|
await dispatchEscapeKey(cdp);
|
|
490
500
|
}
|
|
491
501
|
catch {
|
|
492
|
-
//
|
|
493
|
-
// reports the original not-ready state.
|
|
502
|
+
// reported by the settle loop as the original not-ready state
|
|
494
503
|
}
|
|
495
504
|
finally {
|
|
496
|
-
cdp
|
|
505
|
+
cdp?.close();
|
|
497
506
|
}
|
|
498
507
|
}
|
|
499
508
|
async function ensureVisibleChatGptPage(port, page, status) {
|
|
@@ -541,13 +550,24 @@ function chatGptPageMissingBlocker() {
|
|
|
541
550
|
next_step: "Open https://chatgpt.com/ in the dedicated Chrome profile, or run `prodex pro browser login` to reopen it."
|
|
542
551
|
};
|
|
543
552
|
}
|
|
544
|
-
export function assertChatGptReadyForPrompt(loggedInLikely, hasComposer) {
|
|
553
|
+
export function assertChatGptReadyForPrompt(loggedInLikely, hasComposer, openDialogText) {
|
|
545
554
|
if (loggedInLikely && hasComposer)
|
|
546
555
|
return;
|
|
547
556
|
const missing = [
|
|
548
557
|
loggedInLikely ? undefined : "a clear logged-in ChatGPT session",
|
|
549
558
|
hasComposer ? undefined : "a visible prompt composer"
|
|
550
559
|
].filter(Boolean);
|
|
560
|
+
// A dialog that survived the Escape auto-dismiss is the actual cause - name
|
|
561
|
+
// it so the user closes the modal instead of chasing the login advice.
|
|
562
|
+
const dialogHead = (openDialogText ?? "").trim().split("\n")[0]?.slice(0, 80);
|
|
563
|
+
if (!hasComposer && dialogHead) {
|
|
564
|
+
throw new ChatGptBrowserBlockerError({
|
|
565
|
+
code: "chatgpt_not_ready",
|
|
566
|
+
message: `ChatGPT browser is reachable, but an open dialog ("${dialogHead}") is blocking the prompt composer.`,
|
|
567
|
+
retryable: true,
|
|
568
|
+
next_step: "Close the dialog in the visible browser (it did not respond to Escape), then retry."
|
|
569
|
+
});
|
|
570
|
+
}
|
|
551
571
|
throw new ChatGptBrowserBlockerError({
|
|
552
572
|
code: "chatgpt_not_ready",
|
|
553
573
|
message: `ChatGPT browser is reachable, but it is missing ${missing.join(" and ")}.`,
|
|
@@ -910,11 +930,16 @@ async function selectModelReasoning(cdp, options) {
|
|
|
910
930
|
// Open the Pro sub-mode submenu via the chevron, then pick 기본/확장.
|
|
911
931
|
const expander = await cdp.evaluate(proSubmenuExpanderRectExpression());
|
|
912
932
|
if (!expander.ok || expander.x === undefined || expander.y === undefined) {
|
|
913
|
-
//
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
933
|
+
// Only the expander-specific miss means "the 2026-07 update removed the
|
|
934
|
+
// submenu"; other reasons (no Pro entry on a Plus plan, menu not open)
|
|
935
|
+
// must surface verbatim or the guidance sends users to a --model Pro
|
|
936
|
+
// that fails the same way.
|
|
937
|
+
if (expander.reason && expander.reason !== "Pro sub-mode expander not found next to Pro") {
|
|
938
|
+
throw new Error(expander.reason);
|
|
939
|
+
}
|
|
940
|
+
// The saved setup default injects pro_mode into plain asks, so the user
|
|
941
|
+
// may never have typed --pro-mode - name the clear command too.
|
|
942
|
+
throw new Error("The ChatGPT model menu no longer offers Pro sub-modes (the 2026-07 update removed the Pro submenu; Pro is a single mode now). Use --model Pro instead of --pro-mode - and if this ask did not pass --pro-mode, a saved default is injecting it: run `prodex setup --clear-pro-mode`. If ChatGPT restores sub-modes, update prodex (npm i -g @youdie006/prodex@latest).");
|
|
918
943
|
}
|
|
919
944
|
await verifiedClickAt(cdp, expander.x, expander.y, "Pro sub-mode expander");
|
|
920
945
|
const subLabels = PRO_MODE_SUBMENU_LABELS[options.proMode];
|
|
@@ -953,7 +978,7 @@ async function selectModelReasoning(cdp, options) {
|
|
|
953
978
|
throw new Error(`ChatGPT model/effort option not found: ${primaryLabel}${primary.available ? ` (available: ${primary.available.join(", ")})` : ""}`);
|
|
954
979
|
}
|
|
955
980
|
if (primary.role === "menuitem" && primary.haspopup === "menu") {
|
|
956
|
-
throw new Error(`ChatGPT model "${primaryLabel}" opens a submenu of variants instead of committing; selecting it is not supported yet. Supported today: reasoning efforts (--effort) and Pro
|
|
981
|
+
throw new Error(`ChatGPT model "${primaryLabel}" opens a submenu of variants instead of committing; selecting it is not supported yet. Supported today: reasoning efforts (--effort) and --model Pro.`);
|
|
957
982
|
}
|
|
958
983
|
await verifiedClickAt(cdp, primary.x, primary.y, primaryLabel);
|
|
959
984
|
await assertSelectionCommitted(cdp, primaryLabel);
|
|
@@ -1074,12 +1099,18 @@ async function selectProject(cdp, options) {
|
|
|
1074
1099
|
await verifiedClickAt(cdp, hit.x, hit.y, `project ${options.project}`);
|
|
1075
1100
|
const navigated = await waitForExpressionTrue(cdp, `location.href !== ${JSON.stringify(hrefBefore)}`, PROJECT_NAVIGATION_TIMEOUT_MS);
|
|
1076
1101
|
if (!navigated) {
|
|
1077
|
-
// The href staying put is fine when the tab was
|
|
1078
|
-
// home
|
|
1079
|
-
// (
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1102
|
+
// The href staying put is fine ONLY when the tab was already on THIS
|
|
1103
|
+
// project's home. Requiring the requested project's name in the page title
|
|
1104
|
+
// or heading (both carry it, measured live) prevents a stalled cross-
|
|
1105
|
+
// project navigation from being accepted while still inside the OLD
|
|
1106
|
+
// project - which would silently send the prompt into the wrong project.
|
|
1107
|
+
const alreadyInRequestedProject = await cdp.evaluate(`(() => {
|
|
1108
|
+
if (!/^https:\\/\\/chatgpt\\.com\\/g\\/g-p-/.test(location.href)) return false;
|
|
1109
|
+
const name = ${JSON.stringify(options.project)};
|
|
1110
|
+
if ((document.title || "").includes(name)) return true;
|
|
1111
|
+
return [...document.querySelectorAll('h1,[role="heading"]')].some((h) => (h.innerText || "").includes(name));
|
|
1112
|
+
})()`);
|
|
1113
|
+
if (!alreadyInRequestedProject) {
|
|
1083
1114
|
throw new Error(`Clicking project "${options.project}" did not navigate the visible tab. If the tab is already inside this project, omit --project and retry.`);
|
|
1084
1115
|
}
|
|
1085
1116
|
}
|
|
@@ -1136,7 +1167,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
1136
1167
|
if (blocker) {
|
|
1137
1168
|
throw new ChatGptBrowserBlockerError(blocker);
|
|
1138
1169
|
}
|
|
1139
|
-
assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer);
|
|
1170
|
+
assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer, status.openDialogText);
|
|
1140
1171
|
if (normalizedTargetUrl)
|
|
1141
1172
|
assertChatGptTargetUrlMatches(status.url, normalizedTargetUrl);
|
|
1142
1173
|
assertVisibleChatGptTab(status.visibilityState, status.url, normalizedTargetUrl);
|
|
@@ -1331,7 +1362,7 @@ export async function listChatGptModelOptions(input = {}) {
|
|
|
1331
1362
|
const blocker = detectChatGptPageBlocker(status);
|
|
1332
1363
|
if (blocker)
|
|
1333
1364
|
throw new ChatGptBrowserBlockerError(blocker);
|
|
1334
|
-
assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer);
|
|
1365
|
+
assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer, status.openDialogText);
|
|
1335
1366
|
assertVisibleChatGptTab(status.visibilityState, status.url, undefined);
|
|
1336
1367
|
const cdp = await connectCdp(page.webSocketDebuggerUrl);
|
|
1337
1368
|
try {
|
|
@@ -1594,7 +1625,7 @@ export function statusExpression() {
|
|
|
1594
1625
|
hasComposer,
|
|
1595
1626
|
generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || visibleButtonLabels.some((label) => generatingControlPattern.test(label)),
|
|
1596
1627
|
modelHints: lines.filter((line) => /GPT|Pro|Thinking|ChatGPT|Extra High|Auto/i.test(line)).slice(0, 30),
|
|
1597
|
-
openDialogText: (document.
|
|
1628
|
+
openDialogText: (([...document.querySelectorAll('[role="dialog"]')].find((d) => d.offsetWidth || d.offsetHeight || d.getClientRects().length)?.innerText) || "").trim().slice(0, 200)
|
|
1598
1629
|
};
|
|
1599
1630
|
})()`;
|
|
1600
1631
|
}
|
package/dist/cli-pro.js
CHANGED
|
@@ -298,20 +298,26 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
298
298
|
return 0;
|
|
299
299
|
assertOnlyOptions(browserArgs, "pro browser models", ["--port", "--timeout-ms", "--source-cli"]);
|
|
300
300
|
const modelsSourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
301
|
+
// Parse flags OUTSIDE the browser-error adapter: a flag-validation
|
|
302
|
+
// error is a usage error, not a browser blocker.
|
|
301
303
|
const modelsPort = readPortFlag(browserArgs, "--port");
|
|
304
|
+
const modelsTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
|
|
305
|
+
// Port-awareness must reflect the RESOLVED port (PRODEX_CDP_PORT env
|
|
306
|
+
// included), not just the raw --port flag.
|
|
307
|
+
const modelsResolvedPort = resolveCdpPort(modelsPort);
|
|
302
308
|
let listed;
|
|
303
309
|
try {
|
|
304
310
|
listed = await listChatGptModelOptions({
|
|
305
311
|
port: modelsPort,
|
|
306
|
-
timeoutMs:
|
|
312
|
+
timeoutMs: modelsTimeoutMs
|
|
307
313
|
});
|
|
308
314
|
}
|
|
309
315
|
catch (error) {
|
|
310
|
-
// Keep the next step actionable for a custom
|
|
316
|
+
// Keep the next step actionable for a custom port: the raw blocker
|
|
311
317
|
// suggests the default-port login command, which would not fix a
|
|
312
318
|
// 9444-style setup.
|
|
313
319
|
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), modelsSourceCli, {
|
|
314
|
-
...(
|
|
320
|
+
...(modelsResolvedPort !== DEFAULT_CDP_PORT ? { port: modelsResolvedPort } : {})
|
|
315
321
|
});
|
|
316
322
|
throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
|
|
317
323
|
}
|
|
@@ -1211,58 +1217,62 @@ export async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
|
1211
1217
|
catch {
|
|
1212
1218
|
// config unreadable: already reported by the config line above
|
|
1213
1219
|
}
|
|
1220
|
+
// Parse flags OUTSIDE the probe guard: an invalid --port / PRODEX_CDP_PORT /
|
|
1221
|
+
// --timeout-ms is a usage or config error, not a browser-check failure.
|
|
1222
|
+
const checkPort = resolveCdpPort(readPortFlag(args, "--port"));
|
|
1223
|
+
const checkTimeoutMs = readPositiveIntegerFlag(args, "--timeout-ms") ?? 1500;
|
|
1224
|
+
const browserCommandOptions = {
|
|
1225
|
+
cwd: setupHintCwd,
|
|
1226
|
+
port: checkPort !== DEFAULT_CDP_PORT ? checkPort : undefined
|
|
1227
|
+
};
|
|
1214
1228
|
let browserStatus;
|
|
1215
1229
|
try {
|
|
1216
|
-
browserStatus = await getChatGptBrowserStatus({
|
|
1217
|
-
port: resolveCdpPort(readPortFlag(args, "--port")),
|
|
1218
|
-
timeoutMs: readPositiveIntegerFlag(args, "--timeout-ms") ?? 1500
|
|
1219
|
-
});
|
|
1230
|
+
browserStatus = await getChatGptBrowserStatus({ port: checkPort, timeoutMs: checkTimeoutMs });
|
|
1220
1231
|
}
|
|
1221
1232
|
catch (error) {
|
|
1222
1233
|
// A reachable-but-broken page (e.g. a failing in-page evaluate) must show
|
|
1223
1234
|
// as a check failure like doctor does, not crash the whole check with an
|
|
1224
|
-
// internal
|
|
1235
|
+
// internal error - and the rest of the check (latest_pro) still runs.
|
|
1225
1236
|
io.stdout(`chatgpt: check failed - ${errorMessage(error)}`);
|
|
1226
|
-
|
|
1227
|
-
|
|
1237
|
+
const failedNext = productCheckBrowserNextStep("Reload the ChatGPT tab in the dedicated browser (or rerun `prodex pro browser login`), then retry.", sourceCli, browserCommandOptions);
|
|
1238
|
+
if (failedNext)
|
|
1239
|
+
io.stdout(`next: ${failedNext}`);
|
|
1228
1240
|
}
|
|
1229
|
-
const browserCommandOptions = {
|
|
1230
|
-
cwd: setupHintCwd,
|
|
1231
|
-
port: readPortFlag(args, "--port") ?? undefined
|
|
1232
|
-
};
|
|
1233
1241
|
let chatgptReady = false;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1242
|
+
if (browserStatus) {
|
|
1243
|
+
const visibilityBlocker = chatGptVisibilityBlocker(browserStatus.visibilityState, browserStatus.url);
|
|
1244
|
+
if (!browserStatus.reachable) {
|
|
1245
|
+
io.stdout(`chatgpt: ${browserStatus.blocker?.code ?? "unreachable"} - ${browserStatus.blocker?.message ?? "browser is not reachable"}`);
|
|
1246
|
+
const nextStep = productCheckBrowserNextStep(browserStatus.blocker?.next_step, sourceCli, browserCommandOptions);
|
|
1247
|
+
if (nextStep)
|
|
1248
|
+
io.stdout(`next: ${nextStep}`);
|
|
1249
|
+
}
|
|
1250
|
+
else if (browserStatus.blocker) {
|
|
1251
|
+
const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
|
|
1252
|
+
io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
|
|
1253
|
+
const nextStep = productCheckBrowserNextStep(browserStatus.blocker.next_step, sourceCli, browserCommandOptions);
|
|
1254
|
+
if (nextStep)
|
|
1255
|
+
io.stdout(`next: ${nextStep}`);
|
|
1256
|
+
}
|
|
1257
|
+
else if (visibilityBlocker) {
|
|
1258
|
+
io.stdout(`chatgpt: blocked ${visibilityBlocker.code} visibility=${browserStatus.visibilityState ?? "unknown"} - ${visibilityBlocker.message}`);
|
|
1259
|
+
const nextStep = productCheckBrowserNextStep(visibilityBlocker.next_step, sourceCli, browserCommandOptions);
|
|
1260
|
+
if (nextStep)
|
|
1261
|
+
io.stdout(`next: ${nextStep}`);
|
|
1262
|
+
}
|
|
1263
|
+
else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
|
|
1264
|
+
io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
|
|
1265
|
+
chatgptReady = true;
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
|
|
1269
|
+
const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
|
|
1252
1270
|
io.stdout(`next: ${nextStep}`);
|
|
1271
|
+
}
|
|
1272
|
+
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
1273
|
+
if (modelHints)
|
|
1274
|
+
io.stdout(`model_hints: ${modelHints}`);
|
|
1253
1275
|
}
|
|
1254
|
-
else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
|
|
1255
|
-
io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
|
|
1256
|
-
chatgptReady = true;
|
|
1257
|
-
}
|
|
1258
|
-
else {
|
|
1259
|
-
io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
|
|
1260
|
-
const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
|
|
1261
|
-
io.stdout(`next: ${nextStep}`);
|
|
1262
|
-
}
|
|
1263
|
-
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
1264
|
-
if (modelHints)
|
|
1265
|
-
io.stdout(`model_hints: ${modelHints}`);
|
|
1266
1276
|
if (bridgeReady) {
|
|
1267
1277
|
try {
|
|
1268
1278
|
const latest = await latestTrustedConsult(store, { readOnly: false });
|