@youdie006/prodex 0.40.6 → 0.40.8
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 +25 -10
- package/dist/browser-send-lock.js +10 -131
- package/dist/chatgpt-browser.js +275 -691
- package/dist/cli-help.js +20 -17
- package/dist/cli-ledger.js +2 -0
- package/dist/cli-pro.js +216 -102
- package/dist/cli-server.js +2 -2
- package/dist/cli.js +1 -4
- package/dist/config.js +3 -3
- package/dist/http-mcp.js +1 -1
- package/dist/issue-report.js +9 -4
- package/dist/mcp-tools.js +42 -10
- package/dist/mcp.js +2 -2
- package/dist/registry.js +54 -6
- package/dist/repo-write.js +22 -2
- package/dist/safe-file.js +249 -1
- package/dist/store.js +7 -1
- package/dist/tui-flow.js +0 -1
- package/dist/tui-run.js +62 -25
- package/dist/tui.js +68 -54
- package/docs/claude.md +3 -1
- package/docs/cli-reference.md +17 -6
- package/docs/clients.md +4 -6
- package/docs/http-mcp.md +5 -1
- package/docs/releasing.md +3 -1
- package/package.json +1 -1
package/dist/cli-pro.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync, 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, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch,
|
|
5
|
+
import { DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, formatModelMenuOption, listChatGptModelOptions, deleteChatGptConversation, endWedgedBrowser, browserRecoveryPlan, findWedgedBrowser, wedgedBrowserBlocker, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveBrowserWindowMode, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt, statusMeansBrowserDead, namesPro, sendWarningsFromError, destinationVerification, chatGptProjectIdFromUrl } 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";
|
|
@@ -16,6 +16,15 @@ import { readBridgeRoots } from "./registry.js";
|
|
|
16
16
|
import { BridgeStore, MAX_FETCHABLE_RESULT_ARTIFACT_BYTES } from "./store.js";
|
|
17
17
|
import { CLI_VERSION } from "./cli-help.js";
|
|
18
18
|
import { PRODEX_ISSUE_REPO, buildIssueReport, fileGitHubIssue } from "./issue-report.js";
|
|
19
|
+
function resolveBrowserProfileDirForLaunch(profileDir, launchCwd = process.cwd()) {
|
|
20
|
+
const resolved = path.resolve(launchCwd, profileDir);
|
|
21
|
+
try {
|
|
22
|
+
return statSync(resolved).isDirectory() ? realpathSync(resolved) : resolved;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return resolved;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
19
28
|
export async function runChatgptCommand(rest, io) {
|
|
20
29
|
const [subcommand, ...chatgptArgs] = rest;
|
|
21
30
|
if (!subcommand || isHelpSubcommand(subcommand)) {
|
|
@@ -195,23 +204,45 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
195
204
|
if (browserSubcommand === "login") {
|
|
196
205
|
if (printProBrowserHelpIfRequested(browserArgs, "pro browser login", io, {
|
|
197
206
|
valueFlags: ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms", "--wait-timeout-ms"],
|
|
198
|
-
booleanFlags: ["--dry-run", "--wait", "--no-wait", "--headless", "--minimized", "--virtual-display"]
|
|
207
|
+
booleanFlags: ["--dry-run", "--wait", "--no-wait", "--headed", "--headless", "--minimized", "--virtual-display"]
|
|
199
208
|
})) {
|
|
200
209
|
return 0;
|
|
201
210
|
}
|
|
202
|
-
assertOnlyOptions(browserArgs, "pro browser login", ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms", "--wait-timeout-ms"], ["--dry-run", "--wait", "--no-wait", "--headless", "--minimized", "--virtual-display"]);
|
|
211
|
+
assertOnlyOptions(browserArgs, "pro browser login", ["--cwd", "--profile-dir", "--port", "--url", "--source-cli", "--launch-timeout-ms", "--wait-timeout-ms"], ["--dry-run", "--wait", "--no-wait", "--headed", "--headless", "--minimized", "--virtual-display"]);
|
|
203
212
|
if (browserArgs.includes("--wait") && browserArgs.includes("--no-wait")) {
|
|
204
213
|
throw new Error("pro browser login cannot combine --wait and --no-wait");
|
|
205
214
|
}
|
|
206
215
|
const loginUrl = readChatGptBrowserUrlFlag(browserArgs);
|
|
207
216
|
const sourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
208
217
|
const targetCwd = readFlag(browserArgs, "--cwd") ? resolveCwdFlag(io.cwd, browserArgs) : undefined;
|
|
209
|
-
const
|
|
218
|
+
const requestedProfileFlag = readFlag(browserArgs, "--profile-dir");
|
|
219
|
+
const requestedProfileDir = requestedProfileFlag
|
|
220
|
+
? resolveBrowserProfileDirForLaunch(requestedProfileFlag)
|
|
221
|
+
: undefined;
|
|
210
222
|
const port = resolveCdpPort(readPortFlag(browserArgs, "--port"));
|
|
211
223
|
const launchTimeoutMs = readPositiveIntegerFlag(browserArgs, "--launch-timeout-ms");
|
|
224
|
+
const savedLaunch = await readLastBrowserLoginLaunch();
|
|
225
|
+
// A saved launch belongs to a control port. Reusing its profile for a
|
|
226
|
+
// different port would silently widen the existing custom-port rules.
|
|
227
|
+
const savedLaunchForPort = savedLaunch?.port === port ? savedLaunch : undefined;
|
|
228
|
+
const savedProfileDir = savedLaunchForPort?.profile_dir
|
|
229
|
+
? resolveBrowserProfileDirForLaunch(savedLaunchForPort.profile_dir)
|
|
230
|
+
: undefined;
|
|
231
|
+
const profileDir = requestedProfileDir ?? savedProfileDir;
|
|
232
|
+
const windowMode = resolveBrowserWindowMode({
|
|
233
|
+
flags: {
|
|
234
|
+
...(browserArgs.includes("--headed") ? { headed: true } : {}),
|
|
235
|
+
...(browserArgs.includes("--headless") ? { headless: true } : {}),
|
|
236
|
+
...(browserArgs.includes("--virtual-display") ? { virtualDisplay: true } : {}),
|
|
237
|
+
...(browserArgs.includes("--minimized") ? { minimized: true } : {})
|
|
238
|
+
},
|
|
239
|
+
// Window mode is a user preference across launches; only the saved
|
|
240
|
+
// profile and display identity are scoped to this resolved port.
|
|
241
|
+
...(savedLaunch ? { lastLogin: savedLaunch } : {})
|
|
242
|
+
});
|
|
212
243
|
const commandOptions = {
|
|
213
244
|
...(targetCwd ? { cwd: targetCwd } : {}),
|
|
214
|
-
...(
|
|
245
|
+
...(requestedProfileDir ? { profileDir: requestedProfileDir } : {}),
|
|
215
246
|
...(port !== DEFAULT_CDP_PORT ? { port } : {}),
|
|
216
247
|
...(readFlag(browserArgs, "--url") ? { url: loginUrl } : {}),
|
|
217
248
|
...(launchTimeoutMs !== undefined ? { launchTimeoutMs } : {})
|
|
@@ -219,6 +250,8 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
219
250
|
if (browserArgs.includes("--dry-run")) {
|
|
220
251
|
printBrowserLoginGuide(io.stdout, {
|
|
221
252
|
opened: false,
|
|
253
|
+
headless: windowMode.headless,
|
|
254
|
+
virtualDisplay: windowMode.virtualDisplay,
|
|
222
255
|
loginUrl,
|
|
223
256
|
profileDir: profileDir ?? defaultChatGptProfileDir(),
|
|
224
257
|
port,
|
|
@@ -231,48 +264,41 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
231
264
|
// again: Chrome's singleton would just open ANOTHER window (the recurring
|
|
232
265
|
// "extra windows" problem, which then blocks sends as
|
|
233
266
|
// ambiguous_chatgpt_tabs). Reuse the running instance instead.
|
|
234
|
-
if (browserArgs.includes("--virtual-display") && browserArgs.includes("--headless")) {
|
|
235
|
-
throw new Error("pro browser login cannot combine --headless and --virtual-display (a virtual display already hides the window).");
|
|
236
|
-
}
|
|
237
|
-
// Reopen the browser the way it was last opened unless a flag or the
|
|
238
|
-
// environment says otherwise. A virtual-display user who follows the
|
|
239
|
-
// `browser_unreachable` advice used to get a visible window back.
|
|
240
|
-
const savedLaunch = await readLastBrowserLoginLaunch();
|
|
241
|
-
const windowMode = resolveBrowserWindowMode({
|
|
242
|
-
flags: {
|
|
243
|
-
...(browserArgs.includes("--headless") ? { headless: true } : {}),
|
|
244
|
-
...(browserArgs.includes("--virtual-display") ? { virtualDisplay: true } : {}),
|
|
245
|
-
...(browserArgs.includes("--minimized") ? { minimized: true } : {})
|
|
246
|
-
},
|
|
247
|
-
...(savedLaunch ? { lastLogin: savedLaunch } : {})
|
|
248
|
-
});
|
|
249
267
|
const headless = windowMode.headless;
|
|
250
|
-
// A real browser on a virtual X display: no window anywhere, and
|
|
251
|
-
// Cloudflare sees an ordinary headed Chrome (headless it rejects).
|
|
252
268
|
const wantsVirtualDisplay = windowMode.virtualDisplay;
|
|
253
|
-
const virtualDisplay = wantsVirtualDisplay
|
|
254
|
-
? await ensureVirtualDisplay(savedLaunch?.virtual_display !== undefined && !browserArgs.includes("--virtual-display")
|
|
255
|
-
? { displayNumber: savedLaunch.virtual_display }
|
|
256
|
-
: {})
|
|
257
|
-
: undefined;
|
|
258
269
|
const alreadyRunning = (await getChatGptBrowserStatus({ port })).reachable;
|
|
259
270
|
if (alreadyRunning) {
|
|
271
|
+
if (requestedProfileDir &&
|
|
272
|
+
savedProfileDir &&
|
|
273
|
+
requestedProfileDir !== savedProfileDir) {
|
|
274
|
+
throw new Error(`A ChatGPT browser is already running on port ${port} with profile ${savedProfileDir}; a different profile was requested. Close the existing browser yourself, then rerun with the intended profile.`);
|
|
275
|
+
}
|
|
260
276
|
// One Chrome profile cannot serve a headed and a headless instance at
|
|
261
277
|
// once, and reusing the running one would silently ignore the
|
|
262
278
|
// requested mode. Say so instead of pretending the switch took.
|
|
263
|
-
const
|
|
264
|
-
const runningHeadless = previous?.port === port ? previous.headless === true : undefined;
|
|
279
|
+
const runningHeadless = savedLaunchForPort ? savedLaunchForPort.headless === true : undefined;
|
|
265
280
|
if (runningHeadless !== undefined && runningHeadless !== headless) {
|
|
266
|
-
throw new Error(`A ${runningHeadless ? "headless" : "headed"} ChatGPT browser is already running on port ${port}, but ${headless ? "headless" : "headed"} was requested. Close
|
|
281
|
+
throw new Error(`A ${runningHeadless ? "headless" : "headed"} ChatGPT browser is already running on port ${port}, but ${headless ? "headless" : "headed"} was requested. Close the existing browser yourself, then rerun; prodex will not end it.`);
|
|
267
282
|
}
|
|
268
283
|
// Same for the display: a browser already on your desktop cannot be
|
|
269
284
|
// moved onto a virtual display by reusing it, and silently reusing
|
|
270
285
|
// it would leave the window exactly where the user asked it not to be.
|
|
271
|
-
const runningVirtual =
|
|
286
|
+
const runningVirtual = savedLaunchForPort ? savedLaunchForPort.virtual_display !== undefined : undefined;
|
|
272
287
|
if (runningVirtual !== undefined && runningVirtual !== wantsVirtualDisplay) {
|
|
273
|
-
throw new Error(`A ChatGPT browser is already running on port ${port} on ${runningVirtual ? "a virtual display" : "your desktop"}, but ${wantsVirtualDisplay ? "a virtual display" : "your desktop"} was requested. Close
|
|
288
|
+
throw new Error(`A ChatGPT browser is already running on port ${port} on ${runningVirtual ? "a virtual display" : "your desktop"}, but ${wantsVirtualDisplay ? "a virtual display" : "your desktop"} was requested. Close the existing browser yourself, then rerun; prodex will not end it.`);
|
|
289
|
+
}
|
|
290
|
+
if (savedLaunchForPort?.minimized === true && !windowMode.minimized) {
|
|
291
|
+
throw new Error(`A minimized ChatGPT browser is already running on port ${port}, but a visible headed browser was requested. Close the existing browser yourself, then rerun with --headed; prodex will not end it or pretend the minimized window was restored.`);
|
|
274
292
|
}
|
|
275
293
|
}
|
|
294
|
+
// Allocate/rejoin a display only for a new browser. An already-running
|
|
295
|
+
// virtual browser owns its saved display identity and needs no new X
|
|
296
|
+
// server just because login was invoked again.
|
|
297
|
+
const virtualDisplay = wantsVirtualDisplay && !alreadyRunning
|
|
298
|
+
? await ensureVirtualDisplay(savedLaunchForPort?.virtual_display !== undefined
|
|
299
|
+
? { displayNumber: savedLaunchForPort.virtual_display }
|
|
300
|
+
: {})
|
|
301
|
+
: undefined;
|
|
276
302
|
// A wedged browser is unreachable, so without this login would launch a
|
|
277
303
|
// second Chrome onto the same profile and leave the first one burning
|
|
278
304
|
// CPU - the same mistake the unattended recovery path made. Checked
|
|
@@ -291,7 +317,9 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
291
317
|
port,
|
|
292
318
|
profileDir,
|
|
293
319
|
url: loginUrl,
|
|
294
|
-
|
|
320
|
+
// Pass false too: openChatGptBrowser otherwise consults the env
|
|
321
|
+
// again and can resurrect a mode the shared resolver disabled.
|
|
322
|
+
headless,
|
|
295
323
|
...(virtualDisplay ? { virtualDisplay: { displayNumber: virtualDisplay.displayNumber, xauthority: virtualDisplay.xauthority } } : {})
|
|
296
324
|
});
|
|
297
325
|
if (!alreadyRunning) {
|
|
@@ -302,10 +330,9 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
302
330
|
// Minimize BEFORE recording, so the record reflects what actually
|
|
303
331
|
// happened (a desktop that hides minimized windows gets restored and
|
|
304
332
|
// recorded as a normal window).
|
|
305
|
-
const wantsMinimized = browserArgs.includes("--minimized") || resolveMinimizeWindowPreference();
|
|
306
333
|
let minimized = false;
|
|
307
334
|
let minimizeNote;
|
|
308
|
-
if (
|
|
335
|
+
if (windowMode.minimized && !headless) {
|
|
309
336
|
try {
|
|
310
337
|
const outcome = await minimizeChatGptWindow({ port: opened.port });
|
|
311
338
|
minimized = outcome.minimized;
|
|
@@ -317,20 +344,31 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
317
344
|
minimizeNote = `window: could not minimize (${errorMessage(error)}); leaving it as it is.`;
|
|
318
345
|
}
|
|
319
346
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
347
|
+
// A reused browser with neither an explicit nor saved profile is
|
|
348
|
+
// reachable, but its actual profile cannot be inferred from the port.
|
|
349
|
+
// Do not overwrite a useful launch record with an invented default.
|
|
350
|
+
if (!alreadyRunning || profileDir) {
|
|
351
|
+
await recordBrowserLoginLaunch({
|
|
352
|
+
profile_dir: opened.profileDir,
|
|
353
|
+
port: opened.port,
|
|
354
|
+
headless,
|
|
355
|
+
minimized,
|
|
356
|
+
...(virtualDisplay
|
|
357
|
+
? { virtual_display: virtualDisplay.displayNumber }
|
|
358
|
+
: wantsVirtualDisplay && savedLaunchForPort?.virtual_display !== undefined
|
|
359
|
+
? { virtual_display: savedLaunchForPort.virtual_display }
|
|
360
|
+
: {})
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
const virtualDisplayNumber = virtualDisplay?.displayNumber ?? savedLaunchForPort?.virtual_display;
|
|
364
|
+
if (wantsVirtualDisplay && virtualDisplayNumber !== undefined) {
|
|
365
|
+
io.stdout(`window: none - running on virtual display :${virtualDisplayNumber}${virtualDisplay?.startedNow ? " (started now)" : " (reused)"}.`);
|
|
329
366
|
}
|
|
330
367
|
printBrowserLoginGuide(io.stdout, {
|
|
331
368
|
opened: !alreadyRunning,
|
|
332
369
|
reused: alreadyRunning,
|
|
333
370
|
headless,
|
|
371
|
+
virtualDisplay: wantsVirtualDisplay,
|
|
334
372
|
loginUrl,
|
|
335
373
|
profileDir: opened.profileDir,
|
|
336
374
|
port: opened.port,
|
|
@@ -345,7 +383,12 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
345
383
|
// Verify it here (bounded, no human to wait for) instead of letting
|
|
346
384
|
// the first consult fail with a confusing not-logged-in blocker.
|
|
347
385
|
const headlessWaitMs = readPositiveIntegerFlag(browserArgs, "--wait-timeout-ms") ?? 30_000;
|
|
348
|
-
const headlessReady = await waitForChatGptLoginReady(io.stderr, {
|
|
386
|
+
const headlessReady = await waitForChatGptLoginReady(io.stderr, {
|
|
387
|
+
port: opened.port,
|
|
388
|
+
timeoutMs: headlessWaitMs,
|
|
389
|
+
windowMode,
|
|
390
|
+
headedLoginCommand: formatHeadedBrowserLoginCommand(sourceCli, commandOptions)
|
|
391
|
+
});
|
|
349
392
|
if (!headlessReady) {
|
|
350
393
|
// Name the real cause. Cloudflare rejects headless Chrome by
|
|
351
394
|
// design (its docs list headless browsers as unsupported), and
|
|
@@ -355,8 +398,8 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
355
398
|
const challenged = finalStatus?.blocker?.code === "cloudflare_check" || /just a moment/i.test(finalStatus?.title ?? "");
|
|
356
399
|
io.stdout("");
|
|
357
400
|
io.stdout(challenged
|
|
358
|
-
?
|
|
359
|
-
: `headless: the profile is not signed in. Run \`${
|
|
401
|
+
? `headless: Cloudflare challenged the headless browser and never let ChatGPT load. Run \`${formatHeadedBrowserLoginCommand(sourceCli, commandOptions)}\` for a visible interactive check; headless browsers are not supported by Cloudflare.`
|
|
402
|
+
: `headless: the profile is not signed in. Run \`${formatHeadedBrowserLoginCommand(sourceCli, commandOptions)}\` for a visible interactive login, sign in, close that browser yourself, then rerun with --headless.`);
|
|
360
403
|
return 1;
|
|
361
404
|
}
|
|
362
405
|
io.stdout("headless: signed-in session confirmed - consults will run with no visible window.");
|
|
@@ -370,7 +413,12 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
370
413
|
if (!shouldWaitForReady)
|
|
371
414
|
return 0;
|
|
372
415
|
const waitTimeoutMs = readPositiveIntegerFlag(browserArgs, "--wait-timeout-ms") ?? 300_000;
|
|
373
|
-
const ready = await waitForChatGptLoginReady(io.stderr, {
|
|
416
|
+
const ready = await waitForChatGptLoginReady(io.stderr, {
|
|
417
|
+
port: opened.port,
|
|
418
|
+
timeoutMs: waitTimeoutMs,
|
|
419
|
+
windowMode,
|
|
420
|
+
headedLoginCommand: formatHeadedBrowserLoginCommand(sourceCli, commandOptions)
|
|
421
|
+
});
|
|
374
422
|
return ready ? 0 : 1;
|
|
375
423
|
}
|
|
376
424
|
if (browserSubcommand === "ask") {
|
|
@@ -933,12 +981,6 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
933
981
|
export async function runConsultsCommand(rest, io) {
|
|
934
982
|
throw new Error("The legacy `consults` alias is retired. Use `prodex pro list`, `prodex pro latest`, or `prodex pro show <task-id|latest>`.");
|
|
935
983
|
}
|
|
936
|
-
// PRODEX_MINIMIZE_WINDOW=1 gives the no-window setup to every entry point,
|
|
937
|
-
// including the MCP server's auto-recovery, without a CLI flag.
|
|
938
|
-
export function resolveMinimizeWindowPreference(env = process.env) {
|
|
939
|
-
const raw = (env.PRODEX_MINIMIZE_WINDOW ?? "").trim().toLowerCase();
|
|
940
|
-
return raw === "1" || raw === "true" || raw === "yes";
|
|
941
|
-
}
|
|
942
984
|
function autoLoginDisabledByEnv(env = process.env) {
|
|
943
985
|
const raw = (env.PRODEX_NO_AUTO_LOGIN ?? "").trim().toLowerCase();
|
|
944
986
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
@@ -1328,9 +1370,9 @@ export async function runAskProCommand(rest, io) {
|
|
|
1328
1370
|
port: browserPort,
|
|
1329
1371
|
prompt: bundle.sendText,
|
|
1330
1372
|
targetUrl: normalizedTargetUrl,
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
...(continuedFromTaskId ? { navigateToTargetUrl: true } : {}),
|
|
1373
|
+
// Continuations and explicit TUI picks navigate while this lock is
|
|
1374
|
+
// held. Direct CLI targets keep their existing verification-only semantics.
|
|
1375
|
+
...(continuedFromTaskId || (io.navigateInteractiveTarget === true && normalizedTargetUrl) ? { navigateToTargetUrl: true } : {}),
|
|
1334
1376
|
timeoutMs: browserTimeoutMs,
|
|
1335
1377
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
1336
1378
|
...(tools.length > 0 ? { tools } : {}),
|
|
@@ -1363,7 +1405,9 @@ export async function runAskProCommand(rest, io) {
|
|
|
1363
1405
|
}
|
|
1364
1406
|
catch (error) {
|
|
1365
1407
|
const firstBlocker = browserSendBlockerFromError(error);
|
|
1366
|
-
|
|
1408
|
+
// Loss while reading an already-submitted prompt must not send it again.
|
|
1409
|
+
if (firstBlocker.code !== "browser_unreachable" ||
|
|
1410
|
+
!firstBlocker.retryable || firstBlocker.thread || !autoLoginAllowed)
|
|
1367
1411
|
throw error;
|
|
1368
1412
|
const recovered = await attemptBrowserAutoRecovery(io.stderr, {
|
|
1369
1413
|
...(browserPort !== undefined ? { port: browserPort } : {}),
|
|
@@ -1801,6 +1845,12 @@ function autoClearDisabledByEnv(env = process.env) {
|
|
|
1801
1845
|
const raw = (env.PRODEX_NO_AUTO_CLEAR ?? "").trim().toLowerCase();
|
|
1802
1846
|
return raw === "1" || raw === "true" || raw === "yes";
|
|
1803
1847
|
}
|
|
1848
|
+
function reportResidualBrowserRecoveryBlocker(stderr, pids, port) {
|
|
1849
|
+
const blocker = wedgedBrowserBlocker([...new Set(pids)].sort((left, right) => left - right), port);
|
|
1850
|
+
stderr(`recover: failed - ${blocker.message}`);
|
|
1851
|
+
stderr(`recover: ${blocker.next_step}`);
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1804
1854
|
/**
|
|
1805
1855
|
* What recovery did to the browser, in the words the receipt keeps.
|
|
1806
1856
|
*
|
|
@@ -1825,9 +1875,26 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1825
1875
|
// scanning the DEFAULT profile for a custom-profile user put a second,
|
|
1826
1876
|
// healthy browser's renderers on the list this function kills.
|
|
1827
1877
|
const lastLogin = await readLastBrowserLoginLaunch().catch(() => undefined);
|
|
1878
|
+
const recoveryPort = resolveCdpPort(options.port);
|
|
1879
|
+
const savedIdentityBelongsToAnotherPort = lastLogin?.port !== undefined &&
|
|
1880
|
+
lastLogin.port !== recoveryPort &&
|
|
1881
|
+
(lastLogin.profile_dir !== undefined || lastLogin.virtual_display !== undefined);
|
|
1882
|
+
if (savedIdentityBelongsToAnotherPort) {
|
|
1883
|
+
stderr(`recover: failed - saved browser identity belongs to port ${lastLogin.port}, not requested port ${recoveryPort}. Run \`prodex pro browser login --port ${recoveryPort} --headed\` with the intended profile before retrying; prodex will not launch an unknown account.`);
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
const lastLoginForPort = lastLogin?.port === undefined || lastLogin.port === recoveryPort ? lastLogin : undefined;
|
|
1887
|
+
let windowMode;
|
|
1888
|
+
try {
|
|
1889
|
+
windowMode = resolveBrowserWindowMode({ ...(lastLogin ? { lastLogin } : {}) });
|
|
1890
|
+
}
|
|
1891
|
+
catch (error) {
|
|
1892
|
+
stderr(`recover: failed - ${errorMessage(error)}`);
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1828
1895
|
const scanFor = {
|
|
1829
1896
|
...(options.port !== undefined ? { port: options.port } : {}),
|
|
1830
|
-
...(
|
|
1897
|
+
...(lastLoginForPort?.profile_dir ? { profileDir: lastLoginForPort.profile_dir } : {})
|
|
1831
1898
|
};
|
|
1832
1899
|
const wedged = findWedgedBrowser(scanFor);
|
|
1833
1900
|
if (wedged.length > 0) {
|
|
@@ -1847,17 +1914,25 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1847
1914
|
return false;
|
|
1848
1915
|
}
|
|
1849
1916
|
stderr(`recover: the browser stopped answering; ending it (pid ${wedged.join(", ")}) and starting a fresh one...`);
|
|
1850
|
-
//
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1917
|
+
// Keep the attempt even if launch fails; completion is recorded after READY.
|
|
1918
|
+
options.notes?.push(`browser_recovery_started: prodex began restarting the unresponsive dedicated browser (pid ${wedged.join(", ")}).`);
|
|
1919
|
+
const termination = await endWedgedBrowser(wedged);
|
|
1920
|
+
if (termination.failed.length > 0) {
|
|
1921
|
+
return reportResidualBrowserRecoveryBlocker(stderr, termination.failed, recoveryPort);
|
|
1922
|
+
}
|
|
1854
1923
|
// Wait for the profile lock to actually clear rather than guessing at a
|
|
1855
1924
|
// delay: the replacement launch fails outright if the old process still
|
|
1856
1925
|
// holds it, which is how the first self-heal attempt ended.
|
|
1926
|
+
let residualPids = [];
|
|
1857
1927
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
1858
|
-
|
|
1928
|
+
residualPids = findWedgedBrowser(scanFor);
|
|
1929
|
+
if (residualPids.length === 0)
|
|
1859
1930
|
break;
|
|
1860
|
-
|
|
1931
|
+
if (attempt < 9)
|
|
1932
|
+
await sleep(1_000);
|
|
1933
|
+
}
|
|
1934
|
+
if (residualPids.length > 0) {
|
|
1935
|
+
return reportResidualBrowserRecoveryBlocker(stderr, residualPids, recoveryPort);
|
|
1861
1936
|
}
|
|
1862
1937
|
}
|
|
1863
1938
|
stderr("recover: browser is not running - launching the dedicated ChatGPT browser (Ctrl+C aborts)...");
|
|
@@ -1865,37 +1940,50 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
|
|
|
1865
1940
|
// Reuse the profile the user last logged in with; launching the default
|
|
1866
1941
|
// profile for a custom-profile user would wait on the wrong (logged-out)
|
|
1867
1942
|
// profile or, worse, silently send to a different account.
|
|
1868
|
-
//
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1871
|
-
const
|
|
1872
|
-
|
|
1873
|
-
// put a window back on a desktop they deliberately keep empty.
|
|
1874
|
-
const virtualDisplay = lastLogin?.virtual_display !== undefined || resolveVirtualDisplayPreference()
|
|
1875
|
-
? await ensureVirtualDisplay(lastLogin?.virtual_display !== undefined ? { displayNumber: lastLogin.virtual_display } : {}).catch(() => undefined)
|
|
1943
|
+
// Recovery follows the same precedence as login. In particular, one
|
|
1944
|
+
// supplied false env setting selects the env group and disables saved
|
|
1945
|
+
// modes instead of being mistaken for "not configured".
|
|
1946
|
+
const virtualDisplay = windowMode.virtualDisplay
|
|
1947
|
+
? await ensureVirtualDisplay(lastLoginForPort?.virtual_display !== undefined ? { displayNumber: lastLoginForPort.virtual_display } : {})
|
|
1876
1948
|
: undefined;
|
|
1877
1949
|
const opened = openChatGptBrowser({
|
|
1878
1950
|
...(options.port !== undefined ? { port: options.port } : {}),
|
|
1879
|
-
...(
|
|
1880
|
-
|
|
1951
|
+
...(lastLoginForPort?.profile_dir ? { profileDir: lastLoginForPort.profile_dir } : {}),
|
|
1952
|
+
// Always pass the resolved boolean. Omitting false lets the browser
|
|
1953
|
+
// boundary re-read PRODEX_HEADLESS and revive stale environment state.
|
|
1954
|
+
headless: windowMode.headless,
|
|
1881
1955
|
...(virtualDisplay ? { virtualDisplay: { displayNumber: virtualDisplay.displayNumber, xauthority: virtualDisplay.xauthority } } : {})
|
|
1882
1956
|
});
|
|
1883
1957
|
await assertBrowserLaunchStayedAlive(opened);
|
|
1884
|
-
|
|
1885
|
-
if (
|
|
1886
|
-
return false;
|
|
1887
|
-
// Restore the no-window setup too: someone who runs minimized does not
|
|
1888
|
-
// want recovery to leave a window sitting on their desktop.
|
|
1889
|
-
if (!headless && (lastLogin?.minimized === true || resolveMinimizeWindowPreference())) {
|
|
1958
|
+
let minimized = false;
|
|
1959
|
+
if (windowMode.minimized) {
|
|
1890
1960
|
const outcome = await minimizeChatGptWindow({ port: opened.port }).catch(() => undefined);
|
|
1891
|
-
|
|
1961
|
+
minimized = outcome?.minimized === true;
|
|
1962
|
+
if (minimized)
|
|
1892
1963
|
stderr("recover: window minimized again");
|
|
1893
1964
|
}
|
|
1965
|
+
// Record the actual launch before waiting for authentication. A manual
|
|
1966
|
+
// login must target this profile and display even if readiness is blocked.
|
|
1967
|
+
await recordBrowserLoginLaunch({
|
|
1968
|
+
profile_dir: opened.profileDir,
|
|
1969
|
+
port: opened.port,
|
|
1970
|
+
headless: windowMode.headless,
|
|
1971
|
+
minimized,
|
|
1972
|
+
...(virtualDisplay ? { virtual_display: virtualDisplay.displayNumber } : {})
|
|
1973
|
+
});
|
|
1974
|
+
const ready = await waitForChatGptLoginReady(stderr, {
|
|
1975
|
+
port: opened.port,
|
|
1976
|
+
timeoutMs: 120_000,
|
|
1977
|
+
windowMode,
|
|
1978
|
+
headedLoginCommand: formatHeadedBrowserLoginCommand(undefined, {
|
|
1979
|
+
profileDir: opened.profileDir,
|
|
1980
|
+
...(opened.port !== DEFAULT_CDP_PORT ? { port: opened.port } : {})
|
|
1981
|
+
})
|
|
1982
|
+
});
|
|
1983
|
+
if (!ready)
|
|
1984
|
+
return false;
|
|
1894
1985
|
stderr("recover: browser READY - retrying the send...");
|
|
1895
|
-
|
|
1896
|
-
// the browser was gone and this answer came from one prodex started.
|
|
1897
|
-
if (wedged.length === 0)
|
|
1898
|
-
options.notes?.push(browserRecoveredNote([]));
|
|
1986
|
+
options.notes?.push(browserRecoveredNote(wedged));
|
|
1899
1987
|
return true;
|
|
1900
1988
|
}
|
|
1901
1989
|
catch (error) {
|
|
@@ -1915,8 +2003,13 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
1915
2003
|
const openTabFn = deps.openTabFn ?? openChatGptTab;
|
|
1916
2004
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
1917
2005
|
const pollMs = options.pollMs ?? 2_000;
|
|
2006
|
+
const hasInteractiveWindow = options.windowMode?.headless !== true && options.windowMode?.virtualDisplay !== true;
|
|
2007
|
+
const headedLoginCommand = options.headedLoginCommand ?? "prodex pro browser login --headed";
|
|
2008
|
+
const headedLoginHint = ` No interactive window is available; run \`${headedLoginCommand}\` to complete login, captcha, or human verification visibly.`;
|
|
1918
2009
|
const startedAt = now();
|
|
1919
|
-
stderr(
|
|
2010
|
+
stderr(hasInteractiveWindow
|
|
2011
|
+
? "login: waiting for a logged-in ChatGPT tab (finish login in the dedicated Chrome browser; Ctrl+C stops waiting)..."
|
|
2012
|
+
: `login: waiting for a logged-in ChatGPT tab (no interactive window; use \`${headedLoginCommand}\` if login or verification is required; Ctrl+C stops waiting)...`);
|
|
1920
2013
|
let lastState = "";
|
|
1921
2014
|
let openMissingTabAttempts = 0;
|
|
1922
2015
|
while (now() - startedAt < timeoutMs) {
|
|
@@ -1933,19 +2026,26 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
1933
2026
|
? "login: the running Chrome had no ChatGPT tab - opening a ChatGPT tab in it..."
|
|
1934
2027
|
: `login: still no ChatGPT tab - opening one again (attempt ${attempt})...`);
|
|
1935
2028
|
const opened = await openTabFn(options.port);
|
|
1936
|
-
if (opened === false)
|
|
1937
|
-
stderr(
|
|
2029
|
+
if (opened === false) {
|
|
2030
|
+
stderr(hasInteractiveWindow
|
|
2031
|
+
? "login: could not open a ChatGPT tab through the debug port; open https://chatgpt.com/ in that browser."
|
|
2032
|
+
: `login: could not open a ChatGPT tab through the debug port; run \`${headedLoginCommand}\` to open it visibly.`);
|
|
2033
|
+
}
|
|
1938
2034
|
await sleepFn(pollMs);
|
|
1939
2035
|
continue;
|
|
1940
2036
|
}
|
|
1941
2037
|
const state = !status.reachable
|
|
1942
2038
|
? "login: browser starting..."
|
|
1943
2039
|
: status.blocker
|
|
1944
|
-
? `login: blocked - ${status.blocker.message}`
|
|
2040
|
+
? `login: blocked - ${status.blocker.message}${hasInteractiveWindow ? "" : headedLoginHint}`
|
|
1945
2041
|
: !status.loggedInLikely
|
|
1946
|
-
?
|
|
2042
|
+
? hasInteractiveWindow
|
|
2043
|
+
? "login: waiting for ChatGPT login in the dedicated Chrome browser..."
|
|
2044
|
+
: `login: waiting for a saved ChatGPT login.${headedLoginHint}`
|
|
1947
2045
|
: !status.hasComposer
|
|
1948
|
-
?
|
|
2046
|
+
? hasInteractiveWindow
|
|
2047
|
+
? "login: logged in; open a chat so the prompt composer is visible..."
|
|
2048
|
+
: `login: logged in, but no prompt composer is ready.${headedLoginHint}`
|
|
1949
2049
|
: "";
|
|
1950
2050
|
if (state === "") {
|
|
1951
2051
|
stderr(`login: READY - logged-in ChatGPT tab with composer detected (${Math.round((now() - startedAt) / 1000)}s).`);
|
|
@@ -1960,7 +2060,9 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
1960
2060
|
break;
|
|
1961
2061
|
await sleepFn(Math.min(pollMs, Math.max(1, remainingMs)));
|
|
1962
2062
|
}
|
|
1963
|
-
stderr(
|
|
2063
|
+
stderr(hasInteractiveWindow
|
|
2064
|
+
? `login: not ready after ${Math.round(timeoutMs / 1000)}s. Finish login in the browser, then verify with \`prodex pro browser check\`.`
|
|
2065
|
+
: `login: not ready after ${Math.round(timeoutMs / 1000)}s. Run \`${headedLoginCommand}\` to complete login, captcha, or human verification visibly, then retry.`);
|
|
1964
2066
|
return false;
|
|
1965
2067
|
}
|
|
1966
2068
|
export async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
|
|
@@ -2551,9 +2653,14 @@ export async function listConsultListEntries(store, options = { readOnly: true }
|
|
|
2551
2653
|
}
|
|
2552
2654
|
return entries;
|
|
2553
2655
|
}
|
|
2656
|
+
function formatHeadedBrowserLoginCommand(sourceCli, options = {}) {
|
|
2657
|
+
return `${formatBrowserLoginCommand(sourceCli, options)} --headed`;
|
|
2658
|
+
}
|
|
2554
2659
|
export function printBrowserLoginGuide(stdout, input) {
|
|
2555
|
-
const
|
|
2660
|
+
const noInteractiveWindow = input.headless === true || input.virtualDisplay === true;
|
|
2661
|
+
const windowAvailable = (input.opened || input.reused === true) && !noInteractiveWindow;
|
|
2556
2662
|
const loginCommand = formatBrowserLoginCommand(input.sourceCli, input.commandOptions);
|
|
2663
|
+
const headedLoginCommand = formatHeadedBrowserLoginCommand(input.sourceCli, input.commandOptions);
|
|
2557
2664
|
const runtimeCommandOptions = {
|
|
2558
2665
|
...(input.commandOptions?.cwd ? { cwd: input.commandOptions.cwd } : {}),
|
|
2559
2666
|
...(input.commandOptions?.port ? { port: input.commandOptions.port } : {})
|
|
@@ -2565,13 +2672,18 @@ export function printBrowserLoginGuide(stdout, input) {
|
|
|
2565
2672
|
? input.reused
|
|
2566
2673
|
? "Headless ChatGPT browser is already running - reusing it (no window)."
|
|
2567
2674
|
: "Started the dedicated ChatGPT browser headless (no window). It reuses the profile you signed in with."
|
|
2568
|
-
: input.reused
|
|
2569
|
-
?
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2675
|
+
: input.virtualDisplay && (input.opened || input.reused)
|
|
2676
|
+
? input.reused
|
|
2677
|
+
? "ChatGPT browser is already running on its saved virtual display - reusing it (no visible window)."
|
|
2678
|
+
: "Started the dedicated ChatGPT browser on a virtual display (no visible window)."
|
|
2679
|
+
: input.reused
|
|
2680
|
+
? "Chrome is already running for ChatGPT - reusing it (no new window opened)."
|
|
2681
|
+
: input.opened
|
|
2682
|
+
? "Opened the dedicated Chrome window for ChatGPT."
|
|
2683
|
+
: "Dry run: no browser was opened.");
|
|
2684
|
+
if (noInteractiveWindow && (input.opened || input.reused)) {
|
|
2574
2685
|
stdout("");
|
|
2686
|
+
stdout(`If ChatGPT needs login, captcha, or human verification, run \`${headedLoginCommand}\` to handle it in a visible window.`);
|
|
2575
2687
|
stdout(`Next: run \`${checkCommand}\` to confirm the session, then consult as usual.`);
|
|
2576
2688
|
return;
|
|
2577
2689
|
}
|
|
@@ -2587,7 +2699,9 @@ export function printBrowserLoginGuide(stdout, input) {
|
|
|
2587
2699
|
stdout(`7. Run \`${smokeCommand}\` to verify a real Pro response path.`);
|
|
2588
2700
|
}
|
|
2589
2701
|
else {
|
|
2590
|
-
stdout(
|
|
2702
|
+
stdout(noInteractiveWindow
|
|
2703
|
+
? `1. Run \`${headedLoginCommand}\` to open a visible dedicated Chrome window for login or verification.`
|
|
2704
|
+
: `1. Run \`${loginCommand}\` without \`--dry-run\` to open the dedicated Chrome window.`);
|
|
2591
2705
|
stdout(`2. Log in manually at ${input.loginUrl} in that Chrome window.`);
|
|
2592
2706
|
stdout("3. If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, complete it in the browser.");
|
|
2593
2707
|
stdout("4. For usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.");
|
package/dist/cli-server.js
CHANGED
|
@@ -40,8 +40,8 @@ export async function runSetupCommand(rest, io) {
|
|
|
40
40
|
? await runBrowserDefaultsWizard(resolvePromptUser(io), io.stdout)
|
|
41
41
|
: parseBrowserDefaultFlags(rest);
|
|
42
42
|
const config = await writeLocalConfig(targetCwd, {
|
|
43
|
-
host: readFlag(rest, "--host")
|
|
44
|
-
port: readPortFlag(rest, "--port")
|
|
43
|
+
host: readFlag(rest, "--host"),
|
|
44
|
+
port: readPortFlag(rest, "--port"),
|
|
45
45
|
token: readFlag(rest, "--token"),
|
|
46
46
|
tokenTtlHours: readPositiveNumberFlag(rest, "--token-ttl-hours"),
|
|
47
47
|
browserDefaults
|
package/dist/cli.js
CHANGED
|
@@ -82,10 +82,6 @@ async function runInteractiveUi(io) {
|
|
|
82
82
|
const { listRecentChatGptConversations } = await import("./chatgpt-browser.js");
|
|
83
83
|
return listRecentChatGptConversations({});
|
|
84
84
|
},
|
|
85
|
-
openThread: async (url) => {
|
|
86
|
-
const { navigateChatGptTabTo } = await import("./chatgpt-browser.js");
|
|
87
|
-
return navigateChatGptTabTo(url, {});
|
|
88
|
-
},
|
|
89
85
|
listProjectsWithIds: async () => {
|
|
90
86
|
const { listChatGptProjectsWithIds } = await import("./chatgpt-browser.js");
|
|
91
87
|
return listChatGptProjectsWithIds({});
|
|
@@ -97,6 +93,7 @@ async function runInteractiveUi(io) {
|
|
|
97
93
|
},
|
|
98
94
|
runConsult: (args, onProgress) => runCli(args, {
|
|
99
95
|
...io,
|
|
96
|
+
navigateInteractiveTarget: true,
|
|
100
97
|
stderr: (line) => {
|
|
101
98
|
if (line.startsWith("progress:"))
|
|
102
99
|
onProgress(line);
|
package/dist/config.js
CHANGED
|
@@ -102,9 +102,9 @@ export async function writeLocalConfig(cwd, input = {}) {
|
|
|
102
102
|
await ensureBridgeLocalFiles(cwd);
|
|
103
103
|
await assertLocalConfigTargetSafe(cwd);
|
|
104
104
|
const now = new Date().toISOString();
|
|
105
|
-
const host = normalizeLoopbackHttpHost(input.host ?? "127.0.0.1");
|
|
106
|
-
const port = input.port ?? 8787;
|
|
107
105
|
const existing = await readExistingConfig(cwd);
|
|
106
|
+
const host = normalizeLoopbackHttpHost(input.host ?? existing?.host ?? "127.0.0.1");
|
|
107
|
+
const port = input.port ?? existing?.port ?? 8787;
|
|
108
108
|
// A setup re-run that only adjusts defaults/host/port must NOT rotate the
|
|
109
109
|
// token - that would silently 401 every client holding the old MCP URL.
|
|
110
110
|
// Rotation happens only when the caller explicitly asks for a token
|
|
@@ -275,7 +275,7 @@ export function getTokenExpiryStatus(config, now = new Date()) {
|
|
|
275
275
|
? {
|
|
276
276
|
status: "expired",
|
|
277
277
|
token_expires_at: config.token_expires_at,
|
|
278
|
-
warning: `Token expired at ${config.token_expires_at}. Run \`prodex setup
|
|
278
|
+
warning: `Token expired at ${config.token_expires_at}. Run \`prodex setup --token-ttl-hours <hours>\` to create a new URL, then restart \`prodex start\`.`
|
|
279
279
|
}
|
|
280
280
|
: { status: "valid", token_expires_at: config.token_expires_at };
|
|
281
281
|
}
|
package/dist/http-mcp.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function startHttpMcpServer(options) {
|
|
|
27
27
|
// HOW to authorize without leaking anything token-specific.
|
|
28
28
|
writeJson(res, 401, {
|
|
29
29
|
error: "unauthorized",
|
|
30
|
-
hint: "Provide a valid token via `?prodex_token=<token>` or `Authorization: Bearer <token>`. If your token expired,
|
|
30
|
+
hint: "Provide a valid token via `?prodex_token=<token>` or `Authorization: Bearer <token>`. If your token expired, run `prodex setup --token-ttl-hours <hours>`, restart `prodex start`, and read the new URL with `prodex status --show-token --url-only`."
|
|
31
31
|
});
|
|
32
32
|
return;
|
|
33
33
|
}
|