@youdie006/prodex 0.39.0 → 0.39.1
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 +104 -21
- package/dist/cli-pro.js +3 -0
- package/dist/picker-interaction.js +27 -0
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -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 { menuKeyboardStep, readPowerSliderSelection } from "./picker-interaction.js";
|
|
9
|
+
import { chatSurfaceChoice, menuKeyboardStep, readPowerSliderSelection, sliderRestoreStep } from "./picker-interaction.js";
|
|
10
10
|
export class ChatGptBrowserBlockerError extends Error {
|
|
11
11
|
blocker;
|
|
12
12
|
constructor(blocker) {
|
|
@@ -22,6 +22,8 @@ export class ChatGptBrowserBlockerError extends Error {
|
|
|
22
22
|
// answered with advice to "say --effort Pro", which then failed.
|
|
23
23
|
/** A full trip round the menu is far more than any real picker needs. */
|
|
24
24
|
const MENU_KEYBOARD_WALK_LIMIT = 16;
|
|
25
|
+
/** Enough presses to cross any slider this code accepts, twice over. */
|
|
26
|
+
const POWER_SLIDER_RESTORE_ATTEMPTS = 26;
|
|
25
27
|
// Two more rungs than before. Measured live: with the model row chosen rather
|
|
26
28
|
// than the recommended set, the slider reads Light / Medium / High / Extra High
|
|
27
29
|
// / Max / Ultra, and Ultra is the top of the machine. Pro is kept because older
|
|
@@ -1445,6 +1447,20 @@ export function activeMenuItemExpression() {
|
|
|
1445
1447
|
return { role: a.getAttribute("role"), label };
|
|
1446
1448
|
})()`;
|
|
1447
1449
|
}
|
|
1450
|
+
export function chatWorkSurfaceExpression() {
|
|
1451
|
+
return `(() => {${CLICK_POINT_SNIPPET}
|
|
1452
|
+
const buttons = [...document.querySelectorAll('[role="radio"],[role="tab"]')].filter((el) => {
|
|
1453
|
+
const t = ((el.innerText || el.textContent || "").trim());
|
|
1454
|
+
return t === "Chat" || t === "Work";
|
|
1455
|
+
});
|
|
1456
|
+
const surfaces = buttons.map((el) => ({
|
|
1457
|
+
label: ((el.innerText || el.textContent || "").trim()),
|
|
1458
|
+
checked: el.getAttribute("aria-checked") === "true" || el.getAttribute("aria-selected") === "true"
|
|
1459
|
+
}));
|
|
1460
|
+
const chat = buttons.find((el) => ((el.innerText || el.textContent || "").trim()) === "Chat");
|
|
1461
|
+
return { surfaces, chat: chat ? clickPoint(chat) : { ok: false, reason: "no Chat toggle" } };
|
|
1462
|
+
})()`;
|
|
1463
|
+
}
|
|
1448
1464
|
export function powerSliderPresentExpression() {
|
|
1449
1465
|
return `Boolean(document.querySelector('[data-testid="composer-intelligence-picker-content"] [role="slider"]'))`;
|
|
1450
1466
|
}
|
|
@@ -1782,6 +1798,33 @@ async function selectPickerModel(cdp, requested, warnings = []) {
|
|
|
1782
1798
|
* without this the step that follows reported "the picker did not expose its
|
|
1783
1799
|
* power slider" on a picker that was simply shut.
|
|
1784
1800
|
*/
|
|
1801
|
+
/**
|
|
1802
|
+
* Put the browser back on ChatGPT's Chat surface when it has drifted onto Work.
|
|
1803
|
+
*
|
|
1804
|
+
* The two have different pickers - Work's offers no Pro at all - and nothing on
|
|
1805
|
+
* the page announces which one is live, so a drifted browser silently drives
|
|
1806
|
+
* the wrong picker. Returns a warning to carry to the caller when it moved.
|
|
1807
|
+
*/
|
|
1808
|
+
async function ensureChatSurface(cdp) {
|
|
1809
|
+
let probe;
|
|
1810
|
+
try {
|
|
1811
|
+
probe = await cdp.evaluate(chatWorkSurfaceExpression());
|
|
1812
|
+
}
|
|
1813
|
+
catch {
|
|
1814
|
+
return undefined;
|
|
1815
|
+
}
|
|
1816
|
+
if (!probe)
|
|
1817
|
+
return undefined;
|
|
1818
|
+
if (chatSurfaceChoice(probe.surfaces ?? []) !== "switch-to-chat")
|
|
1819
|
+
return undefined;
|
|
1820
|
+
const chat = probe.chat;
|
|
1821
|
+
if (!chat?.ok || chat.x === undefined || chat.y === undefined) {
|
|
1822
|
+
return "ChatGPT is on its Work surface, whose picker offers different models and no Pro, and the Chat toggle could not be clicked.";
|
|
1823
|
+
}
|
|
1824
|
+
await verifiedClickAt(cdp, chat.x, chat.y, "Chat surface toggle");
|
|
1825
|
+
await sleep(1_500);
|
|
1826
|
+
return "ChatGPT was on its Work surface, whose picker offers different models and no Pro. Switched back to Chat.";
|
|
1827
|
+
}
|
|
1785
1828
|
async function ensurePickerOpen(cdp) {
|
|
1786
1829
|
// The slider, not the menu, is what the caller needs - and an open menu that
|
|
1787
1830
|
// has not painted its slider yet looks identical to one that has.
|
|
@@ -1870,6 +1913,11 @@ export function sliderWalkPlan(state) {
|
|
|
1870
1913
|
async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
1871
1914
|
if (!options.model && !options.proMode && !options.effort)
|
|
1872
1915
|
return;
|
|
1916
|
+
// Which surface is live decides which picker exists at all, so settle that
|
|
1917
|
+
// before looking for anything in it.
|
|
1918
|
+
const surfaceWarning = await ensureChatSurface(cdp);
|
|
1919
|
+
if (surfaceWarning)
|
|
1920
|
+
selectionWarnings.push(surfaceWarning);
|
|
1873
1921
|
// --pro-mode selects a Pro sub-mode, so it is meaningless with a non-Pro
|
|
1874
1922
|
// --model. Fail loudly instead of silently dropping the requested sub-mode.
|
|
1875
1923
|
if (options.proMode && options.model && !/pro/i.test(options.model)) {
|
|
@@ -3380,25 +3428,25 @@ export async function listChatGptSidebarProjects(input = {}) {
|
|
|
3380
3428
|
* listing exactly as it was rather than failing a read-only command.
|
|
3381
3429
|
*/
|
|
3382
3430
|
async function readEffortSteps(cdp) {
|
|
3431
|
+
const focused = await cdp.evaluate(focusPowerSliderExpression());
|
|
3432
|
+
if (!focused?.ok)
|
|
3433
|
+
return undefined;
|
|
3434
|
+
const start = await cdp.evaluate(powerSliderStateExpression());
|
|
3435
|
+
if (!start?.ok)
|
|
3436
|
+
return undefined;
|
|
3437
|
+
const plan = sliderWalkPlan({ position: start.position, min: start.min, max: start.max });
|
|
3438
|
+
if (!plan)
|
|
3439
|
+
return undefined;
|
|
3440
|
+
const current = start.effort ?? null;
|
|
3441
|
+
const startedAt = start.position;
|
|
3383
3442
|
try {
|
|
3384
|
-
const focused = await cdp.evaluate(focusPowerSliderExpression());
|
|
3385
|
-
if (!focused?.ok)
|
|
3386
|
-
return undefined;
|
|
3387
|
-
const start = await cdp.evaluate(powerSliderStateExpression());
|
|
3388
|
-
if (!start?.ok)
|
|
3389
|
-
return undefined;
|
|
3390
|
-
const plan = sliderWalkPlan({ position: start.position, min: start.min, max: start.max });
|
|
3391
|
-
if (!plan)
|
|
3392
|
-
return undefined;
|
|
3393
|
-
const current = start.effort ?? null;
|
|
3394
3443
|
for (let i = 0; i < plan.toBottom; i += 1) {
|
|
3395
3444
|
await dispatchArrowKey(cdp, "ArrowLeft");
|
|
3396
3445
|
await sleep(120);
|
|
3397
3446
|
}
|
|
3398
|
-
// Every position is recorded, including repeats.
|
|
3399
|
-
// ladder of model-and-effort pairs, so
|
|
3400
|
-
// collapsing by label
|
|
3401
|
-
// each one sends with.
|
|
3447
|
+
// Every position is recorded, including repeats. The slider can walk a
|
|
3448
|
+
// ladder of model-and-effort pairs, so one effort name is several rungs -
|
|
3449
|
+
// collapsing by label lost which model each one would send with.
|
|
3402
3450
|
const rungs = [];
|
|
3403
3451
|
const record = async () => {
|
|
3404
3452
|
const state = await cdp.evaluate(powerSliderStateExpression());
|
|
@@ -3415,16 +3463,40 @@ async function readEffortSteps(cdp) {
|
|
|
3415
3463
|
await sleep(220);
|
|
3416
3464
|
await record();
|
|
3417
3465
|
}
|
|
3418
|
-
// Put it back. Reading someone's setting must not change it.
|
|
3419
|
-
for (let i = 0; i < plan.climb - plan.back; i += 1) {
|
|
3420
|
-
await dispatchArrowKey(cdp, "ArrowLeft");
|
|
3421
|
-
await sleep(120);
|
|
3422
|
-
}
|
|
3423
3466
|
return rungs.length > 0 ? { rungs, current } : undefined;
|
|
3424
3467
|
}
|
|
3425
3468
|
catch {
|
|
3426
3469
|
return undefined;
|
|
3427
3470
|
}
|
|
3471
|
+
finally {
|
|
3472
|
+
// Whatever happened above, the setting goes back. Replaying a precomputed
|
|
3473
|
+
// count skipped this whenever the walk was interrupted, and one run left a
|
|
3474
|
+
// slider on High that had been on Pro.
|
|
3475
|
+
await restorePowerSlider(cdp, startedAt);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
async function restorePowerSlider(cdp, target) {
|
|
3479
|
+
for (let guard = 0; guard < POWER_SLIDER_RESTORE_ATTEMPTS; guard += 1) {
|
|
3480
|
+
let state;
|
|
3481
|
+
try {
|
|
3482
|
+
state = await cdp.evaluate(powerSliderStateExpression());
|
|
3483
|
+
}
|
|
3484
|
+
catch {
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
if (!state?.ok)
|
|
3488
|
+
return;
|
|
3489
|
+
const move = sliderRestoreStep({ ...(state.position !== undefined ? { current: state.position } : {}), ...(target !== undefined ? { target } : {}) });
|
|
3490
|
+
if (move === "done")
|
|
3491
|
+
return;
|
|
3492
|
+
try {
|
|
3493
|
+
await dispatchArrowKey(cdp, move === "left" ? "ArrowLeft" : "ArrowRight");
|
|
3494
|
+
}
|
|
3495
|
+
catch {
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
await sleep(150);
|
|
3499
|
+
}
|
|
3428
3500
|
}
|
|
3429
3501
|
export async function listChatGptModelOptions(input = {}) {
|
|
3430
3502
|
const port = resolveCdpPort(input.port);
|
|
@@ -3460,7 +3532,18 @@ export async function listChatGptModelOptions(input = {}) {
|
|
|
3460
3532
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
3461
3533
|
const options = await cdp.evaluate(modelMenuOptionsExpression());
|
|
3462
3534
|
const effortSteps = await readEffortSteps(cdp);
|
|
3463
|
-
|
|
3535
|
+
// Chat and Work have different pickers - Work's offers no Pro - and a
|
|
3536
|
+
// listing that does not say which one it read is how a browser sitting on
|
|
3537
|
+
// Work convinced two machines that Pro had been removed from the account.
|
|
3538
|
+
let surface;
|
|
3539
|
+
try {
|
|
3540
|
+
const probe = await cdp.evaluate(chatWorkSurfaceExpression());
|
|
3541
|
+
surface = probe?.surfaces?.find((entry) => entry.checked)?.label;
|
|
3542
|
+
}
|
|
3543
|
+
catch {
|
|
3544
|
+
// the toggle predates some builds; saying nothing is right there
|
|
3545
|
+
}
|
|
3546
|
+
return { url: status.url, options, ...(effortSteps ? { effortSteps } : {}), ...(surface ? { surface } : {}) };
|
|
3464
3547
|
}
|
|
3465
3548
|
finally {
|
|
3466
3549
|
try {
|
package/dist/cli-pro.js
CHANGED
|
@@ -443,6 +443,9 @@ export async function runProCommand(rest, io, runCliFn) {
|
|
|
443
443
|
});
|
|
444
444
|
throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
|
|
445
445
|
}
|
|
446
|
+
if (listed.surface) {
|
|
447
|
+
io.stdout(`Read from ChatGPT's ${listed.surface} surface. Chat and Work have different pickers - only one of them offers Pro.`);
|
|
448
|
+
}
|
|
446
449
|
io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
|
|
447
450
|
for (const option of listed.options) {
|
|
448
451
|
io.stdout(formatModelMenuOption(option));
|
|
@@ -70,3 +70,30 @@ export function menuKeyboardStep(input) {
|
|
|
70
70
|
}
|
|
71
71
|
return pressed < limit ? "down" : "exhausted";
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* One move towards putting the power slider back where the walk found it.
|
|
75
|
+
*
|
|
76
|
+
* Driven by the position actually read rather than by a replayed count, so an
|
|
77
|
+
* interrupted walk still converges instead of abandoning the setting wherever
|
|
78
|
+
* it stopped.
|
|
79
|
+
*/
|
|
80
|
+
export function sliderRestoreStep(input) {
|
|
81
|
+
const { current, target } = input;
|
|
82
|
+
if (!Number.isInteger(current) || !Number.isInteger(target))
|
|
83
|
+
return "done";
|
|
84
|
+
if (current === target)
|
|
85
|
+
return "done";
|
|
86
|
+
return current > target ? "left" : "right";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether the browser needs putting back on ChatGPT's Chat surface.
|
|
90
|
+
*
|
|
91
|
+
* Chat and Work have different model pickers - Work's has no Pro at all - so a
|
|
92
|
+
* browser that has drifted onto Work silently drives the wrong one.
|
|
93
|
+
*/
|
|
94
|
+
export function chatSurfaceChoice(surfaces) {
|
|
95
|
+
const chat = surfaces.find((surface) => surface.label.trim().toLowerCase() === "chat");
|
|
96
|
+
if (!chat)
|
|
97
|
+
return "no-toggle";
|
|
98
|
+
return chat.checked ? "already-chat" : "switch-to-chat";
|
|
99
|
+
}
|