@youdie006/prodex 0.40.46 → 0.40.47

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.
@@ -315,11 +315,21 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
315
315
  // The renderer web-search answers use since 2026-10-10 marks a source as a
316
316
  // badge inside a popover trigger; its urls only exist once it is opened.
317
317
  const isSourceBadge = (node) => attr(node, "data-d-component") === "popover-trigger" && findAll(node, (n) => attr(n, "data-pill") !== null).length > 0;
318
+ // ChatGPT's link component appends utm_source=chatgpt.com to every link
319
+ // it renders (measured 2026-10-28), a parameter the answer never wrote.
320
+ const untracked = (href) => {
321
+ const hashAt = href.indexOf("#");
322
+ const base = hashAt === -1 ? href : href.slice(0, hashAt);
323
+ const queryAt = base.indexOf("?");
324
+ if (queryAt === -1) return href;
325
+ const params = base.slice(queryAt + 1).split("&").filter((param) => param !== "utm_source=chatgpt.com");
326
+ return base.slice(0, queryAt) + (params.length > 0 ? "?" + params.join("&") : "") + (hashAt === -1 ? "" : href.slice(hashAt));
327
+ };
318
328
  const pill = (node) => {
319
329
  const label = collapse(node.textContent || "").trim();
320
330
  if (!label) return "";
321
331
  const link = findAll(node, (n) => tagOf(n) === "a" && (attr(n, "href") || "").startsWith("http"))[0];
322
- return link ? "[" + label + "](" + attr(link, "href") + ")" : "[" + label + "]";
332
+ return link ? "[" + label + "](" + untracked(attr(link, "href")) + ")" : "[" + label + "]";
323
333
  };
