@youdie006/prodex 0.19.1 → 0.20.0
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 +223 -11
- package/dist/cli-args.js +2 -0
- package/dist/cli-help.js +8 -8
- package/dist/cli-pro.js +20 -0
- package/dist/mcp.js +5 -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
|
|
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. 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
|
@@ -1065,6 +1065,26 @@ async function verifiedClickAt(cdp, x, y, label) {
|
|
|
1065
1065
|
await cdp.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
|
|
1066
1066
|
await cdp.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
|
|
1067
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Whether the model picker BUTTON already advertises the requested model, so
|
|
1070
|
+
* the menu never has to be opened. ChatGPT moved the models behind a "Model"
|
|
1071
|
+
* submenu (the top level is now Advanced / Model / Effort), which broke the
|
|
1072
|
+
* flat radio lookup with "Pro option not found in the model menu" - while the
|
|
1073
|
+
* button itself read "Pro, 5 of 5". Selecting what is already selected is
|
|
1074
|
+
* pointless work that a UI change can only break.
|
|
1075
|
+
*/
|
|
1076
|
+
export function modelButtonAlreadyShows(requestedModel, buttonLabel) {
|
|
1077
|
+
if (!requestedModel || !buttonLabel)
|
|
1078
|
+
return false;
|
|
1079
|
+
const wanted = requestedModel.trim().toLowerCase();
|
|
1080
|
+
if (!wanted)
|
|
1081
|
+
return false;
|
|
1082
|
+
// The label carries decoration ("Pro, 5 of 5.", "GPT-5.6 Pro"), so match the
|
|
1083
|
+
// model name as a WORD inside it rather than by equality.
|
|
1084
|
+
const label = buttonLabel.trim().toLowerCase();
|
|
1085
|
+
const escaped = wanted.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1086
|
+
return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`).test(label);
|
|
1087
|
+
}
|
|
1068
1088
|
export function modelButtonRectExpression() {
|
|
1069
1089
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
1070
1090
|
const c = document.querySelector('#prompt-textarea,[contenteditable="true"],textarea');
|
|
@@ -1075,7 +1095,10 @@ export function modelButtonRectExpression() {
|
|
|
1075
1095
|
return /\\S/.test(t) && !/파일|첨부|받아쓰기|음성|dictation|attach|file|voice|record|search|mic/i.test(t + aria);
|
|
1076
1096
|
});
|
|
1077
1097
|
if (!b) return { ok: false, reason: "model selector button not found" };
|
|
1078
|
-
|
|
1098
|
+
// Return the label too: the caller compares it against the requested model
|
|
1099
|
+
// to skip opening the menu when it is already selected.
|
|
1100
|
+
const label = ((b.getAttribute("aria-label") || b.textContent || "").trim().split(String.fromCharCode(10))[0] || "").trim();
|
|
1101
|
+
return { ...clickPoint(b), label };
|
|
1079
1102
|
})()`;
|
|
1080
1103
|
}
|
|
1081
1104
|
export function menuItemRectExpression(label) {
|
|
@@ -1236,6 +1259,13 @@ async function selectModelReasoning(cdp, options) {
|
|
|
1236
1259
|
if (!button.ok || button.x === undefined || button.y === undefined) {
|
|
1237
1260
|
throw new Error(button.reason ?? "Could not open the ChatGPT model selector");
|
|
1238
1261
|
}
|
|
1262
|
+
// Skip the menu entirely when the picker already shows the requested model:
|
|
1263
|
+
// it is the same end state, and it survives ChatGPT reshuffling the menu
|
|
1264
|
+
// (which it did - the models moved behind a "Model" submenu and every
|
|
1265
|
+
// --model Pro send started failing).
|
|
1266
|
+
if (options.model && !options.proMode && !options.effort && modelButtonAlreadyShows(options.model, button.label)) {
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1239
1269
|
try {
|
|
1240
1270
|
// The hover-verified click can be transiently refused right after a page
|
|
1241
1271
|
// transition (measured live: the just-closed create-project modal's
|
|
@@ -1729,7 +1759,14 @@ export async function sendChatGptPrompt(options) {
|
|
|
1729
1759
|
// into; a --project/--project-new hop lands on a page with its own counts.
|
|
1730
1760
|
beforeSubmit = await evaluateOnPage(page, answerExpression());
|
|
1731
1761
|
dbgSend(`baseline url=${beforeSubmit.url} user=${beforeSubmit.userMessageCount} assistant=${beforeSubmit.assistantMessageCount}`);
|
|
1732
|
-
|
|
1762
|
+
// Attach BEFORE typing: the upload is the slow part, and a file that
|
|
1763
|
+
// arrives after the prompt is submitted is a file ChatGPT never saw.
|
|
1764
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
1765
|
+
emitProgress("selecting", `uploading ${options.attachments.length} file(s)`);
|
|
1766
|
+
const uploaded = await attachFilesToComposer(cdp, options.attachments);
|
|
1767
|
+
emitProgress("selecting", `attached ${uploaded.attached.join(", ")}`);
|
|
1768
|
+
}
|
|
1769
|
+
await insertComposerTextViaCdp(cdp, options.prompt, page);
|
|
1733
1770
|
// The send button renders asynchronously after the prompt lands. Poll for it
|
|
1734
1771
|
// BEFORE submitting so (a) submitButtonFound reflects whether the control
|
|
1735
1772
|
// actually EXISTS - otherwise a successful Enter-key submit skips the fallback
|
|
@@ -1738,7 +1775,12 @@ export async function sendChatGptPrompt(options) {
|
|
|
1738
1775
|
// live: submitExpression finds data-testid="send-button" fine, yet the
|
|
1739
1776
|
// timeout error blamed a UI change) - and (b) we never press Enter before the
|
|
1740
1777
|
// composer is submit-ready.
|
|
1741
|
-
|
|
1778
|
+
// With an attachment, ChatGPT keeps the send control disabled while it
|
|
1779
|
+
// ingests the file server-side; 3s expired mid-ingest and the send never
|
|
1780
|
+
// posted (measured live on a markdown attachment: the button was enabled
|
|
1781
|
+
// and clickable a moment after prodex gave up).
|
|
1782
|
+
const submitReadyBudgetMs = options.attachments && options.attachments.length > 0 ? 120_000 : 3_000;
|
|
1783
|
+
submitButtonFound = await waitForExpressionTrue(cdp, `(${submitExpression()}).ok === true`, submitReadyBudgetMs);
|
|
1742
1784
|
// Submit. Prefer the Enter key: it goes to the focused composer and does
|
|
1743
1785
|
// not depend on coordinates, whereas the send button moves ~100px as the
|
|
1744
1786
|
// composer grows after the prompt lands, so a click at captured coordinates
|
|
@@ -2263,6 +2305,147 @@ export function prepareComposerExpression() {
|
|
|
2263
2305
|
// just that it is non-empty): a failed clear would leave stale text prepended,
|
|
2264
2306
|
// silently submitting a contaminated prompt. Whitespace is collapsed on both
|
|
2265
2307
|
// sides because ProseMirror round-trips newlines as extra blank lines.
|
|
2308
|
+
/**
|
|
2309
|
+
* Insert the whole prompt with ONE in-page execCommand("insertText"). The
|
|
2310
|
+
* editor applies it as a single input event, so - unlike a chunked
|
|
2311
|
+
* Input.insertText sequence - nothing can interleave at a boundary and the
|
|
2312
|
+
* text lands byte-for-byte. Used for prompts too large to push through
|
|
2313
|
+
* Input.insertText in one CDP command.
|
|
2314
|
+
*/
|
|
2315
|
+
export function insertComposerTextInPageExpression(text) {
|
|
2316
|
+
const textJson = JSON.stringify(text);
|
|
2317
|
+
return `(() => {
|
|
2318
|
+
${composerExpressionHelpers()}
|
|
2319
|
+
const el = findChatGptComposerCandidate();
|
|
2320
|
+
if (!el) return { ok: false, reason: "No visible composer" };
|
|
2321
|
+
el.focus();
|
|
2322
|
+
if ("value" in el) {
|
|
2323
|
+
el.value = ${textJson};
|
|
2324
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2325
|
+
return { ok: true };
|
|
2326
|
+
}
|
|
2327
|
+
const selection = window.getSelection();
|
|
2328
|
+
const all = document.createRange();
|
|
2329
|
+
all.selectNodeContents(el);
|
|
2330
|
+
selection.removeAllRanges();
|
|
2331
|
+
selection.addRange(all);
|
|
2332
|
+
document.execCommand("delete");
|
|
2333
|
+
const inserted = document.execCommand("insertText", false, ${textJson});
|
|
2334
|
+
if (!inserted) return { ok: false, reason: "The ChatGPT composer refused the prompt text" };
|
|
2335
|
+
return { ok: true };
|
|
2336
|
+
})()`;
|
|
2337
|
+
}
|
|
2338
|
+
// ---------------------------------------------------------------------------
|
|
2339
|
+
// Attachments (real file upload)
|
|
2340
|
+
//
|
|
2341
|
+
// `--file` inlines a file's TEXT into the prompt; this uploads the file itself
|
|
2342
|
+
// through the composer's file input, which is the only way to hand ChatGPT a
|
|
2343
|
+
// pdf/pptx/image and let it parse the original. CDP's DOM.setFileInputFiles
|
|
2344
|
+
// sets the input without any file dialog.
|
|
2345
|
+
// ---------------------------------------------------------------------------
|
|
2346
|
+
/**
|
|
2347
|
+
* The composer's general file input. Measured live: ChatGPT renders three
|
|
2348
|
+
* inputs - one general (accept="") plus two accept="image/*" - and attaching a
|
|
2349
|
+
* document to an image-only input silently does nothing.
|
|
2350
|
+
*/
|
|
2351
|
+
export function composerFileInputSelector() {
|
|
2352
|
+
return 'input[type="file"]:not([accept*="image"])';
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Which of the expected attachments the composer shows, and whether one is
|
|
2356
|
+
* still uploading.
|
|
2357
|
+
*
|
|
2358
|
+
* Measured live: a DOCUMENT chip carries its filename in visible text, but an
|
|
2359
|
+
* IMAGE renders as a blob: thumbnail whose only filename is on the remove
|
|
2360
|
+
* button's aria-label ("Remove file 1: cli-banner.png"). Reading innerText
|
|
2361
|
+
* alone therefore reported a perfectly good image upload as never accepted.
|
|
2362
|
+
*/
|
|
2363
|
+
export function attachmentStateExpression(fileNames) {
|
|
2364
|
+
const namesJson = JSON.stringify(fileNames);
|
|
2365
|
+
return `(() => {
|
|
2366
|
+
const names = ${namesJson};
|
|
2367
|
+
const text = document.body ? document.body.innerText || "" : "";
|
|
2368
|
+
const labels = [...document.querySelectorAll('[aria-label],[title],[alt]')]
|
|
2369
|
+
.map((el) => (el.getAttribute("aria-label") || el.getAttribute("title") || el.getAttribute("alt") || ""))
|
|
2370
|
+
.join(String.fromCharCode(10));
|
|
2371
|
+
const haystack = text + String.fromCharCode(10) + labels;
|
|
2372
|
+
const present = names.filter((name) => haystack.includes(name));
|
|
2373
|
+
const attachedFiles = [...document.querySelectorAll('input[type="file"]')]
|
|
2374
|
+
.reduce((total, el) => total + (el.files ? el.files.length : 0), 0);
|
|
2375
|
+
// A visible progressbar means a file is still going up; sending now would
|
|
2376
|
+
// post the prompt without it.
|
|
2377
|
+
const uploading = document.querySelectorAll('[role="progressbar"]').length > 0;
|
|
2378
|
+
return { ok: true, present, attachedFiles, uploading };
|
|
2379
|
+
})()`;
|
|
2380
|
+
}
|
|
2381
|
+
/** How many attachments are already sitting in the composer (read-only). */
|
|
2382
|
+
export function attachmentPresenceExpression() {
|
|
2383
|
+
return `(() => {
|
|
2384
|
+
const buttons = [...document.querySelectorAll('button[aria-label]')].filter((b) =>
|
|
2385
|
+
/remove file|파일 제거|첨부 제거/i.test(b.getAttribute("aria-label") || "")
|
|
2386
|
+
);
|
|
2387
|
+
return { ok: true, removed: buttons.length };
|
|
2388
|
+
})()`;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Attach files to the composer and wait until ChatGPT has taken them. The
|
|
2392
|
+
* files must be readable by the BROWSER process, so a browser on another
|
|
2393
|
+
* machine (or another container) cannot see the caller's paths - that shows up
|
|
2394
|
+
* as the attachment never appearing, which this reports as such.
|
|
2395
|
+
*/
|
|
2396
|
+
export async function attachFilesToComposer(cdp, absolutePaths, options = {}) {
|
|
2397
|
+
if (absolutePaths.length === 0)
|
|
2398
|
+
return { attached: [] };
|
|
2399
|
+
await cdp.send("DOM.enable");
|
|
2400
|
+
// Drop anything a previous (failed) send left attached: it would ride along
|
|
2401
|
+
// with this prompt, which is exactly the kind of silent contamination the
|
|
2402
|
+
// composer text check already guards against. This must happen BEFORE the
|
|
2403
|
+
// input node is resolved - removing a chip re-renders the composer and
|
|
2404
|
+
// replaces the input element, and setting files on the old (detached) node
|
|
2405
|
+
// silently does nothing (measured live: the file input held the file while
|
|
2406
|
+
// the UI showed no attachment at all).
|
|
2407
|
+
// Leftover attachments from a previous (failed) send would ride along with
|
|
2408
|
+
// this prompt. Clearing them by clicking the remove buttons WEDGES the
|
|
2409
|
+
// composer: measured live, after a removal the file input accepts files
|
|
2410
|
+
// (input.files becomes 1) while the UI never renders the chip again, and
|
|
2411
|
+
// every later attach in that tab silently does nothing. A reload is the only
|
|
2412
|
+
// reliable reset, and it costs a few seconds only when there is something to
|
|
2413
|
+
// clear.
|
|
2414
|
+
const stale = await cdp.evaluate(attachmentPresenceExpression());
|
|
2415
|
+
if ((stale?.removed ?? 0) > 0) {
|
|
2416
|
+
await cdp.evaluate("location.reload()");
|
|
2417
|
+
await sleep(6_000);
|
|
2418
|
+
const settleDeadline = Date.now() + 15_000;
|
|
2419
|
+
for (;;) {
|
|
2420
|
+
const ready = await cdp.evaluate(composerTextStateExpression());
|
|
2421
|
+
if (ready?.ok || Date.now() >= settleDeadline)
|
|
2422
|
+
break;
|
|
2423
|
+
await sleep(500);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const document = await cdp.send("DOM.getDocument", { depth: -1, pierce: true });
|
|
2427
|
+
const rootNodeId = document.result?.root?.nodeId;
|
|
2428
|
+
if (rootNodeId === undefined)
|
|
2429
|
+
throw new Error("Could not read the ChatGPT page DOM to attach files.");
|
|
2430
|
+
const input = await cdp.send("DOM.querySelector", { nodeId: rootNodeId, selector: composerFileInputSelector() });
|
|
2431
|
+
const inputNodeId = input.result?.nodeId;
|
|
2432
|
+
if (!inputNodeId) {
|
|
2433
|
+
throw new Error("The ChatGPT composer has no file input to attach to. Open a normal chat (not a shared or read-only view) and retry.");
|
|
2434
|
+
}
|
|
2435
|
+
await cdp.send("DOM.setFileInputFiles", { files: absolutePaths, nodeId: inputNodeId });
|
|
2436
|
+
const fileNames = absolutePaths.map((file) => path.basename(file));
|
|
2437
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
2438
|
+
let lastPresent = [];
|
|
2439
|
+
while (Date.now() < deadline) {
|
|
2440
|
+
await sleep(1_000);
|
|
2441
|
+
const state = await cdp.evaluate(attachmentStateExpression(fileNames));
|
|
2442
|
+
lastPresent = state?.present ?? [];
|
|
2443
|
+
if (lastPresent.length === fileNames.length && !state?.uploading)
|
|
2444
|
+
return { attached: lastPresent };
|
|
2445
|
+
}
|
|
2446
|
+
const missing = fileNames.filter((name) => !lastPresent.includes(name));
|
|
2447
|
+
throw new Error(`ChatGPT did not finish accepting ${missing.length > 0 ? missing.join(", ") : fileNames.join(", ")} within the upload budget. The file must be readable by the browser process (same machine), and ChatGPT enforces its own size and type limits.`);
|
|
2448
|
+
}
|
|
2266
2449
|
export function composerTextStateExpression(expectedText) {
|
|
2267
2450
|
const expectedJson = JSON.stringify(expectedText ?? null);
|
|
2268
2451
|
return `(() => {
|
|
@@ -2283,7 +2466,7 @@ export function composerTextStateExpression(expectedText) {
|
|
|
2283
2466
|
// Focus the composer, clear any leftover text submit-safely, type the prompt
|
|
2284
2467
|
// with native CDP input so ProseMirror registers it, then verify the composer
|
|
2285
2468
|
// holds exactly the prompt.
|
|
2286
|
-
async function insertComposerTextViaCdp(cdp, text) {
|
|
2469
|
+
async function insertComposerTextViaCdp(cdp, text, page) {
|
|
2287
2470
|
const prepared = await cdp.evaluate(prepareComposerExpression());
|
|
2288
2471
|
if (!prepared.ok)
|
|
2289
2472
|
throw new Error(prepared.reason ?? "Could not focus the ChatGPT composer");
|
|
@@ -2299,13 +2482,42 @@ async function insertComposerTextViaCdp(cdp, text) {
|
|
|
2299
2482
|
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 });
|
|
2300
2483
|
await sleep(100);
|
|
2301
2484
|
}
|
|
2302
|
-
//
|
|
2303
|
-
//
|
|
2304
|
-
//
|
|
2305
|
-
//
|
|
2306
|
-
//
|
|
2307
|
-
|
|
2308
|
-
|
|
2485
|
+
// Insertion path, chosen by size:
|
|
2486
|
+
//
|
|
2487
|
+
// Short prompts go through Input.insertText - real key-level input events,
|
|
2488
|
+
// which is what ChatGPT's composer is built for.
|
|
2489
|
+
//
|
|
2490
|
+
// Long prompts do NOT. One giant insertText stalls ProseMirror past the CDP
|
|
2491
|
+
// command timeout, and splitting it into chunks corrupts the text at the
|
|
2492
|
+
// chunk boundaries: measured live on a 95 KB prompt, the composer ended up
|
|
2493
|
+
// the right LENGTH but with content shifted from the first boundary on
|
|
2494
|
+
// (first divergence at 3,938 chars with a 4,000-char chunk size), which is
|
|
2495
|
+
// what surfaced to users as "Composer text did not match after insertion".
|
|
2496
|
+
// Both failures come from crossing the CDP boundary mid-edit, so large text
|
|
2497
|
+
// is inserted by a single in-page execCommand instead: the editor applies it
|
|
2498
|
+
// as one input event and nothing can interleave. Measured: 67 KB inserted in
|
|
2499
|
+
// ~20s, verified byte-for-byte.
|
|
2500
|
+
if (text.length <= COMPOSER_INSERT_CHUNK_CHARS) {
|
|
2501
|
+
await cdp.send("Input.insertText", { text });
|
|
2502
|
+
}
|
|
2503
|
+
else {
|
|
2504
|
+
// A 67 KB insert measured ~20s in the page, which the default 20s CDP
|
|
2505
|
+
// command budget would cut off, so this one call gets its own connection
|
|
2506
|
+
// with a size-scaled budget instead of loosening the budget for every
|
|
2507
|
+
// command on the shared connection.
|
|
2508
|
+
const budgetMs = Math.max(60_000, Math.ceil(text.length / 1_000) * 1_000);
|
|
2509
|
+
const slowCdp = page ? await connectCdp(page.webSocketDebuggerUrl, budgetMs) : undefined;
|
|
2510
|
+
try {
|
|
2511
|
+
const target = slowCdp ?? cdp;
|
|
2512
|
+
if (slowCdp)
|
|
2513
|
+
await slowCdp.send("Runtime.enable");
|
|
2514
|
+
const inserted = await target.evaluate(insertComposerTextInPageExpression(text));
|
|
2515
|
+
if (!inserted?.ok)
|
|
2516
|
+
throw new Error(inserted?.reason ?? "Could not insert the prompt into the ChatGPT composer");
|
|
2517
|
+
}
|
|
2518
|
+
finally {
|
|
2519
|
+
slowCdp?.close();
|
|
2520
|
+
}
|
|
2309
2521
|
}
|
|
2310
2522
|
await sleep(200);
|
|
2311
2523
|
const state = await cdp.evaluate(composerTextStateExpression(text));
|
package/dist/cli-args.js
CHANGED
|
@@ -261,6 +261,8 @@ export const ASK_PRO_SELECTION_VALUE_FLAGS = ["--project", "--project-new", "--m
|
|
|
261
261
|
export const ASK_PRO_VALUE_FLAGS = new Set([
|
|
262
262
|
"--cwd",
|
|
263
263
|
"--file",
|
|
264
|
+
// Upload the file itself (pdf/pptx/image) instead of inlining its text.
|
|
265
|
+
"--attach",
|
|
264
266
|
"--port",
|
|
265
267
|
"--timeout-ms",
|
|
266
268
|
"--busy-wait-ms",
|
package/dist/cli-help.js
CHANGED
|
@@ -17,7 +17,7 @@ First-time setup:
|
|
|
17
17
|
|
|
18
18
|
Ask / consult commands:
|
|
19
19
|
prodex ask [same flags as pro browser ask] "prompt" # top-level shortcut for pro browser ask
|
|
20
|
-
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] "prompt" # dry-run preview
|
|
20
|
+
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] [--attach path] "prompt" # dry-run preview
|
|
21
21
|
prodex pro debate-prompt [--topic "..."] [--rounds 2] [--source-cli /absolute/path/to/dist/cli.js] # print an agent prompt for a structured GPT Pro debate
|
|
22
22
|
prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--profile-dir path] [--port 9333] [--url https://chatgpt.com/...] [--launch-timeout-ms 5000] [--wait|--no-wait] [--headless|--minimized|--virtual-display] [--wait-timeout-ms 300000] # preview/open visible browser login
|
|
23
23
|
prodex pro browser help [--source-cli /absolute/path/to/dist/cli.js]
|
|
@@ -26,14 +26,14 @@ 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] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--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] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
30
30
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
31
31
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
32
32
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
33
33
|
|
|
34
34
|
Bridge ledger (durable tasks/results/receipts/sessions under .bridge/):
|
|
35
35
|
prodex init [--cwd /absolute/path/to/repo]
|
|
36
|
-
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt" [--repo-id id] [--file path]
|
|
36
|
+
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt" [--repo-id id] [--file path] [--attach path]
|
|
37
37
|
prodex tasks list [--status new|claimed|done|blocked] [--cwd /absolute/path/to/repo] [--json]
|
|
38
38
|
prodex tasks show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
39
39
|
prodex tasks claim <task-id> [--cwd /absolute/path/to/repo] [--by codex]
|
|
@@ -159,14 +159,14 @@ export function printProHelp(stdout) {
|
|
|
159
159
|
stdout(`prodex pro
|
|
160
160
|
|
|
161
161
|
Commands:
|
|
162
|
-
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] "prompt"
|
|
162
|
+
prodex pro ask [--dry-run] [--cwd /absolute/path/to/repo] [--file path] [--attach path] "prompt"
|
|
163
163
|
prodex pro debate-prompt [--topic "..."] [--rounds 2] [--source-cli /absolute/path/to/dist/cli.js]
|
|
164
164
|
prodex pro browser help [--source-cli /absolute/path/to/dist/cli.js]
|
|
165
165
|
prodex pro browser login [--cwd /absolute/path/to/repo] [--dry-run] [--source-cli /absolute/path/to/dist/cli.js] [--launch-timeout-ms 5000] [--wait|--no-wait] [--headless|--minimized|--virtual-display] [--wait-timeout-ms 300000]
|
|
166
166
|
prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
167
167
|
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
168
168
|
prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
|
|
169
|
-
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt"
|
|
169
|
+
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt"
|
|
170
170
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
171
171
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
|
172
172
|
prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
@@ -203,7 +203,7 @@ export function printTasksHelp(stdout) {
|
|
|
203
203
|
stdout(`prodex tasks
|
|
204
204
|
|
|
205
205
|
Commands:
|
|
206
|
-
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt" [--repo-id id] [--file path]
|
|
206
|
+
prodex tasks create [--cwd /absolute/path/to/repo] --title "Title" --prompt "Prompt" [--repo-id id] [--file path] [--attach path]
|
|
207
207
|
prodex tasks list [--status new|claimed|done|blocked] [--cwd /absolute/path/to/repo] [--json]
|
|
208
208
|
prodex tasks show <task-id|latest> [--cwd /absolute/path/to/repo]
|
|
209
209
|
prodex tasks claim <task-id> [--cwd /absolute/path/to/repo] [--by codex]
|
|
@@ -252,8 +252,8 @@ export function printProBrowserHelp(stdout, sourceCli) {
|
|
|
252
252
|
: "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
|
|
253
253
|
const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"]';
|
|
254
254
|
const askUsage = sourceCli
|
|
255
|
-
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`
|
|
256
|
-
: `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`;
|
|
255
|
+
? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] ${selectionUsage} "prompt"`
|
|
256
|
+
: `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] ${selectionUsage} "prompt"`;
|
|
257
257
|
const modelsUsage = sourceCli
|
|
258
258
|
? `${cli} pro browser models${sourceCliOption} [--port 9333] [--timeout-ms 15000]`
|
|
259
259
|
: "prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000]";
|
package/dist/cli-pro.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
1
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { buildDryRunBundle } from "./bundle.js";
|
|
@@ -722,6 +723,23 @@ export async function runAskProCommand(rest, io) {
|
|
|
722
723
|
}
|
|
723
724
|
return rel;
|
|
724
725
|
});
|
|
726
|
+
// --attach UPLOADS the file (pdf, pptx, image) instead of inlining its
|
|
727
|
+
// text like --file. Same escape guard: an agent must not be able to upload
|
|
728
|
+
// ~/.ssh or anything else outside the repo to a chat.
|
|
729
|
+
const attachments = readRepeatedFlag(parsedAskPro.optionArgs, "--attach").map((file) => {
|
|
730
|
+
const absolute = path.resolve(targetCwd, file);
|
|
731
|
+
const rel = path.relative(targetCwd, absolute);
|
|
732
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
733
|
+
throw new Error(`--attach "${file}" is outside the repo root (${targetCwd}). Pass a path inside the repo, or point --cwd at that repo.`);
|
|
734
|
+
}
|
|
735
|
+
if (!existsSync(absolute) || !statSync(absolute).isFile()) {
|
|
736
|
+
throw new Error(`--attach "${file}" is not a readable file (looked at ${absolute}).`);
|
|
737
|
+
}
|
|
738
|
+
return absolute;
|
|
739
|
+
});
|
|
740
|
+
if (attachments.length > 0 && !hasSendMode) {
|
|
741
|
+
throw new Error("--attach only applies when sending (`prodex pro browser ask`); the dry-run preview cannot upload files.");
|
|
742
|
+
}
|
|
725
743
|
const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
|
|
726
744
|
const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
|
|
727
745
|
if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
@@ -880,6 +898,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
880
898
|
prompt: bundle.text,
|
|
881
899
|
targetUrl: normalizedTargetUrl,
|
|
882
900
|
timeoutMs: browserTimeoutMs,
|
|
901
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
883
902
|
...(newChat ? { newChat: true } : {}),
|
|
884
903
|
...(busyWaitMs !== undefined ? { busyWaitMs } : {}),
|
|
885
904
|
project: selectionProject,
|
|
@@ -1148,6 +1167,7 @@ export async function performBrowserConsultForMcp(cwd, input, onProgress) {
|
|
|
1148
1167
|
...(input.project !== undefined ? ["--project", input.project] : []),
|
|
1149
1168
|
...(input.timeout_ms !== undefined ? ["--timeout-ms", String(input.timeout_ms)] : []),
|
|
1150
1169
|
...(input.files ?? []).flatMap((file) => ["--file", file]),
|
|
1170
|
+
...(input.attach ?? []).flatMap((file) => ["--attach", file]),
|
|
1151
1171
|
...(input.new_chat ? ["--new-chat"] : []),
|
|
1152
1172
|
"--",
|
|
1153
1173
|
input.prompt
|
package/dist/mcp.js
CHANGED
|
@@ -150,6 +150,11 @@ export function createServer(cwd = process.cwd(), options = {}) {
|
|
|
150
150
|
project: McpShortTextSchema.optional(),
|
|
151
151
|
timeout_ms: z.number().int().positive().max(3_600_000).optional(),
|
|
152
152
|
files: z.array(McpShortTextSchema).max(20).optional(),
|
|
153
|
+
attach: z
|
|
154
|
+
.array(McpShortTextSchema)
|
|
155
|
+
.max(10)
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("Repo-relative paths to UPLOAD to ChatGPT as real attachments (pdf, pptx, xlsx, images, or any file you want ChatGPT to parse itself). Use this instead of `files` for binaries and for large documents; `files` inlines a text file's contents into the prompt, which cannot carry a binary and bloats the prompt."),
|
|
153
158
|
new_chat: z
|
|
154
159
|
.boolean()
|
|
155
160
|
.optional()
|