@youdie006/prodex 0.37.0 → 0.38.0

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 CHANGED
@@ -424,6 +424,47 @@ During local development, you can run the TypeScript source directly:
424
424
  npm run dev -- tasks list
425
425
  ```
426
426
 
427
+ ## When a send breaks
428
+
429
+ prodex drives a web UI that changes underneath it, so the interesting question is what you have when that happens.
430
+
431
+ **Report it from the receipt, not from memory.** A blocked consult already records the blocker, the version and the platform, so `prodex pro report-issue` builds the report out of that:
432
+
433
+ ```bash
434
+ prodex pro report-issue # prints it, files nothing
435
+ prodex pro report-issue --confirm # opens it as a GitHub issue, through `gh`
436
+ ```
437
+
438
+ The prompt, the answer and the summary never travel - a public issue is not where a private consult should leak - and filing is deduplicated by blocker code, so something that stays broken adds to one issue instead of opening a new one every time.
439
+
440
+ **Capture what the page looked like.** With diagnostics on, a failing send leaves a screenshot and a snapshot of the picker and composer next to each other:
441
+
442
+ ```bash
443
+ PRODEX_BROWSER_DIAGNOSTICS=1 prodex ask "..."
444
+ # .bridge/diagnostics/<when>/{screen.png,page-shape.json}
445
+ ```
446
+
447
+ The snapshot carries roles, testids, rects, `aria-value*` and `pointer-events`. Those last two are what identified a model row that no coordinate could click and a slider step prodex could not read. Captures stay on your machine and are never attached to a report; a screenshot of ChatGPT shows the conversation.
448
+
449
+ **Notice before a person does.** `scripts/ui-watchdog.mjs` sends a token and checks the reply - a real round trip rather than a guess from the page's shape - and can file what it finds:
450
+
451
+ ```bash
452
+ node scripts/ui-watchdog.mjs # says ok or broken
453
+ node scripts/ui-watchdog.mjs --file-issue # and reports it
454
+ ```
455
+
456
+ It needs the logged-in browser, so it belongs on a machine that has one rather than in CI. A daily cron entry is enough.
457
+
458
+ ## Sending without leaving a trace
459
+
460
+ `--temporary` sends into a ChatGPT Temporary Chat, which leaves your chat list untouched:
461
+
462
+ ```bash
463
+ prodex ask --new-chat --temporary "..."
464
+ ```
465
+
466
+ It costs something, and prodex says so on every such send: an unsaved chat is not in the transcript API, so the answer is read off the page, where tables and citation links can be lost - and neither `pro browser recover` nor `--target-url` can reach it afterwards. It requires `--new-chat`, because a throwaway chat cannot be continued.
467
+
427
468
  ## FAQ
428
469
 
429
470
  **A send failed with `send_ui_changed` / "the ChatGPT web UI may have changed".** prodex drives the visible ChatGPT web UI, so an OpenAI redesign of the composer or send control can break sends. When a send times out without the prompt ever posting (the composer still holds the text, or no send button was found), prodex reports this as a likely UI change instead of a misleading "slow model" timeout. Fix: update prodex (`npm i -g @youdie006/prodex@latest`); if it persists, open an issue at https://github.com/youdie006/prodex/issues, and paste the prompt manually in the visible browser in the meantime.
@@ -15,7 +15,12 @@ export class ChatGptBrowserBlockerError extends Error {
15
15
  this.blocker = blocker;
16
16
  }
17
17
  }
18
- const REASONING_EFFORTS = ["즉시", "중간", "높음", "매우 높음"];
18
+ // Five steps, not four. Measured at both ends of the live slider: position 0 of
19
+ // 4 reads "Instant, 1 of 5" and position 4 reads "Pro, 5 of 5", so Pro is a step
20
+ // of this control rather than a model. Leaving it out meant --effort could not
21
+ // reach the top of the slider at all, and the only route there - --model Pro -
22
+ // answered with advice to "say --effort Pro", which then failed.
23
+ const REASONING_EFFORTS = ["즉시", "중간", "높음", "매우 높음", "Pro"];
19
24
  const PRO_MODES = ["기본", "확장"];
20
25
  // Aliases map friendly CLI input onto the exact Korean menu labels the picker
21
26
  // clicks by text. Keys are lowercased and space-stripped before lookup.
@@ -25,7 +30,9 @@ const REASONING_EFFORT_ALIASES = {
25
30
  medium: "중간",
26
31
  high: "높음",
27
32
  max: "매우 높음",
28
- extrahigh: "매우 높음"
33
+ extrahigh: "매우 높음",
34
+ pro: "Pro",
35
+ "프로": "Pro"
29
36
  };
30
37
  // Menu labels per canonical value, verified live in both the Korean and the
31
38
  // English (US) ChatGPT UI. Matching tries every candidate so either UI works.
@@ -33,7 +40,8 @@ const EFFORT_MENU_LABELS = {
33
40
  "즉시": ["즉시", "Instant"],
34
41
  "중간": ["중간", "Medium"],
35
42
  "높음": ["높음", "High"],
36
- "매우 높음": ["매우 높음", "Extra High"]
43
+ "매우 높음": ["매우 높음", "Extra High"],
44
+ Pro: ["Pro", "프로"]
37
45
  };
38
46
  const PRO_MODE_SUBMENU_LABELS = {
39
47
  "기본": ["Pro 기본", "Pro Standard", "Standard", "기본"],
@@ -1637,7 +1645,21 @@ export function pinnedSelectionWarning(pinned, offered) {
1637
1645
  if (offered.length === 0)
1638
1646
  return undefined;
1639
1647
  const has = (wanted) => offered.some((label) => label.trim().toLowerCase() === wanted.trim().toLowerCase());
1640
- const missing = [pinned.model, pinned.effort].find((wanted) => wanted !== undefined && !has(wanted));
1648
+ // A value prodex already knows to be a slider step needs no listing to
1649
+ // vouch for it: the slider shows one step at a time, so a listing that
1650
+ // happens to read "Extra High" says nothing about Pro. Warning there was a
1651
+ // false alarm about a setting the very next send used successfully, and an
1652
+ // alarm that cries about known-good settings is one people learn to skip.
1653
+ const knownStep = (wanted) => {
1654
+ try {
1655
+ parseReasoningEffort(wanted);
1656
+ return true;
1657
+ }
1658
+ catch {
1659
+ return false;
1660
+ }
1661
+ };
1662
+ const missing = [pinned.model, pinned.effort].find((wanted) => wanted !== undefined && !has(wanted) && !knownStep(wanted));
1641
1663
  if (missing === undefined)
1642
1664
  return undefined;
1643
1665
  // Say only what was seen. The picker lists its models, but the effort slider
@@ -1714,6 +1736,31 @@ async function dispatchArrowKey(cdp, key) {
1714
1736
  await cdp.send("Input.dispatchKeyEvent", { type: "rawKeyDown", key, code, windowsVirtualKeyCode: virtualKey });
1715
1737
  await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: virtualKey });
1716
1738
  }
1739
+ /**
1740
+ * How to read every step of the slider and leave it as it was.
1741
+ *
1742
+ * The control shows one step at a time, and that single visible label was all
1743
+ * `pro browser models` ever reported - which is how a machine on this same
1744
+ * account concluded "Pro does not exist here" from five listings that each read
1745
+ * Instant / GPT-5.6 Sol / GPT-5.5, with the slider parked on Instant every
1746
+ * time. Seeing the rest means walking it, and walking someone's setting is only
1747
+ * acceptable if it is put back, so the plan always ends where it started.
1748
+ *
1749
+ * Refuses bounds that do not look like this control, so a misread never becomes
1750
+ * thousands of keystrokes in someone's browser.
1751
+ */
1752
+ export function sliderWalkPlan(state) {
1753
+ const { position, min, max } = state;
1754
+ if (!Number.isInteger(position) || !Number.isInteger(min) || !Number.isInteger(max))
1755
+ return undefined;
1756
+ const steps = max - min;
1757
+ if (steps < 1 || steps > 12)
1758
+ return undefined;
1759
+ if (position < min || position > max)
1760
+ return undefined;
1761
+ const offset = position - min;
1762
+ return { toBottom: offset, climb: steps, back: offset };
1763
+ }
1717
1764
  async function selectModelReasoning(cdp, options, selectionWarnings = []) {
1718
1765
  if (!options.model && !options.proMode && !options.effort)
1719
1766
  return;
@@ -3206,6 +3253,52 @@ export async function listChatGptSidebarProjects(input = {}) {
3206
3253
  }
3207
3254
  return { url: status.url, projects };
3208
3255
  }
3256
+ /**
3257
+ * Every step of the effort slider, read by walking it and putting it back.
3258
+ *
3259
+ * Best effort: a slider that cannot be focused, read or walked leaves the
3260
+ * listing exactly as it was rather than failing a read-only command.
3261
+ */
3262
+ async function readEffortSteps(cdp) {
3263
+ try {
3264
+ const focused = await cdp.evaluate(focusPowerSliderExpression());
3265
+ if (!focused?.ok)
3266
+ return undefined;
3267
+ const start = await cdp.evaluate(powerSliderStateExpression());
3268
+ if (!start?.ok)
3269
+ return undefined;
3270
+ const plan = sliderWalkPlan({ position: start.position, min: start.min, max: start.max });
3271
+ if (!plan)
3272
+ return undefined;
3273
+ const current = start.effort ?? null;
3274
+ for (let i = 0; i < plan.toBottom; i += 1) {
3275
+ await dispatchArrowKey(cdp, "ArrowLeft");
3276
+ await sleep(120);
3277
+ }
3278
+ const labels = [];
3279
+ const record = async () => {
3280
+ const state = await cdp.evaluate(powerSliderStateExpression());
3281
+ const label = state?.effort?.trim();
3282
+ if (label && !labels.includes(label))
3283
+ labels.push(label);
3284
+ };
3285
+ await record();
3286
+ for (let i = 0; i < plan.climb; i += 1) {
3287
+ await dispatchArrowKey(cdp, "ArrowRight");
3288
+ await sleep(120);
3289
+ await record();
3290
+ }
3291
+ // Put it back. Reading someone's setting must not change it.
3292
+ for (let i = 0; i < plan.climb - plan.back; i += 1) {
3293
+ await dispatchArrowKey(cdp, "ArrowLeft");
3294
+ await sleep(120);
3295
+ }
3296
+ return labels.length > 0 ? { labels, current } : undefined;
3297
+ }
3298
+ catch {
3299
+ return undefined;
3300
+ }
3301
+ }
3209
3302
  export async function listChatGptModelOptions(input = {}) {
3210
3303
  const port = resolveCdpPort(input.port);
3211
3304
  const timeoutMs = input.timeoutMs ?? 15_000;
@@ -3239,7 +3332,8 @@ export async function listChatGptModelOptions(input = {}) {
3239
3332
  if (!opened)
3240
3333
  throw new Error("ChatGPT model menu did not open after clicking the selector");
3241
3334
  const options = await cdp.evaluate(modelMenuOptionsExpression());
3242
- return { url: status.url, options };
3335
+ const effortSteps = await readEffortSteps(cdp);
3336
+ return { url: status.url, options, ...(effortSteps ? { effortSteps } : {}) };
3243
3337
  }
3244
3338
  finally {
3245
3339
  try {
package/dist/cli-help.js CHANGED
@@ -26,10 +26,11 @@ Ask / consult commands:
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
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
29
- prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
29
+ prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name" | --project-new "name"] "prompt" # explicit visible-browser send
30
30
  prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
31
31
  prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
32
32
  prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
33
+ prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
33
34
 
34
35
  Bridge ledger (durable tasks/results/receipts/sessions under .bridge/):
35
36
  prodex init [--cwd /absolute/path/to/repo]
@@ -51,7 +52,7 @@ Bridge ledger (durable tasks/results/receipts/sessions under .bridge/):
51
52
 
52
53
  Agent / MCP integration:
53
54
  prodex mcp [--cwd /absolute/path/to/repo]
54
- prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
55
+ prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
55
56
  prodex start [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js]
56
57
  prodex status [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] [--show-token] [--url-only] [--unsafe-show-non-expiring-token]
57
58
  prodex tunnel url [--cwd /absolute/path/to/repo] [--source-cli /absolute/path/to/dist/cli.js] --public-url https://... [--show-token] [--url-only]
@@ -79,7 +80,7 @@ export function printSetupHelp(stdout) {
79
80
  stdout(`prodex setup
