@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.
@@ -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
- const cdp = await connectCdp(page.webSocketDebuggerUrl);
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
- // Best effort: if the Escape cannot be delivered the settle loop simply
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.close();
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
- // The 2026-07 ChatGPT update removed the Pro sub-mode chevron from the
914
- // model picker (verified live, including on hover) - Pro is a plain
915
- // radio now. Name the change and the working alternative instead of a
916
- // bare "expander not found".
917
- 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. If ChatGPT restores sub-modes, update prodex (npm i -g @youdie006/prodex@latest).");
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 via --pro-mode.`);
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 ALREADY on this project's
1078
- // home: the click landed on the requested row's own navigation control
1079
- // (hover-verified above), so an unchanged project URL means "already
1080
- // there", not a failed click.
1081
- const alreadyInProject = await cdp.evaluate(`/^https:\\/\\/chatgpt\\.com\\/g\\/g-p-/.test(location.href)`);
1082
- if (!alreadyInProject) {
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.querySelector('[role="dialog"]')?.innerText || "").trim().slice(0, 200)
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 listed = await listChatGptModelOptions({
301
- port: readPortFlag(browserArgs, "--port"),
302
- timeoutMs: readPositiveIntegerFlag(browserArgs, "--timeout-ms")
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`; Pro sub-modes via --pro-mode 기본|확장.");
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
- const browserStatus = await getChatGptBrowserStatus({
1201
- port: resolveCdpPort(readPortFlag(args, "--port")),
1202
- timeoutMs: readPositiveIntegerFlag(args, "--timeout-ms") ?? 1500
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: readPortFlag(args, "--port") ?? undefined
1226
+ port: checkPort !== DEFAULT_CDP_PORT ? checkPort : undefined
1207
1227
  };
1208
- let chatgptReady = false;
1209
- const visibilityBlocker = chatGptVisibilityBlocker(browserStatus.visibilityState, browserStatus.url);
1210
- if (!browserStatus.reachable) {
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
- else if (browserStatus.blocker) {
1217
- const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
1218
- io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
1219
- const nextStep = productCheckBrowserNextStep(browserStatus.blocker.next_step, sourceCli, browserCommandOptions);
1220
- if (nextStep)
1221
- io.stdout(`next: ${nextStep}`);
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
- else if (visibilityBlocker) {
1224
- io.stdout(`chatgpt: blocked ${visibilityBlocker.code} visibility=${browserStatus.visibilityState ?? "unknown"} - ${visibilityBlocker.message}`);
1225
- const nextStep = productCheckBrowserNextStep(visibilityBlocker.next_step, sourceCli, browserCommandOptions);
1226
- if (nextStep)
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 });
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.16.5",
3
+ "version": "0.16.7",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",