@youdie006/prodex 0.19.2 → 0.21.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 +392 -9
- package/dist/cli-args.js +4 -0
- package/dist/cli-help.js +8 -8
- package/dist/cli-pro.js +30 -2
- package/dist/mcp.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
|
|
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, and ChatGPT often replies with a clarifying question first, which you answer with a normal follow-up in the same thread), `--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
|
@@ -1065,6 +1065,81 @@ 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
|
+
}
|
|
1088
|
+
// ---------------------------------------------------------------------------
|
|
1089
|
+
// Power slider (model + effort)
|
|
1090
|
+
//
|
|
1091
|
+
// ChatGPT replaced the model radio list with ONE slider. Measured live, its
|
|
1092
|
+
// five positions render on GPT-5.6 Sol as:
|
|
1093
|
+
// 0 Instant · 1 Medium · 2 High · 3 Extra High · 4 Pro
|
|
1094
|
+
// so "Pro" is now the top EFFORT, not a model - which is exactly why looking
|
|
1095
|
+
// for a "Pro" radio started failing. The slider takes focus and responds to
|
|
1096
|
+
// Arrow keys, so selection is: focus, step toward the wanted label, stop.
|
|
1097
|
+
// ---------------------------------------------------------------------------
|
|
1098
|
+
const POWER_LABEL_SYNONYMS = [
|
|
1099
|
+
{ canonical: "instant", aliases: ["instant", "즉시", "빠름", "fast"] },
|
|
1100
|
+
{ canonical: "medium", aliases: ["medium", "중간", "보통"] },
|
|
1101
|
+
{ canonical: "high", aliases: ["high", "높음"] },
|
|
1102
|
+
{ canonical: "extra high", aliases: ["extra high", "extrahigh", "very high", "매우 높음", "매우높음"] },
|
|
1103
|
+
{ canonical: "pro", aliases: ["pro", "프로"] }
|
|
1104
|
+
];
|
|
1105
|
+
function canonicalPowerLabel(value) {
|
|
1106
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1107
|
+
const hit = POWER_LABEL_SYNONYMS.find((entry) => entry.aliases.includes(normalized));
|
|
1108
|
+
return hit ? hit.canonical : normalized;
|
|
1109
|
+
}
|
|
1110
|
+
/** Whether a requested model/effort names the same step the slider renders. */
|
|
1111
|
+
export function powerLabelMatches(requested, rendered) {
|
|
1112
|
+
if (!requested || !rendered)
|
|
1113
|
+
return false;
|
|
1114
|
+
return canonicalPowerLabel(requested) === canonicalPowerLabel(rendered);
|
|
1115
|
+
}
|
|
1116
|
+
/** Slider position plus the Model/Effort readout next to it. */
|
|
1117
|
+
export function powerSliderStateExpression() {
|
|
1118
|
+
return `(() => {
|
|
1119
|
+
const slider = document.querySelector('[role="slider"]');
|
|
1120
|
+
const menu = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
|
|
1121
|
+
const lines = menu ? (menu.innerText || "").split(String.fromCharCode(10)).map((l) => l.trim()).filter(Boolean) : [];
|
|
1122
|
+
const after = (label) => { const i = lines.indexOf(label); return i >= 0 ? lines[i + 1] : null; };
|
|
1123
|
+
if (!slider) return { ok: false, reason: "power slider not found", lines };
|
|
1124
|
+
return {
|
|
1125
|
+
ok: true,
|
|
1126
|
+
position: Number(slider.getAttribute("aria-valuenow")),
|
|
1127
|
+
min: Number(slider.getAttribute("aria-valuemin")),
|
|
1128
|
+
max: Number(slider.getAttribute("aria-valuemax")),
|
|
1129
|
+
model: after("Model"),
|
|
1130
|
+
effort: after("Effort"),
|
|
1131
|
+
lines
|
|
1132
|
+
};
|
|
1133
|
+
})()`;
|
|
1134
|
+
}
|
|
1135
|
+
export function focusPowerSliderExpression() {
|
|
1136
|
+
return `(() => {
|
|
1137
|
+
const slider = document.querySelector('[role="slider"]');
|
|
1138
|
+
if (!slider) return { ok: false, reason: "power slider not found" };
|
|
1139
|
+
slider.focus();
|
|
1140
|
+
return { ok: document.activeElement === slider };
|
|
1141
|
+
})()`;
|
|
1142
|
+
}
|
|
1068
1143
|
export function modelButtonRectExpression() {
|
|
1069
1144
|
return `(() => {${CLICK_POINT_SNIPPET}
|
|
1070
1145
|
const c = document.querySelector('#prompt-textarea,[contenteditable="true"],textarea');
|
|
@@ -1075,7 +1150,10 @@ export function modelButtonRectExpression() {
|
|
|
1075
1150
|
return /\\S/.test(t) && !/파일|첨부|받아쓰기|음성|dictation|attach|file|voice|record|search|mic/i.test(t + aria);
|
|
1076
1151
|
});
|
|
1077
1152
|
if (!b) return { ok: false, reason: "model selector button not found" };
|
|
1078
|
-
|
|
1153
|
+
// Return the label too: the caller compares it against the requested model
|
|
1154
|
+
// to skip opening the menu when it is already selected.
|
|
1155
|
+
const label = ((b.getAttribute("aria-label") || b.textContent || "").trim().split(String.fromCharCode(10))[0] || "").trim();
|
|
1156
|
+
return { ...clickPoint(b), label };
|
|
1079
1157
|
})()`;
|
|
1080
1158
|
}
|
|
1081
1159
|
export function menuItemRectExpression(label) {
|
|
@@ -1211,7 +1289,44 @@ async function assertSelectionCommitted(cdp, label) {
|
|
|
1211
1289
|
throw new Error(`ChatGPT selection "${label}" did not commit; the model menu stayed open. Retry, or pick it manually in the visible browser.`);
|
|
1212
1290
|
}
|
|
1213
1291
|
}
|
|
1214
|
-
|
|
1292
|
+
/**
|
|
1293
|
+
* Move the power slider until its Effort readout is the requested step. The
|
|
1294
|
+
* menu must already be open. Returns the quota line so the caller can warn
|
|
1295
|
+
* when Pro runs are nearly spent.
|
|
1296
|
+
*/
|
|
1297
|
+
async function selectPowerStep(cdp, requested) {
|
|
1298
|
+
const focused = await cdp.evaluate(focusPowerSliderExpression());
|
|
1299
|
+
if (!focused?.ok) {
|
|
1300
|
+
throw new Error(focused?.reason ??
|
|
1301
|
+
"ChatGPT's model picker did not expose its power slider, so the requested model/effort could not be selected.");
|
|
1302
|
+
}
|
|
1303
|
+
let state = await cdp.evaluate(powerSliderStateExpression());
|
|
1304
|
+
if (!state?.ok)
|
|
1305
|
+
throw new Error(state?.reason ?? "Could not read ChatGPT's power slider");
|
|
1306
|
+
const steps = (state.max ?? 4) - (state.min ?? 0) + 1;
|
|
1307
|
+
for (let attempt = 0; attempt <= steps * 2; attempt += 1) {
|
|
1308
|
+
if (state.effort && powerLabelMatches(requested, state.effort))
|
|
1309
|
+
return { effort: state.effort };
|
|
1310
|
+
// Walk upward first, then back down: the labels are ordered, but their
|
|
1311
|
+
// exact set can change, so this never assumes a fixed index for a name.
|
|
1312
|
+
const atTop = (state.position ?? 0) >= (state.max ?? 4);
|
|
1313
|
+
const key = attempt < steps && !atTop ? "ArrowRight" : "ArrowLeft";
|
|
1314
|
+
await dispatchArrowKey(cdp, key);
|
|
1315
|
+
await sleep(400);
|
|
1316
|
+
state = await cdp.evaluate(powerSliderStateExpression());
|
|
1317
|
+
if (!state?.ok)
|
|
1318
|
+
throw new Error(state?.reason ?? "Could not read ChatGPT's power slider");
|
|
1319
|
+
}
|
|
1320
|
+
const available = (state.lines ?? []).join(" / ");
|
|
1321
|
+
throw new Error(`ChatGPT's model picker has no "${requested}" step. It showed: ${available}`);
|
|
1322
|
+
}
|
|
1323
|
+
async function dispatchArrowKey(cdp, key) {
|
|
1324
|
+
const code = key;
|
|
1325
|
+
const virtualKey = key === "ArrowLeft" ? 37 : 39;
|
|
1326
|
+
await cdp.send("Input.dispatchKeyEvent", { type: "rawKeyDown", key, code, windowsVirtualKeyCode: virtualKey });
|
|
1327
|
+
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: virtualKey });
|
|
1328
|
+
}
|
|
1329
|
+
async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
1215
1330
|
if (!options.model && !options.proMode && !options.effort)
|
|
1216
1331
|
return;
|
|
1217
1332
|
// --pro-mode selects a Pro sub-mode, so it is meaningless with a non-Pro
|
|
@@ -1236,6 +1351,13 @@ async function selectModelReasoning(cdp, options) {
|
|
|
1236
1351
|
if (!button.ok || button.x === undefined || button.y === undefined) {
|
|
1237
1352
|
throw new Error(button.reason ?? "Could not open the ChatGPT model selector");
|
|
1238
1353
|
}
|
|
1354
|
+
// Skip the menu entirely when the picker already shows the requested model:
|
|
1355
|
+
// it is the same end state, and it survives ChatGPT reshuffling the menu
|
|
1356
|
+
// (which it did - the models moved behind a "Model" submenu and every
|
|
1357
|
+
// --model Pro send started failing).
|
|
1358
|
+
if (options.model && !options.proMode && !options.effort && modelButtonAlreadyShows(options.model, button.label)) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1239
1361
|
try {
|
|
1240
1362
|
// The hover-verified click can be transiently refused right after a page
|
|
1241
1363
|
// transition (measured live: the just-closed create-project modal's
|
|
@@ -1260,6 +1382,19 @@ async function selectModelReasoning(cdp, options) {
|
|
|
1260
1382
|
const opened = await waitForExpressionTrue(cdp, menuOpenExpression(), MENU_OPEN_TIMEOUT_MS);
|
|
1261
1383
|
if (!opened)
|
|
1262
1384
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
1385
|
+
// Current ChatGPT: one power slider (Instant/Medium/High/Extra High/Pro on
|
|
1386
|
+
// GPT-5.6 Sol) instead of a model radio list, so "Pro" is the top EFFORT.
|
|
1387
|
+
// Drive it when it is there and fall through to the legacy radio path when
|
|
1388
|
+
// it is not, so both UI generations work.
|
|
1389
|
+
const sliderState = await cdp.evaluate(powerSliderStateExpression());
|
|
1390
|
+
if (sliderState?.ok) {
|
|
1391
|
+
const wanted = options.effort ?? options.model;
|
|
1392
|
+
if (wanted) {
|
|
1393
|
+
await selectPowerStep(cdp, wanted);
|
|
1394
|
+
}
|
|
1395
|
+
await dispatchEscapeKey(cdp);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1263
1398
|
const wantsProMode = Boolean(options.proMode) && (!options.model || /pro/i.test(options.model));
|
|
1264
1399
|
if (wantsProMode && options.proMode) {
|
|
1265
1400
|
// Open the Pro sub-mode submenu via the chevron, then pick 기본/확장.
|
|
@@ -1709,7 +1844,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
1709
1844
|
await cdp.send("Runtime.enable");
|
|
1710
1845
|
await selectProject(cdp, options);
|
|
1711
1846
|
try {
|
|
1712
|
-
await selectModelReasoning(cdp, options);
|
|
1847
|
+
await selectModelReasoning(cdp, options, sendWarnings);
|
|
1713
1848
|
}
|
|
1714
1849
|
catch (modelError) {
|
|
1715
1850
|
// Pro sub-mode isn't exposed in this UI yet (staged rollout). Pro itself is
|
|
@@ -1729,7 +1864,17 @@ export async function sendChatGptPrompt(options) {
|
|
|
1729
1864
|
// into; a --project/--project-new hop lands on a page with its own counts.
|
|
1730
1865
|
beforeSubmit = await evaluateOnPage(page, answerExpression());
|
|
1731
1866
|
dbgSend(`baseline url=${beforeSubmit.url} user=${beforeSubmit.userMessageCount} assistant=${beforeSubmit.assistantMessageCount}`);
|
|
1732
|
-
|
|
1867
|
+
// Attach BEFORE typing: the upload is the slow part, and a file that
|
|
1868
|
+
// arrives after the prompt is submitted is a file ChatGPT never saw.
|
|
1869
|
+
if (options.attachments && options.attachments.length > 0) {
|
|
1870
|
+
emitProgress("selecting", `uploading ${options.attachments.length} file(s)`);
|
|
1871
|
+
const uploaded = await attachFilesToComposer(cdp, options.attachments);
|
|
1872
|
+
emitProgress("selecting", `attached ${uploaded.attached.join(", ")}`);
|
|
1873
|
+
}
|
|
1874
|
+
const toolLabels = (options.tools ?? []).map(resolveComposerToolLabel);
|
|
1875
|
+
if (toolLabels.length > 0)
|
|
1876
|
+
emitProgress("selecting", `tools=${toolLabels.join(", ")}`);
|
|
1877
|
+
await insertComposerTextViaCdp(cdp, options.prompt, page, toolLabels);
|
|
1733
1878
|
// The send button renders asynchronously after the prompt lands. Poll for it
|
|
1734
1879
|
// BEFORE submitting so (a) submitButtonFound reflects whether the control
|
|
1735
1880
|
// actually EXISTS - otherwise a successful Enter-key submit skips the fallback
|
|
@@ -1738,7 +1883,12 @@ export async function sendChatGptPrompt(options) {
|
|
|
1738
1883
|
// live: submitExpression finds data-testid="send-button" fine, yet the
|
|
1739
1884
|
// timeout error blamed a UI change) - and (b) we never press Enter before the
|
|
1740
1885
|
// composer is submit-ready.
|
|
1741
|
-
|
|
1886
|
+
// With an attachment, ChatGPT keeps the send control disabled while it
|
|
1887
|
+
// ingests the file server-side; 3s expired mid-ingest and the send never
|
|
1888
|
+
// posted (measured live on a markdown attachment: the button was enabled
|
|
1889
|
+
// and clickable a moment after prodex gave up).
|
|
1890
|
+
const submitReadyBudgetMs = options.attachments && options.attachments.length > 0 ? 120_000 : 3_000;
|
|
1891
|
+
submitButtonFound = await waitForExpressionTrue(cdp, `(${submitExpression()}).ok === true`, submitReadyBudgetMs);
|
|
1742
1892
|
// Submit. Prefer the Enter key: it goes to the focused composer and does
|
|
1743
1893
|
// not depend on coordinates, whereas the send button moves ~100px as the
|
|
1744
1894
|
// composer grows after the prompt lands, so a click at captured coordinates
|
|
@@ -2293,13 +2443,191 @@ export function insertComposerTextInPageExpression(text) {
|
|
|
2293
2443
|
return { ok: true };
|
|
2294
2444
|
})()`;
|
|
2295
2445
|
}
|
|
2296
|
-
|
|
2446
|
+
// ---------------------------------------------------------------------------
|
|
2447
|
+
// Attachments (real file upload)
|
|
2448
|
+
//
|
|
2449
|
+
// `--file` inlines a file's TEXT into the prompt; this uploads the file itself
|
|
2450
|
+
// through the composer's file input, which is the only way to hand ChatGPT a
|
|
2451
|
+
// pdf/pptx/image and let it parse the original. CDP's DOM.setFileInputFiles
|
|
2452
|
+
// sets the input without any file dialog.
|
|
2453
|
+
// ---------------------------------------------------------------------------
|
|
2454
|
+
/**
|
|
2455
|
+
* The composer's general file input. Measured live: ChatGPT renders three
|
|
2456
|
+
* inputs - one general (accept="") plus two accept="image/*" - and attaching a
|
|
2457
|
+
* document to an image-only input silently does nothing.
|
|
2458
|
+
*/
|
|
2459
|
+
export function composerFileInputSelector() {
|
|
2460
|
+
return 'input[type="file"]:not([accept*="image"])';
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Which of the expected attachments the composer shows, and whether one is
|
|
2464
|
+
* still uploading.
|
|
2465
|
+
*
|
|
2466
|
+
* Measured live: a DOCUMENT chip carries its filename in visible text, but an
|
|
2467
|
+
* IMAGE renders as a blob: thumbnail whose only filename is on the remove
|
|
2468
|
+
* button's aria-label ("Remove file 1: cli-banner.png"). Reading innerText
|
|
2469
|
+
* alone therefore reported a perfectly good image upload as never accepted.
|
|
2470
|
+
*/
|
|
2471
|
+
export function attachmentStateExpression(fileNames) {
|
|
2472
|
+
const namesJson = JSON.stringify(fileNames);
|
|
2473
|
+
return `(() => {
|
|
2474
|
+
const names = ${namesJson};
|
|
2475
|
+
const text = document.body ? document.body.innerText || "" : "";
|
|
2476
|
+
const labels = [...document.querySelectorAll('[aria-label],[title],[alt]')]
|
|
2477
|
+
.map((el) => (el.getAttribute("aria-label") || el.getAttribute("title") || el.getAttribute("alt") || ""))
|
|
2478
|
+
.join(String.fromCharCode(10));
|
|
2479
|
+
const haystack = text + String.fromCharCode(10) + labels;
|
|
2480
|
+
const present = names.filter((name) => haystack.includes(name));
|
|
2481
|
+
const attachedFiles = [...document.querySelectorAll('input[type="file"]')]
|
|
2482
|
+
.reduce((total, el) => total + (el.files ? el.files.length : 0), 0);
|
|
2483
|
+
// A visible progressbar means a file is still going up; sending now would
|
|
2484
|
+
// post the prompt without it.
|
|
2485
|
+
const uploading = document.querySelectorAll('[role="progressbar"]').length > 0;
|
|
2486
|
+
return { ok: true, present, attachedFiles, uploading };
|
|
2487
|
+
})()`;
|
|
2488
|
+
}
|
|
2489
|
+
/** How many attachments are already sitting in the composer (read-only). */
|
|
2490
|
+
export function attachmentPresenceExpression() {
|
|
2491
|
+
return `(() => {
|
|
2492
|
+
const buttons = [...document.querySelectorAll('button[aria-label]')].filter((b) =>
|
|
2493
|
+
/remove file|파일 제거|첨부 제거/i.test(b.getAttribute("aria-label") || "")
|
|
2494
|
+
);
|
|
2495
|
+
return { ok: true, removed: buttons.length };
|
|
2496
|
+
})()`;
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Attach files to the composer and wait until ChatGPT has taken them. The
|
|
2500
|
+
* files must be readable by the BROWSER process, so a browser on another
|
|
2501
|
+
* machine (or another container) cannot see the caller's paths - that shows up
|
|
2502
|
+
* as the attachment never appearing, which this reports as such.
|
|
2503
|
+
*/
|
|
2504
|
+
export async function attachFilesToComposer(cdp, absolutePaths, options = {}) {
|
|
2505
|
+
if (absolutePaths.length === 0)
|
|
2506
|
+
return { attached: [] };
|
|
2507
|
+
await cdp.send("DOM.enable");
|
|
2508
|
+
// Drop anything a previous (failed) send left attached: it would ride along
|
|
2509
|
+
// with this prompt, which is exactly the kind of silent contamination the
|
|
2510
|
+
// composer text check already guards against. This must happen BEFORE the
|
|
2511
|
+
// input node is resolved - removing a chip re-renders the composer and
|
|
2512
|
+
// replaces the input element, and setting files on the old (detached) node
|
|
2513
|
+
// silently does nothing (measured live: the file input held the file while
|
|
2514
|
+
// the UI showed no attachment at all).
|
|
2515
|
+
// Leftover attachments from a previous (failed) send would ride along with
|
|
2516
|
+
// this prompt. Clearing them by clicking the remove buttons WEDGES the
|
|
2517
|
+
// composer: measured live, after a removal the file input accepts files
|
|
2518
|
+
// (input.files becomes 1) while the UI never renders the chip again, and
|
|
2519
|
+
// every later attach in that tab silently does nothing. A reload is the only
|
|
2520
|
+
// reliable reset, and it costs a few seconds only when there is something to
|
|
2521
|
+
// clear.
|
|
2522
|
+
const stale = await cdp.evaluate(attachmentPresenceExpression());
|
|
2523
|
+
if ((stale?.removed ?? 0) > 0) {
|
|
2524
|
+
await cdp.evaluate("location.reload()");
|
|
2525
|
+
await sleep(6_000);
|
|
2526
|
+
const settleDeadline = Date.now() + 15_000;
|
|
2527
|
+
for (;;) {
|
|
2528
|
+
const ready = await cdp.evaluate(composerTextStateExpression());
|
|
2529
|
+
if (ready?.ok || Date.now() >= settleDeadline)
|
|
2530
|
+
break;
|
|
2531
|
+
await sleep(500);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const document = await cdp.send("DOM.getDocument", { depth: -1, pierce: true });
|
|
2535
|
+
const rootNodeId = document.result?.root?.nodeId;
|
|
2536
|
+
if (rootNodeId === undefined)
|
|
2537
|
+
throw new Error("Could not read the ChatGPT page DOM to attach files.");
|
|
2538
|
+
const input = await cdp.send("DOM.querySelector", { nodeId: rootNodeId, selector: composerFileInputSelector() });
|
|
2539
|
+
const inputNodeId = input.result?.nodeId;
|
|
2540
|
+
if (!inputNodeId) {
|
|
2541
|
+
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.");
|
|
2542
|
+
}
|
|
2543
|
+
await cdp.send("DOM.setFileInputFiles", { files: absolutePaths, nodeId: inputNodeId });
|
|
2544
|
+
const fileNames = absolutePaths.map((file) => path.basename(file));
|
|
2545
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
2546
|
+
let lastPresent = [];
|
|
2547
|
+
while (Date.now() < deadline) {
|
|
2548
|
+
await sleep(1_000);
|
|
2549
|
+
const state = await cdp.evaluate(attachmentStateExpression(fileNames));
|
|
2550
|
+
lastPresent = state?.present ?? [];
|
|
2551
|
+
if (lastPresent.length === fileNames.length && !state?.uploading)
|
|
2552
|
+
return { attached: lastPresent };
|
|
2553
|
+
}
|
|
2554
|
+
const missing = fileNames.filter((name) => !lastPresent.includes(name));
|
|
2555
|
+
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.`);
|
|
2556
|
+
}
|
|
2557
|
+
// ---------------------------------------------------------------------------
|
|
2558
|
+
// Composer tools (Deep research, Web search, Create image, connectors)
|
|
2559
|
+
//
|
|
2560
|
+
// Measured live: a tool is NOT a chip beside the composer - selecting it
|
|
2561
|
+
// inserts its name as a token INSIDE the ProseMirror editor, and it survives a
|
|
2562
|
+
// page reload. Two consequences drive this code: the tool must be enabled
|
|
2563
|
+
// AFTER the composer is cleared (clearing removes it), and the composer text
|
|
2564
|
+
// check has to ignore the token or it reads as leftover contamination.
|
|
2565
|
+
// ---------------------------------------------------------------------------
|
|
2566
|
+
const COMPOSER_TOOL_ALIASES = [
|
|
2567
|
+
{ label: "Deep research", aliases: ["deep research", "deep-research", "deepresearch", "deep", "research"] },
|
|
2568
|
+
{ label: "Web search", aliases: ["web search", "web-search", "websearch", "search", "web"] },
|
|
2569
|
+
{ label: "Create image", aliases: ["create image", "create-image", "image", "img"] }
|
|
2570
|
+
];
|
|
2571
|
+
/**
|
|
2572
|
+
* Map what a caller typed to the label ChatGPT renders. An unknown value is
|
|
2573
|
+
* passed through unchanged, so a tool ChatGPT adds tomorrow is reachable by
|
|
2574
|
+
* its label without a prodex release.
|
|
2575
|
+
*/
|
|
2576
|
+
export function resolveComposerToolLabel(requested) {
|
|
2577
|
+
const normalized = requested.trim().toLowerCase();
|
|
2578
|
+
const known = COMPOSER_TOOL_ALIASES.find((tool) => tool.aliases.includes(normalized) || tool.label.toLowerCase() === normalized);
|
|
2579
|
+
return known ? known.label : requested.trim();
|
|
2580
|
+
}
|
|
2581
|
+
export const DEEP_RESEARCH_TOOL_LABEL = "Deep research";
|
|
2582
|
+
const DEEP_RESEARCH_MIN_TIMEOUT_MS = 1_800_000;
|
|
2583
|
+
/** Deep research browses for minutes; the ordinary budget abandons it mid-report. */
|
|
2584
|
+
export function defaultTimeoutForTools(tools, fallbackMs) {
|
|
2585
|
+
const wantsDeepResearch = tools.some((tool) => resolveComposerToolLabel(tool) === DEEP_RESEARCH_TOOL_LABEL);
|
|
2586
|
+
return wantsDeepResearch ? Math.max(fallbackMs, DEEP_RESEARCH_MIN_TIMEOUT_MS) : fallbackMs;
|
|
2587
|
+
}
|
|
2588
|
+
export function composerToolsButtonRectExpression() {
|
|
2589
|
+
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2590
|
+
const b = document.querySelector('[data-testid="composer-plus-btn"]');
|
|
2591
|
+
if (!b) return { ok: false, reason: "composer tools button not found" };
|
|
2592
|
+
return clickPoint(b);
|
|
2593
|
+
})()`;
|
|
2594
|
+
}
|
|
2595
|
+
/** Click point for a tools-menu entry, matched by its visible label. */
|
|
2596
|
+
export function composerToolEntryRectExpression(label) {
|
|
2597
|
+
const labelJson = JSON.stringify(label);
|
|
2598
|
+
return `(() => {${CLICK_POINT_SNIPPET}
|
|
2599
|
+
const wanted = ${labelJson}.trim().toLowerCase();
|
|
2600
|
+
const leaves = [...document.querySelectorAll("div,span,button,a")].filter((el) => el.children.length === 0);
|
|
2601
|
+
const leaf = leaves.find((el) => (el.textContent || "").trim().toLowerCase() === wanted);
|
|
2602
|
+
if (!leaf) {
|
|
2603
|
+
const available = [...new Set(leaves.map((el) => (el.textContent || "").trim()).filter((t) => t.length > 1 && t.length < 30))].slice(0, 20);
|
|
2604
|
+
return { ok: false, reason: "tool not found in the composer tools menu", available };
|
|
2605
|
+
}
|
|
2606
|
+
const target = leaf.closest('[role="menuitem"],[role="option"],button,a') || leaf.parentElement || leaf;
|
|
2607
|
+
return clickPoint(target);
|
|
2608
|
+
})()`;
|
|
2609
|
+
}
|
|
2610
|
+
/** Tool tokens currently sitting in the composer. */
|
|
2611
|
+
export function activeComposerToolsExpression(labels) {
|
|
2612
|
+
const labelsJson = JSON.stringify(labels);
|
|
2613
|
+
return `(() => {
|
|
2614
|
+
const el = document.querySelector('#prompt-textarea,[contenteditable="true"]');
|
|
2615
|
+
const text = el ? (el.innerText || "") : "";
|
|
2616
|
+
return { ok: true, active: ${labelsJson}.filter((label) => text.includes(label)) };
|
|
2617
|
+
})()`;
|
|
2618
|
+
}
|
|
2619
|
+
export function composerTextStateExpression(expectedText, toolLabels = []) {
|
|
2297
2620
|
const expectedJson = JSON.stringify(expectedText ?? null);
|
|
2621
|
+
const toolLabelsJson = JSON.stringify(toolLabels);
|
|
2298
2622
|
return `(() => {
|
|
2299
2623
|
${composerExpressionHelpers()}
|
|
2300
2624
|
const el = findChatGptComposerCandidate();
|
|
2301
2625
|
if (!el) return { ok: false, reason: "No visible composer" };
|
|
2302
|
-
|
|
2626
|
+
let raw = ("value" in el ? el.value : el.innerText || el.textContent || "").trim();
|
|
2627
|
+
// An enabled tool lives INSIDE the composer as a token; it is not leftover
|
|
2628
|
+
// text, so strip it before comparing against the prompt.
|
|
2629
|
+
for (const label of ${toolLabelsJson}) raw = raw.split(label).join(" ");
|
|
2630
|
+
raw = raw.trim();
|
|
2303
2631
|
if (!raw) return { ok: false, reason: "Composer stayed empty after text insertion" };
|
|
2304
2632
|
const expected = ${expectedJson};
|
|
2305
2633
|
if (expected === null) return { ok: true, actualText: raw.slice(0, 120) };
|
|
@@ -2313,7 +2641,56 @@ export function composerTextStateExpression(expectedText) {
|
|
|
2313
2641
|
// Focus the composer, clear any leftover text submit-safely, type the prompt
|
|
2314
2642
|
// with native CDP input so ProseMirror registers it, then verify the composer
|
|
2315
2643
|
// holds exactly the prompt.
|
|
2316
|
-
|
|
2644
|
+
/**
|
|
2645
|
+
* Turn on a composer tool by its menu label and confirm the token landed in
|
|
2646
|
+
* the composer. Called after the composer is cleared and before the prompt is
|
|
2647
|
+
* typed, because clearing the composer removes the token.
|
|
2648
|
+
*/
|
|
2649
|
+
export async function enableComposerTools(cdp, labels) {
|
|
2650
|
+
const enabled = [];
|
|
2651
|
+
for (const label of labels) {
|
|
2652
|
+
const already = await cdp.evaluate(activeComposerToolsExpression([label]));
|
|
2653
|
+
if ((already?.active ?? []).includes(label)) {
|
|
2654
|
+
enabled.push(label);
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
const button = await cdp.evaluate(composerToolsButtonRectExpression());
|
|
2658
|
+
if (!button.ok || button.x === undefined || button.y === undefined) {
|
|
2659
|
+
throw new Error(button.reason ?? "Could not open the ChatGPT composer tools menu");
|
|
2660
|
+
}
|
|
2661
|
+
await dispatchMouseClickAt(cdp, button.x, button.y);
|
|
2662
|
+
let entry = { ok: false };
|
|
2663
|
+
const menuDeadline = Date.now() + 6_000;
|
|
2664
|
+
for (;;) {
|
|
2665
|
+
entry = await cdp.evaluate(composerToolEntryRectExpression(label));
|
|
2666
|
+
if (entry.ok && entry.x !== undefined && entry.y !== undefined)
|
|
2667
|
+
break;
|
|
2668
|
+
if (Date.now() >= menuDeadline)
|
|
2669
|
+
break;
|
|
2670
|
+
await sleep(250);
|
|
2671
|
+
}
|
|
2672
|
+
if (!entry.ok || entry.x === undefined || entry.y === undefined) {
|
|
2673
|
+
await dispatchEscapeKey(cdp);
|
|
2674
|
+
const available = entry.available?.length ? ` Menu showed: ${entry.available.slice(0, 12).join(", ")}.` : "";
|
|
2675
|
+
throw new Error(`ChatGPT's composer tools menu has no "${label}".${available}`);
|
|
2676
|
+
}
|
|
2677
|
+
await dispatchMouseClickAt(cdp, entry.x, entry.y);
|
|
2678
|
+
const activeDeadline = Date.now() + 8_000;
|
|
2679
|
+
let active = false;
|
|
2680
|
+
for (;;) {
|
|
2681
|
+
const state = await cdp.evaluate(activeComposerToolsExpression([label]));
|
|
2682
|
+
active = (state?.active ?? []).includes(label);
|
|
2683
|
+
if (active || Date.now() >= activeDeadline)
|
|
2684
|
+
break;
|
|
2685
|
+
await sleep(250);
|
|
2686
|
+
}
|
|
2687
|
+
if (!active)
|
|
2688
|
+
throw new Error(`Selected "${label}" but the composer never showed it as active.`);
|
|
2689
|
+
enabled.push(label);
|
|
2690
|
+
}
|
|
2691
|
+
return enabled;
|
|
2692
|
+
}
|
|
2693
|
+
async function insertComposerTextViaCdp(cdp, text, page, toolLabels = []) {
|
|
2317
2694
|
const prepared = await cdp.evaluate(prepareComposerExpression());
|
|
2318
2695
|
if (!prepared.ok)
|
|
2319
2696
|
throw new Error(prepared.reason ?? "Could not focus the ChatGPT composer");
|
|
@@ -2329,6 +2706,12 @@ async function insertComposerTextViaCdp(cdp, text, page) {
|
|
|
2329
2706
|
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 });
|
|
2330
2707
|
await sleep(100);
|
|
2331
2708
|
}
|
|
2709
|
+
// The composer is clear at this point, so the tool tokens can go in now -
|
|
2710
|
+
// enabling them earlier would have been wiped by the clear above.
|
|
2711
|
+
if (toolLabels.length > 0) {
|
|
2712
|
+
await enableComposerTools(cdp, toolLabels);
|
|
2713
|
+
await sleep(300);
|
|
2714
|
+
}
|
|
2332
2715
|
// Insertion path, chosen by size:
|
|
2333
2716
|
//
|
|
2334
2717
|
// Short prompts go through Input.insertText - real key-level input events,
|
|
@@ -2367,7 +2750,7 @@ async function insertComposerTextViaCdp(cdp, text, page) {
|
|
|
2367
2750
|
}
|
|
2368
2751
|
}
|
|
2369
2752
|
await sleep(200);
|
|
2370
|
-
const state = await cdp.evaluate(composerTextStateExpression(text));
|
|
2753
|
+
const state = await cdp.evaluate(composerTextStateExpression(text, toolLabels));
|
|
2371
2754
|
if (!state.ok)
|
|
2372
2755
|
throw new Error(state.reason ?? "Composer stayed empty after text insertion");
|
|
2373
2756
|
}
|
package/dist/cli-args.js
CHANGED
|
@@ -261,6 +261,10 @@ 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",
|
|
266
|
+
// Composer tools: deep-research, web-search, create-image, ...
|
|
267
|
+
"--tool",
|
|
264
268
|
"--port",
|
|
265
269
|
"--timeout-ms",
|
|
266
270
|
"--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] [--tool deep-research|web-search|create-image] "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] [--tool deep-research|web-search|create-image] [--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] [--tool deep-research|web-search|create-image]
|
|
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] [--tool deep-research|web-search|create-image] "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] [--tool deep-research|web-search|create-image] [--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] [--tool deep-research|web-search|create-image]
|
|
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] [--tool deep-research|web-search|create-image] ${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] [--tool deep-research|web-search|create-image] ${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,7 +1,8 @@
|
|
|
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";
|
|
4
|
-
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
|
|
5
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";
|
|
6
7
|
import { printProBrowserHelp, printProHelp } from "./cli-help.js";
|
|
7
8
|
import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
|
|
@@ -722,6 +723,29 @@ 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
|
+
}
|
|
743
|
+
// --tool turns on a composer tool (deep-research, web-search,
|
|
744
|
+
// create-image, or any label the menu shows) for this send.
|
|
745
|
+
const tools = readRepeatedFlag(parsedAskPro.optionArgs, "--tool");
|
|
746
|
+
if (tools.length > 0 && !hasSendMode) {
|
|
747
|
+
throw new Error("--tool only applies when sending (`prodex pro browser ask`); the dry-run preview cannot open ChatGPT's tools menu.");
|
|
748
|
+
}
|
|
725
749
|
const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
|
|
726
750
|
const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
|
|
727
751
|
if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
@@ -815,7 +839,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
815
839
|
// consults (observed in several field sessions).
|
|
816
840
|
const defaultBrowserTimeoutMs = effectiveProSelection ? 1_200_000 : 300_000;
|
|
817
841
|
const browserTimeoutMs = hasSendMode
|
|
818
|
-
? (readPositiveIntegerFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultBrowserTimeoutMs)
|
|
842
|
+
? (readPositiveIntegerFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultTimeoutForTools(tools, defaultBrowserTimeoutMs))
|
|
819
843
|
: undefined;
|
|
820
844
|
const sourceCli = resolveOptionalFileFlag(io.cwd, parsedAskPro.optionArgs, "--source-cli");
|
|
821
845
|
const bundle = await buildDryRunBundle(targetCwd, { prompt: promptText, files });
|
|
@@ -880,6 +904,8 @@ export async function runAskProCommand(rest, io) {
|
|
|
880
904
|
prompt: bundle.text,
|
|
881
905
|
targetUrl: normalizedTargetUrl,
|
|
882
906
|
timeoutMs: browserTimeoutMs,
|
|
907
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
908
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
883
909
|
...(newChat ? { newChat: true } : {}),
|
|
884
910
|
...(busyWaitMs !== undefined ? { busyWaitMs } : {}),
|
|
885
911
|
project: selectionProject,
|
|
@@ -1148,6 +1174,8 @@ export async function performBrowserConsultForMcp(cwd, input, onProgress) {
|
|
|
1148
1174
|
...(input.project !== undefined ? ["--project", input.project] : []),
|
|
1149
1175
|
...(input.timeout_ms !== undefined ? ["--timeout-ms", String(input.timeout_ms)] : []),
|
|
1150
1176
|
...(input.files ?? []).flatMap((file) => ["--file", file]),
|
|
1177
|
+
...(input.attach ?? []).flatMap((file) => ["--attach", file]),
|
|
1178
|
+
...(input.tools ?? []).flatMap((tool) => ["--tool", tool]),
|
|
1151
1179
|
...(input.new_chat ? ["--new-chat"] : []),
|
|
1152
1180
|
"--",
|
|
1153
1181
|
input.prompt
|
package/dist/mcp.js
CHANGED
|
@@ -150,6 +150,16 @@ 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
|
+
tools: z
|
|
154
|
+
.array(McpShortTextSchema)
|
|
155
|
+
.max(4)
|
|
156
|
+
.optional()
|
|
157
|
+
.describe("ChatGPT composer tools to enable for this consult: \"deep-research\" (a multi-minute browsed report - the timeout rises to 30 minutes automatically), \"web-search\" (current facts), \"create-image\". Deep research often replies with a CLARIFYING QUESTION first; answer it with a normal follow-up consult in the same thread."),
|
|
158
|
+
attach: z
|
|
159
|
+
.array(McpShortTextSchema)
|
|
160
|
+
.max(10)
|
|
161
|
+
.optional()
|
|
162
|
+
.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
163
|
new_chat: z
|
|
154
164
|
.boolean()
|
|
155
165
|
.optional()
|