80
81
 
81
82
  Commands:
82
- prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
83
+ prodex setup [--cwd /absolute/path/to/repo] [--host 127.0.0.1] [--port 8787] [--token-ttl-hours <hours>] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name"] [--clear-model|--clear-pro-mode|--clear-effort|--clear-project] [--interactive]
83
84
 
84
85
  Save a loopback-only HTTP MCP profile in .bridge/config.local.json. Use --token-ttl-hours before tunnels or ChatGPT Project use.
85
86
 
@@ -167,10 +168,11 @@ Commands:
167
168
  prodex pro browser check [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
168
169
  prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo]
169
170
  prodex pro browser models [--source-cli /absolute/path/to/dist/cli.js]
170
- prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"] "prompt"
171
+ prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] [--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name" | --project-new "name"] "prompt"
171
172
  prodex pro latest [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
172
173
  prodex pro list [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
173
174
  prodex pro show <task-id|latest> [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--json]
175
+ prodex pro report-issue [--cwd /absolute/path/to/repo] [--task <task-id>] [--repo owner/name] [--confirm] # bug report from a failed consult's receipt; previews unless --confirm, never carries the prompt or the answer
174
176
 
175
177
  Use \`prodex pro ask\` for dry-run/manual previews.
176
178
  Use \`prodex pro browser ask\` only when you want an explicit visible-browser send.
@@ -251,7 +253,7 @@ export function printProBrowserHelp(stdout, sourceCli) {
251
253
  const smokeUsage = sourceCli
252
254
  ? `${cli} pro browser smoke${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]`
253
255
  : "prodex pro browser smoke [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 90000]";
254
- const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"] [--project "name" | --project-new "name"]';
256
+ const selectionUsage = '[--model Pro] [--pro-mode 기본|확장] [--effort 즉시|중간|높음|"매우 높음"|Pro] [--project "name" | --project-new "name"]';
255
257
  const askUsage = sourceCli
256
258
  ? `${cli} pro browser ask${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`
257
259
  : `prodex pro browser ask [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] [--timeout-ms 300000] [--busy-wait-ms 600000] [--target-url url --confirm-target] [--new-chat] [--temporary] [--stdin] [--json] [--auto-login|--no-auto-login] [--file path] [--attach path] [--tool deep-research|web-search|create-image] ${selectionUsage} "prompt"`;
package/dist/cli-pro.js CHANGED
@@ -447,7 +447,17 @@ export async function runProCommand(rest, io, runCliFn) {
447
447
  for (const option of listed.options) {
448
448
  io.stdout(formatModelMenuOption(option));
449
449
  }
450
- io.stdout("An arrow shows what that row is set to now; --model / --effort reach into those submenus (e.g. --model Pro).");
450
+ if (listed.effortSteps) {
451
+ io.stdout("");
452
+ io.stdout("Effort steps on this account (the slider was walked and put back):");
453
+ for (const label of listed.effortSteps.labels) {
454
+ io.stdout(`${label === listed.effortSteps.current ? "*" : " "} ${label}`);
455
+ }
456
+ io.stdout("Pass any of these to --effort. The list is complete: the slider shows one step at a time, so reading it any other way sees only the current one.");
457
+ }
458
+ else {
459
+ io.stdout("An arrow shows what that row is set to now; --model / --effort reach into those submenus (e.g. --model Pro).");
460
+ }
451
461
  return 0;
452
462
  }
453
463
  if (browserSubcommand === "projects") {
package/dist/cli.js CHANGED
@@ -436,7 +436,9 @@ repo: ${cwd}
436
436
 
437
437
  Safety notes:
438
438
  - This command only prints commands; it does not start servers, open browsers, or write files.
439
- - Visible-browser sends require a manual, visible browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers, plus response_choice_pending when ChatGPT is waiting for you to pick which of two answers you prefer.`;
439
+ - Visible-browser sends require a manual, visible browser session and stop on login, captcha, Cloudflare, permission, rate-limit, or usage-limit blockers, plus response_choice_pending when ChatGPT is waiting for you to pick which of two answers you prefer.
440
+ - When a send is blocked, \`prodex pro report-issue\` turns that receipt into a bug report (prints it; --confirm files it). It carries the blocker, version and platform, never the prompt or the answer.
441
+ - PRODEX_BROWSER_DIAGNOSTICS=1 makes a failing send leave a screenshot and a page snapshot under .bridge/diagnostics for whoever debugs it. They stay local.`;
440
442
  }
441
443
  async function hasOnboardingReadme(cwd) {
442
444
  try {
package/dist/config.js CHANGED
@@ -10,7 +10,8 @@ const BRIDGE_DIRECTORY_MODE = 0o700;
10
10
  const BrowserDefaultsSchema = z.object({
11
11
  model: z.string().min(1).optional(),
12
12
  pro_mode: z.enum(["기본", "확장"]).optional(),
13
- effort: z.enum(["즉시", "중간", "높음", "매우 높음"]).optional(),
13
+ // Pro is the slider's fifth step, so a saved default may name it.
14
+ effort: z.enum(["즉시", "중간", "높음", "매우 높음", "Pro"]).optional(),
14
15
  project: z.string().min(1).optional()
15
16
  });
16
17
  const LocalConfigSchema = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",