@youdie006/prodex 0.39.0 → 0.39.2
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 +161 -521
- package/dist/chatgpt-browser.js +494 -49
- package/dist/cli-help.js +10 -10
- package/dist/cli-pro.js +50 -7
- package/dist/cli-server.js +33 -1
- package/dist/mcp.js +1 -1
- package/dist/picker-interaction.js +112 -0
- package/docs/cli-reference.md +380 -0
- package/docs/releasing.md +83 -0
- package/package.json +3 -1
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 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
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] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--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] [--json]
|
|
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] [--json]
|
|
@@ -52,7 +52,7 @@ Bridge ledger (durable tasks/results/receipts/sessions under .bridge/):
|
|
|
52
52
|
|
|
53
53
|
Agent / MCP integration:
|
|
54
54
|
prodex mcp [--cwd /absolute/path/to/repo]
|
|
55
|
-
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
|
|
55
|
+
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
|
|
56
56
|
prodex start [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
|
|
57
57
|
prodex status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] [--show-token] [--url-only] [--unsafe-show-non-expiring-token]
|
|
58
58
|
prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
|
|
@@ -80,14 +80,14 @@ export function printSetupHelp(stdout) {
|
|
|
80
80
|
stdout(`prodex setup
|
|
81
81
|
|
|
82
82
|
Commands:
|
|
83
|
-
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
|
|
83
|
+
prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
|
|
84
84
|
|
|
85
85
|
Save a loopback-only HTTP MCP profile in .bridge/config.local.json. Use --token-ttl-hours before tunnels or ChatGPT Project use.
|
|
86
86
|
|
|
87
87
|
Optional visible-browser send defaults (applied by \`pro browser ask\` when the matching per-ask flag is omitted):
|
|
88
|
-
--model Composer model
|
|
88
|
+
--model Composer model by its exact menu label. Only Pro applies on the current picker: it is a slider step, and the model rows cannot be clicked
|
|
89
89
|
--pro-mode Pro sub-mode: 기본 (standard) or 확장 (extended)
|
|
90
|
-
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음
|
|
90
|
+
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음 / Max / Ultra / Pro. Max and Ultra are rungs of ChatGPT's Work surface and only apply when the browser is already on Work (prodex does not switch to Work for them); every other value is sent on Chat, whose top step is Pro
|
|
91
91
|
--project Sidebar project to enter before sending
|
|
92
92
|
Clear a saved default with --clear-model / --clear-pro-mode / --clear-effort / --clear-project.
|
|
93
93
|
--pro-mode and --effort are different model axes and cannot be combined. View saved defaults with \`prodex status\`.`);
|
|
@@ -168,7 +168,7 @@ Commands:
|
|
|
168
168
|
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
169
169
|
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
170
170
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
|
|
171
|
-
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name" | --project-new "name"] "prompt"
|
|
171
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"] "prompt"
|
|
172
172
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
173
173
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
174
174
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
@@ -179,7 +179,7 @@ Use \`prodex pro browser ask\` only when you want an explicit visible-browser se
|
|
|
179
179
|
Model/project selection (visible-browser send):
|
|
180
180
|
--model "label" Pick the composer model by its exact menu label (verified: Pro). Submenu models (e.g. the GPT-5.6 Sol variants) are rejected for now.
|
|
181
181
|
--pro-mode 기본 | 확장 Pro sub-mode (only when the model is Pro); a Pro selection raises the default timeout to 1200000 ms
|
|
182
|
-
--effort 즉시|중간|높음|매우
|
|
182
|
+
--effort 즉시|중간|높음|매우 높음|Max|Ultra|Pro Reasoning effort (aliases: instant/light, medium, high, extrahigh/max, ultra); picking one moves the single power slider
|
|
183
183
|
--project "name" Enter an existing sidebar project first (cannot combine with --target-url)
|
|
184
184
|
Labels are matched in both the Korean and English (US) UI; run \`prodex pro browser models\` to list what your account shows.
|
|
185
185
|
Persist defaults with \`prodex setup --model/--pro-mode/--effort/--project\`; clear them with setup --clear-model/--clear-pro-mode/--clear-effort/--clear-project.
|
|
@@ -253,7 +253,7 @@ export function printProBrowserHelp(stdout, sourceCli) {
|
|
|
253
253
|
const smokeUsage = sourceCli
|
|
254
254
|
? `${cli} pro browser smoke${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]`
|
|
255
255
|
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
256
|
-
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name" | --project-new "name"]';
|
|
256
|
+
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Max|Ultra|Pro] [--project "name" | --project-new "name"]';
|
|
257
257
|
const askUsage = sourceCli
|
|
258
258
|
? `${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] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`
|
|
259
259
|
: `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] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`;
|
|
@@ -299,9 +299,9 @@ Commands:
|
|
|
299
299
|
|
|
300
300
|
Visible-browser sends require a manual browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers, plus response_choice_pending when ChatGPT is waiting for you to pick which of two answers you prefer.
|
|
301
301
|
Model/project selection (ask):
|
|
302
|
-
--model Composer model
|
|
302
|
+
--model Composer model by its exact menu label. On the current picker only Pro applies - it is a step of the power slider, while the model rows carry pointer-events: none and no coordinate reaches them, so any other name warns and the send goes out on whatever the composer already had. Models whose menu entry opens a submenu of variants are rejected with a clear error for now.
|
|
303
303
|
--pro-mode Pro sub-mode: 기본 (standard) or 확장 (extended), used when the model is Pro. A Pro selection raises the default --timeout-ms to 1200000.
|
|
304
|
-
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음 (aliases: instant/medium
|
|
304
|
+
--effort Reasoning effort: 즉시 / 중간 / 높음 / 매우 높음 / Max / Ultra / Pro (aliases: instant/light, medium, high, extrahigh/max, ultra). One power slider sets the model and the effort together, so picking an effort moves off Pro. Max and Ultra are rungs of the Work surface and only apply when the browser is already on Work; everything else is sent on Chat.
|
|
305
305
|
--project Enter an existing sidebar project before sending. Cannot be combined with --target-url.
|
|
306
306
|
--pro-mode and --effort cannot be combined. Labels are matched in both the Korean and English (US) ChatGPT UI (e.g. 높음/High, Pro 확장/Pro Extended).
|
|
307
307
|
Run \`${cli} pro browser models${sourceCliOption}\` to list the labels your account currently shows.
|
package/dist/cli-pro.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDryRunBundle } from "./bundle.js";
|
|
5
|
-
import { DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt, statusMeansBrowserDead } from "./chatgpt-browser.js";
|
|
6
6
|
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";
|
|
7
7
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
8
8
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
@@ -429,10 +429,14 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
429
429
|
const modelsResolvedPort = resolveCdpPort(modelsPort);
|
|
430
430
|
let listed;
|
|
431
431
|
try {
|
|
432
|
-
|
|
432
|
+
// Reading the ladder WALKS the slider, so it must not interleave with
|
|
433
|
+
// a send: two processes driving one browser is how a listing ended up
|
|
434
|
+
// changing the model a consult was about to use. Waits for a send in
|
|
435
|
+
// flight rather than barging in.
|
|
436
|
+
listed = await withBrowserSendLock(modelsTimeoutMs ?? 60_000, (detail) => io.stderr(`progress: ${detail}`), () => listChatGptModelOptions({
|
|
433
437
|
port: modelsPort,
|
|
434
438
|
timeoutMs: modelsTimeoutMs
|
|
435
|
-
});
|
|
439
|
+
}));
|
|
436
440
|
}
|
|
437
441
|
catch (error) {
|
|
438
442
|
// Keep the next step actionable for a custom port: the raw blocker
|
|
@@ -443,6 +447,9 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
443
447
|
});
|
|
444
448
|
throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
|
|
445
449
|
}
|
|
450
|
+
if (listed.surface) {
|
|
451
|
+
io.stdout(`Read from ChatGPT's ${listed.surface} surface. Chat and Work have different pickers - only one of them offers Pro.`);
|
|
452
|
+
}
|
|
446
453
|
io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
|
|
447
454
|
for (const option of listed.options) {
|
|
448
455
|
io.stdout(formatModelMenuOption(option));
|
|
@@ -525,7 +532,12 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
525
532
|
const recoverResolvedPort = resolveCdpPort(recoverPort);
|
|
526
533
|
let consult;
|
|
527
534
|
try {
|
|
528
|
-
|
|
535
|
+
// Recovery NAVIGATES the tab to the thread it is reading. Doing that
|
|
536
|
+
// under a send in flight walks the browser off the conversation that
|
|
537
|
+
// send is streaming into, which is the consult it would destroy - and
|
|
538
|
+
// recovery is exactly what gets run after a send appears to hang, so
|
|
539
|
+
// the two meeting is a when rather than an if.
|
|
540
|
+
consult = await withBrowserSendLock(recoverTimeoutMs ?? 60_000, (detail) => io.stderr(`progress: ${detail}`), () => recoverChatGptAnswerFromThread({ port: recoverPort, targetUrl, timeoutMs: recoverTimeoutMs }));
|
|
529
541
|
}
|
|
530
542
|
catch (error) {
|
|
531
543
|
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), recoverSourceCli, {
|
|
@@ -1206,12 +1218,16 @@ export async function runAskProCommand(rest, io) {
|
|
|
1206
1218
|
const firstBlocker = browserSendBlockerFromError(error);
|
|
1207
1219
|
if (firstBlocker.code !== "browser_unreachable" || !autoLoginAllowed)
|
|
1208
1220
|
throw error;
|
|
1221
|
+
const recoveryNotes = [];
|
|
1209
1222
|
const recovered = await attemptBrowserAutoRecovery(io.stderr, {
|
|
1210
|
-
...(browserPort !== undefined ? { port: browserPort } : {})
|
|
1223
|
+
...(browserPort !== undefined ? { port: browserPort } : {}),
|
|
1224
|
+
notes: recoveryNotes
|
|
1211
1225
|
});
|
|
1212
1226
|
if (!recovered)
|
|
1213
1227
|
throw error;
|
|
1214
1228
|
consult = await sendOnce();
|
|
1229
|
+
if (recoveryNotes.length > 0)
|
|
1230
|
+
consult = { ...consult, warnings: [...recoveryNotes, ...consult.warnings] };
|
|
1215
1231
|
}
|
|
1216
1232
|
}
|
|
1217
1233
|
catch (error) {
|
|
@@ -1524,7 +1540,9 @@ async function confirmBrowserSilence(port, probes = 3, gapMs = 2_000) {
|
|
|
1524
1540
|
if (attempt > 0)
|
|
1525
1541
|
await sleep(gapMs);
|
|
1526
1542
|
const status = await getChatGptBrowserStatus({ ...(port !== undefined ? { port } : {}), timeoutMs: 2_000 });
|
|
1527
|
-
|
|
1543
|
+
// Slow is not silent. Three 2s timeouts on a loaded machine used to read as
|
|
1544
|
+
// a dead browser, and what followed was SIGTERM to a live one mid-answer.
|
|
1545
|
+
if (!statusMeansBrowserDead(status))
|
|
1528
1546
|
return false;
|
|
1529
1547
|
}
|
|
1530
1548
|
return true;
|
|
@@ -1565,6 +1583,9 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1565
1583
|
return false;
|
|
1566
1584
|
}
|
|
1567
1585
|
stderr(`recover: the browser stopped answering; ending it (pid ${wedged.join(", ")}) and starting a fresh one...`);
|
|
1586
|
+
// Ending someone's browser is not a progress line to scroll past: it goes on
|
|
1587
|
+
// the receipt, where an agent or a person reading `pro latest` will see it.
|
|
1588
|
+
options.notes?.push(`browser_recovered: the dedicated browser stopped answering its control port and prodex ended it (pid ${wedged.join(", ")}) and started a fresh one before sending. Anything it was doing at the time is gone; the profile and login were kept.`);
|
|
1568
1589
|
await endWedgedBrowser(wedged);
|
|
1569
1590
|
// Wait for the profile lock to actually clear rather than guessing at a
|
|
1570
1591
|
// delay: the replacement launch fails outright if the old process still
|
|
@@ -1738,13 +1759,35 @@ export function browserSendBlockerFromError(error) {
|
|
|
1738
1759
|
// A CDP command timeout means the page's renderer stalled, which in the
|
|
1739
1760
|
// field means a very long thread (or a long prompt landing on one). Generic
|
|
1740
1761
|
// "resolve the browser issue manually" advice gave the caller nothing to do.
|
|
1762
|
+
// The browser lock refusing is not a browser issue: another prodex send owns
|
|
1763
|
+
// the window. Sending people to "resolve the visible browser issue" for it
|
|
1764
|
+
// pointed at a problem that did not exist.
|
|
1765
|
+
if (/Another prodex browser send is in progress/.test(message)) {
|
|
1766
|
+
return {
|
|
1767
|
+
code: "browser_busy",
|
|
1768
|
+
message,
|
|
1769
|
+
retryable: true,
|
|
1770
|
+
next_step: "Another prodex send holds the browser. Wait for it to finish and retry, or pass a longer --timeout-ms, which is also the queue budget."
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
// The tab's DevTools connection went away mid-send with no timeout behind
|
|
1774
|
+
// it: the tab was closed, navigated from outside, or the browser exited.
|
|
1775
|
+
if (/Chrome DevTools websocket (closed|is not open)/.test(message)) {
|
|
1776
|
+
return {
|
|
1777
|
+
code: "browser_connection_lost",
|
|
1778
|
+
message,
|
|
1779
|
+
retryable: true,
|
|
1780
|
+
next_step: "The connection to the ChatGPT tab closed while prodex was using it - the tab was closed or navigated by someone else, or the browser exited. Check the visible window and retry; `prodex pro browser login` reopens it."
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1741
1783
|
const cdpTimeout = message.match(/Chrome DevTools command timed out: (\S+)/);
|
|
1742
1784
|
if (cdpTimeout) {
|
|
1743
1785
|
return {
|
|
1744
1786
|
code: "browser_cdp_timeout",
|
|
1745
1787
|
message,
|
|
1746
1788
|
retryable: true,
|
|
1747
|
-
next_step: "The ChatGPT tab stopped responding
|
|
1789
|
+
next_step: "The ChatGPT tab stopped responding. A very long thread does it, and so does an open JavaScript dialog - that one halts the page outright, and no retry gets past it because the browser will not let a late client dismiss it. " +
|
|
1790
|
+
"Look at the visible window and close any dialog sitting on it, or reopen the window with `prodex pro browser login`. For a heavy thread, retry with `--new-chat` for a fresh, light one."
|
|
1748
1791
|
};
|
|
1749
1792
|
}
|
|
1750
1793
|
return {
|
package/dist/cli-server.js
CHANGED
|
@@ -63,10 +63,42 @@ export async function runSetupCommand(rest, io) {
|
|
|
63
63
|
}
|
|
64
64
|
return 0;
|
|
65
65
|
}
|
|
66
|
+
/** A best-effort warning must not outlast the command it decorates. */
|
|
67
|
+
const PINNED_SELECTION_CHECK_BUDGET_MS = 15_000;
|
|
66
68
|
/** What the picker currently offers, compared against what was just pinned. */
|
|
67
69
|
async function pinnedSelectionCheck(defaults) {
|
|
68
70
|
const { listChatGptModelOptions, pinnedSelectionWarning } = await import("./chatgpt-browser.js");
|
|
69
|
-
|
|
71
|
+
// Only the model names are compared here, so the ladder is not walked: this
|
|
72
|
+
// is a config command and it has no business moving the live slider. The
|
|
73
|
+
// whole check is also raced against a deadline, because timeoutMs covers
|
|
74
|
+
// finding the page and not what happens inside the picker afterwards - a
|
|
75
|
+
// best-effort warning must never be what makes `setup` hang.
|
|
76
|
+
// Fail fast if a send holds the browser: this is a decoration on a config
|
|
77
|
+
// command, and opening a menu underneath a streaming consult is not worth a
|
|
78
|
+
// warning. The caller already swallows what this throws.
|
|
79
|
+
const { withBrowserSendLock } = await import("./browser-send-lock.js");
|
|
80
|
+
// The deadline timer is cleared once the listing wins and never holds the
|
|
81
|
+
// process open: the CLI exits through process.exitCode, so a live timer kept
|
|
82
|
+
// `setup` alive for the whole budget after its work was done. And when the
|
|
83
|
+
// deadline wins, the listing is abandoned - its connection closed, the send
|
|
84
|
+
// lock released - rather than left running unattended behind the prompt.
|
|
85
|
+
let deadline;
|
|
86
|
+
const abandon = new AbortController();
|
|
87
|
+
const listed = await Promise.race([
|
|
88
|
+
withBrowserSendLock(0, () => { }, () => listChatGptModelOptions({ timeoutMs: 8_000, walkPowerSlider: false, signal: abandon.signal })),
|
|
89
|
+
new Promise((resolve) => {
|
|
90
|
+
deadline = setTimeout(() => {
|
|
91
|
+
abandon.abort();
|
|
92
|
+
resolve(undefined);
|
|
93
|
+
}, PINNED_SELECTION_CHECK_BUDGET_MS);
|
|
94
|
+
deadline.unref?.();
|
|
95
|
+
})
|
|
96
|
+
]).finally(() => {
|
|
97
|
+
if (deadline)
|
|
98
|
+
clearTimeout(deadline);
|
|
99
|
+
});
|
|
100
|
+
if (!listed)
|
|
101
|
+
return undefined;
|
|
70
102
|
const offered = listed.options.flatMap((option) => [option.label, ...(option.value ? [option.value] : [])]);
|
|
71
103
|
return pinnedSelectionWarning({
|
|
72
104
|
...(defaults.model !== undefined ? { model: defaults.model } : {}),
|
package/dist/mcp.js
CHANGED
|
@@ -190,7 +190,7 @@ export function createServer(cwd = process.cwd(), options = {}) {
|
|
|
190
190
|
const browserRecover = options.browserRecover;
|
|
191
191
|
if (browserRecover) {
|
|
192
192
|
server.registerTool("pro_recover", {
|
|
193
|
-
description: "Fetch a ChatGPT answer that finished AFTER a consult stopped waiting, and record it as a normal consult receipt. Use this whenever pro_consult came back with a timeout or a still-running blocker: those carry the thread URL, and the answer is almost always sitting in that thread. This is also how a deep research report is collected - a research run takes about ten minutes and keeps going even when the consult that started it has already returned.
|
|
193
|
+
description: "Fetch a ChatGPT answer that finished AFTER a consult stopped waiting, and record it as a normal consult receipt. Use this whenever pro_consult came back with a timeout or a still-running blocker: those carry the thread URL, and the answer is almost always sitting in that thread. This is also how a deep research report is collected - a research run takes about ten minutes and keeps going even when the consult that started it has already returned. It sends no prompt, but it does navigate the visible tab to that thread, so it waits for any consult still streaming rather than walking the browser off it.",
|
|
194
194
|
inputSchema: {
|
|
195
195
|
thread: McpShortTextSchema.min(1).describe("The ChatGPT conversation URL from the blocker (its `thread` field)."),
|
|
196
196
|
timeout_ms: z.number().int().positive().max(600_000).optional()
|
|
@@ -70,3 +70,115 @@ export function menuKeyboardStep(input) {
|
|
|
70
70
|
}
|
|
71
71
|
return pressed < limit ? "down" : "exhausted";
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* One move towards putting the power slider back where the walk found it.
|
|
75
|
+
*
|
|
76
|
+
* Driven by the position actually read rather than by a replayed count, so an
|
|
77
|
+
* interrupted walk still converges instead of abandoning the setting wherever
|
|
78
|
+
* it stopped.
|
|
79
|
+
*/
|
|
80
|
+
export function sliderRestoreStep(input) {
|
|
81
|
+
const { current, target } = input;
|
|
82
|
+
if (!Number.isInteger(current) || !Number.isInteger(target))
|
|
83
|
+
return "done";
|
|
84
|
+
if (current === target)
|
|
85
|
+
return "done";
|
|
86
|
+
return current > target ? "left" : "right";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether the browser needs putting back on ChatGPT's Chat surface.
|
|
90
|
+
*
|
|
91
|
+
* Chat and Work have different model pickers - Work's has no Pro at all - so a
|
|
92
|
+
* browser that has drifted onto Work silently drives the wrong one.
|
|
93
|
+
*/
|
|
94
|
+
export function chatSurfaceChoice(surfaces) {
|
|
95
|
+
const chat = surfaces.find((surface) => surface.label.trim().toLowerCase() === "chat");
|
|
96
|
+
if (!chat)
|
|
97
|
+
return "no-toggle";
|
|
98
|
+
return chat.checked ? "already-chat" : "switch-to-chat";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Which surface is live, using the visible toggle where there is one and the
|
|
102
|
+
* value ChatGPT persists where there is not.
|
|
103
|
+
*
|
|
104
|
+
* The toggle is only rendered on the home screen, but threads and project pages
|
|
105
|
+
* keep the surface that was chosen - so a send into a project can be driving
|
|
106
|
+
* Work's picker with nothing on the page to say so.
|
|
107
|
+
*/
|
|
108
|
+
export function chatSurfaceState(input) {
|
|
109
|
+
const fromToggle = chatSurfaceChoice(input.surfaces);
|
|
110
|
+
if (fromToggle !== "no-toggle")
|
|
111
|
+
return fromToggle;
|
|
112
|
+
const stored = (input.storedMode ?? "").trim().replace(/^"|"$/g, "").toLowerCase();
|
|
113
|
+
if (stored === "chat")
|
|
114
|
+
return "already-chat";
|
|
115
|
+
// Only a value that names Work is a reason to move. Anything else - "null",
|
|
116
|
+
// a mode this code has never heard of - is not evidence, and acting on it
|
|
117
|
+
// would rewrite the preference and reload the page on every send.
|
|
118
|
+
if (stored === "work")
|
|
119
|
+
return "switch-to-chat";
|
|
120
|
+
return "unknown";
|
|
121
|
+
}
|
|
122
|
+
/** Steps that exist only on ChatGPT's Work surface, so asking for one means staying there. */
|
|
123
|
+
const WORK_ONLY_EFFORTS = new Set(["max", "ultra"]);
|
|
124
|
+
/**
|
|
125
|
+
* Whether the requested effort only exists on the Work surface.
|
|
126
|
+
*
|
|
127
|
+
* Sends otherwise put the browser back on Chat, which would make these two
|
|
128
|
+
* impossible to apply while still being offered by the help.
|
|
129
|
+
*/
|
|
130
|
+
export function effortNeedsWorkSurface(effort) {
|
|
131
|
+
return effort ? WORK_ONLY_EFFORTS.has(effort.trim().toLowerCase()) : false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* How to answer a JavaScript dialog that appeared in the driven browser.
|
|
135
|
+
*
|
|
136
|
+
* A dialog halts the page's main thread, so every evaluate after it hangs -
|
|
137
|
+
* and a client that had not enabled the Page domain beforehand cannot dismiss
|
|
138
|
+
* it at all. Answering one is therefore about getting out of the way, not about
|
|
139
|
+
* agreeing to whatever it asked: only beforeunload is accepted, because that is
|
|
140
|
+
* the page questioning a navigation prodex itself requested.
|
|
141
|
+
*/
|
|
142
|
+
export function javascriptDialogResponse(type) {
|
|
143
|
+
return { accept: type === "beforeunload" };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* A note that prodex answered a dialog in the driven browser.
|
|
147
|
+
*
|
|
148
|
+
* Answering one is a button pressed in someone's real session, and it is also
|
|
149
|
+
* the likeliest explanation for a send that behaved strangely around it, so it
|
|
150
|
+
* belongs in the receipt rather than in nobody's notes.
|
|
151
|
+
*/
|
|
152
|
+
export function answeredDialogWarning(types) {
|
|
153
|
+
if (types.length === 0)
|
|
154
|
+
return undefined;
|
|
155
|
+
const counts = new Map();
|
|
156
|
+
for (const type of types)
|
|
157
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
158
|
+
const listed = [...counts]
|
|
159
|
+
.map(([type, count]) => (count > 1 ? `${type} x${count}` : type))
|
|
160
|
+
.join(", ");
|
|
161
|
+
const accepted = types.some((type) => type === "beforeunload");
|
|
162
|
+
const action = accepted
|
|
163
|
+
? "accepted the page's leave prompt and dismissed the rest"
|
|
164
|
+
: "dismissed without agreeing to it";
|
|
165
|
+
return (`dialog_answered: a JavaScript dialog appeared in the ChatGPT window during this send (${listed}) and prodex ${action}, ` +
|
|
166
|
+
"because a dialog left open halts the page and nothing after it would have run.");
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The surface a picker listing was read from: the checked toggle where one is
|
|
170
|
+
* drawn (the home screen), else the choice ChatGPT persists - which is what the
|
|
171
|
+
* app itself reads on the pages that draw no toggle. Undefined when neither
|
|
172
|
+
* says anything.
|
|
173
|
+
*/
|
|
174
|
+
export function surfaceFromProbe(probe) {
|
|
175
|
+
const checked = probe?.surfaces?.find((entry) => entry.checked)?.label;
|
|
176
|
+
if (checked)
|
|
177
|
+
return checked;
|
|
178
|
+
const stored = (probe?.storedMode ?? "").trim().replace(/^"|"$/g, "").toLowerCase();
|
|
179
|
+
if (stored === "chat")
|
|
180
|
+
return "Chat";
|
|
181
|
+
if (stored === "work")
|
|
182
|
+
return "Work";
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|