@youdie006/prodex 0.16.3 → 0.16.5

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.
@@ -470,8 +470,32 @@ async function readSettledChatGptPageStatus(page) {
470
470
  await sleep(400);
471
471
  status = await evaluateOnPage(page, statusExpression());
472
472
  }
473
+ // An open modal dialog (e.g. the "ChatGPT for Work" onboarding promo shipped
474
+ // with the 2026-07 update) puts the app behind aria-hidden, so the composer
475
+ // and the logged-in signals are undetectable and every flow would report
476
+ // "not ready". Escape dismisses such dialogs; try it (bounded) only when the
477
+ // composer is actually blocked and a dialog is open.
478
+ for (let attempt = 0; attempt < 2 && !status.hasComposer && (status.openDialogText ?? "").length > 0; attempt += 1) {
479
+ await dismissOpenDialogViaEscape(page);
480
+ await sleep(500);
481
+ status = await evaluateOnPage(page, statusExpression());
482
+ }
473
483
  return status;
474
484
  }
485
+ async function dismissOpenDialogViaEscape(page) {
486
+ const cdp = await connectCdp(page.webSocketDebuggerUrl);
487
+ try {
488
+ await cdp.send("Runtime.enable");
489
+ await dispatchEscapeKey(cdp);
490
+ }
491
+ catch {
492
+ // Best effort: if the Escape cannot be delivered the settle loop simply
493
+ // reports the original not-ready state.
494
+ }
495
+ finally {
496
+ cdp.close();
497
+ }
498
+ }
475
499
  async function ensureVisibleChatGptPage(port, page, status) {
476
500
  if (status.visibilityState === "visible")
477
501
  return status;
@@ -701,7 +725,11 @@ export function menuItemLabelMatches(text, candidates) {
701
725
  }
702
726
  function menuLabelMatchPredicate(label) {
703
727
  const c = JSON.stringify(toLabelCandidates(label));
704
- return `((r) => { const t = (r.textContent || "").trim(); return ${c}.includes(t) || ${c}.includes(t.split(String.fromCharCode(10))[0].trim()); })`;
728
+ // Prefer innerText over textContent: a badge rendered next to the label (e.g.
729
+ // the "5.5" chip on "Instant") concatenates in textContent ("Instant5.5",
730
+ // unmatchable) but keeps a line break in innerText ("Instant\n5.5"), which the
731
+ // first-line match handles (measured live on the 2026-07 ChatGPT update).
732
+ return `((r) => { const t = ((r.innerText || r.textContent || "")).trim(); return ${c}.includes(t) || ${c}.includes(t.split(String.fromCharCode(10))[0].trim()); })`;
705
733
  }
706
734
  export function menuItemPresentExpression(label) {
707
735
  return `[...document.querySelectorAll('[role="menuitemradio"],[role="menuitem"]')].some(${menuLabelMatchPredicate(label)})`;
@@ -764,7 +792,7 @@ export function menuItemRectExpression(label) {
764
792
  if (!m) return { ok: false, reason: "reasoning/model menu did not open" };
765
793
  const items = [...m.querySelectorAll('[role="menuitemradio"],[role="menuitem"]')];
766
794
  const it = items.find(${menuLabelMatchPredicate(label)});
767
- if (!it) return { ok: false, reason: "menu item not found", available: items.map((r) => (r.textContent || "").trim()).slice(0, 12) };
795
+ if (!it) return { ok: false, reason: "menu item not found", available: items.map((r) => ((r.innerText || r.textContent || "").trim().split(String.fromCharCode(10))[0] || "").trim()).slice(0, 12) };
768
796
  const point = clickPoint(it);
769
797
  if (!point.ok) return point;
770
798
  return { ...point, role: it.getAttribute("role"), haspopup: it.getAttribute("aria-haspopup") };
@@ -785,7 +813,7 @@ export function submenuItemRectExpression(label) {
785
813
  // or "Pro 확장"), so it must be matched by prefix, never by exact text.
786
814
  const PRO_RADIO_FINDER_SNIPPET = `
787
815
  const findProRadio = (scope) =>
788
- [...scope.querySelectorAll('[role="menuitemradio"]')].find((r) => /^Pro( |$)/.test((r.textContent || "").trim()));`;
816
+ [...scope.querySelectorAll('[role="menuitemradio"]')].find((r) => /^Pro( |$)/.test(((r.innerText || r.textContent || "").trim().split(String.fromCharCode(10))[0] || "").trim()));`;
789
817
  export function proRadioRectExpression() {
790
818
  return `(() => {${CLICK_POINT_SNIPPET}${PRO_RADIO_FINDER_SNIPPET}
791
819
  const m = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
@@ -829,6 +857,13 @@ export function projectItemRectExpression(name) {
829
857
  }
830
858
  }
831
859
  if (!target) return { ok: false, reason: "project not found in sidebar" };
860
+ // 2026-07 ChatGPT update: the project row (li) is no longer a link - the
861
+ // navigation affordance is a dedicated "Open project home" button inside
862
+ // the row (verified live: clicking the row does nothing, clicking the home
863
+ // button navigates to /g/g-p-...). Prefer it; fall back to the row click
864
+ // for the old UI.
865
+ const home = target.querySelector('[aria-label*="project home" i],[aria-label*="프로젝트 홈"]');
866
+ if (home) return clickPoint(home);
832
867
  return clickPoint(target, 18);
833
868
  })()`;
834
869
  }
@@ -875,7 +910,11 @@ async function selectModelReasoning(cdp, options) {
875
910
  // Open the Pro sub-mode submenu via the chevron, then pick 기본/확장.
876
911
  const expander = await cdp.evaluate(proSubmenuExpanderRectExpression());
877
912
  if (!expander.ok || expander.x === undefined || expander.y === undefined) {
878
- throw new Error(expander.reason ?? "Could not open the ChatGPT Pro sub-mode submenu");
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).");
879
918
  }
880
919
  await verifiedClickAt(cdp, expander.x, expander.y, "Pro sub-mode expander");
881
920
  const subLabels = PRO_MODE_SUBMENU_LABELS[options.proMode];
@@ -1035,7 +1074,14 @@ async function selectProject(cdp, options) {
1035
1074
  await verifiedClickAt(cdp, hit.x, hit.y, `project ${options.project}`);
1036
1075
  const navigated = await waitForExpressionTrue(cdp, `location.href !== ${JSON.stringify(hrefBefore)}`, PROJECT_NAVIGATION_TIMEOUT_MS);
1037
1076
  if (!navigated) {
1038
- 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.`);
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) {
1083
+ 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
+ }
1039
1085
  }
