@youdie006/prodex 0.22.0 → 0.23.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 +1 -1
- package/dist/chatgpt-browser.js +166 -8
- package/dist/cli-help.js +10 -0
- package/dist/cli.js +10 -0
- 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 report - the timeout rises to 30 minutes automatically; prodex presses the start control, waits out the run and returns the full report. The report is read from the conversation transcript rather than the page, because deep research renders inside a widget iframe that leaves the thread looking empty - which also means `prodex pro browser recover --target-url <thread>` fetches a research report that finished after a timeout), `--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.
|
|
38
|
+
The answer prints to your terminal and is saved under `.bridge/` for later (`prodex pro latest` re-prints it). It is read from the conversation transcript rather than scraped off the page, so markdown tables and fenced code arrive intact, citations keep their links, and a tab that drifts to another conversation mid-wait no longer costs you the answer; the rendered page stays as a fallback. 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 report - the timeout rises to 30 minutes automatically; prodex presses the start control, waits out the run and returns the full report. The report is read from the conversation transcript rather than the page, because deep research renders inside a widget iframe that leaves the thread looking empty - which also means `prodex pro browser recover --target-url <thread>` fetches a research report that finished after a timeout), `--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
|
@@ -1685,7 +1685,7 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1685
1685
|
return {
|
|
1686
1686
|
url,
|
|
1687
1687
|
title: "",
|
|
1688
|
-
answer: report.report.trim(),
|
|
1688
|
+
answer: resolveTranscriptCitations(report.report, report.references).trim(),
|
|
1689
1689
|
modelHints: [],
|
|
1690
1690
|
warnings: []
|
|
1691
1691
|
};
|
|
@@ -1750,6 +1750,26 @@ export async function recoverChatGptAnswerFromThread(options) {
|
|
|
1750
1750
|
warnings: []
|
|
1751
1751
|
};
|
|
1752
1752
|
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Read the answer from the conversation transcript, or undefined when it is not
|
|
1755
|
+
* there yet. The transcript trails the rendered stream by a beat, so callers
|
|
1756
|
+
* either poll it or fall back to the DOM text.
|
|
1757
|
+
*/
|
|
1758
|
+
async function readTranscriptAnswer(page, conversationId) {
|
|
1759
|
+
let transcript;
|
|
1760
|
+
try {
|
|
1761
|
+
transcript = await evaluateOnPage(page, transcriptAnswerExpression(conversationId), { timeoutMs: 30_000 });
|
|
1762
|
+
}
|
|
1763
|
+
catch {
|
|
1764
|
+
// Transcript unavailable (endpoint changed, transient failure): the DOM
|
|
1765
|
+
// reader still runs, so this never blocks a send.
|
|
1766
|
+
return undefined;
|
|
1767
|
+
}
|
|
1768
|
+
if (!transcript.ok || transcript.text.trim().length === 0)
|
|
1769
|
+
return undefined;
|
|
1770
|
+
const answer = resolveTranscriptCitations(transcript.text, transcript.references).trim();
|
|
1771
|
+
return answer.length > 0 ? { answer, modelSlug: transcript.modelSlug } : undefined;
|
|
1772
|
+
}
|
|
1753
1773
|
export async function sendChatGptPrompt(options) {
|
|
1754
1774
|
const port = resolveCdpPort(options.port);
|
|
1755
1775
|
const timeoutMs = options.timeoutMs ?? 90_000;
|
|
@@ -2029,6 +2049,23 @@ export async function sendChatGptPrompt(options) {
|
|
|
2029
2049
|
// silently, with a receipt (caught live). Nothing about that is recoverable
|
|
2030
2050
|
// after the fact, so the wait either stays on this thread or fails loudly.
|
|
2031
2051
|
const pinnedThreadUrl = finalState?.url;
|
|
2052
|
+
// Pin the CONVERSATION, not the tab. The transcript reader fetches by id, so
|
|
2053
|
+
// it keeps working when the tab wanders off the thread - which is exactly how
|
|
2054
|
+
// a finished answer was lost: the tab returned to the project page, the url
|
|
2055
|
+
// still matched the pin taken before ChatGPT rewrote it, and the DOM reader
|
|
2056
|
+
// sat on zero assistant messages until the budget ran out.
|
|
2057
|
+
let transcriptConversationId = pinnedThreadUrl ? conversationIdFromThreadUrl(pinnedThreadUrl) : undefined;
|
|
2058
|
+
const transcriptResult = (transcript) => {
|
|
2059
|
+
emitProgress("answered", `transcript (${transcript.answer.length} chars)`);
|
|
2060
|
+
return {
|
|
2061
|
+
url: finalState?.url ?? pinnedThreadUrl ?? "",
|
|
2062
|
+
title: finalState?.title ?? "",
|
|
2063
|
+
answer: transcript.answer,
|
|
2064
|
+
modelHints: finalState?.modelHints ?? [],
|
|
2065
|
+
...(transcript.modelSlug ? { modelSlug: transcript.modelSlug } : finalState?.modelSlug ? { modelSlug: finalState.modelSlug } : {}),
|
|
2066
|
+
warnings: [...sendWarnings, modelSelectionWarning(options.model, transcript.modelSlug || finalState?.modelSlug)].filter((warning) => Boolean(warning))
|
|
2067
|
+
};
|
|
2068
|
+
};
|
|
2032
2069
|
// Deep research never reaches the DOM answer wait below: the report is
|
|
2033
2070
|
// rendered by a widget app in an iframe, so the main frame stays empty even
|
|
2034
2071
|
// when the run has finished. Read the run out of the conversation transcript
|
|
@@ -2048,11 +2085,12 @@ export async function sendChatGptPrompt(options) {
|
|
|
2048
2085
|
continue;
|
|
2049
2086
|
}
|
|
2050
2087
|
if (lastState.ok && lastState.report.trim().length > 0) {
|
|
2051
|
-
|
|
2088
|
+
const report = resolveTranscriptCitations(lastState.report, lastState.references).trim();
|
|
2089
|
+
emitProgress("answered", `deep research report (${report.length} chars)`);
|
|
2052
2090
|
return {
|
|
2053
2091
|
url: pinnedThreadUrl ?? "",
|
|
2054
2092
|
title: finalState?.title ?? "",
|
|
2055
|
-
answer:
|
|
2093
|
+
answer: report,
|
|
2056
2094
|
modelHints: finalState?.modelHints ?? [],
|
|
2057
2095
|
...(finalState?.modelSlug ? { modelSlug: finalState.modelSlug } : {}),
|
|
2058
2096
|
warnings: sendWarnings
|
|
@@ -2077,7 +2115,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
2077
2115
|
await sleep(1000);
|
|
2078
2116
|
try {
|
|
2079
2117
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
2080
|
-
if (
|
|
2118
|
+
if (!transcriptConversationId && finalState?.url)
|
|
2119
|
+
transcriptConversationId = conversationIdFromThreadUrl(finalState.url);
|
|
2120
|
+
// The transcript is the same data the page renders, minus the rendering:
|
|
2121
|
+
// markdown instead of flattened innerText, an explicit finish state
|
|
2122
|
+
// instead of caret heuristics, and the model that actually answered.
|
|
2123
|
+
if (transcriptConversationId && !finalState.generating) {
|
|
2124
|
+
const transcript = await readTranscriptAnswer(page, transcriptConversationId);
|
|
2125
|
+
if (transcript)
|
|
2126
|
+
return transcriptResult(transcript);
|
|
2127
|
+
}
|
|
2128
|
+
// Only the DOM reader depends on which thread the tab is showing; once the
|
|
2129
|
+
// conversation id is known, a wandering tab is harmless.
|
|
2130
|
+
if (!transcriptConversationId && pinnedThreadUrl && finalState?.url && !chatGptUrlsReferToSameTarget(finalState.url, pinnedThreadUrl)) {
|
|
2081
2131
|
if (recoveredNavigations >= 2) {
|
|
2082
2132
|
throw new ChatGptBrowserBlockerError({
|
|
2083
2133
|
code: "thread_navigated_away",
|
|
@@ -2112,8 +2162,20 @@ export async function sendChatGptPrompt(options) {
|
|
|
2112
2162
|
// mid-stream, and the streaming caret renders as a literal trailing
|
|
2113
2163
|
// character that can outlive the stop button. The tracker requires extra
|
|
2114
2164
|
// confirmations for caret-suspect tails (see its doc comment).
|
|
2115
|
-
if (answerIsStable(finalState.answer, finalState.generating))
|
|
2165
|
+
if (answerIsStable(finalState.answer, finalState.generating)) {
|
|
2166
|
+
// The rendered answer settles a beat before the server transcript does.
|
|
2167
|
+
// Give the transcript that beat: it carries markdown (tables and fenced
|
|
2168
|
+
// code that innerText flattens) and the model that actually answered.
|
|
2169
|
+
if (transcriptConversationId) {
|
|
2170
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
2171
|
+
const transcript = await readTranscriptAnswer(page, transcriptConversationId);
|
|
2172
|
+
if (transcript)
|
|
2173
|
+
return transcriptResult(transcript);
|
|
2174
|
+
await sleep(1_500);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2116
2177
|
break;
|
|
2178
|
+
}
|
|
2117
2179
|
}
|
|
2118
2180
|
const completed = finalState;
|
|
2119
2181
|
if (completed && hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
|
|
@@ -2959,9 +3021,103 @@ export function deepResearchUnreadableBlocker(threadUrl) {
|
|
|
2959
3021
|
thread: threadUrl
|
|
2960
3022
|
};
|
|
2961
3023
|
}
|
|
3024
|
+
/**
|
|
3025
|
+
* The transcript is the same data the UI renders, minus the rendering: it
|
|
3026
|
+
* carries the answer as markdown (tables and fenced code survive, which
|
|
3027
|
+
* innerText flattens), an explicit finish state, and the model that actually
|
|
3028
|
+
* answered. Walk from `current_node` up the parents so a regenerated turn reads
|
|
3029
|
+
* the branch the UI is on, not an abandoned sibling.
|
|
3030
|
+
*/
|
|
3031
|
+
export function transcriptAnswerExpression(conversationId) {
|
|
3032
|
+
return `(async () => {
|
|
3033
|
+
const fail = (reason, extra) => Object.assign({ ok: false, reason, status: "", endTurn: false, isComplete: false, text: "", modelSlug: "", references: [] }, extra || {});
|
|
3034
|
+
let token = "";
|
|
3035
|
+
try {
|
|
3036
|
+
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
3037
|
+
if (!session.ok) return fail("session_http_" + session.status);
|
|
3038
|
+
const parsed = await session.json();
|
|
3039
|
+
token = (parsed && parsed.accessToken) || "";
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
return fail("session_error");
|
|
3042
|
+
}
|
|
3043
|
+
let conversation;
|
|
3044
|
+
try {
|
|
3045
|
+
const response = await fetch("/backend-api/conversation/" + ${JSON.stringify(conversationId)}, {
|
|
3046
|
+
credentials: "include",
|
|
3047
|
+
headers: token ? { Authorization: "Bearer " + token } : {}
|
|
3048
|
+
});
|
|
3049
|
+
if (!response.ok) return fail("conversation_http_" + response.status);
|
|
3050
|
+
conversation = await response.json();
|
|
3051
|
+
} catch (error) {
|
|
3052
|
+
return fail("conversation_error");
|
|
3053
|
+
}
|
|
3054
|
+
const mapping = (conversation && conversation.mapping) || {};
|
|
3055
|
+
const chain = [];
|
|
3056
|
+
let nodeId = conversation && conversation.current_node;
|
|
3057
|
+
let guard = 0;
|
|
3058
|
+
while (nodeId && mapping[nodeId] && guard < 2000) {
|
|
3059
|
+
guard += 1;
|
|
3060
|
+
if (mapping[nodeId].message) chain.push(mapping[nodeId].message);
|
|
3061
|
+
nodeId = mapping[nodeId].parent;
|
|
3062
|
+
}
|
|
3063
|
+
const message = chain.find(
|
|
3064
|
+
(entry) => entry && entry.author && entry.author.role === "assistant" && entry.content && entry.content.content_type === "text"
|
|
3065
|
+
);
|
|
3066
|
+
if (!message) return fail("no_assistant_message");
|
|
3067
|
+
const parts = (message.content.parts || []).filter((part) => typeof part === "string");
|
|
3068
|
+
const text = parts.join("");
|
|
3069
|
+
const metadata = message.metadata || {};
|
|
3070
|
+
const state = {
|
|
3071
|
+
status: message.status || "",
|
|
3072
|
+
endTurn: message.end_turn === true,
|
|
3073
|
+
isComplete: metadata.is_complete === true,
|
|
3074
|
+
text,
|
|
3075
|
+
modelSlug: metadata.model_slug || "",
|
|
3076
|
+
references: Array.isArray(metadata.content_references) ? metadata.content_references : []
|
|
3077
|
+
};
|
|
3078
|
+
if (state.status !== "finished_successfully" || !state.endTurn) return fail("answer_not_finished", state);
|
|
3079
|
+
if (!text) return fail("answer_empty", state);
|
|
3080
|
+
return Object.assign({ ok: true, reason: "" }, state);
|
|
3081
|
+
})()`;
|
|
3082
|
+
}
|
|
3083
|
+
// ChatGPT marks citations with private-use delimiters (U+E200 opens, U+E202
|
|
3084
|
+
// separates, U+E201 closes) and keeps the real sources in content_references.
|
|
3085
|
+
const CITATION_MARKER_PATTERN = /\uE200[^\uE200-\uE206]*(?:[\uE202\uE204-\uE206][^\uE200-\uE206]*)*[\uE201\uE203]/g;
|
|
3086
|
+
/**
|
|
3087
|
+
* Turn those markers into ordinary markdown links, so a saved answer keeps the
|
|
3088
|
+
* sources instead of the private-use noise (or, as in the rendered DOM, nothing
|
|
3089
|
+
* at all). Markers with no matching reference are dropped.
|
|
3090
|
+
*/
|
|
3091
|
+
export function resolveTranscriptCitations(text, references = []) {
|
|
3092
|
+
const byMarker = new Map();
|
|
3093
|
+
for (const reference of references) {
|
|
3094
|
+
if (reference && typeof reference.matched_text === "string" && reference.matched_text.length > 0) {
|
|
3095
|
+
byMarker.set(reference.matched_text, reference);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
const linksFor = (reference) => {
|
|
3099
|
+
const seen = new Set();
|
|
3100
|
+
const links = [];
|
|
3101
|
+
for (const item of reference?.items ?? []) {
|
|
3102
|
+
const url = item?.url;
|
|
3103
|
+
if (!url || seen.has(url))
|
|
3104
|
+
continue;
|
|
3105
|
+
seen.add(url);
|
|
3106
|
+
links.push(`[${(item.title || url).trim()}](${url})`);
|
|
3107
|
+
}
|
|
3108
|
+
return links.length > 0 ? ` ${links.join(" ")}` : "";
|
|
3109
|
+
};
|
|
3110
|
+
let resolved = text;
|
|
3111
|
+
for (const [marker, reference] of byMarker) {
|
|
3112
|
+
resolved = resolved.split(marker).join(linksFor(reference));
|
|
3113
|
+
}
|
|
3114
|
+
// Anything still delimited had no reference to restore: strip it so private-use
|
|
3115
|
+
// characters never reach a receipt.
|
|
3116
|
+
return resolved.replace(CITATION_MARKER_PATTERN, "");
|
|
3117
|
+
}
|
|
2962
3118
|
export function deepResearchReportExpression(conversationId) {
|
|
2963
3119
|
return `(async () => {
|
|
2964
|
-
const fail = (reason, status) => ({ ok: false, reason, status: status || "", report: "", chars: 0 });
|
|
3120
|
+
const fail = (reason, status) => ({ ok: false, reason, status: status || "", report: "", chars: 0, references: [] });
|
|
2965
3121
|
let token = "";
|
|
2966
3122
|
try {
|
|
2967
3123
|
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
@@ -2994,10 +3150,12 @@ export function deepResearchReportExpression(conversationId) {
|
|
|
2994
3150
|
return fail("widget_state_unparsable");
|
|
2995
3151
|
}
|
|
2996
3152
|
const status = (state && state.status) || "";
|
|
2997
|
-
const
|
|
3153
|
+
const message = (state && state.report_message) || null;
|
|
3154
|
+
const parts = message && message.content && message.content.parts;
|
|
2998
3155
|
const report = Array.isArray(parts) ? parts.filter((part) => typeof part === "string").join("") : "";
|
|
3156
|
+
const references = message && message.metadata && Array.isArray(message.metadata.content_references) ? message.metadata.content_references : [];
|
|
2999
3157
|
if (!report) return fail("report_not_ready", status);
|
|
3000
|
-
return { ok: true, reason: "", status, report, chars: report.length };
|
|
3158
|
+
return { ok: true, reason: "", status, report, chars: report.length, references };
|
|
3001
3159
|
})()`;
|
|
3002
3160
|
}
|
|
3003
3161
|
/**
|
package/dist/cli-help.js
CHANGED
|
@@ -257,6 +257,14 @@ export function printProBrowserHelp(stdout, sourceCli) {
|
|
|
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]";
|
|
260
|
+
const projectsUsage = sourceCli
|
|
261
|
+
? `${cli} pro browser projects${sourceCliOption} [--port 9333] [--timeout-ms 15000] # read-only: exact sidebar project names`
|
|
262
|
+
: "prodex pro browser projects [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only: exact sidebar project names";
|
|
263
|
+
// A send that outlives its budget is not a lost answer, but only if agents
|
|
264
|
+
// know this exists - and this help is where onboarding sends them.
|
|
265
|
+
const recoverUsage = sourceCli
|
|
266
|
+
? `${cli} pro browser recover${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # fetch a finished answer (deep research reports too) from a thread whose send timed out`
|
|
267
|
+
: "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] # fetch a finished answer (deep research reports too) from a thread whose send timed out";
|
|
260
268
|
stdout(`${cli} pro browser
|
|
261
269
|
|
|
262
270
|
Commands:
|
|
@@ -264,7 +272,9 @@ Commands:
|
|
|
264
272
|
${checkUsage}
|
|
265
273
|
${smokeUsage}
|
|
266
274
|
${modelsUsage}
|
|
275
|
+
${projectsUsage}
|
|
267
276
|
${askUsage}
|
|
277
|
+
${recoverUsage}
|
|
268
278
|
|
|
269
279
|
Visible-browser sends require a manual browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers.
|
|
270
280
|
Model/project selection (ask):
|
package/dist/cli.js
CHANGED
|
@@ -322,6 +322,16 @@ repo: ${cwd}
|
|
|
322
322
|
${cli} pro browser check${sourceCliOption} --cwd ${quotedCwd}
|
|
323
323
|
${cli} pro browser smoke${sourceCliOption} --cwd ${quotedCwd}
|
|
324
324
|
Sharing the browser with other agents? Sends queue behind an in-flight response automatically; pass --busy-wait-ms 0 to fail fast instead.
|
|
325
|
+
Hand ChatGPT a real file - the only way it can open a pdf, pptx, xlsx or image:
|
|
326
|
+
${cli} ask --cwd ${quotedCwd} --attach deck.pptx "Review slides 40-60" # uploads the file itself
|
|
327
|
+
${cli} ask --cwd ${quotedCwd} --file notes.md "Summarize" # pastes a text file's CONTENTS into the prompt
|
|
328
|
+
Both are restricted to paths inside the repo. Repeat either flag for several files.
|
|
329
|
+
Turn on a composer tool for one send:
|
|
330
|
+
${cli} ask --cwd ${quotedCwd} --tool web-search "What shipped in Node 24?" # current facts with sources
|
|
331
|
+
${cli} ask --cwd ${quotedCwd} --tool deep-research "Compare managed Postgres providers" # full browsed report; runs ~10 minutes, budget rises to 30
|
|
332
|
+
Any other label the composer menu shows works too, so a tool ChatGPT adds later needs no prodex release.
|
|
333
|
+
A send that outlives its budget did not lose the answer - ChatGPT usually finishes after prodex stops waiting:
|
|
334
|
+
${cli} pro browser recover --target-url <thread-url> --cwd ${quotedCwd} # fetches the finished answer (deep research reports too) and records it
|
|
325
335
|
|
|
326
336
|
2. Let coding agents consult ChatGPT (stdio MCP: Claude, Codex, Cursor, ...):
|
|
327
337
|
${cli} claude config --cwd ${quotedCwd}${sourceCliOption}
|