@youdie006/prodex 0.40.20 → 0.40.22
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 +147 -12
- package/docs/releasing.md +65 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -222,7 +222,15 @@ const CHATGPT_GENERATING_CONTROL_PATTERN = /\bstop\s+(?:generating|responding|re
|
|
|
222
222
|
// are more reliable than the button label text, which the pattern above can
|
|
223
223
|
// miss on an icon-only/relabeled control - a miss would let a mid-stream pause
|
|
224
224
|
// be accepted as the final answer, silently truncating it.
|
|
225
|
-
|
|
225
|
+
//
|
|
226
|
+
// Measured 2026-09-29: the stop control lost its test id and is now a composer
|
|
227
|
+
// button labelled just "Stop". Sampled every 1.5 s through a streamed answer,
|
|
228
|
+
// prodex reported generating=false on all 23 samples while that button was on
|
|
229
|
+
// screen - so a pause mid-answer (a Pro reasoning gap, a network stall) could
|
|
230
|
+
// be taken for the end and a truncated answer returned as complete. The label
|
|
231
|
+
// is matched only inside the composer's form, where the send control lives,
|
|
232
|
+
// so a "Stop" elsewhere on the page cannot answer for it.
|
|
233
|
+
export const CHATGPT_STREAMING_SELECTOR = '[data-testid="stop-button"],form button[aria-label="Stop"],form button[aria-label="중지"],[data-message-author-role="assistant"][aria-busy="true"],[data-message-author-role="assistant"] [aria-busy="true"]';
|
|
226
234
|
export function defaultChatGptProfileDir() {
|
|
227
235
|
return path.join(os.homedir(), ".local", "share", "prodex", "chrome-chatgpt-pro");
|
|
228
236
|
}
|
|
@@ -2127,15 +2135,36 @@ export function focusPowerSliderExpression() {
|
|
|
2127
2135
|
return { ok: document.activeElement === slider };
|
|
2128
2136
|
})()`;
|
|
2129
2137
|
}
|
|
2138
|
+
/**
|
|
2139
|
+
* The composer's model trigger - and nothing outside the composer.
|
|
2140
|
+
*
|
|
2141
|
+
* With no composer rendered yet the lookup used to fall back to the whole
|
|
2142
|
+
* document and take the first menu button with text in it. On the current
|
|
2143
|
+
* sidebar that is "Pinned" or "Explore": measured 2026-09-29, every send that
|
|
2144
|
+
* started on a fresh chat - a new chat, an attachment, a tool, a temporary chat
|
|
2145
|
+
* - clicked a sidebar menu, then failed with "model menu did not open". A
|
|
2146
|
+
* project send passed only because the project page had already rendered its
|
|
2147
|
+
* composer. Now the trigger is looked for inside the rendered composer's form
|
|
2148
|
+
* only (the exact "Select ChatGPT model" control first, the older heuristic
|
|
2149
|
+
* second), and an unrendered composer is reported as not found, which the
|
|
2150
|
+
* caller's poll waits out.
|
|
2151
|
+
*/
|
|
2130
2152
|
export function modelButtonRectExpression() {
|
|
2131
2153
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2154
|
+
const composer = [...document.querySelectorAll('#prompt-textarea,[contenteditable="true"],textarea')].find((el) => {
|
|
2155
|
+
const r = el.getBoundingClientRect();
|
|
2156
|
+
return r.width > 0 && r.height > 0;
|
|
2157
|
+
});
|
|
2158
|
+
if (!composer) return { ok: false, reason: "model selector button not found: the composer has not rendered" };
|
|
2159
|
+
const scope = composer.closest('form');
|
|
2160
|
+
if (!scope) return { ok: false, reason: "model selector button not found: the composer has no form around it" };
|
|
2161
|
+
const exact = [...scope.querySelectorAll('button[aria-label="Select ChatGPT model"][aria-haspopup="menu"],[data-codex-intelligence-trigger="true"][aria-haspopup="menu"]')];
|
|
2162
|
+
const b = exact.length === 1 ? exact[0] : exact.length > 1 ? null : [...scope.querySelectorAll('[aria-haspopup="menu"]')].find((el) => {
|
|
2135
2163
|
const t = (el.textContent || "").trim();
|
|
2136
2164
|
const aria = el.getAttribute("aria-label") || "";
|
|
2137
2165
|
return /\\S/.test(t) && !/파일|첨부|받아쓰기|음성|dictation|attach|file|voice|record|search|mic/i.test(t + aria);
|
|
2138
2166
|
});
|
|
2167
|
+
if (exact.length > 1) return { ok: false, reason: "model selector button not found: more than one model trigger in the composer" };
|
|
2139
2168
|
if (!b) return { ok: false, reason: "model selector button not found" };
|
|
2140
2169
|
// Return the label too: the caller compares it against the requested model
|
|
2141
2170
|
// to skip opening the menu when it is already selected.
|
|
@@ -3002,14 +3031,42 @@ async function dispatchEnterKey(cdp) {
|
|
|
3002
3031
|
// Creates a project in the user's real ChatGPT account: sidebar 새 프로젝트 →
|
|
3003
3032
|
// type the name into the popover input → Enter → wait for navigation into the
|
|
3004
3033
|
// new project. Backs out with Escape on any failure.
|
|
3034
|
+
/**
|
|
3035
|
+
* Open the create-project dialog on the current sidebar by keyboard.
|
|
3036
|
+
*
|
|
3037
|
+
* Measured 2026-09-29: the sidebar's "Add new project" button sits at the
|
|
3038
|
+
* sidebar's edge underneath the composer's overlay (z-20), so the point prodex
|
|
3039
|
+
* clicked resolved to the overlay and nothing opened - `--project-new` failed
|
|
3040
|
+
* with "new-project name input did not appear". The button carries a stable
|
|
3041
|
+
* data-app-action-sidebar-project-create attribute; focusing it and pressing
|
|
3042
|
+
* Enter opens the same "Create project" dialog without depending on what is
|
|
3043
|
+
* drawn over it. Returns false on a sidebar without that control, where the
|
|
3044
|
+
* older click path still applies.
|
|
3045
|
+
*/
|
|
3046
|
+
async function openNewProjectDialogByKeyboard(cdp) {
|
|
3047
|
+
const focused = await cdp.evaluate(`(() => {
|
|
3048
|
+
const button = document.querySelector('[data-app-action-sidebar-project-create]');
|
|
3049
|
+
if (!button || button.disabled) return false;
|
|
3050
|
+
button.focus();
|
|
3051
|
+
return document.activeElement === button;
|
|
3052
|
+
})()`);
|
|
3053
|
+
if (!focused)
|
|
3054
|
+
return false;
|
|
3055
|
+
// A button is activated by the key's text, not by the keydown alone:
|
|
3056
|
+
// without text the event reaches the page and nothing opens (measured).
|
|
3057
|
+
await dispatchEnterKey(cdp);
|
|
3058
|
+
return true;
|
|
3059
|
+
}
|
|
3005
3060
|
async function createChatGptProject(cdp, name) {
|
|
3006
3061
|
const hrefBefore = await cdp.evaluate("location.href");
|
|
3007
|
-
const button = await cdp.evaluate(newProjectButtonRectExpression());
|
|
3008
|
-
if (!button.ok || button.x === undefined || button.y === undefined) {
|
|
3009
|
-
throw new Error(button.reason ?? "new-project button not found in the sidebar");
|
|
3010
|
-
}
|
|
3011
3062
|
try {
|
|
3012
|
-
await
|
|
3063
|
+
if (!(await openNewProjectDialogByKeyboard(cdp))) {
|
|
3064
|
+
const button = await cdp.evaluate(newProjectButtonRectExpression());
|
|
3065
|
+
if (!button.ok || button.x === undefined || button.y === undefined) {
|
|
3066
|
+
throw new Error(button.reason ?? "new-project button not found in the sidebar");
|
|
3067
|
+
}
|
|
3068
|
+
await verifiedClickAt(cdp, button.x, button.y, "새 프로젝트");
|
|
3069
|
+
}
|
|
3013
3070
|
const inputVisible = await waitForExpressionTrue(cdp, newProjectNameInputVisibleExpression(), MENU_OPEN_TIMEOUT_MS);
|
|
3014
3071
|
if (!inputVisible)
|
|
3015
3072
|
throw new Error("new-project name input did not appear");
|
|
@@ -3921,6 +3978,8 @@ export async function sendChatGptPrompt(options) {
|
|
|
3921
3978
|
: undefined;
|
|
3922
3979
|
let consecutiveReadFailures = 0;
|
|
3923
3980
|
let answerSettled = false;
|
|
3981
|
+
let nonTextPolls = 0;
|
|
3982
|
+
let nonTextSettled = false;
|
|
3924
3983
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
3925
3984
|
while (Date.now() - started < timeoutMs) {
|
|
3926
3985
|
await sleep(1000);
|
|
@@ -3973,6 +4032,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
3973
4032
|
if (runtimeBlocker)
|
|
3974
4033
|
throw new ChatGptBrowserBlockerError(submittedRequestBlocker(runtimeBlocker, requestId, pinnedThreadUrl));
|
|
3975
4034
|
emitProgress("waiting", finalState.generating ? "generating" : "stabilizing");
|
|
4035
|
+
// A turn that answered with an image leaves no assistant message, so the
|
|
4036
|
+
// count below never moves and the send used to wait out its whole budget
|
|
4037
|
+
// and report a timeout - measured again on 2026-09-29 after the page-only
|
|
4038
|
+
// reader replaced the transcript. Two quiet polls in a row settle it.
|
|
4039
|
+
if (finalState.nonTextAnswer && !finalState.generating && finalState.userMessageCount > beforeSubmit.userMessageCount) {
|
|
4040
|
+
nonTextPolls += 1;
|
|
4041
|
+
if (nonTextPolls >= 2) {
|
|
4042
|
+
nonTextSettled = true;
|
|
4043
|
+
break;
|
|
4044
|
+
}
|
|
4045
|
+
continue;
|
|
4046
|
+
}
|
|
4047
|
+
nonTextPolls = 0;
|
|
3976
4048
|
if (!hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, finalState))
|
|
3977
4049
|
continue;
|
|
3978
4050
|
// A "fresh" answer must also be stable: ChatGPT can momentarily look done
|
|
@@ -3985,6 +4057,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
3985
4057
|
}
|
|
3986
4058
|
}
|
|
3987
4059
|
const completed = finalState;
|
|
4060
|
+
if (nonTextSettled && completed) {
|
|
4061
|
+
emitProgress("answered", "non-text");
|
|
4062
|
+
return {
|
|
4063
|
+
url: pinnedThreadUrl ?? completed.url,
|
|
4064
|
+
title: completed.title,
|
|
4065
|
+
answer: CHATGPT_NON_TEXT_ANSWER_NOTE,
|
|
4066
|
+
modelHints: completed.modelHints,
|
|
4067
|
+
...(boundProjectId ? { boundProjectId } : {}),
|
|
4068
|
+
requestId,
|
|
4069
|
+
requestVerified: true,
|
|
4070
|
+
warnings: withDialogNote([...sendWarnings]).filter((warning) => Boolean(warning))
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
3988
4073
|
if (answerSettled && completed && hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
|
|
3989
4074
|
emitProgress("answered");
|
|
3990
4075
|
return {
|
|
@@ -5013,7 +5098,23 @@ export function attachmentStateExpression(fileNames) {
|
|
|
5013
5098
|
.map((el) => (el.getAttribute("aria-label") || el.getAttribute("title") || el.getAttribute("alt") || ""))
|
|
5014
5099
|
.join(String.fromCharCode(10));
|
|
5015
5100
|
const haystack = text + String.fromCharCode(10) + labels;
|
|
5016
|
-
|
|
5101
|
+
// Uploading a name that was used before makes ChatGPT show it as
|
|
5102
|
+
// "stem(3).ext", so that spelling counts as the same file.
|
|
5103
|
+
const shownAsRenamed = (name) => {
|
|
5104
|
+
const dot = name.lastIndexOf(".");
|
|
5105
|
+
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
5106
|
+
const ext = dot > 0 ? name.slice(dot) : "";
|
|
5107
|
+
let from = 0;
|
|
5108
|
+
while ((from = haystack.indexOf(stem + "(", from)) !== -1) {
|
|
5109
|
+
let i = from + stem.length + 1;
|
|
5110
|
+
const digitsStart = i;
|
|
5111
|
+
while (i < haystack.length && haystack[i] >= "0" && haystack[i] <= "9") i += 1;
|
|
5112
|
+
if (i > digitsStart && haystack.startsWith(")" + ext, i)) return true;
|
|
5113
|
+
from += 1;
|
|
5114
|
+
}
|
|
5115
|
+
return false;
|
|
5116
|
+
};
|
|
5117
|
+
const present = names.filter((name) => haystack.includes(name) || shownAsRenamed(name));
|
|
5017
5118
|
const attachedFiles = [...document.querySelectorAll('input[type="file"]')]
|
|
5018
5119
|
.reduce((total, el) => total + (el.files ? el.files.length : 0), 0);
|
|
5019
5120
|
// A visible progressbar means a file is still going up; sending now would
|
|
@@ -5142,9 +5243,27 @@ export function defaultTimeoutForTools(tools, fallbackMs) {
|
|
|
5142
5243
|
const wantsDeepResearch = tools.some((tool) => resolveComposerToolLabel(tool) === DEEP_RESEARCH_TOOL_LABEL);
|
|
5143
5244
|
return wantsDeepResearch ? Math.max(fallbackMs, DEEP_RESEARCH_MIN_TIMEOUT_MS) : fallbackMs;
|
|
5144
5245
|
}
|
|
5246
|
+
/**
|
|
5247
|
+
* The composer's "+" control that opens its tools.
|
|
5248
|
+
*
|
|
5249
|
+
* Measured 2026-09-29: ChatGPT dropped data-testid="composer-plus-btn"; the
|
|
5250
|
+
* control is now `Add files and more` with
|
|
5251
|
+
* data-composer-navigation-target="add-context", and every `--tool` send
|
|
5252
|
+
* failed with "composer tools button not found". Looked for inside the
|
|
5253
|
+
* rendered composer's form, so a similarly labelled control elsewhere cannot
|
|
5254
|
+
* answer for it; the old test id still wins where it exists.
|
|
5255
|
+
*/
|
|
5145
5256
|
export function composerToolsButtonRectExpression() {
|
|
5146
5257
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
5147
|
-
const
|
|
5258
|
+
const legacy = document.querySelector('[data-testid="composer-plus-btn"]');
|
|
5259
|
+
if (legacy) return clickPoint(legacy);
|
|
5260
|
+
const composer = [...document.querySelectorAll('#prompt-textarea,[contenteditable="true"],textarea')].find((el) => {
|
|
5261
|
+
const r = el.getBoundingClientRect();
|
|
5262
|
+
return r.width > 0 && r.height > 0;
|
|
5263
|
+
});
|
|
5264
|
+
const form = composer && composer.closest('form');
|
|
5265
|
+
if (!form) return { ok: false, reason: "composer tools button not found: the composer has not rendered" };
|
|
5266
|
+
const b = form.querySelector('button[data-composer-navigation-target="add-context"],button[aria-label="Add files and more"]');
|
|
5148
5267
|
if (!b) return { ok: false, reason: "composer tools button not found" };
|
|
5149
5268
|
return clickPoint(b);
|
|
5150
5269
|
})()`;
|
|
@@ -5779,7 +5898,7 @@ export function answerExpression() {
|
|
|
5779
5898
|
const tagged = node.closest('[data-message-model-slug]');
|
|
5780
5899
|
if (tagged) modelSlug = tagged.getAttribute('data-message-model-slug') || undefined;
|
|
5781
5900
|
}
|
|
5782
|
-
return { role, text: textNode.innerText || "", modelSlug };
|
|
5901
|
+
return { role, node, text: textNode.innerText || "", modelSlug };
|
|
5783
5902
|
});
|
|
5784
5903
|
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
5785
5904
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
@@ -5816,6 +5935,22 @@ export function answerExpression() {
|
|
|
5816
5935
|
assistantMessageCount: assistantMessages.length,
|
|
5817
5936
|
userMessageCount: userMessages.length,
|
|
5818
5937
|
lastUserText: userMessages.at(-1)?.text || "",
|
|
5938
|
+
// An image result is rendered outside every message unit and leaves no
|
|
5939
|
+
// assistant message at all (measured 2026-09-29), so it is reported here
|
|
5940
|
+
// for the waiter to recognize: a generated image that comes after the
|
|
5941
|
+
// last user turn, with no assistant turn after that user turn.
|
|
5942
|
+
nonTextAnswer: (() => {
|
|
5943
|
+
const lastUser = userMessages.at(-1);
|
|
5944
|
+
if (!lastUser || !lastUser.node) return false;
|
|
5945
|
+
const lastRole = messages.at(-1) ? messages.at(-1).role : null;
|
|
5946
|
+
if (lastRole !== "user") return false;
|
|
5947
|
+
return [...document.querySelectorAll('main img')].some((img) => {
|
|
5948
|
+
const r = img.getBoundingClientRect();
|
|
5949
|
+
if (r.width < 60 || r.height < 60) return false;
|
|
5950
|
+
if (!/^generated image/i.test(img.getAttribute("alt") || "")) return false;
|
|
5951
|
+
return Boolean(lastUser.node.compareDocumentPosition(img) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
5952
|
+
});
|
|
5953
|
+
})(),
|
|
5819
5954
|
// ChatGPT tags each assistant message with the model that produced it -
|
|
5820
5955
|
// the only ground truth for "did the Pro selection actually take".
|
|
5821
5956
|
modelSlug: assistant ? assistant.modelSlug : undefined,
|
package/docs/releasing.md
CHANGED
|
@@ -16,6 +16,71 @@ 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
|
+
## Browser Container Rebuild 2026-10-01
|
|
20
|
+
|
|
21
|
+
The container image was rebuilt again from `feat/headless-browser-compatibility` `b5eac7d` with uncommitted patches. None of them is pushed to that branch; they still have to be reconciled when that branch merges with `main`.
|
|
22
|
+
|
|
23
|
+
**Patches**
|
|
24
|
+
- The streaming selector from 2026-09-30.
|
|
25
|
+
- The new-chat wait raised from 8 s to 15 s.
|
|
26
|
+
- The non-text (image) answer detection from 0.40.21, adapted to the branch's message reader.
|
|
27
|
+
- The renamed-upload attachment fix from this change.
|
|
28
|
+
|
|
29
|
+
**Image and checks**
|
|
30
|
+
- Image `sha256:d2880fda5ab9`.
|
|
31
|
+
- Full suite 1989 passed on the patched tree.
|
|
32
|
+
- Each container swap was gated on the send lock being free (lock file absent, or its holder pid dead). The 2026-09-30 check only looked at process names.
|
|
33
|
+
|
|
34
|
+
**Verified live in the container after the swap**
|
|
35
|
+
- Three fresh-chat sends in a row answered, 35-36 s each; this flow failed about one send in three before the patch.
|
|
36
|
+
- A create-image send returned the non-text note in 49 s, where it had timed out after 6 min 40 s.
|
|
37
|
+
- The same file attached twice in a row answered `plum-7` both times. Before the fix, the second upload, shown by ChatGPT as `attach-probe(3).txt`, waited out the upload budget.
|
|
38
|
+
- An MCP `initialize` through `scripts/container-client.mjs` answered (server reports 0.40.18, the branch's version).
|
|
39
|
+
|
|
40
|
+
**Impact on clients**
|
|
41
|
+
- Clients connected to the container's MCP have to reconnect after each swap.
|
|
42
|
+
|
|
43
|
+
## Browser Container Rebuild 2026-09-30
|
|
44
|
+
|
|
45
|
+
Not an npm release; the container `prodex-browser-browser-1` builds from `feat/headless-browser-compatibility`, so this section records its state.
|
|
46
|
+
|
|
47
|
+
A live check of the container build of `b5eac7d` (2026-09-30) found: web search and temporary chats answered; fresh-chat sends failed intermittently with `fresh_chat_not_ready`; `--attach` failed on the upload budget; a create-image send timed out after 6 min 40 s; and streaming was never detected (15 of 15 samples reported idle while ChatGPT showed its Stop button), because the branch's `CHATGPT_STREAMING_SELECTOR` does not match the redesigned composer's plain `aria-label="Stop"` button.
|
|
48
|
+
|
|
49
|
+
**Rebuild.** Image `sha256:45951848a88b`, built from `b5eac7d` plus one uncommitted line: `CHATGPT_STREAMING_SELECTOR` gained `form button[aria-label="Stop"]` and `form button[aria-label="중지"]`, the same selectors `main` shipped in 0.40.21. Nothing was pushed to that branch; the change lives only in the image. Verified: the selector is present in `/app/dist/chatgpt-browser.js`; container healthy with the profile volume kept; `pro browser login --wait` found the session READY; a fresh-chat send answered in 20 s while a 1 s sampler read the page as generating on all 9 samples during which the Stop button was visible (task `task_20260930_014256`).
|
|
50
|
+
|
|
51
|
+
**Interrupted consult.** The swap at about 01:37 UTC interrupted a consult that another session had started over MCP at 01:34:31 (`task_20260930_013431`, left `claimed` with no thread), and dropped every client's MCP connection to the container; clients have to reconnect with `/mcp`. The pre-swap check only looked for `pro browser ask` processes and missed MCP consults. Before swapping the container again, check whether the send lock's holder process is alive, not the process names. Whether ChatGPT accepted that prompt was never recorded, so it must not be resent blindly.
|
|
52
|
+
|
|
53
|
+
**Still open on the branch, not fixed in this image.**
|
|
54
|
+
|
|
55
|
+
- Fresh-chat flake: the new-chat page took 5.2, 5.2, 5.6 and 8.5 s to become ready in four timed runs inside the container, while `waitForFreshChatGptPage` allows 8 s. The fix is a longer budget.
|
|
56
|
+
- `--attach` and create-image failures: `main` 0.40.21 has fixes for both.
|
|
57
|
+
- These belong to the branch owner and have to be reconciled when the branch merges with `main`.
|
|
58
|
+
|
|
59
|
+
## 0.40.21 Release Record
|
|
60
|
+
|
|
61
|
+
Release commit `936b009` on `main` (cut after PR #10, merge `5e74ba2`); tag `v0.40.21`. `publish.yml` run 36538816944 passed all six platforms on the first attempt, then npm publish with SLSA v1 provenance and the GitHub Release (2026-09-29 08:08 UTC).
|
|
62
|
+
|
|
63
|
+
What it fixes: what 0.40.20 left broken after the ChatGPT redesign - sends starting on a fresh chat, composer tools, `--project-new`, image answers that timed out, and response streaming that was never detected. Found by a live feature matrix against the published 0.40.20; each fix re-run live before release. `--project-new` created a probe project in the user's account, which was deleted afterwards through the project's own actions menu after checking the menu belonged to that project; the other nine projects were compared by id and left intact.
|
|
64
|
+
|
|
65
|
+
Installation on 2026-09-29, global npm, each verified with `prodex --version` = 0.40.21:
|
|
66
|
+
|
|
67
|
+
- WSL, M3, Mac mini: installed. On WSL a fresh-chat consult through the installed binary answered (`v21-ok`). Running services were not restarted: 3 stdio MCP servers on WSL and 4 on M3 keep older code until their clients reconnect; the Mac mini had none.
|
|
68
|
+
- M4: not reachable (hostname does not resolve); not updated.
|
|
69
|
+
- The browser container still runs its own build of `feat/headless-browser-compatibility` `b5eac7d` (see 0.40.20); it was not rebuilt for this release.
|
|
70
|
+
|
|
71
|
+
## 0.40.20 Release Record
|
|
72
|
+
|
|
73
|
+
Release commit `b8f8004` on `main` (cut after PR #9, merge `b134eae`); tag `v0.40.20`. `publish.yml` run 36522237267: the first attempt failed on macOS 15 Intel, where `tests/cli.test.ts` "uses an explicit --cwd target for doctor checks" exceeded its 30 s timeout on the slowest runner (full suite 9.5 min there; the same code had passed that runner in PR #9's CI, and no earlier failed CI run showed this test). Nothing was published by that attempt. Re-running the failed job passed all six platforms, then npm publish with SLSA v1 provenance and the GitHub Release (2026-09-29 05:18 UTC).
|
|
74
|
+
|
|
75
|
+
What it restores: project consults, and every consult on a machine with a pinned default project, after the ChatGPT redesign measured on 2026-09-29 (sidebar project rows, the hover-only "New chat in" control, the model picker, message markup). Verified live on the shared browser before release: a named-project send and a pinned-default Codex + Pro send both answered inside the requested project.
|
|
76
|
+
|
|
77
|
+
Installation on 2026-09-29, global npm, each verified with `prodex --version` = 0.40.20:
|
|
78
|
+
|
|
79
|
+
- WSL, M3, Mac mini: installed. Running services were not restarted: 3 stdio MCP servers on WSL and 4 on M3 keep older code until their clients reconnect; the Mac mini had none.
|
|
80
|
+
- M4: not reachable (hostname does not resolve); not updated.
|
|
81
|
+
|
|
82
|
+
Browser container, same day. The Claude Code MCP entry on WSL goes through `prodex-browser-browser-1`, which builds from `feat/headless-browser-compatibility`, not from npm. That branch had already adapted to the same redesign on its own (not yet merged to `main`), so the image was rebuilt from its CI-passing tip `b5eac7d` as-is - not merged with `main`, because the two parallel implementations conflict in 17 places in `src/chatgpt-browser.ts` and a hand-made hybrid would be unverified. New image `sha256:7b3e53b2fb7f`; it reports 0.40.18, the version in that branch's `package.json`. Verified: no consult in flight before the swap; container healthy with the profile volume kept; MCP handshake through `scripts/container-client.mjs`; `pro browser login --wait` found the session READY; a project consult inside the container answered in the requested project with the destination verified. The two implementations still have to be reconciled when that branch is merged.
|
|
83
|
+
|
|
19
84
|
## 0.40.19 Release Record
|
|
20
85
|
|
|
21
86
|
Release commit `8650136` on `main` (cut after PR #8, merge `b5a223f`); tag `v0.40.19`. Published by `publish.yml` run 36510352704: all six native verification jobs passed (Ubuntu Node 20/22/24, macOS 15 arm64 and Intel, Windows Server 2025 / Node 22), then npm publish with SLSA v1 provenance, then the GitHub Release (2026-09-29 02:17 UTC). Before tagging, PR #8 passed the same six jobs and `npm run release:verify` passed locally on the release commit after a build (a fresh checkout without `dist` fails two package-bin cases, because `release:verify` runs tests before building).
|