@youdie006/prodex 0.16.5 → 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 +52 -21
- package/dist/cli-pro.js +74 -39
- package/dist/repo-write.js +6 -0
- 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
|
}
|
|
@@ -1742,7 +1773,7 @@ function composerExpressionHelpers() {
|
|
|
1742
1773
|
};
|
|
1743
1774
|
`;
|
|
1744
1775
|
}
|
|
1745
|
-
function answerExpression() {
|
|
1776
|
+
export function answerExpression() {
|
|
1746
1777
|
const excludedTextSelector = JSON.stringify(CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS);
|
|
1747
1778
|
const blockerScanExcludedSelector = JSON.stringify(CHATGPT_BLOCKER_SCAN_EXCLUDED_ANCESTORS);
|
|
1748
1779
|
const streamingSelector = JSON.stringify(CHATGPT_STREAMING_SELECTOR);
|
package/dist/cli-pro.js
CHANGED
|
@@ -297,17 +297,37 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
297
297
|
if (printProBrowserHelpIfRequested(browserArgs, "pro browser models", io, { valueFlags: ["--port", "--timeout-ms", "--source-cli"] }))
|
|
298
298
|
return 0;
|
|
299
299
|
assertOnlyOptions(browserArgs, "pro browser models", ["--port", "--timeout-ms", "--source-cli"]);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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.
|
|
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);
|
|
308
|
+
let listed;
|
|
309
|
+
try {
|
|
310
|
+
listed = await listChatGptModelOptions({
|
|
311
|
+
port: modelsPort,
|
|
312
|
+
timeoutMs: modelsTimeoutMs
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
// Keep the next step actionable for a custom port: the raw blocker
|
|
317
|
+
// suggests the default-port login command, which would not fix a
|
|
318
|
+
// 9444-style setup.
|
|
319
|
+
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), modelsSourceCli, {
|
|
320
|
+
...(modelsResolvedPort !== DEFAULT_CDP_PORT ? { port: modelsResolvedPort } : {})
|
|
321
|
+
});
|
|
322
|
+
throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
|
|
323
|
+
}
|
|
304
324
|
io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
|
|
305
325
|
for (const option of listed.options) {
|
|
306
326
|
const marker = option.checked ? "*" : " ";
|
|
307
327
|
const suffix = option.kind === "submenu" ? " (has sub-variants; not selectable via --model yet)" : "";
|
|
308
328
|
io.stdout(`${marker} ${option.label}${suffix}`);
|
|
309
329
|
}
|
|
310
|
-
io.stdout("Use radio entries with `pro browser ask --model/--effort
|
|
330
|
+
io.stdout("Use radio entries with `pro browser ask --model/--effort` (e.g. --model Pro).");
|
|
311
331
|
return 0;
|
|
312
332
|
}
|
|
313
333
|
throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models"]);
|
|
@@ -1197,47 +1217,62 @@ export async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
|
1197
1217
|
catch {
|
|
1198
1218
|
// config unreadable: already reported by the config line above
|
|
1199
1219
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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;
|
|
1204
1224
|
const browserCommandOptions = {
|
|
1205
1225
|
cwd: setupHintCwd,
|
|
1206
|
-
port:
|
|
1226
|
+
port: checkPort !== DEFAULT_CDP_PORT ? checkPort : undefined
|
|
1207
1227
|
};
|
|
1208
|
-
let
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
io.stdout(`chatgpt: ${browserStatus.blocker?.code ?? "unreachable"} - ${browserStatus.blocker?.message ?? "browser is not reachable"}`);
|
|
1212
|
-
const nextStep = productCheckBrowserNextStep(browserStatus.blocker?.next_step, sourceCli, browserCommandOptions);
|
|
1213
|
-
if (nextStep)
|
|
1214
|
-
io.stdout(`next: ${nextStep}`);
|
|
1228
|
+
let browserStatus;
|
|
1229
|
+
try {
|
|
1230
|
+
browserStatus = await getChatGptBrowserStatus({ port: checkPort, timeoutMs: checkTimeoutMs });
|
|
1215
1231
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
// A reachable-but-broken page (e.g. a failing in-page evaluate) must show
|
|
1234
|
+
// as a check failure like doctor does, not crash the whole check with an
|
|
1235
|
+
// internal error - and the rest of the check (latest_pro) still runs.
|
|
1236
|
+
io.stdout(`chatgpt: check failed - ${errorMessage(error)}`);
|
|
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}`);
|
|
1222
1240
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
if (
|
|
1241
|
+
let chatgptReady = false;
|
|
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);
|
|
1227
1270
|
io.stdout(`next: ${nextStep}`);
|
|
1271
|
+
}
|
|
1272
|
+
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
1273
|
+
if (modelHints)
|
|
1274
|
+
io.stdout(`model_hints: ${modelHints}`);
|
|
1228
1275
|
}
|
|
1229
|
-
else if (browserStatus.loggedInLikely && browserStatus.hasComposer) {
|
|
1230
|
-
io.stdout(`chatgpt: ok logged_in=true composer=true${browserStatus.url ? ` url=${browserStatus.url}` : ""}`);
|
|
1231
|
-
chatgptReady = true;
|
|
1232
|
-
}
|
|
1233
|
-
else {
|
|
1234
|
-
io.stdout(`chatgpt: blocked logged_in=${browserStatus.loggedInLikely} composer=${browserStatus.hasComposer}`);
|
|
1235
|
-
const nextStep = productCheckBrowserNextStep(browserReadinessNextStep(browserStatus), sourceCli, browserCommandOptions);
|
|
1236
|
-
io.stdout(`next: ${nextStep}`);
|
|
1237
|
-
}
|
|
1238
|
-
const modelHints = formatBrowserModelHints(browserStatus.modelHints);
|
|
1239
|
-
if (modelHints)
|
|
1240
|
-
io.stdout(`model_hints: ${modelHints}`);
|
|
1241
1276
|
if (bridgeReady) {
|
|
1242
1277
|
try {
|
|
1243
1278
|
const latest = await latestTrustedConsult(store, { readOnly: false });
|
package/dist/repo-write.js
CHANGED
|
@@ -77,6 +77,12 @@ export async function applyRepoWriteDryRun(root, store, input) {
|
|
|
77
77
|
const current = await readWritableExistingFile(root, metadata.path);
|
|
78
78
|
const currentPreimage = sha256(current.content);
|
|
79
79
|
if (currentPreimage !== input.preimage_sha256) {
|
|
80
|
+
// A retry after a successful apply sees the file already holding the new
|
|
81
|
+
// content - name that instead of the generic conflict message so the
|
|
82
|
+
// caller can tell "already done" from "someone else changed the file".
|
|
83
|
+
if (currentPreimage === metadata.new_sha256) {
|
|
84
|
+
throw new Error(`Write for ${metadata.path} was already applied (the file already matches the reviewed content). Re-run repo_write_file_dry_run if you need a new change.`);
|
|
85
|
+
}
|
|
80
86
|
throw new Error(`File preimage changed for ${metadata.path}`);
|
|
81
87
|
}
|
|
82
88
|
const newContent = await readDryRunReplacementContent(store, metadata);
|