@youdie006/prodex 0.16.26 → 0.16.28

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.
@@ -229,8 +229,21 @@ export function hasFreshChatGptAnswer(previousAssistantMessageCount, state) {
229
229
  // changed ChatGPT UI (moved/renamed composer or send control). Distinguish that
230
230
  // from a genuinely clean-but-slow submit and point the user at an update/report
231
231
  // instead of a misleading "raise --timeout-ms".
232
+ // Render a millisecond duration as a human-readable span for timeout guidance:
233
+ // 45000 -> "45s", 90000 -> "1m 30s", 1200000 -> "20 min". Messages keep the raw
234
+ // ms alongside ("20 min (1200000ms)") so the send_timeout blocker can still
235
+ // parse a budget to double.
236
+ export function formatDurationMs(ms) {
237
+ const totalSeconds = Math.max(0, Math.round(ms / 1000));
238
+ if (totalSeconds < 60)
239
+ return `${totalSeconds}s`;
240
+ const minutes = Math.floor(totalSeconds / 60);
241
+ const seconds = totalSeconds % 60;
242
+ return seconds ? `${minutes}m ${seconds}s` : `${minutes} min`;
243
+ }
232
244
  export function acceptanceTimeoutError(ctx) {
233
245
  const uiLikelyChanged = ctx.composerStillHasText || !ctx.submitButtonFound;
246
+ const took = `${formatDurationMs(ctx.timeoutMs)} (${ctx.timeoutMs}ms)`;
234
247
  if (uiLikelyChanged) {
235
248
  const detail = [
236
249
  ctx.composerStillHasText ? "the composer still holds the prompt" : undefined,
@@ -238,12 +251,13 @@ export function acceptanceTimeoutError(ctx) {
238
251
  ]
239
252
  .filter(Boolean)
240
253
  .join(" and ");
241
- return new Error(`Timed out after ${ctx.timeoutMs}ms and ChatGPT never registered the prompt (${detail}). ` +
254
+ return new Error(`Timed out after ${took} and ChatGPT never registered the prompt (${detail}). ` +
242
255
  "The ChatGPT web UI may have changed, so prodex could not submit. Update prodex " +
243
256
  "(npm i -g @youdie006/prodex@latest); if it persists, report it at " +
244
257
  "https://github.com/youdie006/prodex/issues. You can also paste the prompt manually in the visible browser.");
245
258
  }
246
- return new Error(`Timed out after ${ctx.timeoutMs}ms waiting for ChatGPT to accept the prompt. Raise --timeout-ms and retry (Pro extended already uses a higher default).`);
259
+ return new Error(`Timed out after ${took} waiting for ChatGPT to accept the prompt. ` +
260
+ "Pro reasoning can run many minutes. Raise --timeout-ms and retry.");
247
261
  }
248
262
  export function hasPartialChatGptAnswer(previousAssistantMessageCount, state) {
249
263
  return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer);
@@ -1415,7 +1429,7 @@ export async function sendChatGptPrompt(options) {
1415
1429
  finally {
1416
1430
  cdp.close();
1417
1431
  }
1418
- emitProgress("sent", `timeout ${Math.round(timeoutMs / 1000)}s`);
1432
+ emitProgress("sent", `budget ${formatDurationMs(timeoutMs)}`);
1419
1433
  const started = Date.now();
1420
1434
  const acceptDeadline = computePromptAcceptanceDeadline(timeoutMs, started);
1421
1435
  let accepted = false;
@@ -1520,11 +1534,12 @@ export async function sendChatGptPrompt(options) {
1520
1534
  answer: completed.answer.trim(),
1521
1535
  modelHints: completed.modelHints,
1522
1536
  warnings: [
1523
- `answer_incomplete: ChatGPT was still generating after ${timeoutMs}ms, so the answer below may be truncated. Raise --timeout-ms and retry for the full response.`
1537
+ `answer_incomplete: ChatGPT was still generating after ${formatDurationMs(timeoutMs)} (${timeoutMs}ms), so the answer below may be truncated. Raise --timeout-ms and retry for the full response.`
1524
1538
  ]
1525
1539
  };
1526
1540
  }
1527
- throw new Error(`Timed out after ${timeoutMs}ms waiting for ChatGPT to respond. Raise --timeout-ms and retry (Pro extended already uses a higher default).`);
1541
+ throw new Error(`Timed out after ${formatDurationMs(timeoutMs)} (${timeoutMs}ms) waiting for ChatGPT to respond. ` +
1542
+ "Pro reasoning can run many minutes. Raise --timeout-ms and retry.");
1528
1543
  }
1529
1544
  export function modelMenuOptionsExpression() {
1530
1545
  return `(() => {
package/dist/cli-pro.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { buildDryRunBundle } from "./bundle.js";
4
- import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, readLastBrowserLoginLaunch, recordBrowserLoginLaunch, sendChatGptPrompt } from "./chatgpt-browser.js";
4
+ import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, readLastBrowserLoginLaunch, recordBrowserLoginLaunch, sendChatGptPrompt } from "./chatgpt-browser.js";
5
5
  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, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
6
6
  import { printProBrowserHelp, printProHelp } from "./cli-help.js";
7
7
  import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
@@ -520,11 +520,11 @@ export function createBrowserSendProgressPrinter(write, heartbeatMs = 10_000) {
520
520
  if (lastWaitingElapsedMs !== undefined && event.elapsedMs - lastWaitingElapsedMs < heartbeatMs)
521
521
  return;
522
522
  lastWaitingElapsedMs = event.elapsedMs;
523
- write(`progress: waiting ${Math.round(event.elapsedMs / 1000)}s${event.detail ? ` (${event.detail})` : ""}`);
523
+ write(`progress: waiting ${formatDurationMs(event.elapsedMs)}${event.detail ? ` (${event.detail})` : ""}`);
524
524
  return;
525
525
  }
526
526
  if (event.phase === "answered") {
527
- write(`progress: answer received after ${Math.round(event.elapsedMs / 1000)}s${event.detail ? ` (${event.detail})` : ""}`);
527
+ write(`progress: answer received after ${formatDurationMs(event.elapsedMs)}${event.detail ? ` (${event.detail})` : ""}`);
528
528
  return;
529
529
  }
530
530
  write(`progress: ${PROGRESS_PHASE_LABELS[event.phase]}${event.detail ? ` (${event.detail})` : ""}`);
@@ -1077,7 +1077,9 @@ export function browserSendBlockerFromError(error) {
1077
1077
  next_step: "Update prodex (npm i -g @youdie006/prodex@latest); if it persists, report it at https://github.com/youdie006/prodex/issues or paste the prompt manually in the visible browser."
1078
1078
  };
1079
1079
  }
1080
- const timedOut = message.match(/^Timed out after (\d+)ms/);
1080
+ // Match the raw ms whether the message uses the old "after 90000ms" form or
1081
+ // the newer human-readable "after 20 min (1200000ms)" form.
1082
+ const timedOut = message.match(/Timed out after [\s\S]*?(\d+)\s*ms/);
1081
1083
  if (timedOut) {
1082
1084
  // Suggest a concrete doubled budget so the user can paste a rerun command
1083
1085
  // instead of guessing what "raise --timeout-ms" means in milliseconds.
@@ -1087,7 +1089,7 @@ export function browserSendBlockerFromError(error) {
1087
1089
  code: "send_timeout",
1088
1090
  message,
1089
1091
  retryable: true,
1090
- next_step: `Rerun with a bigger budget: \`prodex pro browser ask --timeout-ms ${suggestedMs} "<same prompt>"\`.`
1092
+ next_step: `Rerun with a bigger budget (${formatDurationMs(suggestedMs)}): \`prodex pro browser ask --timeout-ms ${suggestedMs} "<same prompt>"\`.`
1091
1093
  };
1092
1094
  }
1093
1095
  return {
package/dist/config.js CHANGED
@@ -129,16 +129,54 @@ export async function loadLocalConfig(cwd) {
129
129
  assertServerUrlMatchesConfig(config);
130
130
  return config;
131
131
  }
132
+ // Global browser-selection defaults from the environment, used when a repo has
133
+ // no per-repo default for a field. The MCP server (pro_consult) frequently runs
134
+ // as `prodex mcp` with no --cwd, so it reads whatever directory the agent
135
+ // launched in - a per-repo default set elsewhere is then missed. Setting e.g.
136
+ // PRODEX_DEFAULT_PROJECT=Codex PRODEX_DEFAULT_MODEL=Pro pins a default that
137
+ // applies from ANY cwd so consults stop landing in the general chat.
138
+ export function envBrowserDefaults() {
139
+ const model = process.env.PRODEX_DEFAULT_MODEL?.trim() || undefined;
140
+ const project = process.env.PRODEX_DEFAULT_PROJECT?.trim() || undefined;
141
+ const raw = {};
142
+ if (model)
143
+ raw.model = model;
144
+ if (project)
145
+ raw.project = project;
146
+ const proMode = process.env.PRODEX_DEFAULT_PRO_MODE?.trim();
147
+ if (proMode)
148
+ raw.pro_mode = proMode;
149
+ const effort = process.env.PRODEX_DEFAULT_EFFORT?.trim();
150
+ if (effort)
151
+ raw.effort = effort;
152
+ if (Object.keys(raw).length === 0)
153
+ return undefined;
154
+ const parsed = BrowserDefaultsSchema.safeParse(raw);
155
+ if (parsed.success)
156
+ return parsed.data;
157
+ // A bad enum in pro_mode/effort must not drop a valid model/project.
158
+ const freeText = BrowserDefaultsSchema.safeParse({
159
+ ...(model ? { model } : {}),
160
+ ...(project ? { project } : {})
161
+ });
162
+ return (model || project) && freeText.success ? freeText.data : undefined;
163
+ }
132
164
  // Read persisted browser-selection defaults without failing when the local
133
165
  // config is absent or unrelated to this cwd (defaults are optional convenience).
166
+ // Per-repo config wins field-by-field; PRODEX_DEFAULT_* env vars are the global
167
+ // fallback so a pinned default project/model applies from any cwd.
134
168
  export async function loadBrowserDefaults(cwd) {
169
+ const env = envBrowserDefaults();
170
+ let repo;
135
171
  try {
136
- const config = await loadLocalConfig(cwd);
137
- return config.browser_defaults;
172
+ repo = (await loadLocalConfig(cwd)).browser_defaults;
138
173
  }
139
174
  catch {
140
- return undefined;
175
+ repo = undefined;
141
176
  }
177
+ if (!repo && !env)
178
+ return undefined;
179
+ return { ...(env ?? {}), ...(repo ?? {}) };
142
180
  }
143
181
  export function getTokenExpiryStatus(config, now = new Date()) {
144
182
  if (!config.token_expires_at) {
package/dist/mcp.js CHANGED
@@ -138,7 +138,7 @@ export function createServer(cwd = process.cwd(), options = {}) {
138
138
  const browserConsult = options.browserConsult;
139
139
  if (browserConsult) {
140
140
  server.registerTool("pro_consult", {
141
- description: "Ask the user's logged-in ChatGPT (Pro) in the visible browser and wait for the full answer. This drives a real browser send: it can take minutes (Pro extended reasoning), is human-paced, and records a durable receipt under .bridge/. Requires a running `prodex pro browser login` session. Returns task_id, thread URL, and the answer text.",
141
+ description: "Ask the user's logged-in ChatGPT (Pro) in the visible browser and wait for the full answer. This drives a real browser send: it can take minutes (Pro extended reasoning), is human-paced, and records a durable receipt under .bridge/. Requires a running `prodex pro browser login` session. By DEFAULT the consult continues in the currently-open thread, so consecutive follow-ups on the same topic stay in one conversation (keeps context, avoids sidebar clutter). Pass new_chat:true ONLY to start a fresh thread for a genuinely new topic. `project` and `model` come from saved defaults (per-repo config, or PRODEX_DEFAULT_PROJECT / PRODEX_DEFAULT_MODEL env vars) when omitted - do NOT pass them per-call unless deliberately overriding. Returns task_id, thread URL, and the answer text.",
142
142
  inputSchema: {
143
143
  prompt: McpBridgeTextSchema.min(1),
144
144
  model: McpShortTextSchema.optional(),
@@ -147,7 +147,10 @@ export function createServer(cwd = process.cwd(), options = {}) {
147
147
  project: McpShortTextSchema.optional(),
148
148
  timeout_ms: z.number().int().positive().max(3_600_000).optional(),
149
149
  files: z.array(McpShortTextSchema).max(20).optional(),
150
- new_chat: z.boolean().optional()
150
+ new_chat: z
151
+ .boolean()
152
+ .optional()
153
+ .describe("Start a fresh thread. Omit to continue the current thread (preferred for follow-ups).")
151
154
  }
152
155
  }, async (input, extra) => {
153
156
  // Bridge send progress to MCP progress notifications, but only when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.16.26",
3
+ "version": "0.16.28",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",