@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.
- package/dist/chatgpt-browser.js +54 -7
- package/dist/cli-help.js +1 -1
- package/dist/cli.js +7 -4
- package/package.json +1 -1
- package/scripts/release-check.mjs +23 -2
- package/scripts/release-pack.mjs +5 -2
package/dist/chatgpt-browser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
625
|
+
export function parsePackedFiles(stdout) {
|
|
626
|
+
let parsed;
|
|
627
627
|
try {
|
|
628
|
-
|
|
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
|
-
|
|
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
|
@@ -198,7 +198,9 @@ async function readPackedFiles(rootDir) {
|
|
|
198
198
|
}
|
|
199
199
|
try {
|
|
200
200
|
const entries = JSON.parse(stdout);
|
|
201
|
-
|
|
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
|
-
|
|
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
|
}
|
package/scripts/release-pack.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|