@youdie006/prodex 0.8.3 → 0.9.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/README.md +13 -9
- package/dist/chatgpt-browser.d.ts +41 -0
- package/dist/chatgpt-browser.js +181 -24
- package/dist/chatgpt-browser.js.map +1 -1
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli-args.js +10 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-help.js +34 -19
- package/dist/cli-help.js.map +1 -1
- package/dist/cli-pro.d.ts +46 -1
- package/dist/cli-pro.js +173 -25
- package/dist/cli-pro.js.map +1 -1
- package/dist/cli.js +43 -3
- package/dist/cli.js.map +1 -1
- package/dist/mcp.d.ts +20 -1
- package/dist/mcp.js +37 -2
- package/dist/mcp.js.map +1 -1
- package/docs/claude.md +4 -1
- package/docs/clients.md +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,11 +29,13 @@ The most common way to use `prodex` is standalone — get a ChatGPT Pro answer o
|
|
|
29
29
|
```bash
|
|
30
30
|
npm install -g @youdie006/prodex # needs Node 20+, git, ripgrep
|
|
31
31
|
|
|
32
|
-
prodex pro browser login # opens a dedicated Chrome
|
|
33
|
-
prodex
|
|
32
|
+
prodex pro browser login # opens a dedicated Chrome and waits until your ChatGPT login is READY
|
|
33
|
+
prodex ask --file src/auth.ts "Review this for security holes"
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
`prodex ask` is the short form of `prodex pro browser ask`; the full form and every flag work identically. In an interactive terminal, `login` keeps watching the opened window and tells you exactly which manual step is still missing (log in, clear a check, open a chat) until it reports READY. While ChatGPT thinks, `prodex` prints progress to stderr (connecting, prompt sent, elapsed seconds while generating), so a multi-minute Pro answer never looks frozen.
|
|
37
|
+
|
|
38
|
+
The answer prints to your terminal and is saved under `.bridge/` for later (`prodex pro latest` re-prints it). `prodex` drives the picker you can see and deliberately will not send into a window you cannot watch — but a dedicated Chrome window left non-minimized (even behind your editor) counts as watchable, so it sends quietly in the background without stealing focus. Just don't minimize it or switch that window to another tab. Add `--pro-mode 확장` for extended reasoning, or `--file` more than once to attach several files. See [First Pro Login](#first-pro-login) for the full flow, and the [FAQ](#faq) if a send stops.
|
|
37
39
|
|
|
38
40
|
## Core Shape
|
|
39
41
|
|
|
@@ -97,9 +99,11 @@ Not implemented:
|
|
|
97
99
|
- Shell execution tools.
|
|
98
100
|
- Automatic public tunnel setup.
|
|
99
101
|
|
|
100
|
-
## Quick Start
|
|
102
|
+
## Agent Bridge Quick Start
|
|
103
|
+
|
|
104
|
+
This section connects coding agents (Claude, Codex, ChatGPT Projects) to the bridge over MCP. It is not required for the standalone terminal flow above — if you only want Pro answers in your terminal, the [Quickstart](#quickstart-a-pro-second-opinion-from-your-terminal) is complete on its own.
|
|
101
105
|
|
|
102
|
-
Requires Node.js 20 or newer, `git`, and `ripgrep` (`rg`) on PATH. The optional visible-browser adapter
|
|
106
|
+
Requires Node.js 20 or newer, `git`, and `ripgrep` (`rg`) on PATH. The optional visible-browser adapter needs a Chromium-family browser: PATH binaries (`google-chrome`, `chromium`, `chromium-browser`, `microsoft-edge`, `brave-browser`), standard macOS app bundles, Windows Program Files/LOCALAPPDATA installs, and Windows-host browsers under WSL are all probed automatically; anything else via `PRODEX_CHROME=/path/to/browser`.
|
|
103
107
|
|
|
104
108
|
Install from npm — **note the scope**. The unscoped `prodex` on npm is an unrelated third-party package; do **not** install it. Use the scoped name:
|
|
105
109
|
|
|
@@ -153,7 +157,7 @@ prodex pro browser check
|
|
|
153
157
|
prodex pro browser smoke --cwd /absolute/path/to/your/repo
|
|
154
158
|
```
|
|
155
159
|
|
|
156
|
-
If you use a non-default debug port or Chrome profile, pass it to `login`; the printed follow-up `check` and `smoke` commands keep the matching `--port`. If you launch from outside the repo you want to inspect, pass `--cwd /absolute/path/to/your/repo` to `login`, `check`, or `smoke` so the command targets the same bridge. On slower first launches, add `--launch-timeout-ms 12000`.
|
|
160
|
+
If you use a non-default debug port or Chrome profile, pass it to `login`; the printed follow-up `check` and `smoke` commands keep the matching `--port`. To stop repeating `--port` on every command, export `PRODEX_CDP_PORT=<port>` once — explicit `--port` still wins. If you launch from outside the repo you want to inspect, pass `--cwd /absolute/path/to/your/repo` to `login`, `check`, or `smoke` so the command targets the same bridge. On slower first launches, add `--launch-timeout-ms 12000`.
|
|
157
161
|
|
|
158
162
|
For a source checkout, keep the follow-up commands in source-checkout form too:
|
|
159
163
|
|
|
@@ -170,7 +174,7 @@ node "$SOURCE_CLI" pro browser smoke --source-cli "$SOURCE_CLI" --cwd /absolute/
|
|
|
170
174
|
What happens:
|
|
171
175
|
|
|
172
176
|
- `login --dry-run` prints the dedicated Chrome profile, debug URL, and next commands without opening a browser.
|
|
173
|
-
- `login` opens that dedicated Chrome profile at ChatGPT.
|
|
177
|
+
- `login` opens that dedicated Chrome profile at ChatGPT. In an interactive terminal it then waits (default 5 minutes; `--no-wait` skips, `--wait-timeout-ms` tunes) and narrates which manual step is still missing until it reports READY; scripts and agents get the immediate return unless they pass `--wait`.
|
|
174
178
|
- You log in manually in the visible browser.
|
|
175
179
|
- If ChatGPT asks for captcha, Cloudflare/human verification, permission, or account verification, handle it in that browser.
|
|
176
180
|
- If ChatGPT shows a usage limit, message limit, model limit, or rate limit, wait for the reset or choose an available model in the browser.
|
|
@@ -184,11 +188,11 @@ What happens:
|
|
|
184
188
|
|
|
185
189
|
You can close that Chrome window after check/smoke or when you are done. The next time you need it, run `pro browser login` or `pro browser check` again. `check` will tell you what to do if the browser is closed.
|
|
186
190
|
|
|
187
|
-
Actual explicit visible-browser consult:
|
|
191
|
+
Actual explicit visible-browser consult (`prodex ask` is the short form of `prodex pro browser ask`):
|
|
188
192
|
|
|
189
193
|
```bash
|
|
190
194
|
cd /absolute/path/to/your/repo
|
|
191
|
-
prodex
|
|
195
|
+
prodex ask --file README.md "Review the project positioning"
|
|
192
196
|
prodex pro latest
|
|
193
197
|
prodex results show latest
|
|
194
198
|
prodex results artifact latest
|
|
@@ -40,6 +40,29 @@ export type ChatGptProMode = "기본" | "확장";
|
|
|
40
40
|
export declare function parseReasoningEffort(raw: string): ChatGptReasoningEffort;
|
|
41
41
|
/** Normalize a CLI Pro sub-mode value onto the exact ChatGPT menu label. */
|
|
42
42
|
export declare function parseProMode(raw: string): ChatGptProMode;
|
|
43
|
+
export declare const DEFAULT_CDP_PORT = 9333;
|
|
44
|
+
/**
|
|
45
|
+
* Stability policy for the answer poll loop. Returns true once the observed
|
|
46
|
+
* (answer, generating) stream should be accepted as final:
|
|
47
|
+
* - normal answers: two consecutive identical, non-generating polls;
|
|
48
|
+
* - answers whose tail looks like the streaming caret: six additional
|
|
49
|
+
* confirmations (~6s at the 1s poll cadence), so a lingering caret gets
|
|
50
|
+
* dropped by ChatGPT's finalization re-render first, while an answer that
|
|
51
|
+
* genuinely ends with an underscore is still accepted after the extra wait.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createChatGptAnswerStabilityTracker(): (answer: string, generating: boolean) => boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the Chrome DevTools port: explicit flag > PRODEX_CDP_PORT env >
|
|
56
|
+
* default 9333. The env var exists so a non-default port picked at login does
|
|
57
|
+
* not have to be repeated as --port on every later command.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveCdpPort(explicit?: number): number;
|
|
60
|
+
export type SendChatGptProgressPhase = "connecting" | "tab_ready" | "selecting" | "sent" | "waiting" | "answered";
|
|
61
|
+
export interface SendChatGptProgressEvent {
|
|
62
|
+
phase: SendChatGptProgressPhase;
|
|
63
|
+
elapsedMs: number;
|
|
64
|
+
detail?: string;
|
|
65
|
+
}
|
|
43
66
|
export interface SendChatGptPromptOptions {
|
|
44
67
|
port?: number;
|
|
45
68
|
prompt: string;
|
|
@@ -55,6 +78,8 @@ export interface SendChatGptPromptOptions {
|
|
|
55
78
|
proMode?: ChatGptProMode;
|
|
56
79
|
/** Reasoning effort, used for non-Pro models. */
|
|
57
80
|
effort?: ChatGptReasoningEffort;
|
|
81
|
+
/** Progress callback so long sends can report phase + elapsed instead of staying silent. */
|
|
82
|
+
onProgress?: (event: SendChatGptProgressEvent) => void;
|
|
58
83
|
}
|
|
59
84
|
export interface SendChatGptPromptResult {
|
|
60
85
|
url: string;
|
|
@@ -132,6 +157,13 @@ export declare function assertChatGptTargetTabAvailable(targetUrl: string): void
|
|
|
132
157
|
export declare function assertChatGptPageAvailable(): never;
|
|
133
158
|
export declare function assertChatGptReadyForPrompt(loggedInLikely: boolean, hasComposer: boolean): void;
|
|
134
159
|
export declare function chatGptVisibilityBlocker(visibilityState: string | undefined, url: string | undefined): ChatGptBrowserStatus["blocker"] | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* Environment for the browser spawn. On Linux shells without DISPLAY or
|
|
162
|
+
* WAYLAND_DISPLAY (common in WSL where a non-login shell does not export
|
|
163
|
+
* them), a present WSLg X socket means a desktop is actually available -
|
|
164
|
+
* inject DISPLAY=:0 so Chrome does not exit immediately with code 1.
|
|
165
|
+
*/
|
|
166
|
+
export declare function browserLaunchEnv(platform?: NodeJS.Platform, env?: Record<string, string | undefined>, hasX0Socket?: () => boolean): Record<string, string | undefined>;
|
|
135
167
|
export declare function openChatGptBrowser(options?: ChatGptBrowserOptions): ChatGptBrowserLaunch;
|
|
136
168
|
export declare function getChatGptBrowserStatus(options?: {
|
|
137
169
|
port?: number;
|
|
@@ -169,4 +201,13 @@ export declare function statusExpression(): string;
|
|
|
169
201
|
export declare function prepareComposerExpression(): string;
|
|
170
202
|
export declare function composerTextStateExpression(): string;
|
|
171
203
|
export declare function submitExpression(): string;
|
|
204
|
+
/**
|
|
205
|
+
* Ordered browser candidates for the current platform: PATH binary names
|
|
206
|
+
* first, then well-known absolute install locations (macOS app bundles,
|
|
207
|
+
* Windows Program Files/LOCALAPPDATA, and Windows-host browsers under WSL).
|
|
208
|
+
* WSL detection cannot rely on WSL_DISTRO_NAME alone: non-login shells
|
|
209
|
+
* (measured live) may not carry it, so WSL_INTEROP and the kernel string are
|
|
210
|
+
* probed too.
|
|
211
|
+
*/
|
|
212
|
+
export declare function chromeCommandCandidates(platform?: NodeJS.Platform, env?: Record<string, string | undefined>, isWsl?: () => boolean): string[];
|
|
172
213
|
export {};
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, statSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, readFileSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
export class ChatGptBrowserBlockerError extends Error {
|
|
@@ -60,6 +60,62 @@ export function parseProMode(raw) {
|
|
|
60
60
|
return alias;
|
|
61
61
|
throw new Error(`--pro-mode must be one of ${PRO_MODES.join(", ")}`);
|
|
62
62
|
}
|
|
63
|
+
export const DEFAULT_CDP_PORT = 9333;
|
|
64
|
+
// The ChatGPT streaming caret renders as a literal text character at the end
|
|
65
|
+
// of the growing assistant message (a plain "_" in the current UI; "▍" and
|
|
66
|
+
// friends historically). Measured live 2026-07-06: the caret can stay in
|
|
67
|
+
// innerText for a beat AFTER the stop button disappears, so a short answer can
|
|
68
|
+
// look stable and non-generating while still carrying the caret.
|
|
69
|
+
const TRAILING_CARET_SUSPECT = /[_▍▌█▊▋]$/;
|
|
70
|
+
/**
|
|
71
|
+
* Stability policy for the answer poll loop. Returns true once the observed
|
|
72
|
+
* (answer, generating) stream should be accepted as final:
|
|
73
|
+
* - normal answers: two consecutive identical, non-generating polls;
|
|
74
|
+
* - answers whose tail looks like the streaming caret: six additional
|
|
75
|
+
* confirmations (~6s at the 1s poll cadence), so a lingering caret gets
|
|
76
|
+
* dropped by ChatGPT's finalization re-render first, while an answer that
|
|
77
|
+
* genuinely ends with an underscore is still accepted after the extra wait.
|
|
78
|
+
*/
|
|
79
|
+
export function createChatGptAnswerStabilityTracker() {
|
|
80
|
+
let stableAnswer;
|
|
81
|
+
let confirmations = 0;
|
|
82
|
+
return (answer, generating) => {
|
|
83
|
+
if (generating) {
|
|
84
|
+
// A generating poll invalidates any baseline: the text may still grow.
|
|
85
|
+
stableAnswer = undefined;
|
|
86
|
+
confirmations = 0;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (answer !== stableAnswer) {
|
|
90
|
+
stableAnswer = answer;
|
|
91
|
+
confirmations = 0;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
confirmations += 1;
|
|
95
|
+
// Trim before the caret check: the caller trims the returned answer, so a
|
|
96
|
+
// caret followed by trailing whitespace must count as suspect too.
|
|
97
|
+
const required = TRAILING_CARET_SUSPECT.test(answer.trimEnd()) ? 8 : 2;
|
|
98
|
+
return confirmations >= required;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the Chrome DevTools port: explicit flag > PRODEX_CDP_PORT env >
|
|
103
|
+
* default 9333. The env var exists so a non-default port picked at login does
|
|
104
|
+
* not have to be repeated as --port on every later command.
|
|
105
|
+
*/
|
|
106
|
+
export function resolveCdpPort(explicit) {
|
|
107
|
+
if (explicit !== undefined)
|
|
108
|
+
return explicit;
|
|
109
|
+
const fromEnv = process.env.PRODEX_CDP_PORT;
|
|
110
|
+
if (fromEnv !== undefined && fromEnv !== "") {
|
|
111
|
+
const parsed = Number(fromEnv);
|
|
112
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) {
|
|
113
|
+
throw new Error(`PRODEX_CDP_PORT must be an integer between 1 and 65535, got: ${fromEnv}`);
|
|
114
|
+
}
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
return DEFAULT_CDP_PORT;
|
|
118
|
+
}
|
|
63
119
|
export const CHATGPT_RUNTIME_BLOCKER_TEXT_EXCLUDED_ANCESTORS = '[data-message-author-role],script,style,noscript,[aria-hidden="true"],div[role="textbox"],textarea,[contenteditable="true"]';
|
|
64
120
|
export const CHATGPT_COMPOSER_CANDIDATE_EXCLUDED_ANCESTORS = '[data-message-author-role],script,style,noscript,[aria-hidden="true"]';
|
|
65
121
|
export const PRODEX_ACTIVE_COMPOSER_ATTRIBUTE = "data-prodex-active-composer";
|
|
@@ -398,16 +454,36 @@ export function chatGptVisibilityBlocker(visibilityState, url) {
|
|
|
398
454
|
next_step: `Select ${url ?? "the ChatGPT tab"} in the dedicated browser, then retry.`
|
|
399
455
|
};
|
|
400
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Environment for the browser spawn. On Linux shells without DISPLAY or
|
|
459
|
+
* WAYLAND_DISPLAY (common in WSL where a non-login shell does not export
|
|
460
|
+
* them), a present WSLg X socket means a desktop is actually available -
|
|
461
|
+
* inject DISPLAY=:0 so Chrome does not exit immediately with code 1.
|
|
462
|
+
*/
|
|
463
|
+
export function browserLaunchEnv(platform = process.platform, env = process.env, hasX0Socket = () => {
|
|
464
|
+
try {
|
|
465
|
+
statSync("/tmp/.X11-unix/X0");
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
}) {
|
|
472
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY && hasX0Socket()) {
|
|
473
|
+
return { ...env, DISPLAY: ":0" };
|
|
474
|
+
}
|
|
475
|
+
return env;
|
|
476
|
+
}
|
|
401
477
|
export function openChatGptBrowser(options = {}) {
|
|
402
478
|
const command = resolveChromeCommand();
|
|
403
|
-
const port = options.port
|
|
479
|
+
const port = resolveCdpPort(options.port);
|
|
404
480
|
const profileDir = options.profileDir ?? defaultChatGptProfileDir();
|
|
405
481
|
const args = buildChromeLaunchArgs({
|
|
406
482
|
port,
|
|
407
483
|
profileDir,
|
|
408
484
|
url: options.url ?? "https://chatgpt.com/"
|
|
409
485
|
});
|
|
410
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
486
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", env: browserLaunchEnv() });
|
|
411
487
|
let earlyExit;
|
|
412
488
|
const earlyExitWaiters = new Set();
|
|
413
489
|
const recordEarlyExit = (exit) => {
|
|
@@ -444,7 +520,7 @@ export function openChatGptBrowser(options = {}) {
|
|
|
444
520
|
};
|
|
445
521
|
}
|
|
446
522
|
export async function getChatGptBrowserStatus(options = {}) {
|
|
447
|
-
const port = options.port
|
|
523
|
+
const port = resolveCdpPort(options.port);
|
|
448
524
|
const page = await findChatGptPage(port, options.timeoutMs ?? 1500);
|
|
449
525
|
if (!page.ok) {
|
|
450
526
|
return {
|
|
@@ -846,8 +922,20 @@ async function selectProject(cdp, options) {
|
|
|
846
922
|
}
|
|
847
923
|
}
|
|
848
924
|
export async function sendChatGptPrompt(options) {
|
|
849
|
-
const port = options.port
|
|
925
|
+
const port = resolveCdpPort(options.port);
|
|
850
926
|
const timeoutMs = options.timeoutMs ?? 90_000;
|
|
927
|
+
const sendStartedAt = Date.now();
|
|
928
|
+
const emitProgress = (phase, detail) => {
|
|
929
|
+
if (!options.onProgress)
|
|
930
|
+
return;
|
|
931
|
+
try {
|
|
932
|
+
options.onProgress({ phase, elapsedMs: Date.now() - sendStartedAt, ...(detail !== undefined ? { detail } : {}) });
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
// Progress reporting must never break the send itself.
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
emitProgress("connecting", `port ${port}`);
|
|
851
939
|
const normalizedTargetUrl = options.targetUrl ? normalizeChatGptTargetUrl(options.targetUrl) : undefined;
|
|
852
940
|
const pageResult = await findChatGptPage(port, computePageDiscoveryTimeout(timeoutMs), normalizedTargetUrl);
|
|
853
941
|
if (!pageResult.ok) {
|
|
@@ -877,6 +965,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
877
965
|
if (busyBlocker) {
|
|
878
966
|
throw new ChatGptBrowserBlockerError(busyBlocker);
|
|
879
967
|
}
|
|
968
|
+
emitProgress("tab_ready");
|
|
969
|
+
// Progress details deliberately avoid project names (receipts redact them too).
|
|
970
|
+
const selectionSummary = [
|
|
971
|
+
options.model !== undefined ? `model=${options.model}` : undefined,
|
|
972
|
+
options.proMode !== undefined ? `pro_mode=${options.proMode}` : undefined,
|
|
973
|
+
options.effort !== undefined ? `effort=${options.effort}` : undefined,
|
|
974
|
+
options.project !== undefined ? "project=set" : undefined,
|
|
975
|
+
options.projectNew !== undefined ? "project_new=set" : undefined
|
|
976
|
+
]
|
|
977
|
+
.filter((part) => part !== undefined)
|
|
978
|
+
.join(" ");
|
|
979
|
+
if (selectionSummary)
|
|
980
|
+
emitProgress("selecting", selectionSummary);
|
|
880
981
|
let beforeSubmit;
|
|
881
982
|
let submitButtonFound = false;
|
|
882
983
|
const cdp = await connectCdp(page.webSocketDebuggerUrl);
|
|
@@ -905,6 +1006,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
905
1006
|
finally {
|
|
906
1007
|
cdp.close();
|
|
907
1008
|
}
|
|
1009
|
+
emitProgress("sent", `timeout ${Math.round(timeoutMs / 1000)}s`);
|
|
908
1010
|
const started = Date.now();
|
|
909
1011
|
const acceptDeadline = computePromptAcceptanceDeadline(timeoutMs, started);
|
|
910
1012
|
let accepted = false;
|
|
@@ -919,6 +1021,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
919
1021
|
accepted = true;
|
|
920
1022
|
break;
|
|
921
1023
|
}
|
|
1024
|
+
emitProgress("waiting", "prompt posting");
|
|
922
1025
|
}
|
|
923
1026
|
if (!accepted) {
|
|
924
1027
|
// A successful submit clears the composer, so text still sitting there means
|
|
@@ -933,34 +1036,26 @@ export async function sendChatGptPrompt(options) {
|
|
|
933
1036
|
}
|
|
934
1037
|
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound });
|
|
935
1038
|
}
|
|
936
|
-
|
|
937
|
-
let stableConfirmations = 0;
|
|
1039
|
+
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
938
1040
|
while (Date.now() - started < timeoutMs) {
|
|
939
1041
|
await sleep(1000);
|
|
940
1042
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
941
1043
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
942
1044
|
if (runtimeBlocker)
|
|
943
1045
|
throw new ChatGptBrowserBlockerError(runtimeBlocker);
|
|
1046
|
+
emitProgress("waiting", finalState.generating ? "generating" : "stabilizing");
|
|
944
1047
|
if (!hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, finalState))
|
|
945
1048
|
continue;
|
|
946
|
-
// A "fresh" answer must also be stable: ChatGPT can momentarily look done
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
// pause (identical text across one interval while more tokens are still
|
|
953
|
-
// coming) is not mistaken for completion.
|
|
954
|
-
if (stableConfirmations >= 2)
|
|
955
|
-
break;
|
|
956
|
-
}
|
|
957
|
-
else {
|
|
958
|
-
stableAnswer = finalState.answer;
|
|
959
|
-
stableConfirmations = 0;
|
|
960
|
-
}
|
|
1049
|
+
// A "fresh" answer must also be stable: ChatGPT can momentarily look done
|
|
1050
|
+
// mid-stream, and the streaming caret renders as a literal trailing
|
|
1051
|
+
// character that can outlive the stop button. The tracker requires extra
|
|
1052
|
+
// confirmations for caret-suspect tails (see its doc comment).
|
|
1053
|
+
if (answerIsStable(finalState.answer, finalState.generating))
|
|
1054
|
+
break;
|
|
961
1055
|
}
|
|
962
1056
|
const completed = finalState;
|
|
963
1057
|
if (completed && hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
|
|
1058
|
+
emitProgress("answered");
|
|
964
1059
|
return {
|
|
965
1060
|
url: completed.url,
|
|
966
1061
|
title: completed.title,
|
|
@@ -973,6 +1068,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
973
1068
|
// (a truncated-but-real Pro answer is far more useful than losing it) and
|
|
974
1069
|
// flag it so the caller records the incompleteness and the retry hint.
|
|
975
1070
|
if (completed && hasPartialChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
|
|
1071
|
+
emitProgress("answered", "partial");
|
|
976
1072
|
return {
|
|
977
1073
|
url: completed.url,
|
|
978
1074
|
title: completed.title,
|
|
@@ -1002,7 +1098,7 @@ export function modelMenuOptionsExpression() {
|
|
|
1002
1098
|
// and press Escape. Nothing is clicked inside the menu, so the user's model
|
|
1003
1099
|
// selection is never changed.
|
|
1004
1100
|
export async function listChatGptModelOptions(input = {}) {
|
|
1005
|
-
const port = input.port
|
|
1101
|
+
const port = resolveCdpPort(input.port);
|
|
1006
1102
|
const timeoutMs = input.timeoutMs ?? 15_000;
|
|
1007
1103
|
const pageResult = await findChatGptPage(port, computePageDiscoveryTimeout(timeoutMs), undefined);
|
|
1008
1104
|
if (!pageResult.ok) {
|
|
@@ -1424,13 +1520,74 @@ function answerExpression() {
|
|
|
1424
1520
|
function enterKeyEvent(type) {
|
|
1425
1521
|
return { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 };
|
|
1426
1522
|
}
|
|
1523
|
+
const CHROME_PATH_BINARY_NAMES = ["google-chrome", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"];
|
|
1524
|
+
const DARWIN_CHROME_PATHS = [
|
|
1525
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1526
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1527
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1528
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
1529
|
+
];
|
|
1530
|
+
function win32ChromePaths(env) {
|
|
1531
|
+
return [
|
|
1532
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
1533
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
1534
|
+
...(env.LOCALAPPDATA ? [`${env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`] : []),
|
|
1535
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
1536
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
1537
|
+
];
|
|
1538
|
+
}
|
|
1539
|
+
const WSL_WINDOWS_CHROME_PATHS = [
|
|
1540
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
1541
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
1542
|
+
"/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe",
|
|
1543
|
+
"/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
1544
|
+
];
|
|
1545
|
+
function kernelLooksLikeWsl() {
|
|
1546
|
+
try {
|
|
1547
|
+
return /microsoft/i.test(readFileSync("/proc/version", "utf8"));
|
|
1548
|
+
}
|
|
1549
|
+
catch {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Ordered browser candidates for the current platform: PATH binary names
|
|
1555
|
+
* first, then well-known absolute install locations (macOS app bundles,
|
|
1556
|
+
* Windows Program Files/LOCALAPPDATA, and Windows-host browsers under WSL).
|
|
1557
|
+
* WSL detection cannot rely on WSL_DISTRO_NAME alone: non-login shells
|
|
1558
|
+
* (measured live) may not carry it, so WSL_INTEROP and the kernel string are
|
|
1559
|
+
* probed too.
|
|
1560
|
+
*/
|
|
1561
|
+
export function chromeCommandCandidates(platform = process.platform, env = process.env, isWsl = kernelLooksLikeWsl) {
|
|
1562
|
+
const candidates = [...CHROME_PATH_BINARY_NAMES];
|
|
1563
|
+
if (platform === "darwin")
|
|
1564
|
+
candidates.push(...DARWIN_CHROME_PATHS);
|
|
1565
|
+
if (platform === "win32")
|
|
1566
|
+
candidates.push(...win32ChromePaths(env));
|
|
1567
|
+
if (platform === "linux" && (env.WSL_DISTRO_NAME || env.WSL_INTEROP || isWsl())) {
|
|
1568
|
+
candidates.push(...WSL_WINDOWS_CHROME_PATHS);
|
|
1569
|
+
}
|
|
1570
|
+
return candidates;
|
|
1571
|
+
}
|
|
1427
1572
|
function resolveChromeCommand() {
|
|
1428
1573
|
const fromEnv = process.env.PRODEX_CHROME;
|
|
1429
1574
|
if (fromEnv) {
|
|
1430
1575
|
assertChromeCommandAvailable(fromEnv, "PRODEX_CHROME");
|
|
1431
1576
|
return fromEnv;
|
|
1432
1577
|
}
|
|
1433
|
-
for (const command of
|
|
1578
|
+
for (const command of chromeCommandCandidates()) {
|
|
1579
|
+
if (isPathLikeCommand(command)) {
|
|
1580
|
+
try {
|
|
1581
|
+
if (!statSync(command).isFile())
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (hasChromeLikeVersion(command))
|
|
1588
|
+
return command;
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1434
1591
|
if (isCommandOnPath(command) && hasChromeLikeVersion(command))
|
|
1435
1592
|
return command;
|
|
1436
1593
|
}
|