@youdie006/prodex 0.16.11 → 0.16.13

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.
@@ -933,7 +933,26 @@ async function selectModelReasoning(cdp, options) {
933
933
  throw new Error(button.reason ?? "Could not open the ChatGPT model selector");
934
934
  }
935
935
  try {
936
- await verifiedClickAt(cdp, button.x, button.y, "model selector");
936
+ // The hover-verified click can be transiently refused right after a page
937
+ // transition (measured live: the just-closed create-project modal's
938
+ // overlay still covered the selector for a beat, and one refusal aborted
939
+ // the whole send). Retry briefly with FRESH coordinates; a persistent
940
+ // cover still fails with the refusal message.
941
+ const clickDeadline = Date.now() + 5_000;
942
+ for (;;) {
943
+ try {
944
+ await verifiedClickAt(cdp, button.x, button.y, "model selector");
945
+ break;
946
+ }
947
+ catch (error) {
948
+ if (Date.now() >= clickDeadline || !/Refusing to click/.test(error instanceof Error ? error.message : String(error)))
949
+ throw error;
950
+ await sleep(700);
951
+ const fresh = await cdp.evaluate(modelButtonRectExpression());
952
+ if (fresh.ok && fresh.x !== undefined && fresh.y !== undefined)
953
+ button = fresh;
954
+ }
955
+ }
937
956
  const opened = await waitForExpressionTrue(cdp, menuOpenExpression(), MENU_OPEN_TIMEOUT_MS);
938
957
  if (!opened)
939
958
  throw new Error("ChatGPT model menu did not open after clicking the selector");
@@ -1120,7 +1139,7 @@ async function selectProject(cdp, options) {
1120
1139
  }
1121
1140
  if (!hit.ok || hit.x === undefined || hit.y === undefined) {
1122
1141
  const detail = hit.reason && hit.reason !== "project not found in sidebar" ? ` (${hit.reason})` : "";
1123
- throw new Error(`ChatGPT project not found in sidebar: ${options.project}${detail}`);
1142
+ throw new Error(`ChatGPT project not found in sidebar: ${options.project}${detail} List the visible names with \`prodex pro browser projects\`.`);
1124
1143
  }
1125
1144
  await verifiedClickAt(cdp, hit.x, hit.y, `project ${options.project}`);
1126
1145
  const navigated = await waitForExpressionTrue(cdp, `location.href !== ${JSON.stringify(hrefBefore)}`, PROJECT_NAVIGATION_TIMEOUT_MS);
@@ -1205,7 +1224,24 @@ export async function sendChatGptPrompt(options) {
1205
1224
  if (normalizedTargetUrl)
1206
1225
  assertChatGptTargetUrlMatches(status.url, normalizedTargetUrl);
1207
1226
  assertVisibleChatGptTab(status.visibilityState, status.url, normalizedTargetUrl);
1208
- const busyBlocker = chatGptBusyBlocker(status.generating);
1227
+ let busyBlocker = chatGptBusyBlocker(status.generating);
1228
+ if (busyBlocker && (options.busyWaitMs ?? 0) > 0) {
1229
+ // Queue behind the in-flight response instead of failing: shared-tab
1230
+ // contention (another agent or the user mid-generation) is a when, not an
1231
+ // if. Bounded, and a mid-wait page blocker (usage limit etc.) still throws.
1232
+ const busyDeadline = Date.now() + (options.busyWaitMs ?? 0);
1233
+ emitProgress("waiting", "tab busy with another response; waiting");
1234
+ while (busyBlocker && Date.now() < busyDeadline) {
1235
+ await sleep(3_000);
1236
+ status = await evaluateOnPage(page, statusExpression());
1237
+ const midBlocker = detectChatGptPageBlocker(status);
1238
+ if (midBlocker)
1239
+ throw new ChatGptBrowserBlockerError(midBlocker);
1240
+ busyBlocker = chatGptBusyBlocker(status.generating);
1241
+ if (busyBlocker)
1242
+ emitProgress("waiting", "tab busy with another response; waiting");
1243
+ }
1244
+ }
1209
1245
  if (busyBlocker) {
1210
1246
  throw new ChatGptBrowserBlockerError(busyBlocker);
1211
1247
  }
@@ -1378,6 +1414,46 @@ export function modelMenuOptionsExpression() {
1378
1414
  // Read-only discovery: open the composer model menu, read the option labels,
1379
1415
  // and press Escape. Nothing is clicked inside the menu, so the user's model
1380
1416
  // selection is never changed.
1417
+ // Sidebar project names, extracted from the per-row options-button aria-labels
1418
+ // (English "Open project options for <name>", Korean "<name> 프로젝트 옵션 열기").
1419
+ export function sidebarProjectNamesExpression() {
1420
+ return `(() => {
1421
+ const names = [...document.querySelectorAll('[aria-label*="프로젝트 옵션"],[aria-label*="project options" i]')]
1422
+ .map((b) => (b.getAttribute("aria-label") || "").replace(/^open project options for /i, "").replace(/\\s*프로젝트 옵션 열기$/, "").trim())
1423
+ .filter(Boolean);
1424
+ return [...new Set(names)];
1425
+ })()`;
1426
+ }
1427
+ // Read-only discovery for --project/setup --project: list the sidebar project
1428
+ // names exactly as ChatGPT renders them, so nobody has to guess spelling or
1429
+ // case. Polls briefly because the Projects section hydrates after navigation.
1430
+ export async function listChatGptSidebarProjects(input = {}) {
1431
+ const port = resolveCdpPort(input.port);
1432
+ const timeoutMs = input.timeoutMs ?? 15_000;
1433
+ const pageResult = await findChatGptPage(port, computePageDiscoveryTimeout(timeoutMs), undefined);
1434
+ if (!pageResult.ok) {
1435
+ throwBlockerOrError(pageResult.blocker, "ChatGPT browser page is not available");
1436
+ }
1437
+ if (!pageResult.page) {
1438
+ if (pageResult.blocker)
1439
+ throw new ChatGptBrowserBlockerError(pageResult.blocker);
1440
+ assertChatGptPageAvailable();
1441
+ }
1442
+ const page = pageResult.page;
1443
+ const status = await readSettledChatGptPageStatus(page);
1444
+ const blocker = detectChatGptPageBlocker(status);
1445
+ if (blocker)
1446
+ throw new ChatGptBrowserBlockerError(blocker);
1447
+ let projects = [];
1448
+ const deadline = Date.now() + 6_000;
1449
+ for (;;) {
1450
+ projects = await evaluateOnPage(page, sidebarProjectNamesExpression());
1451
+ if (projects.length > 0 || Date.now() >= deadline)
1452
+ break;
1453
+ await sleep(300);
1454
+ }
1455
+ return { url: status.url, projects };
1456
+ }
1381
1457
  export async function listChatGptModelOptions(input = {}) {
1382
1458
  const port = resolveCdpPort(input.port);
1383
1459
  const timeoutMs = input.timeoutMs ?? 15_000;
package/dist/cli-args.js CHANGED
@@ -253,6 +253,7 @@ export const ASK_PRO_VALUE_FLAGS = new Set([
253
253
  "--file",
254
254
  "--port",
255
255
  "--timeout-ms",
256
+ "--busy-wait-ms",
256
257
  "--target-url",
257
258
  "--source-cli",
258
259
  ...ASK_PRO_SELECTION_VALUE_FLAGS
package/dist/cli-help.js CHANGED
@@ -24,7 +24,8 @@ Ask / consult commands:
24
24
  prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 1500]
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
- prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--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
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 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
28
29
  prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
29
30
  prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
30
31
  prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
@@ -250,8 +251,8 @@ export function printProBrowserHelp(stdout, sourceCli) {
250
251
  : "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
251
252
  const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"]';
252
253
  const askUsage = sourceCli
253
- ? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`
254
- : `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000] [--target-url url --confirm-target] [--new-chat] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] ${selectionUsage} "prompt"`;
254
+ ? `${cli} pro browser ask${sourceCliOption} [--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] ${selectionUsage} "prompt"`
255
+ : `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] ${selectionUsage} "prompt"`;
255
256
  const modelsUsage = sourceCli
256
257
  ? `${cli} pro browser models${sourceCliOption} [--port 9333] [--timeout-ms 15000]`
257
258
  : "prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000]";
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, normalizeChatGptTargetUrl, openChatGptBrowser, parseProMode, parseReasoningEffort, readLastBrowserLoginLaunch, recordBrowserLoginLaunch, sendChatGptPrompt } from "./chatgpt-browser.js";
4
+ import { DEFAULT_CDP_PORT, resolveCdpPort, chatGptVisibilityBlocker, defaultChatGptProfileDir, 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";
@@ -145,6 +145,7 @@ export async function runProCommand(rest, io, runCliFn) {
145
145
  const straySendFlag = [
146
146
  "--port",
147
147
  "--timeout-ms",
148
+ "--busy-wait-ms",
148
149
  "--target-url",
149
150
  "--confirm-target",
150
151
  "--project",
@@ -330,7 +331,36 @@ export async function runProCommand(rest, io, runCliFn) {
330
331
  io.stdout("Use radio entries with `pro browser ask --model/--effort` (e.g. --model Pro).");
331
332
  return 0;
332
333
  }
333
- throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models"]);
334
+ if (browserSubcommand === "projects") {
335
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser projects", io, { valueFlags: ["--port", "--timeout-ms", "--source-cli"] }))
336
+ return 0;
337
+ assertOnlyOptions(browserArgs, "pro browser projects", ["--port", "--timeout-ms", "--source-cli"]);
338
+ const projectsSourceCli = resolveOptionalFileFlag(io.cwd, browserArgs, "--source-cli");
339
+ const projectsPort = readPortFlag(browserArgs, "--port");
340
+ const projectsTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
341
+ const projectsResolvedPort = resolveCdpPort(projectsPort);
342
+ let listed;
343
+ try {
344
+ listed = await listChatGptSidebarProjects({ port: projectsPort, timeoutMs: projectsTimeoutMs });
345
+ }
346
+ catch (error) {
347
+ const blocker = sourceAwareBrowserBlocker(browserSendBlockerFromError(error), projectsSourceCli, {
348
+ ...(projectsResolvedPort !== DEFAULT_CDP_PORT ? { port: projectsResolvedPort } : {})
349
+ });
350
+ throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
351
+ }
352
+ if (listed.projects.length === 0) {
353
+ io.stdout("No projects visible in the ChatGPT sidebar (read-only check).");
354
+ io.stdout("If you expect projects, open the sidebar's Projects section once in the visible browser, then retry.");
355
+ return 0;
356
+ }
357
+ io.stdout("ChatGPT sidebar projects (read-only; exact names as rendered):");
358
+ for (const name of listed.projects)
359
+ io.stdout(` ${name}`);
360
+ io.stdout("Use with `pro browser ask --project \"<name>\"` or pin one with `prodex setup --project \"<name>\"`.");
361
+ return 0;
362
+ }
363
+ throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models", "projects"]);
334
364
  }
