@youdie006/prodex 0.8.1 → 0.9.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 +31 -10
- package/dist/chatgpt-browser.d.ts +38 -0
- package/dist/chatgpt-browser.js +166 -23
- package/dist/chatgpt-browser.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 +146 -17
- package/dist/cli-pro.js.map +1 -1
- package/dist/cli.js +30 -3
- package/dist/cli.js.map +1 -1
- package/dist/mcp.d.ts +18 -1
- package/dist/mcp.js +17 -2
- package/dist/mcp.js.map +1 -1
- package/docs/claude.md +4 -1
- package/package.json +2 -2
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
|
|
@@ -386,9 +390,26 @@ npm run dev -- tasks list
|
|
|
386
390
|
|
|
387
391
|
**Windows / macOS?** The code targets all three platforms, but the visible-browser adapter is exercised most on Linux; open an issue with details if a browser step misbehaves on macOS or Windows.
|
|
388
392
|
|
|
393
|
+
## Releasing
|
|
394
|
+
|
|
395
|
+
Publishing to npm runs entirely in CI with **no long-lived token** — auth is npm [trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC), so nothing needs to store or paste an `NPM_TOKEN`, and every release carries a verifiable `--provenance` attestation.
|
|
396
|
+
|
|
397
|
+
Release flow:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
# 1. bump version + update CHANGELOG on main, commit, push main
|
|
401
|
+
# 2. tag the release and push the tag — CI publishes it
|
|
402
|
+
git tag v0.8.2
|
|
403
|
+
git push origin v0.8.2
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`.github/workflows/publish.yml` fires on a `v*.*.*` tag: it checks out, installs, verifies the tag equals `package.json`'s version, runs `release:verify`, and publishes with `npm publish --provenance --access public`. The tag/version guard prevents publishing a mismatched version.
|
|
407
|
+
|
|
408
|
+
One-time setup (owner, on npmjs.com): open the package → Settings → Trusted Publishing → add a GitHub Actions publisher for repo `youdie006/prodex` and workflow `publish.yml`. After that, no npm tokens are needed anywhere; revoke any previously issued automation tokens.
|
|
409
|
+
|
|
389
410
|
## Release Checks
|
|
390
411
|
|
|
391
|
-
GitHub Actions runs `npm ci`, `npm run release:check`, and `npm run release:verify` on pushes to `main` and pull requests. The workflow installs `ripgrep` because the repo-search smoke checks require `rg`. It verifies release readiness only; it does not publish anything.
|
|
412
|
+
GitHub Actions runs `npm ci`, `npm run build`, `npm run release:check`, and `npm run release:verify` on pushes to `main` and pull requests. The workflow installs `ripgrep` because the repo-search smoke checks require `rg`. It verifies release readiness only; it does not publish anything.
|
|
392
413
|
|
|
393
414
|
Before sharing a package tarball, run:
|
|
394
415
|
|
|
@@ -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,10 @@ 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
|
+
*/
|
|
209
|
+
export declare function chromeCommandCandidates(platform?: NodeJS.Platform, env?: Record<string, string | undefined>): string[];
|
|
172
210
|
export {};
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -60,6 +60,60 @@ 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
|
+
const required = TRAILING_CARET_SUSPECT.test(answer) ? 8 : 2;
|
|
96
|
+
return confirmations >= required;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the Chrome DevTools port: explicit flag > PRODEX_CDP_PORT env >
|
|
101
|
+
* default 9333. The env var exists so a non-default port picked at login does
|
|
102
|
+
* not have to be repeated as --port on every later command.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveCdpPort(explicit) {
|
|
105
|
+
if (explicit !== undefined)
|
|
106
|
+
return explicit;
|
|
107
|
+
const fromEnv = process.env.PRODEX_CDP_PORT;
|
|
108
|
+
if (fromEnv !== undefined && fromEnv !== "") {
|
|
109
|
+
const parsed = Number(fromEnv);
|
|
110
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65_535) {
|
|
111
|
+
throw new Error(`PRODEX_CDP_PORT must be an integer between 1 and 65535, got: ${fromEnv}`);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
return DEFAULT_CDP_PORT;
|
|
116
|
+
}
|
|
63
117
|
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
118
|
export const CHATGPT_COMPOSER_CANDIDATE_EXCLUDED_ANCESTORS = '[data-message-author-role],script,style,noscript,[aria-hidden="true"]';
|
|
65
119
|
export const PRODEX_ACTIVE_COMPOSER_ATTRIBUTE = "data-prodex-active-composer";
|
|
@@ -398,16 +452,36 @@ export function chatGptVisibilityBlocker(visibilityState, url) {
|
|
|
398
452
|
next_step: `Select ${url ?? "the ChatGPT tab"} in the dedicated browser, then retry.`
|
|
399
453
|
};
|
|
400
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Environment for the browser spawn. On Linux shells without DISPLAY or
|
|
457
|
+
* WAYLAND_DISPLAY (common in WSL where a non-login shell does not export
|
|
458
|
+
* them), a present WSLg X socket means a desktop is actually available -
|
|
459
|
+
* inject DISPLAY=:0 so Chrome does not exit immediately with code 1.
|
|
460
|
+
*/
|
|
461
|
+
export function browserLaunchEnv(platform = process.platform, env = process.env, hasX0Socket = () => {
|
|
462
|
+
try {
|
|
463
|
+
statSync("/tmp/.X11-unix/X0");
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}) {
|
|
470
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY && hasX0Socket()) {
|
|
471
|
+
return { ...env, DISPLAY: ":0" };
|
|
472
|
+
}
|
|
473
|
+
return env;
|
|
474
|
+
}
|
|
401
475
|
export function openChatGptBrowser(options = {}) {
|
|
402
476
|
const command = resolveChromeCommand();
|
|
403
|
-
const port = options.port
|
|
477
|
+
const port = resolveCdpPort(options.port);
|
|
404
478
|
const profileDir = options.profileDir ?? defaultChatGptProfileDir();
|
|
405
479
|
const args = buildChromeLaunchArgs({
|
|
406
480
|
port,
|
|
407
481
|
profileDir,
|
|
408
482
|
url: options.url ?? "https://chatgpt.com/"
|
|
409
483
|
});
|
|
410
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
484
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore", env: browserLaunchEnv() });
|
|
411
485
|
let earlyExit;
|
|
412
486
|
const earlyExitWaiters = new Set();
|
|
413
487
|
const recordEarlyExit = (exit) => {
|
|
@@ -444,7 +518,7 @@ export function openChatGptBrowser(options = {}) {
|
|
|
444
518
|
};
|
|
445
519
|
}
|
|
446
520
|
export async function getChatGptBrowserStatus(options = {}) {
|
|
447
|
-
const port = options.port
|
|
521
|
+
const port = resolveCdpPort(options.port);
|
|
448
522
|
const page = await findChatGptPage(port, options.timeoutMs ?? 1500);
|
|
449
523
|
if (!page.ok) {
|
|
450
524
|
return {
|
|
@@ -846,8 +920,20 @@ async function selectProject(cdp, options) {
|
|
|
846
920
|
}
|
|
847
921
|
}
|
|
848
922
|
export async function sendChatGptPrompt(options) {
|
|
849
|
-
const port = options.port
|
|
923
|
+
const port = resolveCdpPort(options.port);
|
|
850
924
|
const timeoutMs = options.timeoutMs ?? 90_000;
|
|
925
|
+
const sendStartedAt = Date.now();
|
|
926
|
+
const emitProgress = (phase, detail) => {
|
|
927
|
+
if (!options.onProgress)
|
|
928
|
+
return;
|
|
929
|
+
try {
|
|
930
|
+
options.onProgress({ phase, elapsedMs: Date.now() - sendStartedAt, ...(detail !== undefined ? { detail } : {}) });
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
// Progress reporting must never break the send itself.
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
emitProgress("connecting", `port ${port}`);
|
|
851
937
|
const normalizedTargetUrl = options.targetUrl ? normalizeChatGptTargetUrl(options.targetUrl) : undefined;
|
|
852
938
|
const pageResult = await findChatGptPage(port, computePageDiscoveryTimeout(timeoutMs), normalizedTargetUrl);
|
|
853
939
|
if (!pageResult.ok) {
|
|
@@ -877,6 +963,19 @@ export async function sendChatGptPrompt(options) {
|
|
|
877
963
|
if (busyBlocker) {
|
|
878
964
|
throw new ChatGptBrowserBlockerError(busyBlocker);
|
|
879
965
|
}
|
|
966
|
+
emitProgress("tab_ready");
|
|
967
|
+
// Progress details deliberately avoid project names (receipts redact them too).
|
|
968
|
+
const selectionSummary = [
|
|
969
|
+
options.model !== undefined ? `model=${options.model}` : undefined,
|
|
970
|
+
options.proMode !== undefined ? `pro_mode=${options.proMode}` : undefined,
|
|
971
|
+
options.effort !== undefined ? `effort=${options.effort}` : undefined,
|
|
972
|
+
options.project !== undefined ? "project=set" : undefined,
|
|
973
|
+
options.projectNew !== undefined ? "project_new=set" : undefined
|
|
974
|
+
]
|
|
975
|
+
.filter((part) => part !== undefined)
|
|
976
|
+
.join(" ");
|
|
977
|
+
if (selectionSummary)
|
|
978
|
+
emitProgress("selecting", selectionSummary);
|
|
880
979
|
let beforeSubmit;
|
|
881
980
|
let submitButtonFound = false;
|
|
882
981
|
const cdp = await connectCdp(page.webSocketDebuggerUrl);
|
|
@@ -905,6 +1004,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
905
1004
|
finally {
|
|
906
1005
|
cdp.close();
|
|
907
1006
|
}
|
|
1007
|
+
emitProgress("sent", `timeout ${Math.round(timeoutMs / 1000)}s`);
|
|
908
1008
|
const started = Date.now();
|
|
909
1009
|
const acceptDeadline = computePromptAcceptanceDeadline(timeoutMs, started);
|
|
910
1010
|
let accepted = false;
|
|
@@ -919,6 +1019,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
919
1019
|
accepted = true;
|
|
920
1020
|
break;
|
|
921
1021
|
}
|
|
1022
|
+
emitProgress("waiting", "prompt posting");
|
|
922
1023
|
}
|
|
923
1024
|
if (!accepted) {
|
|
924
1025
|
// A successful submit clears the composer, so text still sitting there means
|
|
@@ -933,34 +1034,26 @@ export async function sendChatGptPrompt(options) {
|
|
|
933
1034
|
}
|
|
934
1035
|
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound });
|
|
935
1036
|
}
|
|
936
|
-
|
|
937
|
-
let stableConfirmations = 0;
|
|
1037
|
+
const answerIsStable = createChatGptAnswerStabilityTracker();
|
|
938
1038
|
while (Date.now() - started < timeoutMs) {
|
|
939
1039
|
await sleep(1000);
|
|
940
1040
|
finalState = await evaluateOnPage(page, answerExpression());
|
|
941
1041
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
942
1042
|
if (runtimeBlocker)
|
|
943
1043
|
throw new ChatGptBrowserBlockerError(runtimeBlocker);
|
|
1044
|
+
emitProgress("waiting", finalState.generating ? "generating" : "stabilizing");
|
|
944
1045
|
if (!hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, finalState))
|
|
945
1046
|
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
|
-
}
|
|
1047
|
+
// A "fresh" answer must also be stable: ChatGPT can momentarily look done
|
|
1048
|
+
// mid-stream, and the streaming caret renders as a literal trailing
|
|
1049
|
+
// character that can outlive the stop button. The tracker requires extra
|
|
1050
|
+
// confirmations for caret-suspect tails (see its doc comment).
|
|
1051
|
+
if (answerIsStable(finalState.answer, finalState.generating))
|
|
1052
|
+
break;
|
|
961
1053
|
}
|
|
962
1054
|
const completed = finalState;
|
|
963
1055
|
if (completed && hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
|
|
1056
|
+
emitProgress("answered");
|
|
964
1057
|
return {
|
|
965
1058
|
url: completed.url,
|
|
966
1059
|
title: completed.title,
|
|
@@ -973,6 +1066,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
973
1066
|
// (a truncated-but-real Pro answer is far more useful than losing it) and
|
|
974
1067
|
// flag it so the caller records the incompleteness and the retry hint.
|
|
975
1068
|
if (completed && hasPartialChatGptAnswer(beforeSubmit.assistantMessageCount, completed)) {
|
|
1069
|
+
emitProgress("answered", "partial");
|
|
976
1070
|
return {
|
|
977
1071
|
url: completed.url,
|
|
978
1072
|
title: completed.title,
|
|
@@ -1002,7 +1096,7 @@ export function modelMenuOptionsExpression() {
|
|
|
1002
1096
|
// and press Escape. Nothing is clicked inside the menu, so the user's model
|
|
1003
1097
|
// selection is never changed.
|
|
1004
1098
|
export async function listChatGptModelOptions(input = {}) {
|
|
1005
|
-
const port = input.port
|
|
1099
|
+
const port = resolveCdpPort(input.port);
|
|
1006
1100
|
const timeoutMs = input.timeoutMs ?? 15_000;
|
|
1007
1101
|
const pageResult = await findChatGptPage(port, computePageDiscoveryTimeout(timeoutMs), undefined);
|
|
1008
1102
|
if (!pageResult.ok) {
|
|
@@ -1424,13 +1518,62 @@ function answerExpression() {
|
|
|
1424
1518
|
function enterKeyEvent(type) {
|
|
1425
1519
|
return { type, key: "Enter", code: "Enter", windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 };
|
|
1426
1520
|
}
|
|
1521
|
+
const CHROME_PATH_BINARY_NAMES = ["google-chrome", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"];
|
|
1522
|
+
const DARWIN_CHROME_PATHS = [
|
|
1523
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1524
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1525
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1526
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
1527
|
+
];
|
|
1528
|
+
function win32ChromePaths(env) {
|
|
1529
|
+
return [
|
|
1530
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
1531
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
1532
|
+
...(env.LOCALAPPDATA ? [`${env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`] : []),
|
|
1533
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
1534
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
1535
|
+
];
|
|
1536
|
+
}
|
|
1537
|
+
const WSL_WINDOWS_CHROME_PATHS = [
|
|
1538
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
1539
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
1540
|
+
"/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe",
|
|
1541
|
+
"/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
1542
|
+
];
|
|
1543
|
+
/**
|
|
1544
|
+
* Ordered browser candidates for the current platform: PATH binary names
|
|
1545
|
+
* first, then well-known absolute install locations (macOS app bundles,
|
|
1546
|
+
* Windows Program Files/LOCALAPPDATA, and Windows-host browsers under WSL).
|
|
1547
|
+
*/
|
|
1548
|
+
export function chromeCommandCandidates(platform = process.platform, env = process.env) {
|
|
1549
|
+
const candidates = [...CHROME_PATH_BINARY_NAMES];
|
|
1550
|
+
if (platform === "darwin")
|
|
1551
|
+
candidates.push(...DARWIN_CHROME_PATHS);
|
|
1552
|
+
if (platform === "win32")
|
|
1553
|
+
candidates.push(...win32ChromePaths(env));
|
|
1554
|
+
if (platform === "linux" && env.WSL_DISTRO_NAME)
|
|
1555
|
+
candidates.push(...WSL_WINDOWS_CHROME_PATHS);
|
|
1556
|
+
return candidates;
|
|
1557
|
+
}
|
|
1427
1558
|
function resolveChromeCommand() {
|
|
1428
1559
|
const fromEnv = process.env.PRODEX_CHROME;
|
|
1429
1560
|
if (fromEnv) {
|
|
1430
1561
|
assertChromeCommandAvailable(fromEnv, "PRODEX_CHROME");
|
|
1431
1562
|
return fromEnv;
|
|
1432
1563
|
}
|
|
1433
|
-
for (const command of
|
|
1564
|
+
for (const command of chromeCommandCandidates()) {
|
|
1565
|
+
if (isPathLikeCommand(command)) {
|
|
1566
|
+
try {
|
|
1567
|
+
if (!statSync(command).isFile())
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
catch {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
if (hasChromeLikeVersion(command))
|
|
1574
|
+
return command;
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1434
1577
|
if (isCommandOnPath(command) && hasChromeLikeVersion(command))
|
|
1435
1578
|
return command;
|
|
1436
1579
|
}
|