@youdie006/prodex 0.40.6 → 0.40.8
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/README.md +25 -10
- package/dist/browser-send-lock.js +10 -131
- package/dist/chatgpt-browser.js +275 -691
- package/dist/cli-help.js +20 -17
- package/dist/cli-ledger.js +2 -0
- package/dist/cli-pro.js +216 -102
- package/dist/cli-server.js +2 -2
- package/dist/cli.js +1 -4
- package/dist/config.js +3 -3
- package/dist/http-mcp.js +1 -1
- package/dist/issue-report.js +9 -4
- package/dist/mcp-tools.js +42 -10
- package/dist/mcp.js +2 -2
- package/dist/registry.js +54 -6
- package/dist/repo-write.js +22 -2
- package/dist/safe-file.js +249 -1
- package/dist/store.js +7 -1
- package/dist/tui-flow.js +0 -1
- package/dist/tui-run.js +62 -25
- package/dist/tui.js +68 -54
- package/docs/claude.md +3 -1
- package/docs/cli-reference.md +17 -6
- package/docs/clients.md +4 -6
- package/docs/http-mcp.md +5 -1
- package/docs/releasing.md +3 -1
- package/package.json +1 -1
package/dist/tui-run.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import readline from "node:readline";
|
|
10
10
|
import { renderBanner } from "./banner.js";
|
|
11
|
+
import { shellQuote } from "./cli-args.js";
|
|
11
12
|
import { destinationChoices, effortChoices, SEND_KINDS } from "./tui-flow.js";
|
|
12
13
|
import { consultArgsFromChoices, conversationsInProject, conversationThreadUrl, moveCursor, parseAttachmentLine, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
|
|
13
14
|
const ESC = "";
|
|
@@ -32,11 +33,18 @@ class KeyQueue {
|
|
|
32
33
|
input;
|
|
33
34
|
pending = [];
|
|
34
35
|
waiting;
|
|
36
|
+
ended = false;
|
|
35
37
|
constructor(input) {
|
|
36
38
|
this.input = input;
|
|
37
39
|
this.input.on("keypress", this.onKey);
|
|
40
|
+
this.input.on("end", this.onEnd);
|
|
41
|
+
this.input.on("close", this.onEnd);
|
|
42
|
+
if (this.input.readableEnded || this.input.destroyed)
|
|
43
|
+
this.onEnd();
|
|
38
44
|
}
|
|
39
45
|
onKey = (_str, key) => {
|
|
46
|
+
if (this.ended)
|
|
47
|
+
return;
|
|
40
48
|
const value = key ?? {};
|
|
41
49
|
if (this.waiting) {
|
|
42
50
|
const resolve = this.waiting;
|
|
@@ -46,7 +54,16 @@ class KeyQueue {
|
|
|
46
54
|
}
|
|
47
55
|
this.pending.push(value);
|
|
48
56
|
};
|
|
57
|
+
onEnd = () => {
|
|
58
|
+
this.ended = true;
|
|
59
|
+
this.pending.length = 0;
|
|
60
|
+
const resolve = this.waiting;
|
|
61
|
+
this.waiting = undefined;
|
|
62
|
+
resolve?.({ name: "c", ctrl: true });
|
|
63
|
+
};
|
|
49
64
|
next() {
|
|
65
|
+
if (this.ended)
|
|
66
|
+
return Promise.resolve({ name: "c", ctrl: true });
|
|
50
67
|
const buffered = this.pending.shift();
|
|
51
68
|
if (buffered)
|
|
52
69
|
return Promise.resolve(buffered);
|
|
@@ -60,6 +77,8 @@ class KeyQueue {
|
|
|
60
77
|
}
|
|
61
78
|
dispose() {
|
|
62
79
|
this.input.off("keypress", this.onKey);
|
|
80
|
+
this.input.off("end", this.onEnd);
|
|
81
|
+
this.input.off("close", this.onEnd);
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
84
|
let keys;
|
|
@@ -69,7 +88,7 @@ function readKey(_io) {
|
|
|
69
88
|
return keys.next();
|
|
70
89
|
}
|
|
71
90
|
function isCancel(key) {
|
|
72
|
-
return (key.ctrl === true && key.name === "c") || key.name === "escape" || key.name === "q";
|
|
91
|
+
return (key.ctrl === true && (key.name === "c" || key.name === "d")) || key.name === "escape" || key.name === "q";
|
|
73
92
|
}
|
|
74
93
|
// Terminals disagree about the Enter key: a carriage return arrives as
|
|
75
94
|
// "return", a line feed as "enter". Accept either, or the picker looks frozen.
|
|
@@ -121,15 +140,38 @@ function numericChoice(key, length) {
|
|
|
121
140
|
async function askLine(io, question) {
|
|
122
141
|
io.input.setRawMode?.(false);
|
|
123
142
|
const rl = readline.createInterface({ input: io.input, output: process.stdout, terminal: true });
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
try {
|
|
144
|
+
const answer = await new Promise((resolve) => {
|
|
145
|
+
let settled = false;
|
|
146
|
+
function settle(value) {
|
|
147
|
+
if (settled)
|
|
148
|
+
return;
|
|
149
|
+
settled = true;
|
|
150
|
+
rl.off("SIGINT", onInterrupt);
|
|
151
|
+
rl.off("close", onClose);
|
|
152
|
+
resolve(value);
|
|
153
|
+
}
|
|
154
|
+
function onInterrupt() {
|
|
155
|
+
settle(undefined);
|
|
156
|
+
}
|
|
157
|
+
function onClose() {
|
|
158
|
+
settle(undefined);
|
|
159
|
+
}
|
|
160
|
+
rl.once("SIGINT", onInterrupt);
|
|
161
|
+
rl.once("close", onClose);
|
|
162
|
+
rl.question(question, (value) => settle(value));
|
|
163
|
+
});
|
|
164
|
+
return answer?.trim();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
rl.close();
|
|
168
|
+
// Closing the line reader detaches the keypress plumbing and pauses the
|
|
169
|
+
// stream, so the next picker would sit there ignoring every key. Re-arm both.
|
|
170
|
+
io.input.setRawMode?.(true);
|
|
171
|
+
readline.emitKeypressEvents(io.input);
|
|
172
|
+
io.input.resume();
|
|
173
|
+
keys?.drain();
|
|
174
|
+
}
|
|
133
175
|
}
|
|
134
176
|
/**
|
|
135
177
|
* Walk the questions a send needs, then run it with a moving progress bar.
|
|
@@ -253,6 +295,8 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
253
295
|
}
|
|
254
296
|
else if (destination === "project-new") {
|
|
255
297
|
projectName = await askLine(io, `${CLEAR}${header}New project\n\n name: `);
|
|
298
|
+
if (projectName === undefined)
|
|
299
|
+
return cancel(io);
|
|
256
300
|
if (!projectName)
|
|
257
301
|
return cancel(io);
|
|
258
302
|
projectMode = "new";
|
|
@@ -265,6 +309,8 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
265
309
|
const kindLabel = SEND_KINDS[kindChoice].label;
|
|
266
310
|
const prompt = await askLine(io, `${kindLabel} Step ${steps} of ${steps}\n\n prompt: `);
|
|
267
311
|
io.write(HIDE_CURSOR);
|
|
312
|
+
if (prompt === undefined)
|
|
313
|
+
return cancel(io);
|
|
268
314
|
if (prompt.length === 0) {
|
|
269
315
|
io.write("Nothing to ask.\n");
|
|
270
316
|
return 1;
|
|
@@ -273,8 +319,11 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
273
319
|
// image, and the picker had no way to say so. Asked after the prompt, and
|
|
274
320
|
// skipped by pressing enter, so the common case costs one keystroke.
|
|
275
321
|
io.write(SHOW_CURSOR);
|
|
276
|
-
const
|
|
322
|
+
const attachmentLine = await askLine(io, "\n attach files? repo-relative paths, space separated, enter to skip\n files: ");
|
|
277
323
|
io.write(HIDE_CURSOR);
|
|
324
|
+
if (attachmentLine === undefined)
|
|
325
|
+
return cancel(io);
|
|
326
|
+
const attachments = parseAttachmentLine(attachmentLine);
|
|
278
327
|
const choices = {
|
|
279
328
|
prompt,
|
|
280
329
|
projectMode,
|
|
@@ -295,20 +344,8 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
295
344
|
// stop", and under raw mode that promise was false for the whole ten
|
|
296
345
|
// minutes a deep research send runs. No key is read from here on.
|
|
297
346
|
io.input.setRawMode?.(false);
|
|
298
|
-
// --target-url confirms which conversation a send means; it deliberately
|
|
299
|
-
// does not navigate. Picking one from a list IS a request to go there, so
|
|
300
|
-
// move the tab first and let the flag confirm it landed.
|
|
301
|
-
if (targetUrl && deps.openThread) {
|
|
302
|
-
io.write("Opening the conversation you picked...\n");
|
|
303
|
-
if (!(await deps.openThread(targetUrl))) {
|
|
304
|
-
io.write(`Could not open ${targetUrl} in the dedicated browser.\n`);
|
|
305
|
-
return 1;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
347
|
io.write(`Sending. Equivalent command:\n prodex ${formatCommand(args)}\n\n`);
|
|
309
|
-
|
|
310
|
-
// Fill the bar against that so the wait has a shape.
|
|
311
|
-
const budgetMs = tools.includes("deep-research") ? 30 * 60_000 : 20 * 60_000;
|
|
348
|
+
const budgetMs = 20 * 60_000;
|
|
312
349
|
const startedAt = now();
|
|
313
350
|
let label = "starting";
|
|
314
351
|
let tick = 0;
|
|
@@ -345,5 +382,5 @@ function cancel(io) {
|
|
|
345
382
|
}
|
|
346
383
|
/** Quote only what a shell would need quoted, so the echo can be pasted. */
|
|
347
384
|
export function formatCommand(args) {
|
|
348
|
-
return args.map(
|
|
385
|
+
return args.map(shellQuote).join(" ");
|
|
349
386
|
}
|
package/dist/tui.js
CHANGED
|
@@ -75,35 +75,33 @@ export function parseAttachmentLine(line) {
|
|
|
75
75
|
/**
|
|
76
76
|
* Projects with the id their conversations are tagged with.
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* inside it" needs exactly that link.
|
|
78
|
+
* Read only rendered links. A project id is accepted only when the visible
|
|
79
|
+
* anchor itself has a canonical ChatGPT project URL.
|
|
81
80
|
*/
|
|
82
81
|
export function projectsWithIdsExpression() {
|
|
83
|
-
return `(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return [];
|
|
82
|
+
return `(() => {
|
|
83
|
+
const out = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (const anchor of document.querySelectorAll("a[href]")) {
|
|
86
|
+
if (out.length >= 100) break;
|
|
87
|
+
if (typeof anchor.getClientRects !== "function" || anchor.getClientRects().length === 0) continue;
|
|
88
|
+
let url;
|
|
89
|
+
try {
|
|
90
|
+
url = new URL(anchor.getAttribute("href") || "", "https://chatgpt.com");
|
|
91
|
+
} catch (error) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (url.protocol !== "https:" || url.hostname !== "chatgpt.com" || url.port || url.username || url.password) continue;
|
|
95
|
+
const match = /^\\/g\\/g-p-([0-9a-f]{1,128})(?:-[^/]+)?\\/project\\/?$/i.exec(url.pathname);
|
|
96
|
+
if (!match) continue;
|
|
97
|
+
const id = "g-p-" + match[1].toLowerCase();
|
|
98
|
+
if (seen.has(id)) continue;
|
|
99
|
+
const name = ((anchor.textContent || anchor.getAttribute("aria-label") || "").replace(/\\s+/g, " ").trim()).slice(0, 200);
|
|
100
|
+
if (!name) continue;
|
|
101
|
+
seen.add(id);
|
|
102
|
+
out.push({ id, name });
|
|
106
103
|
}
|
|
104
|
+
return out;
|
|
107
105
|
})()`;
|
|
108
106
|
}
|
|
109
107
|
/** The conversations that live inside a project, or all of them without one. */
|
|
@@ -114,37 +112,53 @@ export function conversationsInProject(conversations, projectId) {
|
|
|
114
112
|
}
|
|
115
113
|
/**
|
|
116
114
|
* Recent conversations with their titles, for the "continue an existing chat"
|
|
117
|
-
* list.
|
|
118
|
-
*
|
|
115
|
+
* list. This is deliberately limited to rendered links; no account API or
|
|
116
|
+
* transcript is read. A project association is included only when that same
|
|
117
|
+
* visible conversation URL carries the project id.
|
|
119
118
|
*/
|
|
120
119
|
export function recentConversationTitlesExpression(limit = 10) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
if (
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
120
|
+
const boundedLimit = Number.isFinite(limit) ? Math.max(0, Math.min(100, Math.floor(limit))) : 10;
|
|
121
|
+
return `(() => {
|
|
122
|
+
const out = [];
|
|
123
|
+
const seen = new Map();
|
|
124
|
+
const ambiguousProjects = new Set();
|
|
125
|
+
let inspected = 0;
|
|
126
|
+
for (const anchor of document.querySelectorAll("a[href]")) {
|
|
127
|
+
if (++inspected > 2000) break;
|
|
128
|
+
if (typeof anchor.getClientRects !== "function" || anchor.getClientRects().length === 0) continue;
|
|
129
|
+
let url;
|
|
130
|
+
try {
|
|
131
|
+
url = new URL(anchor.getAttribute("href") || "", "https://chatgpt.com");
|
|
132
|
+
} catch (error) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (url.protocol !== "https:" || url.hostname !== "chatgpt.com" || url.port || url.username || url.password) continue;
|
|
136
|
+
const projectMatch = /^\\/g\\/g-p-([0-9a-f]{1,128})(?:-[^/]+)?\\/c\\/([0-9a-f-]{16,128})\\/?$/i.exec(url.pathname);
|
|
137
|
+
const plainMatch = /^\\/c\\/([0-9a-f-]{16,128})\\/?$/i.exec(url.pathname);
|
|
138
|
+
const id = (projectMatch && projectMatch[2]) || (plainMatch && plainMatch[1]);
|
|
139
|
+
if (!id) continue;
|
|
140
|
+
const gizmoId = projectMatch ? "g-p-" + projectMatch[1].toLowerCase() : undefined;
|
|
141
|
+
const existing = seen.get(id);
|
|
142
|
+
if (existing) {
|
|
143
|
+
if (gizmoId && !ambiguousProjects.has(id)) {
|
|
144
|
+
if (existing.gizmoId && existing.gizmoId !== gizmoId) {
|
|
145
|
+
delete existing.gizmoId;
|
|
146
|
+
ambiguousProjects.add(id);
|
|
147
|
+
} else existing.gizmoId = gizmoId;
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (out.length >= ${boundedLimit}) continue;
|
|
152
|
+
const title = ((anchor.textContent || anchor.getAttribute("aria-label") || "").replace(/\\s+/g, " ").trim()).slice(0, 300) || "Untitled";
|
|
153
|
+
const entry = {
|
|
154
|
+
id,
|
|
155
|
+
title,
|
|
156
|
+
...(gizmoId ? { gizmoId } : {})
|
|
157
|
+
};
|
|
158
|
+
seen.set(id, entry);
|
|
159
|
+
out.push(entry);
|
|
147
160
|
}
|
|
161
|
+
return out;
|
|
148
162
|
})()`;
|
|
149
163
|
}
|
|
150
164
|
const ESC = "";
|
package/docs/claude.md
CHANGED
|
@@ -107,7 +107,9 @@ Write tools are narrow and receipt-gated, and they require a git worktree with a
|
|
|
107
107
|
|
|
108
108
|
`pro_consult` lets Claude ask your logged-in ChatGPT (Pro) directly: it drives the same explicit visible-browser consult as `prodex pro browser ask` (human-paced, blocker-gated, receipt-recorded, answer saved under `.bridge/artifacts/pro-consults/`) and can take minutes for Pro extended reasoning. It requires a prior `prodex pro browser login` session and is registered only on the local stdio MCP server — the HTTP MCP surface never exposes it, so nothing reachable through a tunnel or ChatGPT itself can drive your browser.
|
|
109
109
|
|
|
110
|
-
`pro_recover`
|
|
110
|
+
`pro_recover` reads a finished, rendered answer after a consult stopped waiting. It verifies the requested conversation and refuses still-generating or unstable content. It sends nothing. Deep-research widget reports are not supported; current builds block automated deep-research sends before posting. Like `pro_consult`, recovery is registered only on the local stdio MCP server.
|
|
111
|
+
|
|
112
|
+
Generic bridge result/session/task tools redact ChatGPT thread metadata, including nested blockers; local `pro_consult` recovery information remains available. A legacy result artifact with no saved hash returns `legacy_artifact_unverified` rather than implying its bytes were verified. `sessions cancel` only clears stale bookkeeping after a send was interrupted; it does not stop an active consult.
|
|
111
113
|
|
|
112
114
|
No shell, public tunnel, direct ungated write, or direct ungated staging tools are exposed through the Claude stdio MCP server; the only browser-facing tools are the explicit `pro_consult` consult and the read-only `pro_recover` described above.
|
|
113
115
|
|
package/docs/cli-reference.md
CHANGED
|
@@ -82,6 +82,7 @@ Use this only when you explicitly want to use your logged-in ChatGPT Pro web ses
|
|
|
82
82
|
```bash
|
|
83
83
|
prodex pro browser login --dry-run
|
|
84
84
|
prodex pro browser login
|
|
85
|
+
prodex pro browser login --headed # force a visible window for interactive reauthentication
|
|
85
86
|
prodex pro browser help
|
|
86
87
|
prodex pro browser check
|
|
87
88
|
prodex pro browser smoke --cwd /absolute/path/to/your/repo
|
|
@@ -105,6 +106,7 @@ What happens:
|
|
|
105
106
|
|
|
106
107
|
- `login --dry-run` prints the dedicated Chrome profile, debug URL, and next commands without opening a browser.
|
|
107
108
|
- `login` opens that dedicated Chrome profile at ChatGPT. In an interactive terminal it then waits (default 5 minutes; `--no-wait` skips, `--wait-timeout-ms` tunes) and narrates which manual step is still missing until it reports READY; scripts and agents get the immediate return unless they pass `--wait`.
|
|
109
|
+
- `login` reuses the last profile recorded for the resolved debug port when `--profile-dir` is omitted. It does not reuse a saved custom port implicitly: `--port` / `PRODEX_CDP_PORT` / the normal `9333` default still resolve the port exactly as before.
|
|
108
110
|
- You log in manually in the visible browser.
|
|
109
111
|
- If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, handle it in that browser.
|
|
110
112
|
- If ChatGPT shows a usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.
|
|
@@ -131,6 +133,10 @@ prodex sessions show latest
|
|
|
131
133
|
|
|
132
134
|
This uses the currently available ChatGPT web session and model selection. It is not a hidden API client, and it does not read cookies, tokens, localStorage, or sessionStorage.
|
|
133
135
|
|
|
136
|
+
Current builds read rendered page content only. Project/conversation listings are limited to entries exposed by the UI. Automatic chat/project deletion and deep-research report retrieval are unsupported; those commands stop rather than call internal endpoints. A recovered answer must belong to the requested thread and be stable and finished. Formatting may differ from ChatGPT's rendered message.
|
|
137
|
+
|
|
138
|
+
Locks fail closed if a process is killed while reclaiming an abandoned lock. A leftover `.reap` claim then needs manual cleanup: first stop every prodex process using that resource and confirm no request/write/startup is active; only then remove the affected lock and its matching `.reap` file. Browser locks live beside the recorded send lock, repo-write locks under `.bridge`, and virtual-display allocation locks under `~/.local/share/prodex/xvfb`. Do not remove a live request's lock to shorten a wait.
|
|
139
|
+
|
|
134
140
|
#### Choosing the model, reasoning effort, and project
|
|
135
141
|
|
|
136
142
|
The visible-browser send drives the same composer picker you use by hand. Since ChatGPT replaced the model menu with one power slider that walks model and effort together, that slider is the lever:
|
|
@@ -174,13 +180,18 @@ Log in once, then never see the browser again:
|
|
|
174
180
|
```bash
|
|
175
181
|
prodex pro browser login # once, headed - sign in
|
|
176
182
|
prodex pro browser login --virtual-display # from now on: no window anywhere
|
|
183
|
+
prodex pro browser login --headed # force a visible window for login/captcha
|
|
177
184
|
```
|
|
178
185
|
|
|
179
|
-
`--virtual-display
|
|
186
|
+
Window mode is one mutually exclusive choice: `--headed`, `--headless`, `--minimized`, or `--virtual-display`. Supplying a CLI mode flag selects the whole mode and overrides environment and saved state. With no mode flag, any non-empty `PRODEX_HEADLESS`, `PRODEX_MINIMIZE_WINDOW`, or `PRODEX_VIRTUAL_DISPLAY` value selects the whole environment mode; `0`, `false`, and `no` are meaningful false values, so `PRODEX_HEADLESS=0` explicitly selects ordinary headed mode instead of falling back to a saved headless launch. With neither flags nor mode environment settings, `login` and CLI/MCP auto-recovery reuse the last recorded mode. With no saved record, the normal default is headed.
|
|
187
|
+
|
|
188
|
+
`--virtual-display` (or `PRODEX_VIRTUAL_DISPLAY=1`, which also covers the MCP server and its auto-recovery) starts an X virtual framebuffer and runs the dedicated Chrome on it. It is a **real headed browser** without a desktop window, not Chrome's headless mode. Login, captcha, rate limits, and permission checks still apply; this mode does not bypass them.
|
|
189
|
+
|
|
190
|
+
Requires `Xvfb` and `xauth` (`sudo apt install -y xvfb x11-xkb-utils xauth`); prodex names the package if they are missing. Linux and WSL only. New displays use Linux abstract Unix sockets, which also work when WSLg mounts `/tmp/.X11-unix` read-only. TCP and filesystem Unix listeners are disabled. A per-display xauth cookie under `~/.local/share/prodex/xvfb/` restricts access to clients holding that credential; `-ac` is never used. The X server outlives the CLI on purpose (the browser runs on it) and is reused by later commands; `PRODEX_VIRTUAL_DISPLAY_NUM` selects the first number to try if `:99` is taken. A setup failure stops the launch or recovery, without falling back to a desktop window.
|
|
180
191
|
|
|
181
|
-
|
|
192
|
+
Existing browsers and legacy TCP X servers are not stopped or migrated automatically. Finish pending consults, stop the dedicated browser and its old X server, then run the updated `prodex pro browser login --virtual-display` to migrate. New launches skip display numbers with an existing TCP listener and record the display number actually used.
|
|
182
193
|
|
|
183
|
-
A browser already running on your desktop cannot be moved onto a virtual display by reusing it
|
|
194
|
+
A browser already running on your desktop cannot be moved onto a virtual display by reusing it. prodex refuses the switch without ending the browser; close it yourself, then rerun with the intended mode. Reinvoking `login` for an already-running virtual browser reuses its saved display identity and does not allocate another display.
|
|
184
195
|
|
|
185
196
|
### Keeping the window, just out of the way
|
|
186
197
|
|
|
@@ -192,10 +203,10 @@ The catch is what "minimized" means to your desktop. Under WSLg a minimized Chro
|
|
|
192
203
|
|
|
193
204
|
`prodex pro browser login --headless` (or `PRODEX_HEADLESS=1`, which also covers the MCP server and its auto-recovery) runs the dedicated browser with no visible window. Two constraints are real, not cosmetic:
|
|
194
205
|
|
|
195
|
-
- **Sign in headed first.** Nobody can log in to a window that does not exist, so headless reuses a profile you already signed into.
|
|
206
|
+
- **Sign in headed first.** Nobody can log in to a window that does not exist, so headless reuses a profile you already signed into. If login, captcha, Cloudflare, permission, or account verification is needed, close the hidden browser yourself and run `prodex pro browser login --headed` for a visible interactive window. Do not merely omit `--headless`: saved modes persist.
|
|
196
207
|
- **One mode at a time.** A single Chrome profile cannot serve a headed and a headless instance simultaneously; close the running one before switching (prodex refuses the switch instead of silently reusing the wrong mode).
|
|
197
208
|
|
|
198
|
-
**Cloudflare is the catch, and it is not theoretical.** Measured on a real signed-in profile: headless Chrome lands on the "Just a moment..." interstitial and stays there past 60 seconds, so ChatGPT never loads. A signed-in profile does not buy a pass — the challenge keys on the headless browser itself. Treat `--headless` as available-but-unproven against ChatGPT:
|
|
209
|
+
**Cloudflare is the catch, and it is not theoretical.** Measured on a real signed-in profile: headless Chrome lands on the "Just a moment..." interstitial and stays there past 60 seconds, so ChatGPT never loads. A signed-in profile does not buy a pass — the challenge keys on the headless browser itself. Treat `--headless` as available-but-unproven against ChatGPT: if `prodex pro browser check` reports the challenge, run `prodex pro browser login --headed` and complete it visibly. prodex does not invoke a hidden API or bypass login/protection. Only the window is optional; the login is not.
|
|
199
210
|
|
|
200
211
|
If a consult finds the browser closed, prodex now relaunches it in the same mode you last used and retries once — including from the MCP server, which has no terminal to prompt in. `PRODEX_NO_AUTO_LOGIN=1` turns that off.
|
|
201
212
|
|
|
@@ -270,7 +281,7 @@ Token-bearing MCP URLs are secrets. They authorize all enabled bridge tools, inc
|
|
|
270
281
|
prodex status --show-token --url-only
|
|
271
282
|
```
|
|
272
283
|
|
|
273
|
-
`status --show-token` requires a token with an expiry, so run `setup --token-ttl-hours <hours>` before asking for a paste-ready URL. The URL token is stored only in `.bridge/config.local.json`, which is ignored by git. Rotate it with `setup`
|
|
284
|
+
`status --show-token` requires a token with an expiry, so run `setup --token-ttl-hours <hours>` before asking for a paste-ready URL. The URL token is stored only in `.bridge/config.local.json`, which is ignored by git. Rotate it with `setup --token-ttl-hours <hours>`, then restart `prodex start` and update client URLs. Plain `setup` preserves the saved token and expiry, and unspecified listener settings are preserved. If you intentionally created a non-expiring token for local-only debugging, `status --show-token` refuses to reveal it unless you also pass `--unsafe-show-non-expiring-token`. `doctor` and `pro browser check` also print `config_warning` when the saved token is non-expiring.
|
|
274
285
|
|
|
275
286
|
After adding the MCP URL to ChatGPT, generate a paste-ready verification prompt:
|
|
276
287
|
|
package/docs/clients.md
CHANGED
|
@@ -48,12 +48,10 @@ consult you expect. Claude Code needs no change: its default stdio tool
|
|
|
48
48
|
timeout is effectively unlimited (~28h) unless you tightened `MCP_TOOL_TIMEOUT`
|
|
49
49
|
or a per-server `"timeout"`.
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
blocker collects the report afterwards - but a client budget that covers the
|
|
56
|
-
run avoids the round trip.
|
|
51
|
+
Keep the client budget longer than the ordinary consult budget. Current
|
|
52
|
+
browser-only builds block `tools: ["deep-research"]` before sending because
|
|
53
|
+
automatic report retrieval depended on an internal API. Run research manually
|
|
54
|
+
in ChatGPT; `pro_recover` only accepts a finished, rendered conversation answer.
|
|
57
55
|
|
|
58
56
|
Approval gate (verified on Codex 0.142.5): Codex asks for per-call approval
|
|
59
57
|
before invoking prodex MCP tools. In interactive `codex` sessions you simply
|
package/docs/http-mcp.md
CHANGED
|
@@ -4,6 +4,8 @@ Use this path when you want a ChatGPT Project to hand tasks back to this local r
|
|
|
4
4
|
|
|
5
5
|
This is not the ChatGPT Pro browser adapter. It does not open ChatGPT, read cookies, or automate a web session. It starts a local HTTP MCP server that exposes the same bridge/repo tools as the Claude stdio server.
|
|
6
6
|
|
|
7
|
+
Generic task, session and result responses redact personal ChatGPT thread metadata, including nested blockers and recovery URLs in diagnostic text. Local CLI records retain that context for recovery. Legacy result artifacts without a recorded hash are readable with a `legacy_artifact_unverified` warning; a signed completion receipt does not verify unhashed artifact bytes.
|
|
8
|
+
|
|
7
9
|
## What It Is
|
|
8
10
|
|
|
9
11
|
`prodex start` runs a local Streamable HTTP MCP server.
|
|
@@ -63,7 +65,9 @@ Equivalent from outside the repo:
|
|
|
63
65
|
prodex setup --cwd /absolute/path/to/your/repo --token-ttl-hours 24
|
|
64
66
|
```
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
Token-bearing MCP URLs are secrets. Keep the renewed URL in your own trusted private MCP client configuration.
|
|
69
|
+
|
|
70
|
+
Expired tokens are rejected by `prodex start` and by the HTTP MCP server. Run `prodex setup --token-ttl-hours <hours>` to rotate the URL, restart `prodex start`, then reconnect clients using `prodex status --show-token --url-only`. Plain `setup` preserves the existing token and expiry; changing the TTL preserves the listener unless `--host` or `--port` is supplied.
|
|
67
71
|
|
|
68
72
|
## Start The Local Server
|
|
69
73
|
|
package/docs/releasing.md
CHANGED
|
@@ -21,7 +21,7 @@ One-time setup (owner, on npmjs.com): open the package → Settings → Trusted
|
|
|
21
21
|
|
|
22
22
|
## Release checks
|
|
23
23
|
|
|
24
|
-
GitHub Actions runs `npm ci`, `npm run build`, `npm run release:check`, and `npm run release:verify` on pushes to `main` and pull requests. The workflow installs `ripgrep` because the repo-search smoke checks require `rg`. It verifies release readiness only; it does not publish anything.
|
|
24
|
+
GitHub Actions runs `npm ci`, `npm run build`, `npm run release:check -- --metadata-only`, and `npm run release:verify` on pushes to `main` and pull requests. The metadata step checks package readiness; the verification step runs the full test and package checks once. The workflow installs `ripgrep` because the repo-search smoke checks require `rg`. It verifies release readiness only; it does not publish anything.
|
|
25
25
|
|
|
26
26
|
Before sharing a package tarball, run:
|
|
27
27
|
|
|
@@ -39,6 +39,8 @@ npm run release:verify
|
|
|
39
39
|
|
|
40
40
|
This runs tests, typecheck, build, package smoke, and `doctor` without weakening the publish guard.
|
|
41
41
|
|
|
42
|
+
Package smoke runs tarball publish dry-runs against an isolated, read-only loopback registry. This keeps repeat verification working after the package version has already been published. It does not establish that a version is available on npm; the separate release dry-run and actual publish still enforce registry readiness. No package is uploaded by the smoke check.
|
|
43
|
+
|
|
42
44
|
If direct `npm pack` is blocked because a WSL/Windows mount reports normal source files as executable, build the publish tarball from a temporary Linux staging directory:
|
|
43
45
|
|
|
44
46
|
```bash
|