335
365
  if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
336
366
  throw new Error(`Use \`prodex pro browser ${subcommand === "doctor" ? "check" : subcommand}\` for explicit browser automation.`);
@@ -565,6 +595,7 @@ export async function runAskProCommand(rest, io) {
565
595
  ...(selectionEffort ? { effort: selectionEffort } : {})
566
596
  };
567
597
  const browserPort = hasSendMode ? resolveCdpPort(readPortFlag(parsedAskPro.optionArgs, "--port")) : undefined;
598
+ const busyWaitMs = readPositiveIntegerFlag(parsedAskPro.optionArgs, "--busy-wait-ms");
568
599
  // Pro extended can legitimately think for minutes, so its default timeout is
569
600
  // higher; an explicit --timeout-ms always wins.
570
601
  const defaultBrowserTimeoutMs = selectionProMode === "확장" ? 300_000 : 90_000;
@@ -630,6 +661,7 @@ export async function runAskProCommand(rest, io) {
630
661
  targetUrl: normalizedTargetUrl,
631
662
  timeoutMs: browserTimeoutMs,
632
663
  ...(newChat ? { newChat: true } : {}),
664
+ ...(busyWaitMs !== undefined ? { busyWaitMs } : {}),
633
665
  project: selectionProject,
634
666
  projectNew: selectionProjectNew,
635
667
  model: selectionModel,
@@ -699,6 +731,13 @@ export async function runAskProCommand(rest, io) {
699
731
  if (!selectionModel && !selectionProMode && !selectionEffort) {
700
732
  persistenceWarnings.push("model_selection_warning: no model/effort was selected for this send (no per-ask flag, no saved default), so it used whatever the ChatGPT UI last had selected. Pin one with `prodex setup --model Pro` or pass --model/--effort.");
701
733
  }
734
+ // In-project threads carry the project slug in their URL
735
+ // (/g/g-p-<project>/c/<id>); a bare /c/<id> after requesting a project
736
+ // means the thread landed at root - say so instead of leaving it to a
737
+ // sidebar audit (field-verified failure mode).
738
+ if ((selectionMetadata.project || selectionMetadata.project_new) && !/\/g\/g-p-/.test(consult.url ?? "")) {
739
+ persistenceWarnings.push("project_landing_warning: a project was requested but the answered thread URL is a root /c/ thread, so it likely landed OUTSIDE the project. Move it via the thread menu (Move to project) or re-run; list projects with `prodex pro browser projects`.");
740
+ }
702
741
  // Truncation and other send warnings must be visible at runtime, not
703
742
  // only inside the persisted receipt: a caller who never opens .bridge
704
743
  // would otherwise treat a cut-off answer as complete.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.16.11",
3
+ "version": "0.16.13",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",