@youdie006/prodex 0.40.13 → 0.40.14
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 +6 -0
- package/dist/browser-handoff.js +27 -3
- package/dist/chatgpt-browser.js +53 -27
- package/dist/cli-pro.js +39 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,10 +73,16 @@ For a one-time visible login followed by a verified headless handoff, use `prode
|
|
|
73
73
|
|
|
74
74
|
Finish any native browser or OS confirmation before requesting the handoff: Chrome does not expose every native dialog through its page-control interface. The guard detects page targets and rendered dialogs, not every OS prompt. Incognito, guest, and explicitly selected Chrome sub-profiles are refused.
|
|
75
75
|
|
|
76
|
+
A Chrome account-connection chooser is separate from ChatGPT sign-in. If prodex confirms that chooser is displayed, choose for yourself whether to connect Chrome to the account; "Use Chrome without an account" declines that separate connection. A hidden or unreadable internal target still blocks the handoff but is not proof that a prompt is visible or that ChatGPT is logged out.
|
|
77
|
+
|
|
78
|
+
"Login readiness is not yet confirmed" does not mean the saved login was lost. Inspect the dedicated browser before signing in again. An explicit `login_required`, `cloudflare_check`, `captcha_required`, or `permission_required` during a headless/virtual-display wait stops promptly: use `prodex pro browser login --headed` to handle the reported step visibly. A usage limit or missing composer has its own next step instead of a blanket login instruction.
|
|
79
|
+
|
|
76
80
|
While Pro thinks, progress goes to stderr: connecting, prompt sent, elapsed time while generating. A Pro selection raises the send budget to twenty minutes on its own; `--timeout-ms` overrides it. Answers are read from the rendered page, so formatting can differ from the original message. If the dedicated browser is not running, an interactive `ask` starts it, waits for your saved session, and retries once (`--no-auto-login` turns that off; scripts opt in with `--auto-login`).
|
|
77
81
|
|
|
78
82
|
If the browser stops responding after your question was sent, prodex stops without sending it again. Use the `thread` and `request_id` from the error with `prodex pro browser recover --target-url <thread-url> --request-id <32hex>` (MCP: `pro_recover`). The request ID verifies that the recovered assistant answer follows that exact marked user turn. Legacy recovery without it remains available but returns `request_verified: false` and a warning.
|
|
79
83
|
|
|
84
|
+
If submission itself is unconfirmed, do not resend or simply increase the timeout: first inspect the original conversation for the `[prodex-request:...]` marker in the error. Missing login signals or text still in the composer do not prove that the question was never submitted.
|
|
85
|
+
|
|
80
86
|
Useful flags on every send:
|
|
81
87
|
|
|
82
88
|
| Flag | What it does |
|
package/dist/browser-handoff.js
CHANGED
|
@@ -6,7 +6,7 @@ import { ChatGptBrowserBlockerError, attachmentPresenceExpression, composerTextS
|
|
|
6
6
|
function blocked(message) {
|
|
7
7
|
throw new ChatGptBrowserBlockerError({
|
|
8
8
|
code: "browser_handoff_blocked", message, retryable: false,
|
|
9
|
-
next_step: "
|
|
9
|
+
next_step: "Inspect the dedicated browser and keep only the intended idle ChatGPT tab open, then retry `prodex pro browser login --background`. Complete a login or permission step only if it is actually shown. Do not close active work or log in again solely because the handoff stopped."
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
12
|
function browserIdentity(port, profileDir) {
|
|
@@ -62,8 +62,32 @@ async function singlePage(port) {
|
|
|
62
62
|
if (!Array.isArray(response))
|
|
63
63
|
blocked("The browser page list is invalid.");
|
|
64
64
|
const pages = response.filter((page) => page?.type === "page");
|
|
65
|
-
if (pages.length !== 1)
|
|
66
|
-
|
|
65
|
+
if (pages.length !== 1) {
|
|
66
|
+
const chooser = pages.find((page) => page.url === "chrome://signin-dice-web-intercept.top-chrome/chrome-signin");
|
|
67
|
+
if (chooser) {
|
|
68
|
+
let state;
|
|
69
|
+
try {
|
|
70
|
+
const socket = localSocket(chooser.webSocketDebuggerUrl, port, "page");
|
|
71
|
+
const reply = await request(socket, "Runtime.evaluate", {
|
|
72
|
+
expression: `(() => { const r = document.body?.getBoundingClientRect(); return { visibilityState: document.visibilityState, width: r?.width, height: r?.height, isChooser: !!document.querySelector('chrome-signin-app') }; })()`,
|
|
73
|
+
returnByValue: true
|
|
74
|
+
});
|
|
75
|
+
if (!reply.exceptionDetails)
|
|
76
|
+
state = reply.result?.value;
|
|
77
|
+
}
|
|
78
|
+
catch { /* An unreadable internal target is not proof of a visible prompt. */ }
|
|
79
|
+
if (state?.visibilityState === "visible" && state.isChooser === true &&
|
|
80
|
+
typeof state.width === "number" && state.width > 0 && typeof state.height === "number" && state.height > 0) {
|
|
81
|
+
throw new ChatGptBrowserBlockerError({
|
|
82
|
+
code: "browser_account_confirmation", retryable: false,
|
|
83
|
+
message: "Chrome is showing its browser-account connection chooser. This is separate from ChatGPT login; no browser was closed.",
|
|
84
|
+
next_step: "Choose in the dedicated Chrome window yourself. 'Use Chrome without an account' declines Chrome account connection if you do not want it. Keep the ChatGPT tab open, then retry `prodex pro browser login --background`."
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
blocked("Chrome has an internal account target, but its chooser visibility was not confirmed. This does not establish a ChatGPT login problem; nothing was closed.");
|
|
88
|
+
}
|
|
89
|
+
blocked("The dedicated browser has additional page targets; nothing was closed.");
|
|
90
|
+
}
|
|
67
91
|
const page = pages[0];
|
|
68
92
|
const url = new URL(page.url);
|
|
69
93
|
if (url.protocol !== "https:" || url.hostname !== "chatgpt.com" || url.username || url.password || url.port ||
|
package/dist/chatgpt-browser.js
CHANGED
|
@@ -702,6 +702,7 @@ export function formatDurationMs(ms) {
|
|
|
702
702
|
export function acceptanceTimeoutError(ctx) {
|
|
703
703
|
const uiLikelyChanged = ctx.composerStillHasText || !ctx.submitButtonFound;
|
|
704
704
|
const took = `${formatDurationMs(ctx.timeoutMs)} (${ctx.timeoutMs}ms)`;
|
|
705
|
+
const inspect = `Do not resend automatically: submission was attempted but acceptance is unconfirmed. Inspect the original conversation${ctx.requestId ? ` for [prodex-request:${ctx.requestId}]` : ""} before another request.`;
|
|
705
706
|
if (uiLikelyChanged) {
|
|
706
707
|
const detail = [
|
|
707
708
|
ctx.composerStillHasText ? "the composer still holds the prompt" : undefined,
|
|
@@ -709,13 +710,16 @@ export function acceptanceTimeoutError(ctx) {
|
|
|
709
710
|
]
|
|
710
711
|
.filter(Boolean)
|
|
711
712
|
.join(" and ");
|
|
712
|
-
return new
|
|
713
|
-
"
|
|
714
|
-
|
|
715
|
-
|
|
713
|
+
return new ChatGptBrowserBlockerError({
|
|
714
|
+
code: "send_ui_changed", retryable: false,
|
|
715
|
+
message: `Timed out after ${took} without confirming prompt acceptance (${detail}). The ChatGPT web UI may have changed.`,
|
|
716
|
+
next_step: `${inspect} Update prodex (npm i -g @youdie006/prodex@latest); if it persists, report it at https://github.com/youdie006/prodex/issues.`
|
|
717
|
+
});
|
|
716
718
|
}
|
|
717
|
-
return new
|
|
718
|
-
"
|
|
719
|
+
return new ChatGptBrowserBlockerError({
|
|
720
|
+
code: "prompt_acceptance_unconfirmed", retryable: false,
|
|
721
|
+
message: `Timed out after ${took} waiting for ChatGPT to accept the prompt.`, next_step: inspect
|
|
722
|
+
});
|
|
719
723
|
}
|
|
720
724
|
export function hasPartialChatGptAnswer(previousAssistantMessageCount, state) {
|
|
721
725
|
return state.assistantMessageCount > previousAssistantMessageCount && isUsableChatGptAnswer(state.answer);
|
|
@@ -948,7 +952,7 @@ export function detectChatGptBlocker(text, visibleButtonLabels = []) {
|
|
|
948
952
|
code: "cloudflare_check",
|
|
949
953
|
message: "ChatGPT is showing a Cloudflare or human-verification interstitial.",
|
|
950
954
|
retryable: true,
|
|
951
|
-
next_step: "Complete the visible browser check manually
|
|
955
|
+
next_step: "Complete the visible browser check manually."
|
|
952
956
|
};
|
|
953
957
|
}
|
|
954
958
|
if (hasLikelyChatGptLoginPrompt(haystack)) {
|
|
@@ -956,7 +960,7 @@ export function detectChatGptBlocker(text, visibleButtonLabels = []) {
|
|
|
956
960
|
code: "login_required",
|
|
957
961
|
message: "ChatGPT is asking you to log in.",
|
|
958
962
|
retryable: true,
|
|
959
|
-
next_step: "Log in manually in the visible browser
|
|
963
|
+
next_step: "Log in manually in the visible browser."
|
|
960
964
|
};
|
|
961
965
|
}
|
|
962
966
|
// Match real captcha / human-verification phrasing only. Bare words like "robot"/"로봇"/"자동화"
|
|
@@ -967,7 +971,7 @@ export function detectChatGptBlocker(text, visibleButtonLabels = []) {
|
|
|
967
971
|
code: "captcha_required",
|
|
968
972
|
message: "ChatGPT is asking for captcha or human verification.",
|
|
969
973
|
retryable: true,
|
|
970
|
-
next_step: "Solve it manually in the visible browser
|
|
974
|
+
next_step: "Solve it manually in the visible browser."
|
|
971
975
|
};
|
|
972
976
|
}
|
|
973
977
|
if (/message limit|usage limit|model limit|rate limit|you.?ve reached|try again later|limit resets|사용 한도|메시지 한도|모델 한도|요금 제한|나중에 다시/i.test(haystack)) {
|
|
@@ -983,7 +987,7 @@ export function detectChatGptBlocker(text, visibleButtonLabels = []) {
|
|
|
983
987
|
code: "permission_required",
|
|
984
988
|
message: "ChatGPT requires account verification or permission handling.",
|
|
985
989
|
retryable: true,
|
|
986
|
-
next_step: "Complete the visible account or permission prompt manually
|
|
990
|
+
next_step: "Complete the visible account or permission prompt manually."
|
|
987
991
|
};
|
|
988
992
|
}
|
|
989
993
|
return undefined;
|
|
@@ -992,7 +996,15 @@ export function detectChatGptPageBlocker(state) {
|
|
|
992
996
|
// Blocker scan uses the nav-excluded sample so a sidebar chat title cannot
|
|
993
997
|
// fake a blocker; fall back to the nav-included sample / full text when the
|
|
994
998
|
// scan sample is absent (older callers).
|
|
995
|
-
|
|
999
|
+
const rendered = detectChatGptBlocker(state.blockerScanTextSample ?? state.blockerTextSample ?? state.textSample, state.visibleButtonLabels);
|
|
1000
|
+
if (rendered)
|
|
1001
|
+
return rendered;
|
|
1002
|
+
// The interstitial can have an empty body. Never use a conversation title
|
|
1003
|
+
// alone when the composer exists or its state was not actually checked.
|
|
1004
|
+
if (state.hasComposer === false && /^just a moment\.{0,3}$/i.test(state.title?.trim() ?? "")) {
|
|
1005
|
+
return detectChatGptBlocker("Just a moment", []);
|
|
1006
|
+
}
|
|
1007
|
+
return undefined;
|
|
996
1008
|
}
|
|
997
1009
|
export function inferChatGptPageLoggedInLikely(state) {
|
|
998
1010
|
// The logged-in signals live in the sidebar - "New chat", "Projects", the
|
|
@@ -1026,6 +1038,17 @@ export function chatGptBlockerErrorFromAnswerState(state) {
|
|
|
1026
1038
|
export function chatGptBlockerFromAnswerState(state) {
|
|
1027
1039
|
return detectChatGptPageBlocker(state);
|
|
1028
1040
|
}
|
|
1041
|
+
function submittedRequestBlocker(blocker, requestId, thread) {
|
|
1042
|
+
return {
|
|
1043
|
+
...blocker,
|
|
1044
|
+
retryable: false,
|
|
1045
|
+
...(thread ? { thread } : {}),
|
|
1046
|
+
next_step: `${blocker.next_step ?? "Inspect the visible browser."} Do not automatically resend. ` +
|
|
1047
|
+
(thread
|
|
1048
|
+
? `Collect the original answer with \`prodex pro browser recover --target-url ${thread} --request-id ${requestId}\` after resolving the blocker.`
|
|
1049
|
+
: `Submission is unconfirmed; inspect the original conversation for [prodex-request:${requestId}] before another request.`)
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1029
1052
|
export function computePromptAcceptanceDeadline(timeoutMs, startedAt) {
|
|
1030
1053
|
return startedAt + Math.max(1, timeoutMs);
|
|
1031
1054
|
}
|
|
@@ -3508,6 +3531,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3508
3531
|
let beforeSubmit;
|
|
3509
3532
|
let boundProjectId;
|
|
3510
3533
|
let submitButtonFound = false;
|
|
3534
|
+
let submissionAttempted = false;
|
|
3511
3535
|
const sendWarnings = [...preflightWarnings];
|
|
3512
3536
|
// Anything the page put in front of prodex was answered on the caller's
|
|
3513
3537
|
// behalf. The note is read at return time - there are several return paths -
|
|
@@ -3654,6 +3678,8 @@ export async function sendChatGptPrompt(options) {
|
|
|
3654
3678
|
const last = [...document.querySelectorAll('[data-message-author-role="user"]')].at(-1);
|
|
3655
3679
|
return Boolean(last && (last.innerText || "").includes(${JSON.stringify(`[prodex-request:${requestId}]`)}));
|
|
3656
3680
|
})()`;
|
|
3681
|
+
// A dispatch can reach Chrome even if its acknowledgement is lost.
|
|
3682
|
+
submissionAttempted = true;
|
|
3657
3683
|
await cdp.send("Input.dispatchKeyEvent", enterKeyEvent("keyDown"));
|
|
3658
3684
|
await cdp.send("Input.dispatchKeyEvent", enterKeyEvent("keyUp"));
|
|
3659
3685
|
let promptPosted = await waitForExpressionTrue(cdp, promptPostedExpression, 1_500);
|
|
@@ -3677,6 +3703,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
3677
3703
|
await captureOnFailure(`send-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
3678
3704
|
if (isCrashedTabError(error))
|
|
3679
3705
|
throw attachSendWarnings(new ChatGptBrowserBlockerError(crashedTabBlocker(undefined, requestId)), sendWarnings);
|
|
3706
|
+
if (submissionAttempted) {
|
|
3707
|
+
const blocker = error instanceof ChatGptBrowserBlockerError ? error.blocker : {
|
|
3708
|
+
code: "prompt_acceptance_unconfirmed", retryable: false,
|
|
3709
|
+
message: `Browser control failed after submission was attempted: ${error instanceof Error ? error.message : String(error)}`
|
|
3710
|
+
};
|
|
3711
|
+
throw attachSendWarnings(new ChatGptBrowserBlockerError(submittedRequestBlocker(blocker, requestId)), sendWarnings);
|
|
3712
|
+
}
|
|
3680
3713
|
throw attachSendWarnings(error, sendWarnings);
|
|
3681
3714
|
}
|
|
3682
3715
|
finally {
|
|
@@ -3701,7 +3734,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3701
3734
|
}
|
|
3702
3735
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
3703
3736
|
if (runtimeBlocker)
|
|
3704
|
-
throw new ChatGptBrowserBlockerError(runtimeBlocker);
|
|
3737
|
+
throw new ChatGptBrowserBlockerError(submittedRequestBlocker(runtimeBlocker, requestId));
|
|
3705
3738
|
dbgSend(`accept-poll url=${finalState.url} user=${finalState.userMessageCount} assistant=${finalState.assistantMessageCount} generating=${finalState.generating}`);
|
|
3706
3739
|
if (requestMatches(finalState)) {
|
|
3707
3740
|
if (normalizedTargetUrl)
|
|
@@ -3714,19 +3747,13 @@ export async function sendChatGptPrompt(options) {
|
|
|
3714
3747
|
emitProgress("waiting", "prompt posting");
|
|
3715
3748
|
}
|
|
3716
3749
|
if (!accepted) {
|
|
3717
|
-
//
|
|
3718
|
-
//
|
|
3719
|
-
// login state first and, if the session is gone, say so clearly so the user
|
|
3720
|
-
// re-logs in instead of chasing a timeout.
|
|
3750
|
+
// Missing logged-in signals do not prove session expiry. Only rendered
|
|
3751
|
+
// blockers justify an authentication diagnosis after uncertain submission.
|
|
3721
3752
|
try {
|
|
3722
3753
|
const status = await evaluateOnPage(page, statusExpression());
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
message: "The ChatGPT session is no longer logged in - it likely expired during the send.",
|
|
3727
|
-
retryable: true,
|
|
3728
|
-
next_step: "Run `prodex pro browser login`, log in, then retry."
|
|
3729
|
-
});
|
|
3754
|
+
const blocker = detectChatGptPageBlocker(status);
|
|
3755
|
+
if (blocker) {
|
|
3756
|
+
throw new ChatGptBrowserBlockerError(submittedRequestBlocker(blocker, requestId));
|
|
3730
3757
|
}
|
|
3731
3758
|
}
|
|
3732
3759
|
catch (error) {
|
|
@@ -3734,8 +3761,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3734
3761
|
throw error;
|
|
3735
3762
|
// best effort: a CDP eval failure here falls through to the generic timeout
|
|
3736
3763
|
}
|
|
3737
|
-
//
|
|
3738
|
-
// the send control did not register the prompt — the UI-changed signature.
|
|
3764
|
+
// Composer text is only a UI hint, not proof that submission failed.
|
|
3739
3765
|
let composerStillHasText = false;
|
|
3740
3766
|
try {
|
|
3741
3767
|
const composerState = await evaluateOnPage(page, composerTextStateExpression());
|
|
@@ -3744,7 +3770,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3744
3770
|
catch {
|
|
3745
3771
|
// best effort: fall back to submit-button signal only
|
|
3746
3772
|
}
|
|
3747
|
-
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound });
|
|
3773
|
+
throw acceptanceTimeoutError({ timeoutMs, composerStillHasText, submitButtonFound, requestId });
|
|
3748
3774
|
}
|
|
3749
3775
|
// Pin the conversation the prompt actually landed in. The browser is shared
|
|
3750
3776
|
// (other agents, the user, tooling), and a tab that moves mid-wait made
|
|
@@ -3807,7 +3833,7 @@ export async function sendChatGptPrompt(options) {
|
|
|
3807
3833
|
}
|
|
3808
3834
|
const runtimeBlocker = chatGptBlockerFromAnswerState(finalState);
|
|
3809
3835
|
if (runtimeBlocker)
|
|
3810
|
-
throw new ChatGptBrowserBlockerError(runtimeBlocker);
|
|
3836
|
+
throw new ChatGptBrowserBlockerError(submittedRequestBlocker(runtimeBlocker, requestId, pinnedThreadUrl));
|
|
3811
3837
|
emitProgress("waiting", finalState.generating ? "generating" : "stabilizing");
|
|
3812
3838
|
if (!hasFreshChatGptAnswer(beforeSubmit.assistantMessageCount, finalState))
|
|
3813
3839
|
continue;
|
package/dist/cli-pro.js
CHANGED
|
@@ -2234,15 +2234,16 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
2234
2234
|
const pollMs = options.pollMs ?? 2_000;
|
|
2235
2235
|
const hasInteractiveWindow = options.windowMode?.headless !== true && options.windowMode?.virtualDisplay !== true;
|
|
2236
2236
|
const headedLoginCommand = options.headedLoginCommand ?? "prodex pro browser login --headed";
|
|
2237
|
-
const headedLoginHint = ` No interactive window is available; run \`${headedLoginCommand}\` to complete login, captcha, or human verification visibly.`;
|
|
2238
2237
|
const startedAt = now();
|
|
2239
2238
|
stderr(hasInteractiveWindow
|
|
2240
|
-
? "login: waiting for
|
|
2241
|
-
: `login: waiting for
|
|
2239
|
+
? "login: waiting for ChatGPT readiness in the dedicated Chrome browser (complete any visible step it requests; Ctrl+C stops waiting)..."
|
|
2240
|
+
: `login: waiting for ChatGPT readiness (no interactive window; use \`${headedLoginCommand}\` to inspect any requested browser step visibly; Ctrl+C stops waiting)...`);
|
|
2242
2241
|
let lastState = "";
|
|
2242
|
+
let lastStatus;
|
|
2243
2243
|
let openMissingTabAttempts = 0;
|
|
2244
2244
|
while (now() - startedAt < timeoutMs) {
|
|
2245
2245
|
const status = await statusFn({ port: options.port, timeoutMs: 1_500 });
|
|
2246
|
+
lastStatus = status;
|
|
2246
2247
|
// A running Chrome with no chatgpt.com tab leaves the user nothing to log
|
|
2247
2248
|
// into, so prodex opens one. Keep trying while the tab is still missing:
|
|
2248
2249
|
// one silent attempt that fails looks exactly like no attempt, which is how
|
|
@@ -2263,18 +2264,27 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
2263
2264
|
await sleepFn(pollMs);
|
|
2264
2265
|
continue;
|
|
2265
2266
|
}
|
|
2267
|
+
const blocker = status.blocker;
|
|
2268
|
+
const blockerNextStep = blocker?.next_step ? ` Next: ${blocker.next_step}` : "";
|
|
2269
|
+
const needsVisibleAuth = blocker !== undefined &&
|
|
2270
|
+
["login_required", "cloudflare_check", "captcha_required", "permission_required"].includes(blocker.code);
|
|
2271
|
+
if (!hasInteractiveWindow && needsVisibleAuth) {
|
|
2272
|
+
stderr(`login: blocked - ${blocker.message}${blockerNextStep}`);
|
|
2273
|
+
stderr(`login: NOT READY - ${blocker.code} requires visible manual handling. No interactive window is available; run \`${headedLoginCommand}\` to handle it visibly.`);
|
|
2274
|
+
return false;
|
|
2275
|
+
}
|
|
2266
2276
|
const state = !status.reachable
|
|
2267
2277
|
? "login: browser starting..."
|
|
2268
2278
|
: status.blocker
|
|
2269
|
-
? `login: blocked - ${status.blocker.message}${
|
|
2279
|
+
? `login: blocked - ${status.blocker.message}${blockerNextStep}`
|
|
2270
2280
|
: !status.loggedInLikely
|
|
2271
2281
|
? hasInteractiveWindow
|
|
2272
|
-
? "login:
|
|
2273
|
-
: `login:
|
|
2282
|
+
? "login: ChatGPT is reachable, but login readiness is not yet confirmed; review any visible browser prompt..."
|
|
2283
|
+
: `login: ChatGPT is reachable, but login readiness is not yet confirmed; inspect it visibly with \`${headedLoginCommand}\` if needed.`
|
|
2274
2284
|
: !status.hasComposer
|
|
2275
2285
|
? hasInteractiveWindow
|
|
2276
|
-
? "login:
|
|
2277
|
-
: `login:
|
|
2286
|
+
? "login: login looks active; open a chat so the prompt composer is visible..."
|
|
2287
|
+
: `login: login looks active, but no prompt composer is ready; inspect it visibly with \`${headedLoginCommand}\`.`
|
|
2278
2288
|
: "";
|
|
2279
2289
|
if (state === "") {
|
|
2280
2290
|
stderr(`login: READY - logged-in ChatGPT tab with composer detected (${Math.round((now() - startedAt) / 1000)}s).`);
|
|
@@ -2289,9 +2299,26 @@ export async function waitForChatGptLoginReady(stderr, options, deps = {}) {
|
|
|
2289
2299
|
break;
|
|
2290
2300
|
await sleepFn(Math.min(pollMs, Math.max(1, remainingMs)));
|
|
2291
2301
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2302
|
+
const timeoutPrefix = `login: not ready after ${Math.round(timeoutMs / 1000)}s`;
|
|
2303
|
+
if (lastStatus?.blocker) {
|
|
2304
|
+
const nextStep = lastStatus.blocker.next_step ? ` Next: ${lastStatus.blocker.next_step}` : "";
|
|
2305
|
+
stderr(`${timeoutPrefix}; last blocker ${lastStatus.blocker.code}: ${lastStatus.blocker.message}${nextStep}`);
|
|
2306
|
+
}
|
|
2307
|
+
else if (!lastStatus?.reachable) {
|
|
2308
|
+
stderr(hasInteractiveWindow
|
|
2309
|
+
? `${timeoutPrefix}; browser startup was not confirmed. Check the dedicated Chrome browser, then verify with \`prodex pro browser check\`.`
|
|
2310
|
+
: `${timeoutPrefix}; browser startup was not confirmed. Inspect it visibly with \`${headedLoginCommand}\`, then retry.`);
|
|
2311
|
+
}
|
|
2312
|
+
else if (lastStatus.loggedInLikely && !lastStatus.hasComposer) {
|
|
2313
|
+
stderr(hasInteractiveWindow
|
|
2314
|
+
? `${timeoutPrefix}; login looked active, but the prompt composer was not detected. Open a normal ChatGPT chat or Project thread, then verify with \`prodex pro browser check\`.`
|
|
2315
|
+
: `${timeoutPrefix}; login looked active, but the prompt composer was not detected. Inspect it visibly with \`${headedLoginCommand}\`, then retry.`);
|
|
2316
|
+
}
|
|
2317
|
+
else {
|
|
2318
|
+
stderr(hasInteractiveWindow
|
|
2319
|
+
? `${timeoutPrefix}; ChatGPT was reachable, but login readiness was not yet confirmed. Review the dedicated Chrome browser, then verify with \`prodex pro browser check\`.`
|
|
2320
|
+
: `${timeoutPrefix}; ChatGPT was reachable, but login readiness was not yet confirmed. Inspect it visibly with \`${headedLoginCommand}\`; complete a manual step only if requested.`);
|
|
2321
|
+
}
|
|
2295
2322
|
return false;
|
|
2296
2323
|
}
|
|
2297
2324
|
export async function assertBrowserLaunchStayedAlive(opened, timeoutMs) {
|
|
@@ -3150,7 +3177,7 @@ export function assertNoOrphanConsultResults(tasksById, results) {
|
|
|
3150
3177
|
}
|
|
3151
3178
|
export function browserReadinessNextStep(input) {
|
|
3152
3179
|
if (!input.loggedInLikely) {
|
|
3153
|
-
return "
|
|
3180
|
+
return "ChatGPT login readiness is not yet confirmed. Review the visible ChatGPT browser state, then retry.";
|
|
3154
3181
|
}
|
|
3155
3182
|
if (!input.hasComposer) {
|
|
3156
3183
|
return "Open a normal ChatGPT chat or Project thread, select the Pro/Thinking model, and retry.";
|