1040
1086
  const composerReady = await waitForExpressionTrue(cdp, `Boolean(document.querySelector('#prompt-textarea,[contenteditable="true"],textarea'))`, PROJECT_NAVIGATION_TIMEOUT_MS);
1041
1087
  if (!composerReady) {
@@ -1257,7 +1303,7 @@ export function modelMenuOptionsExpression() {
1257
1303
  if (!m) return [];
1258
1304
  return [...m.querySelectorAll('[role="menuitemradio"],[role="menuitem"]')]
1259
1305
  .map((it) => ({
1260
- label: (it.textContent || "").trim(),
1306
+ label: (((it.innerText || it.textContent || "").trim().split(String.fromCharCode(10))[0]) || "").trim(),
1261
1307
  kind: it.getAttribute("aria-haspopup") === "menu" ? "submenu" : "radio",
1262
1308
  checked: it.getAttribute("aria-checked") === "true"
1263
1309
  }))
@@ -1547,7 +1593,8 @@ export function statusExpression() {
1547
1593
  visibleButtonLabels,
1548
1594
  hasComposer,
1549
1595
  generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || visibleButtonLabels.some((label) => generatingControlPattern.test(label)),
1550
- modelHints: lines.filter((line) => /GPT|Pro|Thinking|ChatGPT|Extra High|Auto/i.test(line)).slice(0, 30)
1596
+ 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)
1551
1598
  };
1552
1599
  })()`;
1553
1600
  }
package/dist/cli-help.js CHANGED
@@ -172,7 +172,7 @@ Commands:
172
172
  Use \`prodex pro ask\` for dry-run/manual previews.
173
173
  Use \`prodex pro browser ask\` only when you want an explicit visible-browser send.
174
174
  Model/project selection (visible-browser send):
175
- --model "label" Pick the composer model by its exact menu label (verified: Pro). Submenu models (e.g. GPT-5.5 variants) are rejected for now.
175
+ --model "label" Pick the composer model by its exact menu label (verified: Pro). Submenu models (e.g. the GPT-5.6 Sol variants) are rejected for now.
176
176
  --pro-mode 기본 | 확장 Pro sub-mode (only when the model is Pro); 확장 raises the default timeout to 300000 ms
177
177
  --effort 즉시|중간|높음|매우 높음 Reasoning effort (aliases: instant/medium/high/max); picking one deselects Pro
178
178
  --project "name" Enter an existing sidebar project first (cannot combine with --target-url)
package/dist/cli.js CHANGED
@@ -622,15 +622,18 @@ async function readReleasePackStatus(cwd, packageJson, sourceCli, releaseHintCwd
622
622
  };
623
623
  }
624
624
  }
625
- function parsePackedFiles(stdout) {
626
- let entries;
625
+ export function parsePackedFiles(stdout) {
626
+ let parsed;
627
627
  try {
628
- entries = JSON.parse(stdout);
628
+ parsed = JSON.parse(stdout);
629
629
  }
630
630
  catch {
631
631
  throw new Error("npm pack dry-run did not return valid JSON");
632
632
  }
633
- const files = entries?.[0]?.files;
633
+ // npm <=11 prints an ARRAY of entries; npm 12 changed `pack --json` to an
634
+ // OBJECT keyed by package name (measured on npm 12.0.0). Accept both.
635
+ const first = (Array.isArray(parsed) ? parsed[0] : Object.values(parsed ?? {})[0]);
636
+ const files = first?.files;
634
637
  if (!Array.isArray(files)) {
635
638
  throw new Error("npm pack dry-run did not return a file list");
636
639
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.16.3",
3
+ "version": "0.16.5",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",
@@ -198,7 +198,9 @@ async function readPackedFiles(rootDir) {
198
198
  }
199
199
  try {
200
200
  const entries = JSON.parse(stdout);
201
- const files = entries?.[0]?.files;
201
+ // npm <=11 prints an array; npm 12 prints an object keyed by package name.
202
+ const firstEntry = Array.isArray(entries) ? entries[0] : Object.values(entries ?? {})[0];
203
+ const files = firstEntry?.files;
202
204
  if (!Array.isArray(files)) {
203
205
  throw new Error("release metadata failed: npm pack dry-run did not return a file list");
204
206
  }
@@ -336,10 +338,27 @@ async function run(command, commandArgs, cwd) {
336
338
  maxBuffer: 20 * 1024 * 1024
337
339
  });
338
340
  } catch (error) {
341
+ // Surface the real failure: the one-line detail used to show the FIRST
342
+ // stderr line, which npm noise ("npm warn ...") could occupy while the
343
+ // actual test failure sat in stdout and was silently dropped.
344
+ printCapturedOutputTail(error, commandLine);
339
345
  throw new Error(`release verification failed: ${commandLine}: ${commandFailureDetail(error)}`);
340
346
  }
341
347
  }
342
348
 
349
+ function printCapturedOutputTail(error, commandLine) {
350
+ const failed = error && typeof error === "object" ? error : {};
351
+ for (const [label, value] of [
352
+ ["stdout", failed.stdout],
353
+ ["stderr", failed.stderr]
354
+ ]) {
355
+ if (typeof value !== "string" || value.trim().length === 0) continue;
356
+ const tail = value.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-40);
357
+ console.error(`release_check: ${commandLine} ${label} tail:`);
358
+ for (const line of tail) console.error(` ${line}`);
359
+ }
360
+ }
361
+
343
362
  function commandForPlatform(command) {
344
363
  return process.platform === "win32" && command === "npm" ? "npm.cmd" : command;
345
364
  }
@@ -432,5 +451,7 @@ function firstOutputLine(value) {
432
451
  return value
433
452
  .split(/\r?\n/)
434
453
  .map((line) => line.trim())
435
- .find(Boolean);
454
+ // npm chatter is never the failure - skip it so the one-line summary shows
455
+ // the real error instead of "npm warn Unknown user config ...".
456
+ .find((line) => Boolean(line) && !/^npm (warn|notice)\b/i.test(line));
436
457
  }
@@ -133,7 +133,9 @@ async function readPackedFiles(root) {
133
133
  } catch {
134
134
  throw new Error("npm pack dry-run did not return valid JSON");
135
135
  }
136
- const files = entries?.[0]?.files;
136
+ // npm <=11 prints an array; npm 12 prints an object keyed by package name.
137
+ const first = Array.isArray(entries) ? entries[0] : Object.values(entries ?? {})[0];
138
+ const files = first?.files;
137
139
  if (!Array.isArray(files)) {
138
140
  throw new Error("npm pack dry-run did not return a file list");
139
141
  }
@@ -170,7 +172,8 @@ function resolvePackedTarball(destination, stdout) {
170
172
  } catch {
171
173
  throw new Error("npm pack did not return valid JSON");
172
174
  }
173
- const filename = entries?.[0]?.filename;
175
+ const firstEntry = Array.isArray(entries) ? entries[0] : Object.values(entries ?? {})[0];
176
+ const filename = firstEntry?.filename;
174
177
  if (typeof filename !== "string" || !filename.endsWith(".tgz")) {
175
178
  throw new Error(`could not determine npm pack tarball from output: ${stdout}`);
176
179
  }