@youdie006/prodex 0.40.24 → 0.40.26
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 +103 -24
- package/docs/releasing.md +53 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -192,12 +192,18 @@ export const CHATGPT_BLOCKER_SCAN_EXCLUDED_ANCESTORS = `${'[data-message-author-
|
|
|
192
192
|
* attribute went with it; where it is absent the model is simply not known.
|
|
193
193
|
*/
|
|
194
194
|
const CHATGPT_MESSAGE_NODES_JS = `
|
|
195
|
+
// ChatGPT keeps a previously opened conversation mounted but hidden
|
|
196
|
+
// (measured 2026-10-06: two turns without a box on a project home), and
|
|
197
|
+
// counting them made a fresh chat look like an existing conversation.
|
|
198
|
+
// A display:contents node has no box of its own, so its children decide.
|
|
199
|
+
const isRenderedMessageNode = (node) =>
|
|
200
|
+
node.getClientRects().length > 0 || [...(node.children || [])].some((child) => child.getClientRects().length > 0);
|
|
195
201
|
const chatMessageNodes = () => {
|
|
196
|
-
const legacy = [...document.querySelectorAll('[data-message-author-role]')];
|
|
202
|
+
const legacy = [...document.querySelectorAll('[data-message-author-role]')].filter(isRenderedMessageNode);
|
|
197
203
|
if (legacy.length > 0) {
|
|
198
204
|
return legacy.map((node) => ({ role: node.getAttribute('data-message-author-role'), node, textNode: node }));
|
|
199
205
|
}
|
|
200
|
-
const units = [...document.querySelectorAll('[data-chatgpt-search-unit-key]')];
|
|
206
|
+
const units = [...document.querySelectorAll('[data-chatgpt-search-unit-key]')].filter(isRenderedMessageNode);
|
|
201
207
|
return units
|
|
202
208
|
.filter((unit) => !units.some((other) => other !== unit && unit.contains(other)))
|
|
203
209
|
.map((unit) => {
|
|
@@ -1942,6 +1948,10 @@ export function chatGptThreadReadyExpression(conversationId) {
|
|
|
1942
1948
|
const current = new URL(location.href);
|
|
1943
1949
|
const match = /\\/c\\/([0-9a-f-]{16,})\\/?$/i.exec(current.pathname);
|
|
1944
1950
|
if (current.origin !== "https://chatgpt.com" || match?.[1].toLowerCase() !== ${JSON.stringify(conversationId.toLowerCase())}) return false;
|
|
1951
|
+
// A thread document that is still loading already has its URL and the
|
|
1952
|
+
// server-rendered editor, but no file inputs or messages yet (measured:
|
|
1953
|
+
// 0.6 s after navigating, complete at 6.7 s).
|
|
1954
|
+
if (document.readyState !== "complete") return false;
|
|
1945
1955
|
return Boolean(findChatGptComposerCandidate());
|
|
1946
1956
|
})()`;
|
|
1947
1957
|
}
|
|
@@ -5105,6 +5115,29 @@ export function insertComposerTextInPageExpression(text) {
|
|
|
5105
5115
|
export function composerFileInputSelector() {
|
|
5106
5116
|
return 'input[type="file"]:not([accept*="image"])';
|
|
5107
5117
|
}
|
|
5118
|
+
const COMPOSER_FILE_INPUT_TIMEOUT_MS = 15_000;
|
|
5119
|
+
const COMPOSER_TOOLS_MENU_ATTEMPTS = 3;
|
|
5120
|
+
export const PRODEX_ATTACH_INPUT_ATTRIBUTE = "data-prodex-attach-input";
|
|
5121
|
+
/**
|
|
5122
|
+
* Tags the general file input of the form that holds the VISIBLE composer, so
|
|
5123
|
+
* DOM.querySelector can resolve exactly that node. A project home also keeps
|
|
5124
|
+
* a hidden copy of an earlier page mounted with its own complete composer
|
|
5125
|
+
* form (measured 2026-10-06: two editor forms, the hidden one first), and a
|
|
5126
|
+
* structural selector cannot tell the two apart. Returns false when no
|
|
5127
|
+
* visible composer form carries a general file input.
|
|
5128
|
+
*/
|
|
5129
|
+
export function markComposerFileInputExpression() {
|
|
5130
|
+
const attribute = JSON.stringify(PRODEX_ATTACH_INPUT_ATTRIBUTE);
|
|
5131
|
+
return `(() => {${composerExpressionHelpers()}
|
|
5132
|
+
document.querySelectorAll('[' + ${attribute} + ']').forEach((node) => node.removeAttribute(${attribute}));
|
|
5133
|
+
const editor = findChatGptComposerCandidate();
|
|
5134
|
+
const form = editor && editor.closest('form');
|
|
5135
|
+
const input = form && form.querySelector(${JSON.stringify(composerFileInputSelector())});
|
|
5136
|
+
if (!input) return false;
|
|
5137
|
+
input.setAttribute(${attribute}, "1");
|
|
5138
|
+
return true;
|
|
5139
|
+
})()`;
|
|
5140
|
+
}
|
|
5108
5141
|
/**
|
|
5109
5142
|
* Lookup order for the attachment input. A conversation page also renders a
|
|
5110
5143
|
* second form without the prompt editor, earlier in the document and with its
|
|
@@ -5136,7 +5169,8 @@ export function attachmentStateExpression(fileNames) {
|
|
|
5136
5169
|
.join(String.fromCharCode(10));
|
|
5137
5170
|
const haystack = text + String.fromCharCode(10) + labels;
|
|
5138
5171
|
// Uploading a name that was used before makes ChatGPT show it as
|
|
5139
|
-
// "stem(3).ext",
|
|
5172
|
+
// "stem(3).ext", and since 2026-10 as "stem(20261006-012426).ext", so
|
|
5173
|
+
// either spelling counts as the same file.
|
|
5140
5174
|
const shownAsRenamed = (name) => {
|
|
5141
5175
|
const dot = name.lastIndexOf(".");
|
|
5142
5176
|
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
@@ -5145,7 +5179,7 @@ export function attachmentStateExpression(fileNames) {
|
|
|
5145
5179
|
while ((from = haystack.indexOf(stem + "(", from)) !== -1) {
|
|
5146
5180
|
let i = from + stem.length + 1;
|
|
5147
5181
|
const digitsStart = i;
|
|
5148
|
-
while (i < haystack.length && haystack[i] >= "0" && haystack[i] <= "9") i += 1;
|
|
5182
|
+
while (i < haystack.length && ((haystack[i] >= "0" && haystack[i] <= "9") || (i > digitsStart && haystack[i] === "-"))) i += 1;
|
|
5149
5183
|
if (i > digitsStart && haystack.startsWith(")" + ext, i)) return true;
|
|
5150
5184
|
from += 1;
|
|
5151
5185
|
}
|
|
@@ -5163,9 +5197,14 @@ export function attachmentStateExpression(fileNames) {
|
|
|
5163
5197
|
/** How many attachments are already sitting in the composer (read-only). */
|
|
5164
5198
|
export function attachmentPresenceExpression() {
|
|
5165
5199
|
return `(() => {
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5200
|
+
// The chip's remove button was "Remove file 1: name" and became
|
|
5201
|
+
// "Remove name" (measured 2026-10-06); a tool pill's "Remove Search" is
|
|
5202
|
+
// not a file. Chips on a hidden, still-mounted page have no box.
|
|
5203
|
+
const buttons = [...document.querySelectorAll('button[aria-label]')].filter((b) => {
|
|
5204
|
+
const label = b.getAttribute("aria-label") || "";
|
|
5205
|
+
if (!(/remove file|파일 제거|첨부 제거/i.test(label) || /^remove\\s+\\S.*\\.[A-Za-z0-9]{1,8}$/i.test(label))) return false;
|
|
5206
|
+
return b.getClientRects().length > 0;
|
|
5207
|
+
});
|
|
5169
5208
|
return { ok: true, removed: buttons.length };
|
|
5170
5209
|
})()`;
|
|
5171
5210
|
}
|
|
@@ -5193,24 +5232,43 @@ export async function attachFilesToComposer(cdp, absolutePaths, options = {}) {
|
|
|
5193
5232
|
// every later attach in that tab silently does nothing. A reload is the only
|
|
5194
5233
|
// reliable reset, and it costs a few seconds only when there is something to
|
|
5195
5234
|
// clear.
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5235
|
+
// "complete" is not "usable": the server-rendered editor is there before
|
|
5236
|
+
// the app attaches its file inputs (measured: complete at 3.3 s, inputs at
|
|
5237
|
+
// 5.1 s after a new-chat navigation), and every navigation path reaches
|
|
5238
|
+
// this point in that window sometimes. Wait for the input itself, before
|
|
5239
|
+
// the chip check (chips render with it) and before reading the DOM tree.
|
|
5240
|
+
const waitForComposerFileInput = async () => {
|
|
5241
|
+
const inputDeadline = Date.now() + COMPOSER_FILE_INPUT_TIMEOUT_MS;
|
|
5201
5242
|
for (;;) {
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5243
|
+
try {
|
|
5244
|
+
if (await cdp.evaluate(markComposerFileInputExpression()) === true)
|
|
5245
|
+
return true;
|
|
5246
|
+
}
|
|
5247
|
+
catch (error) {
|
|
5248
|
+
if (cdpCommandTimedOut(error))
|
|
5249
|
+
throw error;
|
|
5250
|
+
}
|
|
5251
|
+
if (Date.now() >= inputDeadline)
|
|
5252
|
+
return false;
|
|
5205
5253
|
await sleep(500);
|
|
5206
5254
|
}
|
|
5255
|
+
};
|
|
5256
|
+
let marked = await waitForComposerFileInput();
|
|
5257
|
+
const stale = await cdp.evaluate(attachmentPresenceExpression());
|
|
5258
|
+
if ((stale?.removed ?? 0) > 0) {
|
|
5259
|
+
// A fixed sleep and a composer check could both pass on the document being
|
|
5260
|
+
// left (a reload takes 5-8 s here), and the file then went into a page that
|
|
5261
|
+
// was replaced under it. The marked reload waits for the new document.
|
|
5262
|
+
await reloadAndAwaitComposer(cdp, 20_000);
|
|
5263
|
+
marked = await waitForComposerFileInput();
|
|
5207
5264
|
}
|
|
5208
5265
|
const document = await cdp.send("DOM.getDocument", { depth: -1, pierce: true });
|
|
5209
5266
|
const rootNodeId = document.result?.root?.nodeId;
|
|
5210
5267
|
if (rootNodeId === undefined)
|
|
5211
5268
|
throw new Error("Could not read the ChatGPT page DOM to attach files.");
|
|
5269
|
+
const selectors = marked ? [`[${PRODEX_ATTACH_INPUT_ATTRIBUTE}]`, ...composerFileInputSelectors()] : composerFileInputSelectors();
|
|
5212
5270
|
let inputNodeId;
|
|
5213
|
-
for (const selector of
|
|
5271
|
+
for (const selector of selectors) {
|
|
5214
5272
|
const input = await cdp.send("DOM.querySelector", { nodeId: rootNodeId, selector });
|
|
5215
5273
|
inputNodeId = input.result?.nodeId || undefined;
|
|
5216
5274
|
if (inputNodeId)
|
|
@@ -5402,20 +5460,41 @@ export async function enableComposerTools(cdp, labels) {
|
|
|
5402
5460
|
enabled.push(label);
|
|
5403
5461
|
continue;
|
|
5404
5462
|
}
|
|
5405
|
-
|
|
5463
|
+
// The server-rendered editor shows before the app attaches the composer's
|
|
5464
|
+
// controls (measured 2026-10-06), so a first miss is usually just early.
|
|
5465
|
+
let button = await cdp.evaluate(composerToolsButtonRectExpression());
|
|
5466
|
+
const buttonDeadline = Date.now() + COMPOSER_FILE_INPUT_TIMEOUT_MS;
|
|
5467
|
+
while ((!button.ok || button.x === undefined || button.y === undefined) && Date.now() < buttonDeadline) {
|
|
5468
|
+
await sleep(500);
|
|
5469
|
+
button = await cdp.evaluate(composerToolsButtonRectExpression());
|
|
5470
|
+
}
|
|
5406
5471
|
if (!button.ok || button.x === undefined || button.y === undefined) {
|
|
5407
5472
|
throw new Error(button.reason ?? "Could not open the ChatGPT composer tools menu");
|
|
5408
5473
|
}
|
|
5409
|
-
|
|
5474
|
+
// A click that lands before the app has wired the button opens nothing
|
|
5475
|
+
// (measured 2026-10-06), which looked exactly like a menu without the
|
|
5476
|
+
// tool. The button is clicked again, freshly located, before concluding
|
|
5477
|
+
// the tool is missing.
|
|
5410
5478
|
let entry = { ok: false };
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5479
|
+
for (let attempt = 0; attempt < COMPOSER_TOOLS_MENU_ATTEMPTS; attempt += 1) {
|
|
5480
|
+
if (attempt > 0) {
|
|
5481
|
+
await dispatchEscapeKey(cdp);
|
|
5482
|
+
const again = await cdp.evaluate(composerToolsButtonRectExpression());
|
|
5483
|
+
if (again.ok && again.x !== undefined && again.y !== undefined)
|
|
5484
|
+
button = again;
|
|
5485
|
+
}
|
|
5486
|
+
await dispatchMouseClickAt(cdp, button.x, button.y);
|
|
5487
|
+
const menuDeadline = Date.now() + 4_000;
|
|
5488
|
+
for (;;) {
|
|
5489
|
+
entry = await cdp.evaluate(composerToolEntryRectExpression(label));
|
|
5490
|
+
if (entry.ok && entry.x !== undefined && entry.y !== undefined)
|
|
5491
|
+
break;
|
|
5492
|
+
if (Date.now() >= menuDeadline)
|
|
5493
|
+
break;
|
|
5494
|
+
await sleep(250);
|
|
5495
|
+
}
|
|
5414
5496
|
if (entry.ok && entry.x !== undefined && entry.y !== undefined)
|
|
5415
5497
|
break;
|
|
5416
|
-
if (Date.now() >= menuDeadline)
|
|
5417
|
-
break;
|
|
5418
|
-
await sleep(250);
|
|
5419
5498
|
}
|
|
5420
5499
|
if (!entry.ok || entry.x === undefined || entry.y === undefined) {
|
|
5421
5500
|
await dispatchEscapeKey(cdp);
|
package/docs/releasing.md
CHANGED
|
@@ -16,6 +16,59 @@ Release target: `v0.40.13` on `main`. The GitHub Release records the resolved co
|
|
|
16
16
|
|
|
17
17
|
The release adds a guarded, opt-in transition and more accurate blocker reporting. It does not claim that headless Pro consultation is verified on either deployment target or that authentication can be retained indefinitely.
|
|
18
18
|
|
|
19
|
+
## 0.40.25 Release Record
|
|
20
|
+
|
|
21
|
+
Release commit `3c66da3` on `main` (cut after PR #15, merge `9bd908c`); tag `v0.40.25`.
|
|
22
|
+
|
|
23
|
+
**Publishing**
|
|
24
|
+
- `publish.yml` run 37394019059 passed all six platforms on the first attempt, then npm publish with SLSA v1 provenance and the GitHub Release (2026-10-06 00:48 UTC).
|
|
25
|
+
- `npm run release:verify` passed on the release commit after a build.
|
|
26
|
+
|
|
27
|
+
**What it fixes**
|
|
28
|
+
- A send into an existing conversation from a tab on another one now waits for that page to finish loading.
|
|
29
|
+
|
|
30
|
+
**Installation on 2026-10-06**
|
|
31
|
+
- Global npm, each verified with `prodex --version` = 0.40.25.
|
|
32
|
+
- WSL, M3, Mac mini: installed. Running services were not restarted: 19 stdio MCP servers on WSL and 7 on M3 keep older code until their clients reconnect. The Mac mini had none running.
|
|
33
|
+
- M4: not reachable (hostname does not resolve); not updated.
|
|
34
|
+
- The browser container already carries the same fix (2026-10-02 rebuild, see 0.40.24). An MCP consult through it answered on 2026-10-06 after the client reconnected.
|
|
35
|
+
|
|
36
|
+
**Live check after install (installed 0.40.25, shared WSL browser)**
|
|
37
|
+
- The tab was first moved to a new chat.
|
|
38
|
+
- `--continue --attach` answered, with a baseline of 5 earlier user messages.
|
|
39
|
+
- `--new-chat --attach` answered.
|
|
40
|
+
- A plain `--continue` recalled the word set at the start of the thread.
|
|
41
|
+
|
|
42
|
+
**Real use since 0.40.24 (2026-10-02 to 2026-10-06)**
|
|
43
|
+
- `prodex pro blockers`: 3 of 18 host consults were blocked, all three the probes that reproduced the bugs fixed in 0.40.24 and 0.40.25.
|
|
44
|
+
- 0 of 5 container consults were blocked.
|
|
45
|
+
|
|
46
|
+
## 0.40.24 Release Record
|
|
47
|
+
|
|
48
|
+
Release commit `e927eee` on `main` (cut after PR #14, merge `393f720`); tag `v0.40.24`.
|
|
49
|
+
|
|
50
|
+
**Publishing**
|
|
51
|
+
- `publish.yml` run 36975443077 passed all six platforms on the first attempt, then npm publish with SLSA v1 provenance and the GitHub Release (2026-10-02 07:10 UTC).
|
|
52
|
+
- `npm run release:verify` passed on the release commit after a build.
|
|
53
|
+
|
|
54
|
+
**What it fixes**
|
|
55
|
+
- Attachments into an existing conversation used a second form on the page that has no prompt editor.
|
|
56
|
+
|
|
57
|
+
**Installation on 2026-10-02**
|
|
58
|
+
- Global npm, each verified with `prodex --version` = 0.40.24.
|
|
59
|
+
- WSL, M3, Mac mini: installed. Running services were not restarted: 5 stdio MCP servers on WSL and 7 on M3 keep older code until their clients reconnect. The Mac mini had none running.
|
|
60
|
+
- M4: not reachable (hostname does not resolve); not updated.
|
|
61
|
+
|
|
62
|
+
**Live check after install (installed 0.40.24, shared WSL browser)**
|
|
63
|
+
- `--new-chat --attach` answered.
|
|
64
|
+
- `--continue --attach` failed with "composer has no file input" when the tab started on another conversation. When the tab was already on the target thread it had passed.
|
|
65
|
+
- Cause: the conversation page was used before it finished loading. Fixed in the next change.
|
|
66
|
+
|
|
67
|
+
**Browser container, same day**
|
|
68
|
+
- Image `sha256:2a2091354d90`, rebuilt from `b5eac7d` with the earlier uncommitted patches plus two more: the composer-form file input lookup and the loaded-document requirement for thread readiness.
|
|
69
|
+
- Full suite 1989 passed on the patched tree. The swap was gated on the send lock being free.
|
|
70
|
+
- Verified in the container: `--continue --attach` from another thread answered, and so did `--new-chat --attach`.
|
|
71
|
+
|
|
19
72
|
## 0.40.23 Release Record
|
|
20
73
|
|
|
21
74
|
Release commit `1c905e3` on `main` (cut after PR #13, merge `92c26ce`); tag `v0.40.23`.
|