324
334
  const inline = (node, keep) => {
325
335
  const keepLines = keepsLineBreaks(node, keep === true);
@@ -344,13 +354,25 @@ export const CHATGPT_RENDERED_MESSAGE_TEXT_JS = `
344
354
  if (attr(node, "data-markdown-copy") === "inline-code" || tag === "code") return BT + (node.textContent || "") + BT;
345
355
  if (tag === "br") return NL;
346
356
  const inner = inline(node, keep);
347
- // The newer renderer marks bold as span[data-d-default-strong].
348
- if (tag === "strong" || tag === "b" || attr(node, "data-d-default-strong") !== null) return inner.trim() ? "**" + inner.trim() + "**" : "";
349
- if (tag === "em" || tag === "i") return inner.trim() ? "*" + inner.trim() + "*" : "";
350
- if (tag === "del" || tag === "s") return inner.trim() ? "~~" + inner.trim() + "~~" : "";
357
+ // The newer renderer marks bold as span[data-d-default-strong], italic
358
+ // as span[data-d-font-style=italic] and strikethrough as
359
+ // span[data-d-text-decoration=line-through] (measured 2026-10-28; only
360
+ // bold was read, so the other two came back as plain words). One
361
+ // element can carry several, so each applies in turn.
362
+ const strong = tag === "strong" || tag === "b" || attr(node, "data-d-default-strong") !== null;
363
+ const italic = tag === "em" || tag === "i" || attr(node, "data-d-font-style") === "italic";
364
+ const struck = tag === "del" || tag === "s" || (attr(node, "data-d-text-decoration") || "").includes("line-through");
365
+ if (strong || italic || struck) {
366
+ let marked = inner.trim();
367
+ if (!marked) return "";
368
+ if (strong) marked = "**" + marked + "**";
369
+ if (italic) marked = "*" + marked + "*";
370
+ if (struck) marked = "~~" + marked + "~~";
371
+ return marked;
372
+ }
351
373
  if (tag === "a") {
352
374
  const href = attr(node, "href") || "";
353
- return href.startsWith("http") && inner.trim() ? "[" + inner.trim() + "](" + href + ")" : inner;
375
+ return href.startsWith("http") && inner.trim() ? "[" + inner.trim() + "](" + untracked(href) + ")" : inner;
354
376
  }
355
377
  return inner;
356
378
  };
@@ -6281,9 +6303,8 @@ export function pickLandedConversation(candidates, sentPrompt) {
6281
6303
  * nothing is the safe answer here.
6282
6304
  */
6283
6305
  export function transcriptContainsWholeSentPrompt(userText, sentPrompt) {
6284
- const normalize = (value) => value
6306
+ const normalize = (value) => undoMarkdownEscapes(value)
6285
6307
  .replace(AUTOLINKED_URL_PATTERN, "$1")
6286
- .replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
6287
6308
  .replace(/\s+/g, " ")
6288
6309
  .trim();
6289
6310
  const seen = normalize(userText);
@@ -6298,6 +6319,14 @@ const NORMALIZED_PROMPT_MATCH_CHARS = 120;
6298
6319
  // halves differently ("Function\_(x)" vs "Function_\(x\)", measured
6299
6320
  // 2026-10-21), so the pattern is applied after markdown escapes are undone.
6300
6321
  const AUTOLINKED_URL_PATTERN = /\[(https?:\/\/[^\]\s]+)\]\(\1\)/g;
6322
+ // The composer escapes more than the markdown it would otherwise misread: an
6323
+ // autolinked url's href kept "\\&" where its text had "&" (measured
6324
+ // 2026-10-28). Any ASCII punctuation can carry a markdown escape, so every such
6325
+ // escape is undone, on both sides alike.
6326
+ const MARKDOWN_ESCAPE_PATTERN = /\\([!-\/:-@[-`{-~])/g;
6327
+ function undoMarkdownEscapes(value) {
6328
+ return value.replace(MARKDOWN_ESCAPE_PATTERN, "$1");
6329
+ }
6301
6330
  /**
6302
6331
  * ChatGPT displays a sent turn's HTML entities decoded (measured 2026-10-19:
6303
6332
  * "&amp; &lt;b&gt;" was shown as "& <b>"), so both sides are compared decoded.
@@ -6314,7 +6343,7 @@ function decodeHtmlEntities(value) {
6314
6343
  });
6315
6344
  }
6316
6345
  function normalizeChatGptPromptText(value) {
6317
- const unescaped = decodeHtmlEntities(value).replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1").replace(AUTOLINKED_URL_PATTERN, "$1");
6346
+ const unescaped = undoMarkdownEscapes(decodeHtmlEntities(value)).replace(AUTOLINKED_URL_PATTERN, "$1");
6318
6347
  const renderedFences = unescaped.replace(/(^|\r?\n)```text[ \t]*\r?\n([\s\S]*?)\r?\n```(?=\r?\n|$)/g, "$1text\n$2");
6319
6348
  return renderedFences.replace(/\s+/g, " ").trim();
6320
6349
  }
@@ -6345,8 +6374,7 @@ export function chatGptRequestMatchesUserTurn(userText, sentPrompt, requestId) {
6345
6374
  return withoutFenceLines(userText).includes(withoutFenceLines(sentPrompt));
6346
6375
  }
6347
6376
  function withoutFenceLines(value) {
6348
- return decodeHtmlEntities(value)
6349
- .replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
6377
+ return undoMarkdownEscapes(decodeHtmlEntities(value))
6350
6378
  .replace(AUTOLINKED_URL_PATTERN, "$1")
6351
6379
  .replace(/^[ \t]*```[\w-]*[ \t]*$/gm, "")
6352
6380
  .replace(/\s+/g, "");
package/docs/releasing.md CHANGED
@@ -35,6 +35,26 @@ 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.46 Release Record
39
+
40
+ 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).
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 40645773352 passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release, which lists the two entries from `CHANGELOG.md`.
45
+
46
+ **Installed**
47
+ - WSL, M3, Mac mini: `prodex --version` reports 0.40.46, and each installed `dist/chatgpt-browser.js` carries the line-break reader. 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 `807f245` and rebuilt.
49
+
50
+ **Container**
51
+ - `container/redesign-patches` `5d021d1` ports #69 and `ad24ce2` ports #70 (the #70 cherry-pick conflicted in `src/chatgpt-browser.ts` and the PR diff was applied instead; tsc, build, the suite and `release:verify` pass on it).
52
+ - The images were still not rebuilt: `docker compose build` is refused by the agent's permission classifier on WSL and over ssh on M3. The running WSL container is the #49-#58 port and the M3 container the 0.40.42 port.
53
+
54
+ **Verified**
55
+ - Before the release, on the live tab: the reader in 0.40.45 returned "alpha beta" for an answer showing "alpha" and "beta" on two lines; the #70 build returned "alpha\nbeta" from the same page, with the list after it unchanged.
56
+ - After installing, the tab no longer showed a multi-line answer, so the line-break fix was not re-read live on the installed build; the installed reader ran on the current answer without error.
57
+
38
58
  ## 0.40.45 Release Record
39
59
 
40
60
  Release commit `5e5f965` on `main` (cut after PRs #59 and #63-#68, last merge `52243f4`); tag `v0.40.45`. A batch released on the user's go-ahead: machine-wide send pacing (#59), accurate advice for a busy tab (#63), word-bounded project-name redaction (#64), the IPv6 loopback message (#65), no cancelling of finished sessions (#66), a blank saved default project stopped before the browser (#67), and the project-delete preview (#68).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.40.46",
3
+ "version": "0.40.47",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",