@youdie006/prodex 0.16.29 → 0.16.31
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 +109 -9
- package/dist/cli-help.js +1 -0
- package/dist/cli-pro.js +79 -7
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1246,13 +1246,15 @@ async function selectProject(cdp, options) {
|
|
|
1246
1246
|
// project - which would silently send the prompt into the wrong project.
|
|
1247
1247
|
const alreadyInRequestedProject = await cdp.evaluate(`(() => {
|
|
1248
1248
|
if (!/^https:\\/\\/chatgpt\\.com\\/g\\/g-p-/.test(location.href)) return false;
|
|
1249
|
-
const name = ${JSON.stringify(options.project)};
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1255
|
-
|
|
1249
|
+
const name = ${JSON.stringify(options.project)}.toLowerCase();
|
|
1250
|
+
// Case-insensitive EQUALITY (not substring): matches the case-insensitive
|
|
1251
|
+
// sidebar-row lookup (so "codex" is accepted while sitting on "Codex"),
|
|
1252
|
+
// but a stalled cross-project navigation must NOT be accepted just because
|
|
1253
|
+
// the current project's name CONTAINS the requested one (e.g. "Research
|
|
1254
|
+
// Lab" while "Research" was requested) - that would send into the wrong
|
|
1255
|
+
// project.
|
|
1256
|
+
if ((document.title || "").trim().toLowerCase() === name) return true;
|
|
1257
|
+
return [...document.querySelectorAll('h1,[role="heading"]')].some((h) => (h.innerText || "").trim().toLowerCase() === name);
|
|
1256
1258
|
})()`);
|
|
1257
1259
|
if (!alreadyInRequestedProject) {
|
|
1258
1260
|
throw new Error(`Clicking project "${options.project}" did not navigate the visible tab. If the tab is already inside this project, omit --project and retry.`);
|
|
@@ -1277,6 +1279,87 @@ async function selectProject(cdp, options) {
|
|
|
1277
1279
|
throw new Error(`ChatGPT composer did not appear after entering project "${options.project}"`);
|
|
1278
1280
|
}
|
|
1279
1281
|
}
|
|
1282
|
+
// Read the finished answer from an existing ChatGPT thread WITHOUT sending a new
|
|
1283
|
+
// prompt. Recovers a consult whose send timed out but whose answer ChatGPT
|
|
1284
|
+
// completed afterwards: the durable receipt is "blocked", yet the full answer
|
|
1285
|
+
// sits in the thread the operator can see. Navigates the visible tab to the
|
|
1286
|
+
// thread and waits for a stable, non-generating answer.
|
|
1287
|
+
export async function recoverChatGptAnswerFromThread(options) {
|
|
1288
|
+
const port = resolveCdpPort(options.port);
|
|
1289
|
+
const timeoutMs = Math.max(1_000, options.timeoutMs ?? 60_000);
|
|
1290
|
+
const url = normalizeChatGptTargetUrl(options.targetUrl);
|
|
1291
|
+
const page = await findChatGptPage(port, 3_000);
|
|
1292
|
+
if (!page.ok || !page.page) {
|
|
1293
|
+
throw new ChatGptBrowserBlockerError(page.blocker ?? {
|
|
1294
|
+
code: "browser_unreachable",
|
|
1295
|
+
message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
|
|
1296
|
+
retryable: true,
|
|
1297
|
+
next_step: "Run `prodex pro browser login`, log in, then retry."
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
const cdp = await connectCdp(page.page.webSocketDebuggerUrl);
|
|
1301
|
+
let state;
|
|
1302
|
+
let generating = false;
|
|
1303
|
+
let stableRuns = 0;
|
|
1304
|
+
let lastAnswer = "";
|
|
1305
|
+
try {
|
|
1306
|
+
await cdp.send("Runtime.enable");
|
|
1307
|
+
// In-tab navigation (location.assign, not Page.navigate which has crashed the
|
|
1308
|
+
// instance) so we read the requested thread, not whatever was open.
|
|
1309
|
+
await cdp.evaluate(`location.assign(${JSON.stringify(url)})`);
|
|
1310
|
+
const deadline = Date.now() + timeoutMs;
|
|
1311
|
+
while (Date.now() < deadline) {
|
|
1312
|
+
await sleep(500);
|
|
1313
|
+
try {
|
|
1314
|
+
state = await evaluateOnPage(page.page, answerExpression());
|
|
1315
|
+
}
|
|
1316
|
+
catch {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
generating = state.generating;
|
|
1320
|
+
const runtimeBlocker = chatGptBlockerFromAnswerState(state);
|
|
1321
|
+
if (runtimeBlocker)
|
|
1322
|
+
throw new ChatGptBrowserBlockerError(runtimeBlocker);
|
|
1323
|
+
// Require a REAL assistant message, not answerExpression's page-chrome
|
|
1324
|
+
// fallback (empty assistant returns sidebar/nav text): the thread's
|
|
1325
|
+
// conversation loads asynchronously after navigation, so keep polling.
|
|
1326
|
+
if (state.assistantMessageCount > 0 && isUsableChatGptAnswer(state.answer) && !state.generating) {
|
|
1327
|
+
// Two identical settled reads: a just-finished streaming caret artifact
|
|
1328
|
+
// must not sneak into the recovered text.
|
|
1329
|
+
stableRuns = state.answer === lastAnswer ? stableRuns + 1 : 0;
|
|
1330
|
+
lastAnswer = state.answer;
|
|
1331
|
+
if (stableRuns >= 1)
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
stableRuns = 0;
|
|
1336
|
+
lastAnswer = "";
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
finally {
|
|
1341
|
+
cdp.close();
|
|
1342
|
+
}
|
|
1343
|
+
if (!state || state.assistantMessageCount < 1 || !isUsableChatGptAnswer(state.answer)) {
|
|
1344
|
+
throw new ChatGptBrowserBlockerError({
|
|
1345
|
+
code: generating ? "still_generating" : "no_recoverable_answer",
|
|
1346
|
+
message: generating
|
|
1347
|
+
? "That thread is still generating - the answer is not complete yet."
|
|
1348
|
+
: "No finished assistant answer loaded from that thread (the conversation may not have rendered, or the URL is not the consult thread).",
|
|
1349
|
+
retryable: true,
|
|
1350
|
+
next_step: generating
|
|
1351
|
+
? "Wait for ChatGPT to finish, then rerun `prodex pro browser recover --target-url <url>`."
|
|
1352
|
+
: "Confirm the URL is the consult thread that shows a finished answer, raise --timeout-ms if the page loads slowly, or send a fresh consult."
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
url: state.url,
|
|
1357
|
+
title: state.title,
|
|
1358
|
+
answer: state.answer.trim(),
|
|
1359
|
+
modelHints: state.modelHints,
|
|
1360
|
+
warnings: []
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1280
1363
|
export async function sendChatGptPrompt(options) {
|
|
1281
1364
|
const port = resolveCdpPort(options.port);
|
|
1282
1365
|
const timeoutMs = options.timeoutMs ?? 90_000;
|
|
@@ -1379,11 +1462,27 @@ export async function sendChatGptPrompt(options) {
|
|
|
1379
1462
|
};
|
|
1380
1463
|
let beforeSubmit;
|
|
1381
1464
|
let submitButtonFound = false;
|
|
1465
|
+
const sendWarnings = [];
|
|
1382
1466
|
const cdp = await connectCdp(page.webSocketDebuggerUrl);
|
|
1383
1467
|
try {
|
|
1384
1468
|
await cdp.send("Runtime.enable");
|
|
1385
1469
|
await selectProject(cdp, options);
|
|
1386
|
-
|
|
1470
|
+
try {
|
|
1471
|
+
await selectModelReasoning(cdp, options);
|
|
1472
|
+
}
|
|
1473
|
+
catch (modelError) {
|
|
1474
|
+
// Pro sub-mode isn't exposed in this UI yet (staged rollout). Pro itself is
|
|
1475
|
+
// already selected by this point, so degrade to plain Pro with a warning
|
|
1476
|
+
// instead of failing the whole consult - an agent's --pro-mode 확장 becomes
|
|
1477
|
+
// a successful plain-Pro send rather than a hard block it has to retry past.
|
|
1478
|
+
const modelMsg = modelError instanceof Error ? modelError.message : String(modelError);
|
|
1479
|
+
if (options.proMode && /does not expose Pro sub-modes/.test(modelMsg)) {
|
|
1480
|
+
sendWarnings.push("pro_mode_unavailable: this ChatGPT UI does not expose Pro sub-modes yet, so the consult was sent as plain Pro. Drop --pro-mode (or run `prodex setup --clear-pro-mode`).");
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
throw modelError;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1387
1486
|
// Capture the answer baseline AFTER any project navigation or model switch
|
|
1388
1487
|
// so assistant-message counts compare within the thread we actually send
|
|
1389
1488
|
// into; a --project/--project-new hop lands on a page with its own counts.
|
|
@@ -1520,7 +1619,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
1520
1619
|
title: completed.title,
|
|
1521
1620
|
answer: completed.answer.trim(),
|
|
1522
1621
|
modelHints: completed.modelHints,
|
|
1523
|
-
warnings: []
|
|
1622
|
+
warnings: [...sendWarnings]
|
|
1524
1623
|
};
|
|
1525
1624
|
}
|
|
1526
1625
|
// Timed out while the answer was still streaming: salvage the partial text
|
|
@@ -1534,6 +1633,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
1534
1633
|
answer: completed.answer.trim(),
|
|
1535
1634
|
modelHints: completed.modelHints,
|
|
1536
1635
|
warnings: [
|
|
1636
|
+
...sendWarnings,
|
|
1537
1637
|
`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.`
|
|
1538
1638
|
]
|
|
1539
1639
|
};
|
package/dist/cli-help.js
CHANGED
|
@@ -25,6 +25,7 @@ Ask / consult commands:
|
|
|
25
25
|
prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]
|
|
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
|
+
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
|
|
28
29
|
prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
|
|
29
30
|
prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
|
|
30
31
|
prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
|
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, formatDurationMs, 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, recoverChatGptAnswerFromThread, 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";
|
|
@@ -366,7 +366,64 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
366
366
|
io.stdout("Use with `pro browser ask --project \"<name>\"` or pin one with `prodex setup --project \"<name>\"`.");
|
|
367
367
|
return 0;
|
|
368
368
|
}
|
|
369
|
-
|
|
369
|
+
if (browserSubcommand === "recover") {
|
|
370
|
+
if (printProBrowserHelpIfRequested(browserArgs, "pro browser recover", io, { valueFlags: ["--cwd", "--port", "--timeout-ms", "--target-url", "--source-cli"] }))
|
|
371
|
+
return 0;
|
|
372
|
+
assertOnlyOptions(browserArgs, "pro browser recover", ["--cwd", "--port", "--timeout-ms", "--target-url", "--source-cli"]);
|
|
373
|
+
const recoverCwd = resolveCwdFlag(io.cwd, browserArgs);
|
|
374
|
+
const recoverSourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
|
|
375
|
+
const targetUrl = readFlag(browserArgs, "--target-url");
|
|
376
|
+
if (!targetUrl) {
|
|
377
|
+
throw new Error("pro browser recover requires --target-url <thread-url> - the ChatGPT conversation URL whose finished answer to recover (e.g. the thread from a send_timeout blocker).");
|
|
378
|
+
}
|
|
379
|
+
const recoverPort = readPortFlag(browserArgs, "--port");
|
|
380
|
+
const recoverTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
|
|
381
|
+
const recoverResolvedPort = resolveCdpPort(recoverPort);
|
|
382
|
+
let consult;
|
|
383
|
+
try {
|
|
384
|
+
consult = await recoverChatGptAnswerFromThread({ port: recoverPort, targetUrl, timeoutMs: recoverTimeoutMs });
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), recoverSourceCli, {
|
|
388
|
+
...(recoverResolvedPort !== DEFAULT_CDP_PORT ? { port: recoverResolvedPort } : {})
|
|
389
|
+
});
|
|
390
|
+
throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
|
|
391
|
+
}
|
|
392
|
+
// Record the recovered answer as a done consult so `pro latest` re-prints it.
|
|
393
|
+
const recoverStore = new BridgeStore(recoverCwd);
|
|
394
|
+
const recoveredTask = await recoverStore.createTask({
|
|
395
|
+
source: "codex",
|
|
396
|
+
title: "GPT Pro consult (recovered)",
|
|
397
|
+
prompt: `Recovered answer from ${consult.url}`,
|
|
398
|
+
repo_id: "default",
|
|
399
|
+
files: [],
|
|
400
|
+
provenance: { adapter: "chatgpt-control", thread: consult.url, warnings: [] }
|
|
401
|
+
});
|
|
402
|
+
const recoveredArtifactText = formatProConsultArtifact(consult);
|
|
403
|
+
let recoveredArtifactPath;
|
|
404
|
+
try {
|
|
405
|
+
recoveredArtifactPath = await recoverStore.writeArtifactText(`.bridge/artifacts/pro-consults/${recoveredTask.id}.md`, recoveredArtifactText);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
io.stderr(`answer_artifact_warning: ${errorMessage(error)}`);
|
|
409
|
+
}
|
|
410
|
+
await recoverStore.completeTask(recoveredTask.id, {
|
|
411
|
+
status: "done",
|
|
412
|
+
summary: consult.answer,
|
|
413
|
+
artifacts: recoveredArtifactPath
|
|
414
|
+
? [{ path: recoveredArtifactPath, role: "result", bytes: Buffer.byteLength(recoveredArtifactText, "utf8") }]
|
|
415
|
+
: [],
|
|
416
|
+
commands: ["recovered ChatGPT answer from thread"],
|
|
417
|
+
warnings: [],
|
|
418
|
+
provenance: { thread: consult.url, warnings: [] }
|
|
419
|
+
});
|
|
420
|
+
io.stdout(`${recoveredTask.id}\tdone\t${consult.url}`);
|
|
421
|
+
io.stdout("");
|
|
422
|
+
io.stdout(consult.answer);
|
|
423
|
+
io.stderr(`recovered: answer saved to .bridge; re-print with \`prodex pro latest --cwd ${recoverCwd}\``);
|
|
424
|
+
return 0;
|
|
425
|
+
}
|
|
426
|
+
throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models", "projects", "recover"]);
|
|
370
427
|
}
|
|
371
428
|
if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
|
|
372
429
|
throw new Error(`Use \`prodex pro browser ${subcommand === "doctor" ? "check" : subcommand}\` for explicit browser automation.`);
|
|
@@ -545,7 +602,19 @@ export async function runAskProCommand(rest, io) {
|
|
|
545
602
|
}
|
|
546
603
|
const targetCwd = resolveCwdFlag(io.cwd, parsedAskPro.optionArgs);
|
|
547
604
|
const targetStore = new BridgeStore(targetCwd);
|
|
548
|
-
const files = readRepeatedFlag(parsedAskPro.optionArgs, "--file")
|
|
605
|
+
const files = readRepeatedFlag(parsedAskPro.optionArgs, "--file").map((file) => {
|
|
606
|
+
if (!path.isAbsolute(file))
|
|
607
|
+
return file;
|
|
608
|
+
// Accept an absolute --file that points INSIDE the repo by converting it to
|
|
609
|
+
// the repo-relative path the reader requires (agents naturally pass absolute
|
|
610
|
+
// paths); reject one that escapes the repo so a file outside the project
|
|
611
|
+
// (secrets, ~/.ssh, ...) is not attached to a consult by mistake.
|
|
612
|
+
const rel = path.relative(targetCwd, path.resolve(file));
|
|
613
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
614
|
+
throw new Error(`--file "${file}" is outside the repo root (${targetCwd}). Pass a path inside the repo (absolute or relative), or point --cwd at that repo.`);
|
|
615
|
+
}
|
|
616
|
+
return rel;
|
|
617
|
+
});
|
|
549
618
|
const targetUrl = readFlag(parsedAskPro.optionArgs, "--target-url");
|
|
550
619
|
const normalizedTargetUrl = targetUrl ? normalizeChatGptTargetUrl(targetUrl) : undefined;
|
|
551
620
|
if (!normalizedTargetUrl && parsedAskPro.optionArgs.includes("--confirm-target")) {
|
|
@@ -559,12 +628,15 @@ export async function runAskProCommand(rest, io) {
|
|
|
559
628
|
throw new Error("--json applies to the visible-browser send output; the dry-run preview does not support it.");
|
|
560
629
|
}
|
|
561
630
|
const prompt = parsedAskPro.promptParts.join(" ").trim();
|
|
562
|
-
|
|
631
|
+
const usingStdin = parsedAskPro.optionArgs.includes("--stdin");
|
|
632
|
+
// A prompt is required UNLESS --stdin supplies one: `git diff | prodex ask
|
|
633
|
+
// --stdin` (piped text is the whole prompt) is as valid as `... --stdin
|
|
634
|
+
// "review this"` (positional instruction + piped data below it).
|
|
635
|
+
if (!prompt && !usingStdin) {
|
|
563
636
|
throw new Error('ask-pro requires a prompt. Example: prodex ask "Explain this stack trace" (or pipe input: git diff | prodex ask --stdin "review this diff").');
|
|
564
637
|
}
|
|
565
638
|
let promptText = prompt;
|
|
566
|
-
if (
|
|
567
|
-
// Unix ergonomics: `git diff | prodex ask --stdin "review this"`.
|
|
639
|
+
if (usingStdin) {
|
|
568
640
|
const piped = ((await io.readStdin?.()) ?? "").trim();
|
|
569
641
|
if (!piped) {
|
|
570
642
|
throw new Error("--stdin was set but no piped input was received on stdin (pipe something in, e.g. `git diff | prodex ask --stdin \"review\"`).");
|
|
@@ -573,7 +645,7 @@ export async function runAskProCommand(rest, io) {
|
|
|
573
645
|
if (piped.length > MAX_STDIN_CHARS) {
|
|
574
646
|
throw new Error(`--stdin input is too large (${piped.length} chars > ${MAX_STDIN_CHARS}); attach a file with --file instead.`);
|
|
575
647
|
}
|
|
576
|
-
promptText = `${prompt}\n\n--- piped input (stdin) ---\n${piped}
|
|
648
|
+
promptText = prompt ? `${prompt}\n\n--- piped input (stdin) ---\n${piped}` : piped;
|
|
577
649
|
}
|
|
578
650
|
const browserDefaults = await loadBrowserDefaults(targetCwd);
|
|
579
651
|
const explicitProject = readFlag(parsedAskPro.optionArgs, "--project");
|