@youdie006/prodex 0.40.47 → 0.40.48
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 +76 -18
- package/docs/releasing.md +21 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -331,6 +331,17 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
331
331
|
const link = findAll(node, (n) => tagOf(n) === "a" && (attr(n, "href") || "").startsWith("http"))[0];
|
|
332
332
|
return link ? "[" + label + "](" + untracked(attr(link, "href")) + ")" : "[" + label + "]";
|
|
333
333
|
};
|
|
334
|
+
// Inline code goes in a backtick run longer than any inside it, padded
|
|
335
|
+
// with a space when it starts or ends with a backtick, or has a space at
|
|
336
|
+
// both ends that markdown would strip. One backtick each side turned the
|
|
337
|
+
// code a\`b into a one-letter span and a stray backtick (measured
|
|
338
|
+
// 2026-10-29).
|
|
339
|
+
const inlineCode = (value) => {
|
|
340
|
+
const longestRun = (value.match(new RegExp(BT + "+", "g")) || []).reduce((most, run) => Math.max(most, run.length), 0);
|
|
341
|
+
const ticks = BT.repeat(longestRun + 1);
|
|
342
|
+
const pad = value.startsWith(BT) || value.endsWith(BT) || (value.startsWith(" ") && value.endsWith(" ") && value.trim() !== "") ? " " : "";
|
|
343
|
+
return ticks + pad + value + pad + ticks;
|
|
344
|
+
};
|
|
334
345
|
const inline = (node, keep) => {
|
|
335
346
|
const keepLines = keepsLineBreaks(node, keep === true);
|
|
336
347
|
let out = "";
|
|
@@ -351,7 +362,7 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
351
362
|
if (isCheckbox(node)) return (attr(node, "aria-checked") === "true" || attr(node, "checked") !== null ? "[x]" : "[ ]") + " ";
|
|
352
363
|
if (isMath(node)) return isDisplayMath(node) ? "$$" + mathSource(node) + "$$" : "$" + mathSource(node) + "$";
|
|
353
364
|
if (attr(node, "data-chatgpt-copy-reference") !== null || isSourceBadge(node)) return pill(node);
|
|
354
|
-
if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return
|
|
365
|
+
if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return inlineCode(node.textContent || "");
|
|
355
366
|
if (tag === "br") return NL;
|
|
356
367
|
const inner = inline(node, keep);
|
|
357
368
|
// The newer renderer marks bold as span[data-d-default-strong], italic
|
|
@@ -406,16 +417,23 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
406
417
|
};
|
|
407
418
|
const table = (node) => {
|
|
408
419
|
const cell = (value) => collapse(value).trim().split("|").join(String.fromCharCode(92) + "|");
|
|
409
|
-
const
|
|
420
|
+
const cellNodes = findAll(node, (n) => tagOf(n) === "tr").map((row) => children(row).filter((c) => c.nodeType === 1));
|
|
421
|
+
const rows = cellNodes.map((cells) => cells.map((c) => cell(inline(c))));
|
|
410
422
|
if (rows.length === 0) return "";
|
|
423
|
+
// The newer renderer marks a column written :---, :---: or ---: with
|
|
424
|
+
// data-d-align start, center or end on its cells, and a plain column
|
|
425
|
+
// with none (measured 2026-10-29); every column came back as ---.
|
|
426
|
+
const rule = (c) => {
|
|
427
|
+
const align = attr(c, "data-d-align");
|
|
428
|
+
return align === "start" ? ":---" : align === "center" ? ":---:" : align === "end" ? "---:" : "---";
|
|
429
|
+
};
|
|
411
430
|
const line = (cells) => "| " + cells.join(" | ") + " |";
|
|
412
|
-
return [line(rows[0]), line(
|
|
431
|
+
return [line(rows[0]), line(cellNodes[0].map(rule)), ...rows.slice(1).map(line)].join(NL);
|
|
413
432
|
};
|
|
414
|
-
const list = (node,
|
|
433
|
+
const list = (node, keep) => {
|
|
415
434
|
const ordered = tagOf(node) === "ol";
|
|
416
435
|
let number = parseInt(attr(node, "start") || "1", 10);
|
|
417
436
|
if (!Number.isFinite(number)) number = 1;
|
|
418
|
-
const indent = " ".repeat(depth);
|
|
419
437
|
const items = [];
|
|
420
438
|
for (const item of children(node)) {
|
|
421
439
|
if (item.nodeType !== 1 || tagOf(item) !== "li") continue;
|
|
@@ -427,16 +445,25 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
427
445
|
const keepItemLines = keepsLineBreaks(item, keep === true);
|
|
428
446
|
for (const child of children(item)) {
|
|
429
447
|
const tag = child.nodeType === 1 ? tagOf(child) : "";
|
|
430
|
-
if (tag === "ul" || tag === "ol") { nested.push(list(child,
|
|
431
|
-
if (isBlock(child)) { if (buffer.trim()) words.push(buffer.trim()); buffer = ""; const b = block(child,
|
|
448
|
+
if (tag === "ul" || tag === "ol") { nested.push(list(child, keepItemLines)); continue; }
|
|
449
|
+
if (isBlock(child)) { if (buffer.trim()) words.push(buffer.trim()); buffer = ""; const b = block(child, keepItemLines); if (b) words.push(b); continue; }
|
|
432
450
|
buffer += inlineNode(child, keepItemLines);
|
|
433
451
|
}
|
|
434
452
|
if (buffer.trim()) words.push(buffer.trim());
|
|
435
|
-
|
|
453
|
+
// A block spanning lines (a code block, a table) is a paragraph of its own.
|
|
454
|
+
const body = words.reduce((text, word) => (!text ? word : text + (text.includes(NL) || word.includes(NL) ? NL + NL : " ") + word), "");
|
|
455
|
+
// An item's later lines sit at its content column, after the marker
|
|
456
|
+
// and its space. They came back at column 0, or three spaces in for a
|
|
457
|
+
// nested list, so a code block, table, quote or second paragraph
|
|
458
|
+
// ended the item and a list under item 10 was not nested (measured
|
|
459
|
+
// 2026-10-29). Nested content is laid out from column 0 and shifted.
|
|
460
|
+
const pad = " ".repeat(marker.length + 1);
|
|
461
|
+
const lines = (body + (nested.length ? NL + nested.join(NL) : "")).split(NL);
|
|
462
|
+
items.push(marker + " " + lines.map((line, at) => (at === 0 || !line ? line : pad + line)).join(NL));
|
|
436
463
|
}
|
|
437
464
|
return items.join(NL);
|
|
438
465
|
};
|
|
439
|
-
const block = (node,
|
|
466
|
+
const block = (node, keep) => {
|
|
440
467
|
if (node.nodeType === 3) return (keep === true ? collapseKeepingLines(node.textContent || "") : collapse(node.textContent || "")).trim();
|
|
441
468
|
if (node.nodeType !== 1 || skipped(node)) return "";
|
|
442
469
|
const tag = tagOf(node);
|
|
@@ -445,25 +472,27 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
|
|
|
445
472
|
if (isMermaidPreview(node)) return mermaidBlock(node);
|
|
446
473
|
if (tag === "table") return table(node);
|
|
447
474
|
if (/^h[1-6]$/.test(tag)) return "#".repeat(Number(tag[1])) + " " + inline(node).trim();
|
|
448
|
-
if (tag === "ul" || tag === "ol") return list(node,
|
|
449
|
-
|
|
450
|
-
|
|
475
|
+
if (tag === "ul" || tag === "ol") return list(node, keepsLineBreaks(node, keep === true));
|
|
476
|
+
// The newer renderer draws a rule as an empty
|
|
477
|
+
// div[data-d-component=divider] (measured 2026-10-29).
|
|
478
|
+
if (tag === "hr" || attr(node, "data-d-component") === "divider") return "---";
|
|
479
|
+
if (tag === "blockquote") return blocks(node, keep).split(NL).map((line) => "> " + line).join(NL);
|
|
451
480
|
if (tag === "p") return inline(node, keep).trim();
|
|
452
|
-
return blocks(node,
|
|
481
|
+
return blocks(node, keep);
|
|
453
482
|
};
|
|
454
|
-
const blocks = (node,
|
|
483
|
+
const blocks = (node, keep) => {
|
|
455
484
|
const keepLines = keepsLineBreaks(node, keep === true);
|
|
456
485
|
const parts = [];
|
|
457
486
|
let buffer = "";
|
|
458
487
|
const flush = () => { if (buffer.trim()) parts.push(buffer.trim()); buffer = ""; };
|
|
459
488
|
for (const child of children(node)) {
|
|
460
|
-
if (isBlock(child)) { flush(); const b = block(child,
|
|
489
|
+
if (isBlock(child)) { flush(); const b = block(child, keepLines); if (b) parts.push(b); continue; }
|
|
461
490
|
buffer += inlineNode(child, keepLines);
|
|
462
491
|
}
|
|
463
492
|
flush();
|
|
464
493
|
return parts.join(NL + NL);
|
|
465
494
|
};
|
|
466
|
-
const markdown = blocks(root
|
|
495
|
+
const markdown = blocks(root).trim();
|
|
467
496
|
return markdown || root.innerText || "";
|
|
468
497
|
};
|
|
469
498
|
// A sent user turn renders inline code as <code>, dropping its backticks
|
|
@@ -2186,6 +2215,13 @@ export function menuOpenExpression() {
|
|
|
2186
2215
|
export function menuClosedExpression() {
|
|
2187
2216
|
return `!document.querySelector('[data-testid="composer-intelligence-picker-content"],[role="menu"]:has([role="slider"])')`;
|
|
2188
2217
|
}
|
|
2218
|
+
/** One visible model selector, marked closed, with no picker on the page. */
|
|
2219
|
+
export function modelSelectorReadsClosedExpression() {
|
|
2220
|
+
return `(() => {
|
|
2221
|
+
const triggers = [...document.querySelectorAll('button[aria-label="Select ChatGPT model"][aria-haspopup="menu"],[data-codex-intelligence-trigger="true"][aria-haspopup="menu"]')].filter((el) => el.getClientRects().length > 0);
|
|
2222
|
+
return triggers.length === 1 && triggers[0].getAttribute("aria-expanded") === "false" && !${menuOpenExpression()};
|
|
2223
|
+
})()`;
|
|
2224
|
+
}
|
|
2189
2225
|
function toLabelCandidates(label) {
|
|
2190
2226
|
return typeof label === "string" ? [label] : [...label];
|
|
2191
2227
|
}
|
|
@@ -2258,6 +2294,28 @@ async function verifiedClickAt(cdp, x, y, label) {
|
|
|
2258
2294
|
await cdp.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
2259
2295
|
await cdp.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
2260
2296
|
}
|
|
2297
|
+
const MODEL_MENU_FIRST_PRESS_MS = 1_500;
|
|
2298
|
+
/**
|
|
2299
|
+
* Waits for the model picker after the selector was pressed, pressing it once
|
|
2300
|
+
* more if the page ignored that press.
|
|
2301
|
+
*
|
|
2302
|
+
* Measured 2026-10-29 with the host under load: in two of every four sends
|
|
2303
|
+
* that started from a conversation, a trusted pointerdown and pointerup reached
|
|
2304
|
+
* the selector while it read closed and nothing happened - aria-expanded stayed
|
|
2305
|
+
* false and no menu was added - so the send failed as "model menu did not
|
|
2306
|
+
* open". A selector that still reads closed is pressed again; one that reads
|
|
2307
|
+
* open is left alone, since a press would shut it.
|
|
2308
|
+
*/
|
|
2309
|
+
export async function waitForModelMenu(cdp) {
|
|
2310
|
+
if (await waitForExpressionTrue(cdp, menuOpenExpression(), MODEL_MENU_FIRST_PRESS_MS))
|
|
2311
|
+
return true;
|
|
2312
|
+
if (await cdp.evaluate(modelSelectorReadsClosedExpression())) {
|
|
2313
|
+
const again = await cdp.evaluate(modelButtonRectExpression());
|
|
2314
|
+
if (again.ok && again.x !== undefined && again.y !== undefined)
|
|
2315
|
+
await verifiedClickAt(cdp, again.x, again.y, "model selector");
|
|
2316
|
+
}
|
|
2317
|
+
return waitForExpressionTrue(cdp, menuOpenExpression(), MENU_OPEN_TIMEOUT_MS);
|
|
2318
|
+
}
|
|
2261
2319
|
/**
|
|
2262
2320
|
* Whether the model picker BUTTON already advertises the requested model, so
|
|
2263
2321
|
* the menu never has to be opened. ChatGPT moved the models behind a "Model"
|
|
@@ -3371,7 +3429,7 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
3371
3429
|
button = fresh;
|
|
3372
3430
|
}
|
|
3373
3431
|
}
|
|
3374
|
-
const opened = await
|
|
3432
|
+
const opened = await waitForModelMenu(cdp);
|
|
3375
3433
|
if (!opened)
|
|
3376
3434
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
3377
3435
|
// Current ChatGPT: one power slider (Instant/Medium/High/Extra High/Pro on
|
|
@@ -5138,7 +5196,7 @@ export async function listChatGptModelOptions(input = {}) {
|
|
|
5138
5196
|
}
|
|
5139
5197
|
try {
|
|
5140
5198
|
await verifiedClickAt(cdp, button.x, button.y, "model selector");
|
|
5141
|
-
const opened = await
|
|
5199
|
+
const opened = await waitForModelMenu(cdp);
|
|
5142
5200
|
if (!opened)
|
|
5143
5201
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
5144
5202
|
const options = await cdp.evaluate(modelMenuOptionsExpression());
|
package/docs/releasing.md
CHANGED
|
@@ -35,6 +35,27 @@ The release adds a guarded, opt-in transition and more accurate blocker reportin
|
|
|
35
35
|
**Open**
|
|
36
36
|
- A long-running headless browser ends up behind Cloudflare's check (also noted on WSL, where the Xvfb display is used instead). On macOS there is no virtual display, and a minimized window counts as hidden, so the remaining no-headless option there is a window kept behind others. Changing the headless preference is a design decision that was not made here.
|
|
37
37
|
|
|
38
|
+
## 0.40.47 Release Record
|
|
39
|
+
|
|
40
|
+
Release commit `609b1c9` on `main` (cut after PRs #71-#73, last merge `40d7eee`); tag `v0.40.47`. Released promptly because of #72: on 0.40.46 any prompt carrying a url with "&" (a query string, bare or in a markdown link) was answered and then refused as `request_mismatch`, so the answer was not returned. #71 (italic and strikethrough in the newer renderer) and #73 (the `utm_source=chatgpt.com` parameter ChatGPT appends to rendered links) went out with it.
|
|
41
|
+
|
|
42
|
+
**Publishing**
|
|
43
|
+
- `npm run release:verify` passed on the release commit after a build, with nothing else running on the machine.
|
|
44
|
+
- `publish.yml` run 40656237042 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the three entries from `CHANGELOG.md`.
|
|
45
|
+
|
|
46
|
+
**Installed**
|
|
47
|
+
- WSL, M3, Mac mini: `prodex --version` reports 0.40.47. Running services were not restarted: no host stdio MCP servers on WSL, 1 on M3, none on the Mac mini.
|
|
48
|
+
- The maintainer watchdog checkout was pulled to `609b1c9` and rebuilt.
|
|
49
|
+
|
|
50
|
+
**Container**
|
|
51
|
+
- `container/redesign-patches` `63b060c` ports #71, `62376fa` #72 and `facbd9d` #73 (#71 and #73 did not cherry-pick cleanly and their hunks were applied by exact replacement; tsc, build, the full suite, 2128 tests, and `release:verify` pass on `facbd9d`).
|
|
52
|
+
- WSL, on the user's request after the release: the image was built from `facbd9d` (`c03be48c060d`) and the container swapped while its send lock was idle. The old container had been `unhealthy` (its health check's fetch failed) and its last consult, from another session at 03:41 UTC, was blocked as `browser_unreachable`; that consult was left for its owner to recover. The new container came up healthy, `pro browser check` reported logged in with a composer, and an Instant smoke whose prompt linked `https://example.com/s?q=a&page=2` returned `PRODEX_PRO_SMOKE_OK`, status done. Its `prodex --version` still prints 0.40.18 because the branch never bumps its version.
|
|
53
|
+
- M3, on 2026-10-29: the build over ssh was refused by the agent's permission classifier, so the user ran the build-and-swap script. The image was built from `facbd9d` in the M3 container checkout (`cfaf881d2fd6`, replacing the 0.40.42 port `59747258b693`) and the container swapped while its send lock was idle. It came up healthy, `pro browser check` reported logged in with a composer, and the same Instant smoke (prompt linking `https://example.com/s?q=a&page=2`) returned `PRODEX_PRO_SMOKE_OK`, status done.
|
|
54
|
+
|
|
55
|
+
**Verified live on installed 0.40.47 (WSL, scratch repo, Instant)**
|
|
56
|
+
- A send whose prompt linked `https://example.com/s?q=a&page=2` returned status done. The user turn was stored with `\&` in the autolink's href half, the shape 0.40.46 refused.
|
|
57
|
+
- Its answer came back as "The ~~old~~ *new* [search](https://example.com/s?q=a&page=2) page." while the page's anchor href was `https://example.com/s?q=a&page=2&utm_source=chatgpt.com`.
|
|
58
|
+
|
|
38
59
|
## 0.40.46 Release Record
|
|
39
60
|
|
|
40
61
|
Release commit `807f245` on `main` (cut after PRs #69 and #70, last merge `9253ee9`); tag `v0.40.46`. A batch released on the user's go-ahead: terminal-column widths in the interactive picker (#69) and line breaks kept inside answers from ChatGPT's newer `white-space: pre-line` renderer (#70).
|