@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.
- package/dist/chatgpt-browser.js +20 -5
- package/dist/cli-pro.js +7 -5
- package/dist/config.js +41 -3
- package/dist/mcp.js +5 -2
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -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 ${
|
|
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 ${
|
|
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", `
|
|
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.
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
return config.browser_defaults;
|
|
172
|
+
repo = (await loadLocalConfig(cwd)).browser_defaults;
|
|
138
173
|
}
|
|
139
174
|
catch {
|
|
140
|
-
|
|
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
|
|
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
|