@youdie006/prodex 0.21.1 → 0.21.3
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/chatgpt-browser.js +61 -3
- package/dist/cli-pro.js +10 -3
- 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 will not send into a tab it cannot read, so leave the dedicated window on a ChatGPT tab; it sends quietly in the background without stealing focus. Prefer no window at all? See [virtual display](#no-window-at-all-virtual-display-recommended). Pin per-repo defaults once - `prodex setup --model Pro --project "your-project"` - so every ask runs Pro (20-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 inline several files. `--file` puts a text file's CONTENTS into the prompt; `--attach` uploads the file itself, which is the only way to hand ChatGPT a pdf, pptx, xlsx or image and let it parse the original (`prodex ask --attach deck.pptx "Review slides 40-60"`). Both are restricted to paths inside the repo, so an agent cannot upload `~/.ssh` by asking nicely. The upload happens before the prompt is submitted and prodex waits for ChatGPT to finish accepting the file - the browser process reads the path, so the file has to live on the machine running the browser. `--tool` turns on a ChatGPT composer tool for that send: `--tool deep-research` (a browsed
|
|
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 will not send into a tab it cannot read, so leave the dedicated window on a ChatGPT tab; it sends quietly in the background without stealing focus. Prefer no window at all? See [virtual display](#no-window-at-all-virtual-display-recommended). Pin per-repo defaults once - `prodex setup --model Pro --project "your-project"` - so every ask runs Pro (20-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 inline several files. `--file` puts a text file's CONTENTS into the prompt; `--attach` uploads the file itself, which is the only way to hand ChatGPT a pdf, pptx, xlsx or image and let it parse the original (`prodex ask --attach deck.pptx "Review slides 40-60"`). Both are restricted to paths inside the repo, so an agent cannot upload `~/.ssh` by asking nicely. The upload happens before the prompt is submitted and prodex waits for ChatGPT to finish accepting the file - the browser process reads the path, so the file has to live on the machine running the browser. `--tool` turns on a ChatGPT composer tool for that send: `--tool deep-research` (starts a browsed research run and hands you back the thread URL - prodex does NOT return the report, because a finished deep research report does not render in prodex's browser session even after a hard reload; read it in your own browser), `--tool web-search` (current facts with sources), `--tool create-image`. Any other label the menu shows works too, so a tool ChatGPT adds later needs no prodex release. 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
|
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1932,6 +1932,11 @@ export async function sendChatGptPrompt(options) {
|
|
|
1932
1932
|
}
|
|
1933
1933
|
await sleep(1_000);
|
|
1934
1934
|
}
|
|
1935
|
+
// Do not wait for a report this browser will never render.
|
|
1936
|
+
const threadNow = await cdp.evaluate("location.href");
|
|
1937
|
+
if (typeof threadNow === "string" && threadNow.length > 0) {
|
|
1938
|
+
throw new ChatGptBrowserBlockerError(deepResearchUnreadableBlocker(threadNow));
|
|
1939
|
+
}
|
|
1935
1940
|
if (!pressed) {
|
|
1936
1941
|
sendWarnings.push("deep_research_start_not_found: no start control appeared for the deep research run. If ChatGPT asked a clarifying question instead, answer it with a follow-up consult in the same thread.");
|
|
1937
1942
|
}
|
|
@@ -1998,13 +2003,38 @@ export async function sendChatGptPrompt(options) {
|
|
|
1998
2003
|
}
|
|
1999
2004
|
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound });
|
|
2000
2005
|
}
|
|
2006
|
+
// Pin the conversation the prompt actually landed in. The browser is shared
|
|
2007
|
+
// (other agents, the user, tooling), and a tab that moves mid-wait made
|
|
2008
|
+
// prodex read a DIFFERENT conversation and save it as this consult's answer -
|
|
2009
|
+
// silently, with a receipt (caught live). Nothing about that is recoverable
|
|
2010
|
+
// after the fact, so the wait either stays on this thread or fails loudly.
|
|
2011
|
+
const pinnedThreadUrl = finalState?.url;
|
|
2012
|
+
let recoveredNavigations = 0;
|
|
2001
2013
|
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
2002
2014
|
while (Date.now() - started < timeoutMs) {
|
|
2003
2015
|
await sleep(1000);
|
|
2004
2016
|
try {
|
|
2005
2017
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2018
|
+
if (pinnedThreadUrl && finalState?.url && !chatGptUrlsReferToSameTarget(finalState.url, pinnedThreadUrl)) {
|
|
2019
|
+
if (recoveredNavigations >= 2) {
|
|
2020
|
+
throw new ChatGptBrowserBlockerError({
|
|
2021
|
+
code: "thread_navigated_away",
|
|
2022
|
+
message: "The browser tab was moved to a different ChatGPT conversation while this consult was waiting for its answer.",
|
|
2023
|
+
retryable: true,
|
|
2024
|
+
next_step: `Keep the dedicated browser on the consult thread, then fetch the answer with \`prodex pro browser recover --target-url ${pinnedThreadUrl}\`.`,
|
|
2025
|
+
thread: pinnedThreadUrl
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
recoveredNavigations += 1;
|
|
2029
|
+
sendWarnings.push(`thread_navigated_away_recovered: something moved the tab to another conversation mid-wait; prodex navigated back to ${pinnedThreadUrl}.`);
|
|
2030
|
+
await evaluateOnPage(page, `location.assign(${JSON.stringify(pinnedThreadUrl)})`);
|
|
2031
|
+
await sleep(3_000);
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2006
2034
|
}
|
|
2007
|
-
catch {
|
|
2035
|
+
catch (error) {
|
|
2036
|
+
if (error instanceof ChatGptBrowserBlockerError)
|
|
2037
|
+
throw error;
|
|
2008
2038
|
// Transient CDP failure while the answer is streaming: retry. A throw here
|
|
2009
2039
|
// would discard an already-streamed partial answer and skip the salvage
|
|
2010
2040
|
// path below, so keep the last good state and poll again until timeout.
|
|
@@ -2852,6 +2882,24 @@ export function chunkComposerText(text, size = COMPOSER_INSERT_CHUNK_CHARS) {
|
|
|
2852
2882
|
* on that: a run left waiting produced zero assistant messages for 30+ minutes
|
|
2853
2883
|
* (measured live), because nothing pressed it.
|
|
2854
2884
|
*/
|
|
2885
|
+
/**
|
|
2886
|
+
* Deep research reports are not readable from prodex's browser session.
|
|
2887
|
+
* Measured against a run the user finished themselves: the SAME thread, same
|
|
2888
|
+
* account, hard-reloaded here, renders only the prompt turn and an empty
|
|
2889
|
+
* result turn (roles ["user"], 72 characters of page) while their own browser
|
|
2890
|
+
* shows the completed report. Rather than spend a 30-minute budget waiting for
|
|
2891
|
+
* text that never arrives, prodex hands back the thread and says where to read
|
|
2892
|
+
* it.
|
|
2893
|
+
*/
|
|
2894
|
+
export function deepResearchUnreadableBlocker(threadUrl) {
|
|
2895
|
+
return {
|
|
2896
|
+
code: "deep_research_not_readable",
|
|
2897
|
+
message: "The deep research run was started, but prodex cannot read deep research reports from its browser session - they do not render there.",
|
|
2898
|
+
retryable: false,
|
|
2899
|
+
next_step: `Open the run in your own browser and read it there: ${threadUrl}`,
|
|
2900
|
+
thread: threadUrl
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2855
2903
|
export function deepResearchStartButtonRectExpression() {
|
|
2856
2904
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2857
2905
|
const buttons = [...document.querySelectorAll('button,[role="button"]')];
|
|
@@ -2977,7 +3025,17 @@ export function answerExpression() {
|
|
|
2977
3025
|
});
|
|
2978
3026
|
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
2979
3027
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
2980
|
-
|
|
3028
|
+
// Deep research renders no assistant-role node at all: the thread is
|
|
3029
|
+
// conversation-turn sections, the prompt in the first and the report in a
|
|
3030
|
+
// later one (measured live - roles were ["user"] only while a research ran).
|
|
3031
|
+
// Fall back to the last turn that is NOT the user's, so such an answer is
|
|
3032
|
+
// readable instead of looking like "no answer" forever.
|
|
3033
|
+
const turnAnswers = assistantMessages.length > 0 ? [] : [...document.querySelectorAll('[data-testid^="conversation-turn"]')]
|
|
3034
|
+
.filter((turn) => !turn.querySelector('[data-message-author-role="user"]'))
|
|
3035
|
+
.map((turn) => ({ role: "assistant", text: (turn.innerText || "").trim(), modelSlug: undefined }))
|
|
3036
|
+
.filter((turn) => turn.text.length > 0);
|
|
3037
|
+
const effectiveAssistants = assistantMessages.length > 0 ? assistantMessages : turnAnswers;
|
|
3038
|
+
const assistant = effectiveAssistants.at(-1);
|
|
2981
3039
|
const buttons = [...document.querySelectorAll('button,[role="button"]')]
|
|
2982
3040
|
.filter((node) => !!(node.offsetWidth || node.offsetHeight || node.getClientRects().length))
|
|
2983
3041
|
.filter((node) => !node.closest(excludedTextSelector))
|
|
@@ -2996,7 +3054,7 @@ export function answerExpression() {
|
|
|
2996
3054
|
blockerScanTextSample: visibleTextOutsideMessages(blockerScanExcludedSelector).slice(0, 12000),
|
|
2997
3055
|
visibleButtonLabels: buttons,
|
|
2998
3056
|
generating: placeholder || Boolean(document.querySelector(${streamingSelector})) || buttons.some((label) => generatingControlPattern.test(label)),
|
|
2999
|
-
assistantMessageCount:
|
|
3057
|
+
assistantMessageCount: effectiveAssistants.length,
|
|
3000
3058
|
userMessageCount: userMessages.length,
|
|
3001
3059
|
// ChatGPT tags each assistant message with the model that produced it -
|
|
3002
3060
|
// the only ground truth for "did the Pro selection actually take".
|
package/dist/cli-pro.js
CHANGED
|
@@ -948,6 +948,10 @@ export async function runAskProCommand(rest, io) {
|
|
|
948
948
|
const name = selectionMetadata.project;
|
|
949
949
|
return name ? text.split(name).join("<project>") : text;
|
|
950
950
|
};
|
|
951
|
+
// Where the prompt actually landed beats where the caller aimed: with
|
|
952
|
+
// --new-chat there is no target url, and a blocker that started a run
|
|
953
|
+
// still has a thread worth handing back.
|
|
954
|
+
const blockedThread = blocker.thread ?? normalizedTargetUrl;
|
|
951
955
|
const persistedBlocker = {
|
|
952
956
|
...blocker,
|
|
953
957
|
message: redactProject(blocker.message),
|
|
@@ -965,7 +969,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
965
969
|
direction: "codex_to_chatgpt",
|
|
966
970
|
backend: "chatgpt-control",
|
|
967
971
|
task_id: task.id,
|
|
968
|
-
thread:
|
|
972
|
+
thread: blockedThread,
|
|
969
973
|
status: "blocked",
|
|
970
974
|
blocker: persistedBlocker,
|
|
971
975
|
warnings: []
|
|
@@ -977,7 +981,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
977
981
|
// Keep stdout machine-parseable for --json consumers on the blocked
|
|
978
982
|
// path too; the human-readable error still goes to stderr via throw.
|
|
979
983
|
if (jsonOutput) {
|
|
980
|
-
io.stdout(JSON.stringify({ task_id: task.id, status: "blocked", thread:
|
|
984
|
+
io.stdout(JSON.stringify({ task_id: task.id, status: "blocked", thread: blockedThread ?? null, answer: null, warnings: [], blocker }, null, 2));
|
|
981
985
|
}
|
|
982
986
|
throw new Error(formatBlockedConsultRecordedMessage(message, task.id, sourceCli, { cwd: targetCwd }));
|
|
983
987
|
}
|
|
@@ -1313,7 +1317,10 @@ export function browserSendBlockerFromError(error) {
|
|
|
1313
1317
|
code: blocker.code,
|
|
1314
1318
|
message: blocker.message,
|
|
1315
1319
|
retryable: blocker.retryable,
|
|
1316
|
-
...("next_step" in blocker && typeof blocker.next_step === "string" ? { next_step: blocker.next_step } : {})
|
|
1320
|
+
...("next_step" in blocker && typeof blocker.next_step === "string" ? { next_step: blocker.next_step } : {}),
|
|
1321
|
+
// A blocker that knows which thread the prompt landed in is the only
|
|
1322
|
+
// place that URL exists - keep it so callers get a link, not prose.
|
|
1323
|
+
...("thread" in blocker && typeof blocker.thread === "string" ? { thread: blocker.thread } : {})
|
|
1317
1324
|
};
|
|
1318
1325
|
}
|
|
1319
1326
|
const message = errorMessage(error);
|