@youdie006/prodex 0.39.2 → 0.39.3

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
@@ -1,11 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/youdie006/prodex/main/assets/logo-wordmark-dark.png">
5
- <img src="https://raw.githubusercontent.com/youdie006/prodex/main/assets/logo-wordmark.png" width="300" alt="PROdex">
6
- </picture>
7
-
8
- **Ask ChatGPT Pro from your terminal, or let Codex, Claude and other coding agents ask it for you, through the logged-in browser you already have, with a receipt for every answer.**
3
+ <img src="https://raw.githubusercontent.com/youdie006/prodex/main/assets/cli-banner.png" alt="prodex - ChatGPT Pro for your terminal and your coding agents, local, with receipts" width="760" />
9
4
 
10
5
  [![CI](https://github.com/youdie006/prodex/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/youdie006/prodex/actions/workflows/ci.yml)
11
6
  [![npm](https://img.shields.io/npm/v/%40youdie006%2Fprodex?logo=npm&color=b91c1c)](https://www.npmjs.com/package/@youdie006/prodex)
@@ -6,7 +6,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { captureBrowserDiagnostics, diagnosticsEnabled, diagnosticsNote } from "./browser-diagnostics.js";
8
8
  import os from "node:os";
9
- import { answeredDialogWarning, chatSurfaceState, effortNeedsWorkSurface, javascriptDialogResponse, menuKeyboardStep, readPowerSliderSelection, sliderRestoreStep, surfaceFromProbe } from "./picker-interaction.js";
9
+ import { answeredDialogWarning, chatSurfaceState, effortNeedsWorkSurface, javascriptDialogResponse, menuKeyboardStep, readPowerSliderSelection, sliderRestoreStep, surfaceFromProbe, sliderPressOutcome, sliderDidNotRespond } from "./picker-interaction.js";
10
10
  export class ChatGptBrowserBlockerError extends Error {
11
11
  blocker;
12
12
  constructor(blocker) {
@@ -1769,6 +1769,10 @@ async function assertSelectionCommitted(cdp, label) {
1769
1769
  }
1770
1770
  // Roughly three seconds of grace for a menu that is still painting.
1771
1771
  const POWER_SLIDER_APPEAR_ATTEMPTS = 5;
1772
+ // Presses the walk will wait out while the focused slider ignores them: on a
1773
+ // page built moments ago the key handler attaches after focus is possible.
1774
+ // About six seconds all told, which is more than the measured gap.
1775
+ const POWER_SLIDER_SWALLOWED_PRESS_RETRIES = 6;
1772
1776
  /**
1773
1777
  * Move the power slider until its Effort readout is the requested step. The
1774
1778
  * menu must already be open. Returns the quota line so the caller can warn
@@ -2050,22 +2054,72 @@ async function selectPowerStep(cdp, requested) {
2050
2054
  if (!state?.ok)
2051
2055
  throw new Error(state?.reason ?? "Could not read ChatGPT's power slider");
2052
2056
  const steps = (state.max ?? 4) - (state.min ?? 0) + 1;
2053
- for (let attempt = 0; attempt <= steps * 2; attempt += 1) {
2057
+ let presses = 0;
2058
+ let swallowed = 0;
2059
+ let everMoved = false;
2060
+ while (presses <= steps * 2) {
2054
2061
  if (state.effort && powerLabelMatches(requested, state.effort))
2055
2062
  return { effort: state.effort };
2056
2063
  // Walk upward first, then back down: the labels are ordered, but their
2057
2064
  // exact set can change, so this never assumes a fixed index for a name.
2058
2065
  const atTop = (state.position ?? 0) >= (state.max ?? 4);
2059
- const key = attempt < steps && !atTop ? "ArrowRight" : "ArrowLeft";
2066
+ const key = presses < steps && !atTop ? "ArrowRight" : "ArrowLeft";
2067
+ const before = state.position ?? 0;
2060
2068
  await dispatchArrowKey(cdp, key);
2061
2069
  await sleep(400);
2062
2070
  state = await cdp.evaluate(powerSliderStateExpression());
2063
2071
  if (!state?.ok)
2064
2072
  throw new Error(state?.reason ?? "Could not read ChatGPT's power slider");
2073
+ const outcome = sliderPressOutcome({
2074
+ before,
2075
+ after: state.position ?? before,
2076
+ key,
2077
+ ...(state.min !== undefined ? { min: state.min } : {}),
2078
+ ...(state.max !== undefined ? { max: state.max } : {})
2079
+ });
2080
+ if (outcome === "swallowed") {
2081
+ // Focus succeeded but the press did nothing with room to move: the
2082
+ // handler is not attached yet. Two consults were blocked with "has no
2083
+ // Pro step. It showed: Instant, 1 of 5" after ten such presses, and a
2084
+ // build that treats that as the picker declining would send at Instant
2085
+ // instead. Wait for the handler rather than counting the press.
2086
+ swallowed += 1;
2087
+ if (swallowed <= POWER_SLIDER_SWALLOWED_PRESS_RETRIES) {
2088
+ await sleep(600);
2089
+ continue;
2090
+ }
2091
+ break;
2092
+ }
2093
+ if (outcome === "moved")
2094
+ everMoved = true;
2095
+ presses += 1;
2096
+ }
2097
+ if (!everMoved) {
2098
+ throw new Error(`ChatGPT's power slider did not respond to arrow keys: it stayed at "${state.effort ?? "?"}" ` +
2099
+ `(${(state.position ?? 0) + 1} of ${(state.max ?? 4) + 1}) while the picker was still hydrating, ` +
2100
+ `so the "${requested}" step could not be reached.`);
2065
2101
  }
2066
2102
  const available = (state.lines ?? []).join(" / ");
2067
2103
  throw new Error(`ChatGPT's model picker has no "${requested}" step. It showed: ${available}`);
2068
2104
  }
2105
+ /**
2106
+ * The slider walk, with one more try when the first found a slider that
2107
+ * ignored every press: by then the handler has had seconds to attach.
2108
+ */
2109
+ async function selectPowerStepWithGrace(cdp, requested) {
2110
+ try {
2111
+ return await selectPowerStep(cdp, requested);
2112
+ }
2113
+ catch (error) {
2114
+ const message = error instanceof Error ? error.message : String(error);
2115
+ if (!sliderDidNotRespond(message))
2116
+ throw error;
2117
+ await sleep(1_500);
2118
+ if (!(await ensurePickerOpen(cdp)))
2119
+ throw error;
2120
+ return await selectPowerStep(cdp, requested);
2121
+ }
2122
+ }
2069
2123
  async function dispatchArrowKey(cdp, key) {
2070
2124
  const code = key;
2071
2125
  const virtualKey = key === "ArrowLeft" ? 37 : 39;
@@ -2183,11 +2237,13 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
2183
2237
  throw new Error("ChatGPT's model picker would not reopen after the model was chosen.");
2184
2238
  }
2185
2239
  try {
2186
- await selectPowerStep(cdp, plan.sliderLabel);
2240
+ await selectPowerStepWithGrace(cdp, plan.sliderLabel);
2187
2241
  }
2188
2242
  catch (error) {
2189
2243
  // Only "this slider has no such step" is the picker declining. A
2190
- // slider that will not open, or will not move, is a real failure.
2244
+ // slider that will not open, or will not move - the hydrating case,
2245
+ // which reads as "did not respond to arrow keys" - is a real
2246
+ // failure, and must not turn into a send at whatever step it was on.
2191
2247
  const message = error instanceof Error ? error.message : String(error);
2192
2248
  const noSuchStep = /has no "[^"]*" step/.test(message);
2193
2249
  if (!noSuchStep)
package/dist/cli-pro.js CHANGED
@@ -1770,6 +1770,17 @@ export function browserSendBlockerFromError(error) {
1770
1770
  next_step: "Another prodex send holds the browser. Wait for it to finish and retry, or pass a longer --timeout-ms, which is also the queue budget."
1771
1771
  };
1772
1772
  }
1773
+ // The picker's slider took focus before its key handler was attached and
1774
+ // ignored every press. Retrying is the cure; nothing in the browser is
1775
+ // broken, so the generic "resolve it manually" pointed at nothing.
1776
+ if (/power slider did not respond to arrow keys/.test(message)) {
1777
+ return {
1778
+ code: "picker_not_responding",
1779
+ message,
1780
+ retryable: true,
1781
+ next_step: "ChatGPT's picker had not finished loading when prodex tried to move its slider, so the requested step was never reached and nothing was sent. Retry the send. If it repeats on a page that has been open for a while, open the picker once in the visible browser and move the slider by hand, then retry."
1782
+ };
1783
+ }
1773
1784
  // The tab's DevTools connection went away mid-send with no timeout behind
1774
1785
  // it: the tab was closed, navigated from outside, or the browser exited.
1775
1786
  if (/Chrome DevTools websocket (closed|is not open)/.test(message)) {
@@ -182,3 +182,25 @@ export function surfaceFromProbe(probe) {
182
182
  return "Work";
183
183
  return undefined;
184
184
  }
185
+ /**
186
+ * What one arrow press on the power slider did. "swallowed" is the case the
187
+ * walk has to wait out: on a page built moments ago the slider takes focus
188
+ * before its key handler is attached, so a press with room to move does
189
+ * nothing - and a walk that counted those presses as steps reported a step
190
+ * that exists as missing ("has no Pro step. It showed: Instant, 1 of 5").
191
+ */
192
+ export function sliderPressOutcome(input) {
193
+ if (input.after !== input.before)
194
+ return "moved";
195
+ const min = input.min ?? 0;
196
+ const max = input.max ?? 4;
197
+ if (input.key === "ArrowRight" && input.before >= max)
198
+ return "at-edge";
199
+ if (input.key === "ArrowLeft" && input.before <= min)
200
+ return "at-edge";
201
+ return "swallowed";
202
+ }
203
+ /** The slider-walk failure that means "hydrating", as opposed to "no such step". */
204
+ export function sliderDidNotRespond(message) {
205
+ return /power slider did not respond to arrow keys/.test(message);
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.39.2",
3
+ "version": "0.39.3",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",