@youdie006/prodex 0.16.30 → 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.
@@ -1279,6 +1279,87 @@ async function selectProject(cdp, options) {
1279
1279
  throw new Error(`ChatGPT composer did not appear after entering project "${options.project}"`);
1280
1280
  }
1281
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
+ }
1282
1363
  export async function sendChatGptPrompt(options) {
1283
1364
  const port = resolveCdpPort(options.port);
1284
1365
  const timeoutMs = options.timeoutMs ?? 90_000;
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
- throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models", "projects"]);
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.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.16.30",
3
+ "version": "0.16.31",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",