@youdie006/prodex 0.35.1 → 0.36.1
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 +2 -0
- package/dist/chatgpt-browser.js +147 -15
- package/dist/cli-help.js +1 -1
- package/dist/cli-pro.js +14 -8
- package/dist/cli.js +5 -2
- package/dist/tui-run.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -440,6 +440,8 @@ npm run dev -- tasks list
|
|
|
440
440
|
|
|
441
441
|
**Sends started failing after many consults in one chat.** Long accumulated threads eventually confuse prompt-acceptance detection (measured live around ten-plus messages). Send repeated consults into fresh chats with `--new-chat` (`new_chat: true` on the MCP tool) — the answer still lands in your account and in `.bridge/` receipts either way.
|
|
442
442
|
|
|
443
|
+
**Every send says "still generating" but nothing is being written.** ChatGPT sometimes parks a thread on "You're giving feedback on a new version of ChatGPT - which response do you prefer?", and while it waits the composer keeps its stop control. prodex reports that as a `response_choice_pending` blocker naming the two "I prefer this response" buttons, because waiting will never clear it. Sends that navigate away (`--new-chat`, or any send into a project) are unaffected. A stop control can also linger for a few seconds after an ordinary answer finishes; prodex checks the conversation transcript before believing it, so a finished thread is not mistaken for a busy one.
|
|
444
|
+
|
|
443
445
|
**"menu item not found" on `--effort`/`--pro-mode`/`--project`.** Selection matches the visible menu labels, verified in the Korean and English (US) ChatGPT UI. On another display language, run `prodex pro browser models` to see your labels and pass `--model "<exact label>"`, which clicks any picker radio by exact text.
|
|
444
446
|
|
|
445
447
|
**Does this risk my ChatGPT account?** `prodex` is deliberately not a stealth bot: it uses a real visible browser, you log in manually, and it stops on captcha/verification instead of solving it. It does drive chatgpt.com, though, so keep usage at the human, occasional-consult volume the auto-pacing enforces — do not build tight recurring loops on top of it. Automating a paid account is your responsibility under OpenAI's terms.
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -596,8 +596,46 @@ export function modelSelectionWarning(requestedModel, modelSlug) {
|
|
|
596
596
|
return undefined;
|
|
597
597
|
return `model_mismatch: you asked for ${requestedModel}, but the answer came from "${modelSlug}". Check the model picker in the browser; the selection did not take.`;
|
|
598
598
|
}
|
|
599
|
-
|
|
600
|
-
|
|
599
|
+
/**
|
|
600
|
+
* The panel ChatGPT raises to ask which of two answers you prefer.
|
|
601
|
+
*
|
|
602
|
+
* Recognized by its testids rather than its wording, so a UI in any language
|
|
603
|
+
* still matches. Read off the live page: the title is `paragen-feedback-title`
|
|
604
|
+
* and both choices are `paragen-prefer-response-button`.
|
|
605
|
+
*/
|
|
606
|
+
export const CHATGPT_RESPONSE_CHOICE_SELECTOR = '[data-testid="paragen-feedback-title"],[data-testid="paragen-prefer-response-button"]';
|
|
607
|
+
/**
|
|
608
|
+
* A thread parked on that question, which no amount of waiting will clear.
|
|
609
|
+
*
|
|
610
|
+
* Measured live: both candidate answers had finished, the page had not changed
|
|
611
|
+
* in twenty seconds, and the composer's submit control was still
|
|
612
|
+
* `data-testid="stop-button"` - which prodex read as "generating". Every send
|
|
613
|
+
* afterwards waited out its whole budget and then advised waiting some more.
|
|
614
|
+
*/
|
|
615
|
+
export function chatGptResponseChoiceBlocker(awaitingResponseChoice) {
|
|
616
|
+
if (!awaitingResponseChoice)
|
|
617
|
+
return undefined;
|
|
618
|
+
return {
|
|
619
|
+
code: "response_choice_pending",
|
|
620
|
+
message: "ChatGPT is asking which of two answers you prefer, and this thread stays locked until one is picked.",
|
|
621
|
+
retryable: true,
|
|
622
|
+
next_step: "Pick one in the browser (the two \"I prefer this response\" buttons), " +
|
|
623
|
+
"or leave the thread alone and send into a new chat (`--new-chat`)."
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Whether that question stands in the way of THIS send. A send that navigates
|
|
628
|
+
* away - a fresh chat, or a project home - never touches the parked thread.
|
|
629
|
+
*/
|
|
630
|
+
export function responseChoiceBlocksThisSend(input) {
|
|
631
|
+
if (!input.awaitingResponseChoice)
|
|
632
|
+
return false;
|
|
633
|
+
return !input.newChat && input.project === undefined && input.projectNew === undefined;
|
|
634
|
+
}
|
|
635
|
+
export function chatGptBusyBlocker(state) {
|
|
636
|
+
// Waiting for a choice is not generating. Conflating them turned a click
|
|
637
|
+
// away from ready into a full send budget spent on "still generating".
|
|
638
|
+
if (!state.generating || state.awaitingResponseChoice === true)
|
|
601
639
|
return undefined;
|
|
602
640
|
return {
|
|
603
641
|
code: "response_in_progress",
|
|
@@ -608,6 +646,39 @@ export function chatGptBusyBlocker(generating) {
|
|
|
608
646
|
"A new topic can go to a new chat instead."
|
|
609
647
|
};
|
|
610
648
|
}
|
|
649
|
+
/**
|
|
650
|
+
* The busy verdict, re-decided against the transcript.
|
|
651
|
+
*
|
|
652
|
+
* Measured on the real page: an ordinary conversation whose answer was complete
|
|
653
|
+
* kept `data-testid="stop-button"` (aria-label "Stop answering") unchanged over
|
|
654
|
+
* thirty seconds, while the transcript for that same conversation reported
|
|
655
|
+
* finished_successfully with end_turn set. The page control is decoration once
|
|
656
|
+
* the turn is over; the transcript is the record. Only a transcript that can be
|
|
657
|
+
* read AND says the turn finished clears the blocker - an unreadable one is no
|
|
658
|
+
* evidence of anything, and typing into a live generation corrupts the thread.
|
|
659
|
+
*/
|
|
660
|
+
export function busyBlockerAfterTranscriptCheck(busyBlocker, transcript) {
|
|
661
|
+
if (!busyBlocker)
|
|
662
|
+
return undefined;
|
|
663
|
+
return transcript?.ok === true && transcript.isComplete === true ? undefined : busyBlocker;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Ask the transcript whether the conversation the tab is showing has finished.
|
|
667
|
+
* Undefined when there is nothing to ask about (a fresh chat or project home
|
|
668
|
+
* carries no conversation id) or the read fails.
|
|
669
|
+
*/
|
|
670
|
+
async function readTranscriptCompletion(page, url) {
|
|
671
|
+
const conversationId = conversationIdFromThreadUrl(url);
|
|
672
|
+
if (!conversationId)
|
|
673
|
+
return undefined;
|
|
674
|
+
try {
|
|
675
|
+
const state = await evaluateOnPage(page, transcriptAnswerExpression(conversationId));
|
|
676
|
+
return { ok: state.ok === true, ...(state.isComplete !== undefined ? { isComplete: state.isComplete } : {}) };
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
611
682
|
export function isLikelyChatGptSubmitButton(label, dataTestId) {
|
|
612
683
|
const normalized = label.trim().toLowerCase();
|
|
613
684
|
return dataTestId === "send-button" || /\b(send|submit)\b|보내기|전송/.test(normalized);
|
|
@@ -878,8 +949,12 @@ function chatGptPageMissingBlocker() {
|
|
|
878
949
|
// readiness assert - otherwise it is misreported as "missing a visible prompt
|
|
879
950
|
// composer" (measured live: continue-by-default consults landing on a thread
|
|
880
951
|
// still generating the previous prodex answer).
|
|
881
|
-
export function assertChatGptIdleAndReadyForPrompt(status
|
|
882
|
-
|
|
952
|
+
export function assertChatGptIdleAndReadyForPrompt(status,
|
|
953
|
+
// The caller may have already weighed the page against the transcript and
|
|
954
|
+
// found the stop control stale. Re-deriving the verdict from the DOM here
|
|
955
|
+
// threw away that answer and failed the send the queue had just cleared.
|
|
956
|
+
decidedBusyBlocker, busyVerdictDecided = false) {
|
|
957
|
+
const busyBlocker = busyVerdictDecided ? decidedBusyBlocker : chatGptBusyBlocker(status);
|
|
883
958
|
if (busyBlocker)
|
|
884
959
|
throw new ChatGptBrowserBlockerError(busyBlocker);
|
|
885
960
|
assertChatGptReadyForPrompt(inferChatGptPageLoggedInLikely(status), status.hasComposer, status.openDialogText);
|
|
@@ -1054,7 +1129,13 @@ export async function getChatGptBrowserStatus(options = {}) {
|
|
|
1054
1129
|
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {})
|
|
1055
1130
|
});
|
|
1056
1131
|
const loggedInLikely = inferChatGptPageLoggedInLikely(state);
|
|
1057
|
-
|
|
1132
|
+
// The busy verdict is checked against the transcript here too, so `check`
|
|
1133
|
+
// does not report a finished conversation as one still being written.
|
|
1134
|
+
const busyBlocker = chatGptBusyBlocker(state);
|
|
1135
|
+
const blocker = chatGptVisibilityBlocker(state.visibilityState, state.url) ??
|
|
1136
|
+
detectChatGptPageBlocker(state) ??
|
|
1137
|
+
chatGptResponseChoiceBlocker(state.awaitingResponseChoice === true) ??
|
|
1138
|
+
(busyBlocker ? busyBlockerAfterTranscriptCheck(busyBlocker, await readTranscriptCompletion(page.page, state.url)) : undefined);
|
|
1058
1139
|
return {
|
|
1059
1140
|
reachable: true,
|
|
1060
1141
|
loggedInLikely,
|
|
@@ -2009,7 +2090,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
2009
2090
|
// timeout: consults continue threads by default, so landing on a thread
|
|
2010
2091
|
// whose previous (often timed-out Pro) answer is still streaming is a when,
|
|
2011
2092
|
// not an if - queueing behind it beats failing.
|
|
2012
|
-
let busyBlocker = chatGptBusyBlocker(status.
|
|
2093
|
+
let busyBlocker = busyBlockerAfterTranscriptCheck(chatGptBusyBlocker(status), await readTranscriptCompletion(page, status.url));
|
|
2013
2094
|
const busyWaitBudgetMs = options.busyWaitMs ?? timeoutMs;
|
|
2014
2095
|
if (busyBlocker && busyWaitBudgetMs > 0) {
|
|
2015
2096
|
// Queue behind the in-flight response instead of failing: shared-tab
|
|
@@ -2023,17 +2104,32 @@ export async function sendChatGptPrompt(options) {
|
|
|
2023
2104
|
const midBlocker = detectChatGptPageBlocker(status);
|
|
2024
2105
|
if (midBlocker)
|
|
2025
2106
|
throw new ChatGptBrowserBlockerError(midBlocker);
|
|
2026
|
-
busyBlocker = chatGptBusyBlocker(status.
|
|
2107
|
+
busyBlocker = busyBlockerAfterTranscriptCheck(chatGptBusyBlocker(status), await readTranscriptCompletion(page, status.url));
|
|
2027
2108
|
if (busyBlocker)
|
|
2028
2109
|
emitProgress("waiting", "tab busy with another response; waiting");
|
|
2029
2110
|
}
|
|
2030
2111
|
if (!busyBlocker) {
|
|
2031
2112
|
// The composer takes a moment to unlock after generation ends; settle
|
|
2032
|
-
// again so the readiness assert below sees the reopened composer
|
|
2113
|
+
// again so the readiness assert below sees the reopened composer, and
|
|
2114
|
+
// weigh that fresh reading the same way - a generation that started in
|
|
2115
|
+
// the meantime must still hold the send back.
|
|
2033
2116
|
status = await readSettledChatGptPageStatus(page);
|
|
2117
|
+
busyBlocker = busyBlockerAfterTranscriptCheck(chatGptBusyBlocker(status), await readTranscriptCompletion(page, status.url));
|
|
2034
2118
|
}
|
|
2035
2119
|
}
|
|
2036
|
-
|
|
2120
|
+
// A thread ChatGPT has parked on "which response do you prefer?" will not
|
|
2121
|
+
// unpark on its own. Sends that navigate away - a fresh chat, a project home
|
|
2122
|
+
// - never touch it; a send that means to continue THAT thread has to say so
|
|
2123
|
+
// now, rather than type into a composer that will not post.
|
|
2124
|
+
if (responseChoiceBlocksThisSend({
|
|
2125
|
+
awaitingResponseChoice: status.awaitingResponseChoice === true,
|
|
2126
|
+
...(options.newChat !== undefined ? { newChat: options.newChat } : {}),
|
|
2127
|
+
...(options.project !== undefined ? { project: options.project } : {}),
|
|
2128
|
+
...(options.projectNew !== undefined ? { projectNew: options.projectNew } : {})
|
|
2129
|
+
})) {
|
|
2130
|
+
throw new ChatGptBrowserBlockerError(chatGptResponseChoiceBlocker(true));
|
|
2131
|
+
}
|
|
2132
|
+
assertChatGptIdleAndReadyForPrompt(status, busyBlocker, true);
|
|
2037
2133
|
if (normalizedTargetUrl)
|
|
2038
2134
|
assertChatGptTargetUrlMatches(status.url, normalizedTargetUrl);
|
|
2039
2135
|
assertVisibleChatGptTab(status.visibilityState, status.url, normalizedTargetUrl);
|
|
@@ -2447,16 +2543,40 @@ export async function sendChatGptPrompt(options) {
|
|
|
2447
2543
|
throw Object.assign(new Error(`Timed out after ${formatDurationMs(timeoutMs)} (${timeoutMs}ms) waiting for ChatGPT to respond. ` +
|
|
2448
2544
|
"Pro reasoning can run many minutes. Raise --timeout-ms and retry."), completed?.url ? { thread: completed.url } : {});
|
|
2449
2545
|
}
|
|
2546
|
+
/**
|
|
2547
|
+
* One line of `pro browser models`.
|
|
2548
|
+
*
|
|
2549
|
+
* The listing used to append "not selectable via --model yet" to every submenu
|
|
2550
|
+
* row, which is now every row that matters - and it is not true: measured
|
|
2551
|
+
* against this same picker, --model Pro, --effort 즉시 and --pro-mode 확장 all
|
|
2552
|
+
* selected, because selection walks into the submenu. Saying what the row is
|
|
2553
|
+
* set to is the useful half; the false warning was the harmful half.
|
|
2554
|
+
*/
|
|
2555
|
+
export function formatModelMenuOption(option) {
|
|
2556
|
+
const marker = option.checked ? "*" : " ";
|
|
2557
|
+
const value = option.kind === "submenu" && option.value ? ` -> ${option.value}` : "";
|
|
2558
|
+
return `${marker} ${option.label}${value}`;
|
|
2559
|
+
}
|
|
2450
2560
|
export function modelMenuOptionsExpression() {
|
|
2451
2561
|
return `(() => {
|
|
2452
2562
|
const m = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
|
|
2453
2563
|
if (!m) return [];
|
|
2454
2564
|
return [...m.querySelectorAll('[role="menuitemradio"],[role="menuitem"]')]
|
|
2455
|
-
.map((it) =>
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2565
|
+
.map((it) => {
|
|
2566
|
+
// A submenu row renders as label over value ("Model" / "GPT-5.6 Sol"),
|
|
2567
|
+
// and the value is the part a person actually wants to read.
|
|
2568
|
+
const lines = (it.innerText || it.textContent || "")
|
|
2569
|
+
.split(String.fromCharCode(10))
|
|
2570
|
+
.map((line) => line.trim())
|
|
2571
|
+
.filter((line) => line.length > 0);
|
|
2572
|
+
const value = lines[1] || "";
|
|
2573
|
+
return {
|
|
2574
|
+
label: lines[0] || "",
|
|
2575
|
+
kind: it.getAttribute("aria-haspopup") === "menu" ? "submenu" : "radio",
|
|
2576
|
+
checked: it.getAttribute("aria-checked") === "true",
|
|
2577
|
+
...(value ? { value } : {})
|
|
2578
|
+
};
|
|
2579
|
+
})
|
|
2460
2580
|
.filter((o) => o.label.length > 0);
|
|
2461
2581
|
})()`;
|
|
2462
2582
|
}
|
|
@@ -2580,7 +2700,15 @@ export function findLaunchedBrowserProcesses(psOutput, input) {
|
|
|
2580
2700
|
// made a check against an unused port report a healthy Chrome as wedged.
|
|
2581
2701
|
if (mains.length === 0)
|
|
2582
2702
|
return [];
|
|
2583
|
-
|
|
2703
|
+
// Which profile the helpers belong to is the browser's answer, not the
|
|
2704
|
+
// caller's: `check` has only a port, and matching against the profile it
|
|
2705
|
+
// assumed both missed this browser's renderers and collected a stranger's.
|
|
2706
|
+
// Read it off the process that answered to the port; fall back to what the
|
|
2707
|
+
// caller passed only when the command line does not say.
|
|
2708
|
+
// Stop at the next flag, so a profile path containing spaces survives.
|
|
2709
|
+
const profileOf = (line) => /--user-data-dir=(.*?)(?=\s+-{1,2}\w|\s*$)/.exec(line)?.[1];
|
|
2710
|
+
const profileDir = profileOf(mains[0]) ?? input.profileDir;
|
|
2711
|
+
const helpers = profileDir.length > 0 ? lines.filter((line) => profileOf(line) === profileDir && !mains.includes(line)) : [];
|
|
2584
2712
|
return [...mains, ...helpers].map(pidOf).filter((pid) => pid !== undefined);
|
|
2585
2713
|
}
|
|
2586
2714
|
/**
|
|
@@ -3074,6 +3202,7 @@ export function statusExpression() {
|
|
|
3074
3202
|
const excludedTextSelector = JSON.stringify(CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS);
|
|
3075
3203
|
const blockerScanExcludedSelector = JSON.stringify(CHATGPT_BLOCKER_SCAN_EXCLUDED_ANCESTORS);
|
|
3076
3204
|
const streamingSelector = JSON.stringify(CHATGPT_STREAMING_SELECTOR);
|
|
3205
|
+
const responseChoiceSelector = JSON.stringify(CHATGPT_RESPONSE_CHOICE_SELECTOR);
|
|
3077
3206
|
const generatingControlPattern = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.source);
|
|
3078
3207
|
const generatingControlFlags = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.flags);
|
|
3079
3208
|
return `(() => {
|
|
@@ -3127,6 +3256,7 @@ export function statusExpression() {
|
|
|
3127
3256
|
visibleButtonLabels,
|
|
3128
3257
|
hasComposer,
|
|
3129
3258
|
generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || visibleButtonLabels.some((label) => generatingControlPattern.test(label)),
|
|
3259
|
+
awaitingResponseChoice: Boolean(document.querySelector(${responseChoiceSelector})),
|
|
3130
3260
|
modelHints: lines.filter((line) => /GPT|Pro|Thinking|ChatGPT|Extra High|Auto/i.test(line)).slice(0, 30),
|
|
3131
3261
|
openDialogText: (([...document.querySelectorAll('[role="dialog"]')].find((d) => d.offsetWidth || d.offsetHeight || d.getClientRects().length)?.innerText) || "").trim().slice(0, 200)
|
|
3132
3262
|
};
|
|
@@ -3961,6 +4091,7 @@ export function answerExpression() {
|
|
|
3961
4091
|
const excludedTextSelector = JSON.stringify(CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS);
|
|
3962
4092
|
const blockerScanExcludedSelector = JSON.stringify(CHATGPT_BLOCKER_SCAN_EXCLUDED_ANCESTORS);
|
|
3963
4093
|
const streamingSelector = JSON.stringify(CHATGPT_STREAMING_SELECTOR);
|
|
4094
|
+
const responseChoiceSelector = JSON.stringify(CHATGPT_RESPONSE_CHOICE_SELECTOR);
|
|
3964
4095
|
const generatingControlPattern = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.source);
|
|
3965
4096
|
const generatingControlFlags = JSON.stringify(CHATGPT_GENERATING_CONTROL_PATTERN.flags);
|
|
3966
4097
|
return `(() => {
|
|
@@ -4025,6 +4156,7 @@ export function answerExpression() {
|
|
|
4025
4156
|
blockerScanTextSample: visibleTextOutsideMessages(blockerScanExcludedSelector).slice(0, 12000),
|
|
4026
4157
|
visibleButtonLabels: buttons,
|
|
4027
4158
|
generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || buttons.some((label) => generatingControlPattern.test(label)),
|
|
4159
|
+
awaitingResponseChoice: Boolean(document.querySelector(${responseChoiceSelector})),
|
|
4028
4160
|
assistantMessageCount: assistantMessages.length,
|
|
4029
4161
|
userMessageCount: userMessages.length,
|
|
4030
4162
|
// ChatGPT tags each assistant message with the model that produced it -
|
package/dist/cli-help.js
CHANGED
|
@@ -295,7 +295,7 @@ Commands:
|
|
|
295
295
|
${askUsage}
|
|
296
296
|
${recoverUsage}
|
|
297
297
|
|
|
298
|
-
Visible-browser sends require a manual browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers.
|
|
298
|
+
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.
|
|
299
299
|
Model/project selection (ask):
|
|
300
300
|
--model Composer model to pick by its exact menu label (verified: Pro). Models whose menu entry opens a submenu of variants are rejected with a clear error for now.
|
|
301
301
|
--pro-mode Pro sub-mode: 기본 (standard) or 확장 (extended), used when the model is Pro. A Pro selection raises the default --timeout-ms to 1200000.
|
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, 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 } 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";
|
|
@@ -442,11 +442,9 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
442
442
|
}
|
|
443
443
|
io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
|
|
444
444
|
for (const option of listed.options) {
|
|
445
|
-
|
|
446
|
-
const suffix = option.kind === "submenu" ? " (has sub-variants; not selectable via --model yet)" : "";
|
|
447
|
-
io.stdout(`${marker} ${option.label}${suffix}`);
|
|
445
|
+
io.stdout(formatModelMenuOption(option));
|
|
448
446
|
}
|
|
449
|
-
io.stdout("
|
|
447
|
+
io.stdout("An arrow shows what that row is set to now; --model / --effort reach into those submenus (e.g. --model Pro).");
|
|
450
448
|
return 0;
|
|
451
449
|
}
|
|
452
450
|
if (browserSubcommand === "projects") {
|
|
@@ -1473,7 +1471,16 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1473
1471
|
// a second Chrome on the same profile joins the wedged one rather than
|
|
1474
1472
|
// replacing it, and the wedged one keeps burning CPU while nobody looks. This
|
|
1475
1473
|
// runs unattended for agents, so it is the path that let four days pass.
|
|
1476
|
-
|
|
1474
|
+
// The same record the relaunch below reads, read once and used for both: the
|
|
1475
|
+
// scan matches the main process by port but its helpers by profile, so
|
|
1476
|
+
// scanning the DEFAULT profile for a custom-profile user put a second,
|
|
1477
|
+
// healthy browser's renderers on the list this function kills.
|
|
1478
|
+
const lastLogin = await readLastBrowserLoginLaunch().catch(() => undefined);
|
|
1479
|
+
const scanFor = {
|
|
1480
|
+
...(options.port !== undefined ? { port: options.port } : {}),
|
|
1481
|
+
...(lastLogin?.profile_dir ? { profileDir: lastLogin.profile_dir } : {})
|
|
1482
|
+
};
|
|
1483
|
+
const wedged = findWedgedBrowser(scanFor);
|
|
1477
1484
|
if (wedged.length > 0) {
|
|
1478
1485
|
// Confirm the silence before ending anything: one missed poll is a busy
|
|
1479
1486
|
// browser, several in a row is a dead one.
|
|
@@ -1496,7 +1503,7 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1496
1503
|
// delay: the replacement launch fails outright if the old process still
|
|
1497
1504
|
// holds it, which is how the first self-heal attempt ended.
|
|
1498
1505
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
1499
|
-
if (findWedgedBrowser(
|
|
1506
|
+
if (findWedgedBrowser(scanFor).length === 0)
|
|
1500
1507
|
break;
|
|
1501
1508
|
await sleep(1_000);
|
|
1502
1509
|
}
|
|
@@ -1506,7 +1513,6 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1506
1513
|
// Reuse the profile the user last logged in with; launching the default
|
|
1507
1514
|
// profile for a custom-profile user would wait on the wrong (logged-out)
|
|
1508
1515
|
// profile or, worse, silently send to a different account.
|
|
1509
|
-
const lastLogin = await readLastBrowserLoginLaunch();
|
|
1510
1516
|
// Relaunch in the SAME window mode the user chose: silently reopening a
|
|
1511
1517
|
// visible window for someone running headless would be exactly the
|
|
1512
1518
|
// surprise window they turned headless to avoid.
|
package/dist/cli.js
CHANGED
|
@@ -116,7 +116,10 @@ export async function runCli(args, io = defaultIo()) {
|
|
|
116
116
|
// A person typing `prodex` used to get the agent-facing command wall and no
|
|
117
117
|
// way in. On a terminal, walk them through a consult instead; piped or
|
|
118
118
|
// scripted callers still get the banner and the command list they parse.
|
|
119
|
-
|
|
119
|
+
// A keyboard as well as a screen: with stdin redirected there is nothing to
|
|
120
|
+
// answer the picker's questions with, and `prodex` alone should still print
|
|
121
|
+
// the command list rather than refuse.
|
|
122
|
+
if (!command && io.isInteractive === true && process.stdin.isTTY === true)
|
|
120
123
|
return runInteractiveUi(io);
|
|
121
124
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
122
125
|
if (shouldColorize())
|
|
@@ -433,7 +436,7 @@ repo: ${cwd}
|
|
|
433
436
|
|
|
434
437
|
Safety notes:
|
|
435
438
|
- This command only prints commands; it does not start servers, open browsers, or write files.
|
|
436
|
-
- Visible-browser sends require a manual, visible browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers.`;
|
|
439
|
+
- Visible-browser sends require a manual, visible 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.`;
|
|
437
440
|
}
|
|
438
441
|
async function hasOnboardingReadme(cwd) {
|
|
439
442
|
try {
|
package/dist/tui-run.js
CHANGED
|
@@ -136,6 +136,17 @@ async function askLine(io, question) {
|
|
|
136
136
|
* Returns the process exit code.
|
|
137
137
|
*/
|
|
138
138
|
export async function runInteractiveConsult(io, deps) {
|
|
139
|
+
// Every question here is answered with a keystroke, so a stdin that cannot
|
|
140
|
+
// deliver one has to be turned away at the door. Measured with stdin
|
|
141
|
+
// redirected: the picker painted the alternate screen, hid the cursor, and
|
|
142
|
+
// waited forever for a key that could not arrive - and never returning meant
|
|
143
|
+
// the screen was never restored either, leaving a terminal that needed
|
|
144
|
+
// `reset`.
|
|
145
|
+
if (io.input.isTTY !== true) {
|
|
146
|
+
io.write("The picker reads single keystrokes, so it needs a terminal on stdin.\n");
|
|
147
|
+
io.write('Send without one: prodex pro browser ask "your prompt"\n');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
139
150
|
const now = deps.now ?? Date.now;
|
|
140
151
|
readline.emitKeypressEvents(io.input);
|
|
141
152
|
io.input.setRawMode?.(true);
|
|
@@ -278,6 +289,12 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
278
289
|
// Leave the alternate screen before the send: the answer, the receipt id
|
|
279
290
|
// and any blocker belong in the scrollback the user keeps.
|
|
280
291
|
io.write(ALT_SCREEN_OFF + SHOW_CURSOR);
|
|
292
|
+
// Hand the terminal back too. Raw mode is what let the pickers read single
|
|
293
|
+
// keys, and it also turns off the terminal's own ctrl-c: the key arrives as
|
|
294
|
+
// a keypress nothing is waiting for. The progress bar promises "ctrl-c to
|
|
295
|
+
// stop", and under raw mode that promise was false for the whole ten
|
|
296
|
+
// minutes a deep research send runs. No key is read from here on.
|
|
297
|
+
io.input.setRawMode?.(false);
|
|
281
298
|
// --target-url confirms which conversation a send means; it deliberately
|
|
282
299
|
// does not navigate. Picking one from a list IS a request to go there, so
|
|
283
300
|
// move the tab first and let the flag confirm it landed.
|