@youdie006/prodex 0.16.31 → 0.16.33
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 +1 -1
- package/dist/browser-send-lock.js +1 -1
- package/dist/chatgpt-browser.js +63 -37
- package/dist/cli-args.js +10 -0
- package/dist/cli-help.js +3 -3
- package/dist/cli-pro.js +41 -8
- package/dist/cli.js +4 -1
- package/dist/mcp.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ prodex ask --file src/auth.ts "Review this for security holes"
|
|
|
35
35
|
|
|
36
36
|
`prodex ask` is the short form of `prodex pro browser ask`; the full form and every flag work identically. In an interactive terminal, `login` keeps watching the opened window and tells you exactly which manual step is still missing (log in, clear a check, open a chat) until it reports READY. If you skip `login` and the browser is not running, an interactive `ask` recovers on its own: it launches the dedicated browser, waits for your saved session to be READY, and retries the send once (disable with `--no-auto-login`; scripts opt in with `--auto-login`). While ChatGPT thinks, `prodex` prints progress to stderr (connecting, prompt sent, elapsed seconds while generating), so a multi-minute Pro answer never looks frozen.
|
|
37
37
|
|
|
38
|
-
The answer prints to your terminal and is saved under `.bridge/` for later (`prodex pro latest` re-prints it). Add `--new-chat` to send into a fresh chat (recommended for repeated consults - long threads eventually confuse send detection). For a structured second-opinion debate between your coding agent and GPT Pro, `prodex pro debate-prompt --topic "..."` prints a ready-to-paste orchestration prompt. `prodex` drives the picker you can see and deliberately will not send into a window you cannot watch — but a dedicated Chrome window left non-minimized (even behind your editor) counts as watchable, so it sends quietly in the background without stealing focus. Just don't minimize it or switch that window to another tab. Pin per-repo defaults once - `prodex setup --model Pro --project "your-project"` - so every ask runs Pro (15-minute timeout) inside that project instead of whatever the ChatGPT UI last had selected; list exact sidebar project names with `prodex pro browser projects`. Pass `--file` more than once to attach several files
|
|
38
|
+
The answer prints to your terminal and is saved under `.bridge/` for later (`prodex pro latest` re-prints it). Add `--new-chat` to send into a fresh chat (recommended for repeated consults - long threads eventually confuse send detection). For a structured second-opinion debate between your coding agent and GPT Pro, `prodex pro debate-prompt --topic "..."` prints a ready-to-paste orchestration prompt. `prodex` drives the picker you can see and deliberately will not send into a window you cannot watch — but a dedicated Chrome window left non-minimized (even behind your editor) counts as watchable, so it sends quietly in the background without stealing focus. Just don't minimize it or switch that window to another tab. Pin per-repo defaults once - `prodex setup --model Pro --project "your-project"` - so every ask runs Pro (15-minute timeout) inside that project instead of whatever the ChatGPT UI last had selected; list exact sidebar project names with `prodex pro browser projects`. Pass `--file` more than once to attach several files. When the thread is still generating a previous answer (common right after a timed-out Pro send), the send automatically queues behind it up to the timeout budget; tune that with `--busy-wait-ms` (0 fails fast with a `response_in_progress` blocker). See [First Pro Login](#first-pro-login) for the full flow, and the [FAQ](#faq) if a send stops.
|
|
39
39
|
|
|
40
40
|
## Core Shape
|
|
41
41
|
|
|
@@ -115,7 +115,7 @@ export async function withBrowserSendLock(waitMs, onWait, fn) {
|
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
if (Date.now() >= deadline) {
|
|
118
|
-
throw new Error(`Another prodex browser send is in progress (pid ${holder.pid}).
|
|
118
|
+
throw new Error(`Another prodex browser send is in progress (pid ${holder.pid}) and did not finish within the wait budget. Retry once it finishes, or raise --timeout-ms (which is also the queue budget).`);
|
|
119
119
|
}
|
|
120
120
|
if (!waited) {
|
|
121
121
|
waited = true;
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants,
|
|
2
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import os from "node:os";
|
|
@@ -298,9 +298,11 @@ export function chatGptBusyBlocker(generating) {
|
|
|
298
298
|
return undefined;
|
|
299
299
|
return {
|
|
300
300
|
code: "response_in_progress",
|
|
301
|
-
message: "ChatGPT is still generating a previous response.",
|
|
301
|
+
message: "ChatGPT is still generating a previous response in this thread.",
|
|
302
302
|
retryable: true,
|
|
303
|
-
next_step: "Wait for
|
|
303
|
+
next_step: "Wait for it to finish and retry (pass --busy-wait-ms to queue behind it longer). " +
|
|
304
|
+
"If that in-flight answer is the one you need, fetch it once it settles: `prodex pro browser recover --target-url <thread-url>`. " +
|
|
305
|
+
"A new topic can go to a new chat instead."
|
|
304
306
|
};
|
|
305
307
|
}
|
|
306
308
|
export function isLikelyChatGptSubmitButton(label, dataTestId) {
|
|
@@ -564,6 +566,18 @@ function chatGptPageMissingBlocker() {
|
|
|
564
566
|
next_step: "Open https://chatgpt.com/ in the dedicated Chrome profile, or run `prodex pro browser login` to reopen it."
|
|
565
567
|
};
|
|
566
568
|
}
|
|
569
|
+
// Pre-send gate over a settled page status. Order matters: while ChatGPT
|
|
570
|
+
// streams a response the composer locks (hasComposer reads false), so a busy
|
|
571
|
+
// thread must be diagnosed as response_in_progress BEFORE the composer
|
|
572
|
+
// readiness assert - otherwise it is misreported as "missing a visible prompt
|
|
573
|
+
// composer" (measured live: continue-by-default consults landing on a thread
|
|
574
|
+
// still generating the previous prodex answer).
|
|
575
|
+
export function assertChatGptIdleAndReadyForPrompt(status) {
|
|
576
|
+
const busyBlocker = chatGptBusyBlocker(status.generating);
|
|
577
|
+
if (busyBlocker)
|
|
578
|
+
throw new ChatGptBrowserBlockerError(busyBlocker);
|
|
579
|
+
assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer, status.openDialogText);
|
|
580
|
+
}
|
|
567
581
|
export function assertChatGptReadyForPrompt(loggedInLikely, hasComposer, openDialogText) {
|
|
568
582
|
if (loggedInLikely && hasComposer)
|
|
569
583
|
return;
|
|
@@ -1294,7 +1308,7 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1294
1308
|
code: "browser_unreachable",
|
|
1295
1309
|
message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
|
|
1296
1310
|
retryable: true,
|
|
1297
|
-
next_step: "Run `prodex pro browser login
|
|
1311
|
+
next_step: "Run `prodex pro browser login` to reopen the dedicated window - it reuses the saved session (no manual login unless it expired) and returns immediately when run non-interactively - then retry."
|
|
1298
1312
|
});
|
|
1299
1313
|
}
|
|
1300
1314
|
const cdp = await connectCdp(page.page.webSocketDebuggerUrl);
|
|
@@ -1416,16 +1430,20 @@ export async function sendChatGptPrompt(options) {
|
|
|
1416
1430
|
if (blocker) {
|
|
1417
1431
|
throw new ChatGptBrowserBlockerError(blocker);
|
|
1418
1432
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1433
|
+
// Busy handling must run BEFORE the composer readiness assert: a thread
|
|
1434
|
+
// still generating locks the composer, and asserting composer presence
|
|
1435
|
+
// first misreports that as "missing a visible prompt composer" (see
|
|
1436
|
+
// assertChatGptIdleAndReadyForPrompt). Default the queue budget to the send
|
|
1437
|
+
// timeout: consults continue threads by default, so landing on a thread
|
|
1438
|
+
// whose previous (often timed-out Pro) answer is still streaming is a when,
|
|
1439
|
+
// not an if - queueing behind it beats failing.
|
|
1423
1440
|
let busyBlocker = chatGptBusyBlocker(status.generating);
|
|
1424
|
-
|
|
1441
|
+
const busyWaitBudgetMs = options.busyWaitMs ?? timeoutMs;
|
|
1442
|
+
if (busyBlocker && busyWaitBudgetMs > 0) {
|
|
1425
1443
|
// Queue behind the in-flight response instead of failing: shared-tab
|
|
1426
1444
|
// contention (another agent or the user mid-generation) is a when, not an
|
|
1427
1445
|
// if. Bounded, and a mid-wait page blocker (usage limit etc.) still throws.
|
|
1428
|
-
const busyDeadline = Date.now() +
|
|
1446
|
+
const busyDeadline = Date.now() + busyWaitBudgetMs;
|
|
1429
1447
|
emitProgress("waiting", "tab busy with another response; waiting");
|
|
1430
1448
|
while (busyBlocker && Date.now() < busyDeadline) {
|
|
1431
1449
|
await sleep(3_000);
|
|
@@ -1437,10 +1455,16 @@ export async function sendChatGptPrompt(options) {
|
|
|
1437
1455
|
if (busyBlocker)
|
|
1438
1456
|
emitProgress("waiting", "tab busy with another response; waiting");
|
|
1439
1457
|
}
|
|
1458
|
+
if (!busyBlocker) {
|
|
1459
|
+
// The composer takes a moment to unlock after generation ends; settle
|
|
1460
|
+
// again so the readiness assert below sees the reopened composer.
|
|
1461
|
+
status = await readSettledChatGptPageStatus(page);
|
|
1462
|
+
}
|
|
1440
1463
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1464
|
+
assertChatGptIdleAndReadyForPrompt(status);
|
|
1465
|
+
if (normalizedTargetUrl)
|
|
1466
|
+
assertChatGptTargetUrlMatches(status.url, normalizedTargetUrl);
|
|
1467
|
+
assertVisibleChatGptTab(status.visibilityState, status.url, normalizedTargetUrl);
|
|
1444
1468
|
emitProgress("tab_ready");
|
|
1445
1469
|
// Progress details deliberately avoid project names (receipts redact them too).
|
|
1446
1470
|
const selectionSummary = [
|
|
@@ -1766,7 +1790,7 @@ async function findChatGptPage(port, timeoutMs, targetUrl) {
|
|
|
1766
1790
|
code: "browser_unreachable",
|
|
1767
1791
|
message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
|
|
1768
1792
|
retryable: true,
|
|
1769
|
-
next_step: "Run `prodex pro browser login
|
|
1793
|
+
next_step: "Run `prodex pro browser login` to reopen the dedicated window - it reuses the saved session (no manual login unless it expired) and returns immediately when run non-interactively - then retry.",
|
|
1770
1794
|
...(error instanceof Error ? { detail: error.message } : {})
|
|
1771
1795
|
}
|
|
1772
1796
|
};
|
|
@@ -2205,37 +2229,23 @@ function win32ChromePaths(env) {
|
|
|
2205
2229
|
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
2206
2230
|
];
|
|
2207
2231
|
}
|
|
2208
|
-
const WSL_WINDOWS_CHROME_PATHS = [
|
|
2209
|
-
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
2210
|
-
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
2211
|
-
"/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe",
|
|
2212
|
-
"/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
2213
|
-
];
|
|
2214
|
-
function kernelLooksLikeWsl() {
|
|
2215
|
-
try {
|
|
2216
|
-
return /microsoft/i.test(readFileSync("/proc/version", "utf8"));
|
|
2217
|
-
}
|
|
2218
|
-
catch {
|
|
2219
|
-
return false;
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
2232
|
/**
|
|
2223
2233
|
* Ordered browser candidates for the current platform: PATH binary names
|
|
2224
2234
|
* first, then well-known absolute install locations (macOS app bundles,
|
|
2225
|
-
* Windows Program Files/LOCALAPPDATA
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2235
|
+
* Windows Program Files/LOCALAPPDATA). Windows-host browsers are deliberately
|
|
2236
|
+
* NOT candidates under WSL: auto-selecting a /mnt/c chrome.exe/msedge.exe
|
|
2237
|
+
* either opened a blank window (the old --version probe - Windows browsers
|
|
2238
|
+
* treat --version as a launch) or launched the user's Windows browser with a
|
|
2239
|
+
* Linux profile path, both measured live as the recurring transient
|
|
2240
|
+
* Edge+Chrome window spam. Under WSL the dedicated browser is a Linux chrome;
|
|
2241
|
+
* a Windows browser is opt-in via PRODEX_CHROME only.
|
|
2229
2242
|
*/
|
|
2230
|
-
export function chromeCommandCandidates(platform = process.platform, env = process.env
|
|
2243
|
+
export function chromeCommandCandidates(platform = process.platform, env = process.env) {
|
|
2231
2244
|
const candidates = [...CHROME_PATH_BINARY_NAMES];
|
|
2232
2245
|
if (platform === "darwin")
|
|
2233
2246
|
candidates.push(...DARWIN_CHROME_PATHS);
|
|
2234
2247
|
if (platform === "win32")
|
|
2235
2248
|
candidates.push(...win32ChromePaths(env));
|
|
2236
|
-
if (platform === "linux" && (env.WSL_DISTRO_NAME || env.WSL_INTEROP || isWsl())) {
|
|
2237
|
-
candidates.push(...WSL_WINDOWS_CHROME_PATHS);
|
|
2238
|
-
}
|
|
2239
2249
|
return candidates;
|
|
2240
2250
|
}
|
|
2241
2251
|
function resolveChromeCommand() {
|
|
@@ -2292,10 +2302,26 @@ function assertChromeLikeVersion(command, label) {
|
|
|
2292
2302
|
throw new Error(`${label} must point to a Chrome/Chromium-compatible browser executable: ${command}`);
|
|
2293
2303
|
}
|
|
2294
2304
|
}
|
|
2305
|
+
// Windows chrome.exe/msedge.exe do not implement a console --version: they
|
|
2306
|
+
// treat it as a normal launch and open a visible blank window. Execing them to
|
|
2307
|
+
// probe was the source of the recurring transient Edge+Chrome window pairs on
|
|
2308
|
+
// WSL (trap-logged live: `cmd=msedge.exe --version parent=wslhost.exe`): under
|
|
2309
|
+
// system load the `google-chrome --version` probe exceeded its old 3s timeout,
|
|
2310
|
+
// the candidate walk fell through to the /mnt/c .exe paths, and each probe
|
|
2311
|
+
// spawned a blank window. The .exe candidates are fixed known install paths,
|
|
2312
|
+
// so file existence (checked by every caller) is the validation - never exec.
|
|
2313
|
+
export function isWindowsBrowserExecutablePath(command) {
|
|
2314
|
+
return /\.exe$/i.test(command);
|
|
2315
|
+
}
|
|
2295
2316
|
function hasChromeLikeVersion(command) {
|
|
2317
|
+
if (isWindowsBrowserExecutablePath(command))
|
|
2318
|
+
return true;
|
|
2319
|
+
// 10s, not 3s: a loaded machine (e.g. a parallel test suite) can stall a
|
|
2320
|
+
// cold `google-chrome --version` past 3s, and a false negative here used to
|
|
2321
|
+
// cascade into the Windows .exe candidates above.
|
|
2296
2322
|
const result = spawnSync(command, ["--version"], {
|
|
2297
2323
|
encoding: "utf8",
|
|
2298
|
-
timeout:
|
|
2324
|
+
timeout: 10_000,
|
|
2299
2325
|
maxBuffer: 1024 * 1024
|
|
2300
2326
|
});
|
|
2301
2327
|
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
package/dist/cli-args.js
CHANGED
|
@@ -136,6 +136,16 @@ export function readPositiveIntegerFlag(args, flag) {
|
|
|
136
136
|
throw new Error(`${flag} must be a positive integer`);
|
|
137
137
|
return value;
|
|
138
138
|
}
|
|
139
|
+
export function readNonNegativeIntegerFlag(args, flag) {
|
|
140
|
+
// Like readPositiveIntegerFlag, but 0 is a meaningful opt-out (e.g.
|
|
141
|
+
// --busy-wait-ms 0 = fail fast instead of queueing behind a busy thread).
|
|
142
|
+
const value = readNumberFlag(args, flag);
|
|
143
|
+
if (value === undefined)
|
|
144
|
+
return undefined;
|
|
145
|
+
if (!Number.isInteger(value) || value < 0)
|
|
146
|
+
throw new Error(`${flag} must be a non-negative integer`);
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
139
149
|
export function readPortFlag(args, flag) {
|
|
140
150
|
const value = readNumberFlag(args, flag);
|
|
141
151
|
if (value === undefined)
|
package/dist/cli-help.js
CHANGED
|
@@ -26,7 +26,7 @@ Ask / consult commands:
|
|
|
26
26
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only list of model menu options
|
|
27
27
|
prodex pro browser projects [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only list of sidebar project names (for --project)
|
|
28
28
|
prodex pro browser recover [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # recover a finished answer from a thread whose send timed out
|
|
29
|
-
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms
|
|
29
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
30
30
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
31
31
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
32
32
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
@@ -252,8 +252,8 @@ export function printProBrowserHelp(stdout, sourceCli) {
|
|
|
252
252
|
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
253
253
|
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"]';
|
|
254
254
|
const askUsage = sourceCli
|
|
255
|
-
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms
|
|
256
|
-
: `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms
|
|
255
|
+
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`
|
|
256
|
+
: `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`;
|
|
257
257
|
const modelsUsage = sourceCli
|
|
258
258
|
? `${cli} pro browser models${sourceCliOption} [--port 9333] [--timeout-ms 15000]`
|
|
259
259
|
: "prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000]";
|
package/dist/cli-pro.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildDryRunBundle } from "./bundle.js";
|
|
4
4
|
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, readLastBrowserLoginLaunch, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
|
-
import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
|
|
5
|
+
import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
|
|
6
6
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
7
7
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
8
8
|
import { formatBrowserDefaults, redactServerUrl } from "./cli-server.js";
|
|
@@ -277,8 +277,7 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
if (browserSubcommand === "open" || browserSubcommand === "status" || browserSubcommand === "doctor") {
|
|
280
|
-
|
|
281
|
-
throw new Error(`Use \`prodex pro browser ${replacement}\` for explicit browser automation.`);
|
|
280
|
+
throw new Error(`Use \`prodex pro browser ${legacyBrowserSubcommandReplacement(browserSubcommand)}\` for explicit browser automation.`);
|
|
282
281
|
}
|
|
283
282
|
if (browserSubcommand === "smoke") {
|
|
284
283
|
if (printProBrowserHelpIfRequested(browserArgs, "pro browser smoke", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--source-cli"] }))
|
|
@@ -426,7 +425,10 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
426
425
|
throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models", "projects", "recover"]);
|
|
427
426
|
}
|
|
428
427
|
if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
|
|
429
|
-
|
|
428
|
+
// Point at a subcommand that EXISTS: `pro status` used to say "use `pro
|
|
429
|
+
// browser status`", which itself errors with "use `pro browser check`" -
|
|
430
|
+
// a two-hop dead end that made an agent abandon the diagnosis.
|
|
431
|
+
throw new Error(`Use \`prodex pro browser ${legacyBrowserSubcommandReplacement(subcommand)}\` for explicit browser automation.`);
|
|
430
432
|
}
|
|
431
433
|
if (subcommand === "list") {
|
|
432
434
|
if (printHelpIfRequested(proArgs, "pro list", io.stdout, printProHelp, { valueFlags: ["--cwd", "--source-cli"] }))
|
|
@@ -531,6 +533,16 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
531
533
|
export async function runConsultsCommand(rest, io) {
|
|
532
534
|
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
533
535
|
}
|
|
536
|
+
// Retired browser subcommands map to the one that replaced them. Every value
|
|
537
|
+
// here must be a subcommand that actually exists, so the error is a single hop
|
|
538
|
+
// to a runnable command.
|
|
539
|
+
function legacyBrowserSubcommandReplacement(subcommand) {
|
|
540
|
+
if (subcommand === "open")
|
|
541
|
+
return "login";
|
|
542
|
+
if (subcommand === "smoke")
|
|
543
|
+
return "smoke";
|
|
544
|
+
return "check";
|
|
545
|
+
}
|
|
534
546
|
// File marker + human-pacing gate for visible-browser sends. Reads the previous
|
|
535
547
|
// send's start time from .bridge/last-browser-send, waits out any remaining
|
|
536
548
|
// minimum interval (auto-throttle, not an error), then stamps the new send.
|
|
@@ -692,7 +704,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
692
704
|
...(selectionEffort ? { effort: selectionEffort } : {})
|
|
693
705
|
};
|
|
694
706
|
const browserPort = hasSendMode ? resolveCdpPort(readPortFlag(parsedAskPro.optionArgs, "--port")) : undefined;
|
|
695
|
-
const busyWaitMs =
|
|
707
|
+
const busyWaitMs = readNonNegativeIntegerFlag(parsedAskPro.optionArgs, "--busy-wait-ms");
|
|
696
708
|
// Pro extended can legitimately think for minutes, so its default timeout is
|
|
697
709
|
// higher; an explicit --timeout-ms always wins.
|
|
698
710
|
// Pro reasoning routinely runs for many minutes (a real consult measured
|
|
@@ -702,7 +714,11 @@ export async function runAskProCommand(rest, io) {
|
|
|
702
714
|
const effectiveProSelection = selectionProMode !== undefined || (selectionModel !== undefined && /pro/i.test(selectionModel));
|
|
703
715
|
// Pro reasoning routinely runs 6-20 minutes; 15 min was still cutting long
|
|
704
716
|
// answers off (field report), so a Pro selection defaults to 20 minutes.
|
|
705
|
-
|
|
717
|
+
// With NO model selection at all (no flag, no saved default) the UI's
|
|
718
|
+
// current model is unknown and may be Pro, so the floor is 5 minutes: the
|
|
719
|
+
// old 90s guess made repos without saved defaults time out on ordinary
|
|
720
|
+
// consults (observed in several field sessions).
|
|
721
|
+
const defaultBrowserTimeoutMs = effectiveProSelection ? 1_200_000 : 300_000;
|
|
706
722
|
const browserTimeoutMs = hasSendMode
|
|
707
723
|
? (readPositiveIntegerFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultBrowserTimeoutMs)
|
|
708
724
|
: undefined;
|
|
@@ -759,7 +775,12 @@ export async function runAskProCommand(rest, io) {
|
|
|
759
775
|
}
|
|
760
776
|
throw new Error(formatBlockedConsultRecordedMessage(blocker.message, task.id, sourceCli, { cwd: targetCwd }));
|
|
761
777
|
}
|
|
762
|
-
|
|
778
|
+
// Queue behind another prodex send by default (same budget as the send
|
|
779
|
+
// itself). MCP consults cannot pass --busy-wait-ms at all, so the old
|
|
780
|
+
// fail-fast default made pro_consult die instantly with advice the
|
|
781
|
+
// caller had no way to follow; --busy-wait-ms 0 opts back into failing
|
|
782
|
+
// fast. A dead holder is still reaped immediately.
|
|
783
|
+
const sendOnce = () => withBrowserSendLock(busyWaitMs ?? browserTimeoutMs ?? 0, (detail) => io.stderr(`progress: ${detail}`), () => sendChatGptPrompt({
|
|
763
784
|
port: browserPort,
|
|
764
785
|
prompt: bundle.text,
|
|
765
786
|
targetUrl: normalizedTargetUrl,
|
|
@@ -1366,7 +1387,10 @@ export async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
|
1366
1387
|
}
|
|
1367
1388
|
catch (error) {
|
|
1368
1389
|
if (isMissingFileError(error)) {
|
|
1369
|
-
|
|
1390
|
+
// Name it optional: browser consults need no config at all, but agents
|
|
1391
|
+
// reading "config: missing" in an otherwise healthy check concluded
|
|
1392
|
+
// prodex was unconfigured and went chasing `setup` (field sessions).
|
|
1393
|
+
io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\` (optional - only the HTTP MCP surface needs it; browser consults work without it)`);
|
|
1370
1394
|
}
|
|
1371
1395
|
else {
|
|
1372
1396
|
io.stdout(`config: failed ${sourceAwareSetupMessage(errorMessage(error), sourceCli, { cwd: setupHintCwd })}`);
|
|
@@ -1412,6 +1436,15 @@ export async function printProductCheck(store, io, args, configCwd = io.cwd) {
|
|
|
1412
1436
|
if (nextStep)
|
|
1413
1437
|
io.stdout(`next: ${nextStep}`);
|
|
1414
1438
|
}
|
|
1439
|
+
else if (browserStatus.blocker?.code === "response_in_progress") {
|
|
1440
|
+
// Busy is not broken: the session is healthy and answering, and sends
|
|
1441
|
+
// queue behind it automatically. Reporting it as "blocked" made agents
|
|
1442
|
+
// conclude the browser was down and start relaunching/logging in (field
|
|
1443
|
+
// failure, observed across several repos).
|
|
1444
|
+
io.stdout(`chatgpt: busy ${browserStatus.blocker.code} - ${browserStatus.blocker.message}`);
|
|
1445
|
+
io.stdout("next: No action needed - a send queues behind it automatically (--busy-wait-ms 0 fails fast instead).");
|
|
1446
|
+
chatgptReady = true;
|
|
1447
|
+
}
|
|
1415
1448
|
else if (browserStatus.blocker) {
|
|
1416
1449
|
const visibilityText = browserStatus.blocker.code === "tab_not_visible" ? ` visibility=${browserStatus.visibilityState ?? "unknown"}` : "";
|
|
1417
1450
|
io.stdout(`chatgpt: blocked ${browserStatus.blocker.code}${visibilityText} - ${browserStatus.blocker.message}`);
|
package/dist/cli.js
CHANGED
|
@@ -929,7 +929,10 @@ async function runDoctor(store, io, sourceCli, setupHintCwd) {
|
|
|
929
929
|
}
|
|
930
930
|
catch (error) {
|
|
931
931
|
if (isMissingFileError(error)) {
|
|
932
|
-
|
|
932
|
+
// Name it optional: browser consults need no config at all, but agents
|
|
933
|
+
// reading "config: missing" in an otherwise healthy check concluded
|
|
934
|
+
// prodex was unconfigured and went chasing `setup` (field sessions).
|
|
935
|
+
io.stdout(`config: missing - run \`${formatSetupCommand(sourceCli, { cwd: setupHintCwd })}\` (optional - only the HTTP MCP surface needs it; browser consults work without it)`);
|
|
933
936
|
}
|
|
934
937
|
else {
|
|
935
938
|
ok = false;
|
package/dist/mcp.js
CHANGED
|
@@ -138,11 +138,14 @@ export function createServer(cwd = process.cwd(), options = {}) {
|
|
|
138
138
|
const browserConsult = options.browserConsult;
|
|
139
139
|
if (browserConsult) {
|
|
140
140
|
server.registerTool("pro_consult", {
|
|
141
|
-
description: "Ask the user's logged-in ChatGPT (Pro) in the visible browser and wait for the full answer. This drives a real browser send: it can take minutes (Pro extended reasoning), is human-paced, and records a durable receipt under .bridge/. Requires a running `prodex pro browser login` session. By DEFAULT the consult continues in the currently-open thread, so consecutive follow-ups on the same topic stay in one conversation (keeps context, avoids sidebar clutter). Pass new_chat:true ONLY to start a fresh thread for a genuinely new topic. `project` and `model` come from saved defaults (per-repo config, or PRODEX_DEFAULT_PROJECT / PRODEX_DEFAULT_MODEL env vars) when omitted - do NOT pass them per-call unless deliberately overriding. Returns task_id, thread URL, and the answer text.",
|
|
141
|
+
description: "Ask the user's logged-in ChatGPT (Pro) in the visible browser and wait for the full answer. This drives a real browser send: it can take minutes (Pro extended reasoning), is human-paced, and records a durable receipt under .bridge/. Requires a running `prodex pro browser login` session. By DEFAULT the consult continues in the currently-open thread, so consecutive follow-ups on the same topic stay in one conversation (keeps context, avoids sidebar clutter). Pass new_chat:true ONLY to start a fresh thread for a genuinely new topic. If the thread is still generating a previous answer, the send automatically queues behind it (up to the timeout budget) - long 'tab busy' progress is normal, not stuck. `project` and `model` come from saved defaults (per-repo config, or PRODEX_DEFAULT_PROJECT / PRODEX_DEFAULT_MODEL env vars) when omitted - do NOT pass them per-call unless deliberately overriding. Returns task_id, thread URL, and the answer text.",
|
|
142
142
|
inputSchema: {
|
|
143
143
|
prompt: McpBridgeTextSchema.min(1),
|
|
144
144
|
model: McpShortTextSchema.optional(),
|
|
145
|
-
pro_mode:
|
|
145
|
+
// pro_mode is deliberately NOT advertised: ChatGPT's 2026-07 update
|
|
146
|
+
// removed Pro sub-modes, and agents that saw the field passed
|
|
147
|
+
// nonsense ("true") and got a hard validation error instead of an
|
|
148
|
+
// answer. Unknown keys are stripped, so a stale caller still works.
|
|
146
149
|
effort: McpShortTextSchema.optional(),
|
|
147
150
|
project: McpShortTextSchema.optional(),
|
|
148
151
|
timeout_ms: z.number().int().positive().max(3_600_000).optional(),
|