@youdie006/prodex 0.20.0 → 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 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. 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). 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
 
@@ -1085,6 +1085,61 @@ export function modelButtonAlreadyShows(requestedModel, buttonLabel) {
1085
1085
  const escaped = wanted.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1086
1086
  return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`).test(label);
1087
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
+ }
1088
1143
  export function modelButtonRectExpression() {
1089
1144
  return `(() => {${CLICK_POINT_SNIPPET}
1090
1145
  const c = document.querySelector('#prompt-textarea,[contenteditable="true"],textarea');
@@ -1234,7 +1289,44 @@ async function assertSelectionCommitted(cdp, label) {
1234
1289
  throw new Error(`ChatGPT selection "${label}" did not commit; the model menu stayed open. Retry, or pick it manually in the visible browser.`);
1235
1290
  }
1236
1291
  }
1237
- async function selectModelReasoning(cdp, options) {
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 = []) {
1238
1330
  if (!options.model && !options.proMode && !options.effort)
1239
1331
  return;
1240
1332
  // --pro-mode selects a Pro sub-mode, so it is meaningless with a non-Pro
@@ -1290,6 +1382,19 @@ async function selectModelReasoning(cdp, options) {
1290
1382
  const opened = await waitForExpressionTrue(cdp, menuOpenExpression(), MENU_OPEN_TIMEOUT_MS);
1291
1383
  if (!opened)
1292
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
+ }
1293
1398
  const wantsProMode = Boolean(options.proMode) && (!options.model || /pro/i.test(options.model));
1294
1399
  if (wantsProMode && options.proMode) {
1295
1400
  // Open the Pro sub-mode submenu via the chevron, then pick 기본/확장.
@@ -1739,7 +1844,7 @@ export async function sendChatGptPrompt(options) {
1739
1844
  await cdp.send("Runtime.enable");
1740
1845
  await selectProject(cdp, options);
1741
1846
  try {
1742
- await selectModelReasoning(cdp, options);
1847
+ await selectModelReasoning(cdp, options, sendWarnings);
1743
1848
  }
1744
1849
  catch (modelError) {
1745
1850
  // Pro sub-mode isn't exposed in this UI yet (staged rollout). Pro itself is
@@ -1766,7 +1871,10 @@ export async function sendChatGptPrompt(options) {
1766
1871
  const uploaded = await attachFilesToComposer(cdp, options.attachments);
1767
1872
  emitProgress("selecting", `attached ${uploaded.attached.join(", ")}`);
1768
1873
  }
1769
- await insertComposerTextViaCdp(cdp, options.prompt, page);
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);
1770
1878
  // The send button renders asynchronously after the prompt lands. Poll for it
1771
1879
  // BEFORE submitting so (a) submitButtonFound reflects whether the control
1772
1880
  // actually EXISTS - otherwise a successful Enter-key submit skips the fallback
@@ -2446,13 +2554,80 @@ export async function attachFilesToComposer(cdp, absolutePaths, options = {}) {
2446
2554
  const missing = fileNames.filter((name) => !lastPresent.includes(name));
2447
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.`);
2448
2556
  }
2449
- export function composerTextStateExpression(expectedText) {
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 = []) {
2450
2620
  const expectedJson = JSON.stringify(expectedText ?? null);
2621
+ const toolLabelsJson = JSON.stringify(toolLabels);
2451
2622
  return `(() => {
2452
2623
  ${composerExpressionHelpers()}
2453
2624
  const el = findChatGptComposerCandidate();
2454
2625
  if (!el) return { ok: false, reason: "No visible composer" };
2455
- const raw = ("value" in el ? el.value : el.innerText || el.textContent || "").trim();
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();
2456
2631
  if (!raw) return { ok: false, reason: "Composer stayed empty after text insertion" };
2457
2632
  const expected = ${expectedJson};
2458
2633
  if (expected === null) return { ok: true, actualText: raw.slice(0, 120) };
@@ -2466,7 +2641,56 @@ export function composerTextStateExpression(expectedText) {
2466
2641
  // Focus the composer, clear any leftover text submit-safely, type the prompt
2467
2642
  // with native CDP input so ProseMirror registers it, then verify the composer
2468
2643
  // holds exactly the prompt.
2469
- async function insertComposerTextViaCdp(cdp, text, page) {
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 = []) {
2470
2694
  const prepared = await cdp.evaluate(prepareComposerExpression());
2471
2695
  if (!prepared.ok)
2472
2696
  throw new Error(prepared.reason ?? "Could not focus the ChatGPT composer");
@@ -2482,6 +2706,12 @@ async function insertComposerTextViaCdp(cdp, text, page) {
2482
2706
  await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 });
2483
2707
  await sleep(100);
2484
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
+ }
2485
2715
  // Insertion path, chosen by size:
2486
2716
  //
2487
2717
  // Short prompts go through Input.insertText - real key-level input events,
@@ -2520,7 +2750,7 @@ async function insertComposerTextViaCdp(cdp, text, page) {
2520
2750
  }
2521
2751
  }
2522
2752
  await sleep(200);
2523
- const state = await cdp.evaluate(composerTextStateExpression(text));
2753
+ const state = await cdp.evaluate(composerTextStateExpression(text, toolLabels));
2524
2754
  if (!state.ok)
2525
2755
  throw new Error(state.reason ?? "Composer stayed empty after text insertion");
2526
2756
  }
package/dist/cli-args.js CHANGED
@@ -263,6 +263,8 @@ export const ASK_PRO_VALUE_FLAGS = new Set([
263
263
  "--file",
264
264
  // Upload the file itself (pdf/pptx/image) instead of inlining its text.
265
265
  "--attach",
266
+ // Composer tools: deep-research, web-search, create-image, ...
267
+ "--tool",
266
268
  "--port",
267
269
  "--timeout-ms",
268
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] [--attach 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] [--attach 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] [--attach 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] [--attach 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] [--attach 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] [--attach 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] [--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"`;
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
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { buildDryRunBundle } from "./bundle.js";
5
- import { DEFAULT_CDP_PORT, resolveCdpPort, 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";
6
6
  import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
7
7
  import { printProBrowserHelp, printProHelp } from "./cli-help.js";
8
8
  import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
@@ -740,6 +740,12 @@ export async function runAskProCommand(rest, io) {
740
740
  if (attachments.length > 0 && !hasSendMode) {
741
741
  throw new Error("--attach only applies when sending (`prodex pro browser ask`); the dry-run preview cannot upload files.");
742
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
+ }
743
749
  const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
744
750
  const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
745
751
  if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
@@ -833,7 +839,7 @@ export async function runAskProCommand(rest, io) {
833
839
  // consults (observed in several field sessions).
834
840
  const defaultBrowserTimeoutMs = effectiveProSelection ? 1_200_000 : 300_000;
835
841
  const browserTimeoutMs = hasSendMode
836
- ? (readPositiveIntegerFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultBrowserTimeoutMs)
842
+ ? (readPositiveIntegerFlag(parsedAskPro.optionArgs, "--timeout-ms") ?? defaultTimeoutForTools(tools, defaultBrowserTimeoutMs))
837
843
  : undefined;
838
844
  const sourceCli = resolveOptionalFileFlag(io.cwd, parsedAskPro.optionArgs, "--source-cli");
839
845
  const bundle = await buildDryRunBundle(targetCwd, { prompt: promptText, files });
@@ -899,6 +905,7 @@ export async function runAskProCommand(rest, io) {
899
905
  targetUrl: normalizedTargetUrl,
900
906
  timeoutMs: browserTimeoutMs,
901
907
  ...(attachments.length > 0 ? { attachments } : {}),
908
+ ...(tools.length > 0 ? { tools } : {}),
902
909
  ...(newChat ? { newChat: true } : {}),
903
910
  ...(busyWaitMs !== undefined ? { busyWaitMs } : {}),
904
911
  project: selectionProject,
@@ -1168,6 +1175,7 @@ export async function performBrowserConsultForMcp(cwd, input, onProgress) {
1168
1175
  ...(input.timeout_ms !== undefined ? ["--timeout-ms", String(input.timeout_ms)] : []),
1169
1176
  ...(input.files ?? []).flatMap((file) => ["--file", file]),
1170
1177
  ...(input.attach ?? []).flatMap((file) => ["--attach", file]),
1178
+ ...(input.tools ?? []).flatMap((tool) => ["--tool", tool]),
1171
1179
  ...(input.new_chat ? ["--new-chat"] : []),
1172
1180
  "--",
1173
1181
  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
+ 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."),
153
158
  attach: z
154
159
  .array(McpShortTextSchema)
155
160
  .max(10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",