@wlv-zedd/dsh-chatgpt-web 1.0.0 → 1.0.2

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.
Files changed (81) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +140 -140
  3. package/cordis.patch.yml +4 -4
  4. package/lib/cli.js +13269 -11860
  5. package/lib/plugin.js +3 -50
  6. package/package.json +89 -88
  7. package/src/adapters/base.ts +16 -16
  8. package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
  9. package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
  10. package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
  11. package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
  12. package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
  13. package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
  14. package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
  15. package/src/adapters/chatgpt-web/concurrency.ts +6 -6
  16. package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
  17. package/src/adapters/chatgpt-web/environment.ts +669 -669
  18. package/src/adapters/chatgpt-web/index.ts +1544 -1544
  19. package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
  20. package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
  21. package/src/adapters/chatgpt-web/markdown.ts +418 -418
  22. package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
  23. package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
  24. package/src/adapters/chatgpt-web/model.ts +70 -70
  25. package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
  26. package/src/adapters/chatgpt-web/output-validation.ts +62 -62
  27. package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
  28. package/src/adapters/chatgpt-web/prompt.ts +702 -702
  29. package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
  30. package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
  31. package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
  32. package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
  33. package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
  34. package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
  35. package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
  36. package/src/adapters/chatgpt-web/usage.ts +121 -121
  37. package/src/adapters/image.ts +9 -9
  38. package/src/bridge.ts +1083 -1083
  39. package/src/browser-login.ts +521 -521
  40. package/src/chatgpt-session.ts +240 -240
  41. package/src/chatgpt-web-models.ts +400 -400
  42. package/src/cli.ts +568 -568
  43. package/src/codex-integration-document.ts +824 -824
  44. package/src/codex-integration-journal.ts +212 -212
  45. package/src/codex-integration-route.ts +515 -515
  46. package/src/codex-integration-shared.ts +332 -332
  47. package/src/codex-integration.ts +529 -529
  48. package/src/codex-interrupt-hook.ts +158 -158
  49. package/src/config.ts +616 -616
  50. package/src/dev-chat/cli.ts +432 -432
  51. package/src/dev-chat/constants.ts +3 -3
  52. package/src/dev-chat/driver.ts +655 -655
  53. package/src/dev-chat/profile.ts +223 -223
  54. package/src/dev-chat/session.ts +287 -287
  55. package/src/dev-chat/transport.ts +54 -54
  56. package/src/doctor.ts +237 -237
  57. package/src/event-queue.ts +45 -45
  58. package/src/http-body.ts +30 -30
  59. package/src/launcher-browser-host.ts +695 -695
  60. package/src/lib/errors.ts +281 -281
  61. package/src/lib/token-estimate.ts +42 -42
  62. package/src/login-helper.cjs +1 -1
  63. package/src/model-catalog.ts +197 -197
  64. package/src/native-passthrough.ts +261 -261
  65. package/src/plugin.ts +191 -191
  66. package/src/process.ts +45 -45
  67. package/src/responses/compaction.ts +199 -199
  68. package/src/responses/parser.ts +633 -633
  69. package/src/responses/reasoning-envelope.ts +49 -49
  70. package/src/responses/schema.ts +172 -172
  71. package/src/responses/state.ts +230 -230
  72. package/src/server.ts +1111 -1111
  73. package/src/service.ts +315 -315
  74. package/src/setup.ts +671 -671
  75. package/src/stall-timeout.ts +23 -23
  76. package/src/tunnel-service.ts +160 -160
  77. package/src/tunnel.ts +417 -417
  78. package/src/turndown-plugin-gfm.d.ts +5 -5
  79. package/src/types.ts +307 -307
  80. package/src/usage/totals.ts +12 -12
  81. package/src/version.ts +1 -1
@@ -1,240 +1,240 @@
1
- import type { Locator, Page } from "playwright-core";
2
- import type { ChatGptWebAccountCapabilities } from "./chatgpt-web-models";
3
-
4
- export const CHATGPT_TEMPORARY_CHAT_URL = "https://chatgpt.com/?temporary-chat=true";
5
- export const CHATGPT_COMPOSER_SELECTOR = [
6
- '[data-testid="prompt-textarea"]',
7
- "#prompt-textarea",
8
- '[contenteditable="true"][data-lexical-editor="true"]',
9
- ].join(", ");
10
- export const CHATGPT_EFFORT_CONTROL_SELECTOR = [
11
- 'button[aria-haspopup="menu"][data-tone="neutral"]',
12
- 'button[data-testid="model-switcher-dropdown-button"][aria-haspopup="menu"]',
13
- ].join(", ");
14
- export const CHATGPT_EFFORT_MENU_SELECTOR = [
15
- '[data-testid="composer-intelligence-picker-content"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
16
- '[role="menu"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
17
- '[role="group"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
18
- ].join(", ");
19
- export const CHATGPT_EFFORT_ITEM_SELECTOR = '[role="menuitemradio"]';
20
- export const CHATGPT_EFFORT_SLIDER_SELECTOR = '[data-model-reasoning-effort-slider] [role="slider"]';
21
- export const CHATGPT_EFFORT_SLIDER_MAX_OPTIONS = 5;
22
- export const CHATGPT_STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
23
- export const CHATGPT_COMPLETION_ACTION_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
24
- export const CHATGPT_ASSISTANT_TURN_SELECTOR = [
25
- '[data-testid^="conversation-turn-"][data-turn="assistant"]',
26
- '[data-testid^="conversation-turn-"][data-message-author-role="assistant"]',
27
- '[data-testid^="conversation-turn-"]:has([data-message-author-role="assistant"])',
28
- ].join(", ");
29
- export const CHATGPT_USER_TURN_SELECTOR = [
30
- '[data-testid^="conversation-turn-"][data-turn="user"]',
31
- '[data-testid^="conversation-turn-"][data-message-author-role="user"]',
32
- '[data-testid^="conversation-turn-"]:has([data-message-author-role="user"])',
33
- ].join(", ");
34
-
35
- export interface ChatGptEffortSliderState {
36
- min: number;
37
- max: number;
38
- value: number;
39
- }
40
-
41
- export interface ChatGptEffortActivation {
42
- method: "already-open" | "click" | "pointerdown";
43
- menu: Locator;
44
- slider: Locator;
45
- }
46
-
47
- function effortMenuSelectorForId(menuId: string): string {
48
- return `[id=${JSON.stringify(menuId)}]`;
49
- }
50
-
51
- export async function chatGptEffortMenuForControl(page: Page, control: Locator): Promise<Locator> {
52
- const menuId = await control.getAttribute("aria-controls").catch(() => null);
53
- if (menuId) return page.locator(effortMenuSelectorForId(menuId));
54
- return page.locator(CHATGPT_EFFORT_MENU_SELECTOR).filter({ visible: true }).last();
55
- }
56
-
57
- async function visibleEffortSurface(
58
- page: Page,
59
- control: Locator,
60
- ): Promise<{ menu: Locator; slider: Locator } | undefined> {
61
- const menu = await chatGptEffortMenuForControl(page, control);
62
- const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
63
- if (await menu.isVisible().catch(() => false) || await slider.isVisible().catch(() => false)) {
64
- return { menu, slider };
65
- }
66
- return undefined;
67
- }
68
-
69
- async function waitForEffortSurface(
70
- page: Page,
71
- control: Locator,
72
- timeoutMs: number,
73
- ): Promise<{ menu: Locator; slider: Locator } | undefined> {
74
- const deadline = Date.now() + timeoutMs;
75
- do {
76
- const surface = await visibleEffortSurface(page, control);
77
- if (surface) return surface;
78
- if (Date.now() >= deadline) return undefined;
79
- await new Promise(resolveSleep => setTimeout(resolveSleep, 50));
80
- } while (true);
81
- }
82
-
83
- async function clearGhostEffortState(page: Page, control: Locator): Promise<void> {
84
- const expanded = await control.getAttribute("aria-expanded").catch(() => null);
85
- const state = await control.getAttribute("data-state").catch(() => null);
86
- if (expanded === "true" || state === "open") {
87
- await page.keyboard.press("Escape").catch(() => {});
88
- }
89
- }
90
-
91
- export async function activateChatGptEffortMenu(
92
- page: Page,
93
- control: Locator,
94
- options: { settleMs?: number } = {},
95
- ): Promise<ChatGptEffortActivation> {
96
- const openSurface = await visibleEffortSurface(page, control);
97
- if (openSurface) return { method: "already-open", ...openSurface };
98
-
99
- const settleMs = options.settleMs ?? 3_000;
100
- await clearGhostEffortState(page, control);
101
- await control.click({ force: true, timeout: Math.max(1, settleMs) });
102
- const clickedSurface = await waitForEffortSurface(page, control, settleMs);
103
- if (clickedSurface) return { method: "click", ...clickedSurface };
104
-
105
- await clearGhostEffortState(page, control);
106
- await control.dispatchEvent("pointerdown", {
107
- button: 0,
108
- buttons: 1,
109
- pointerType: "mouse",
110
- isPrimary: true,
111
- });
112
- const pointerSurface = await waitForEffortSurface(page, control, settleMs);
113
- if (pointerSurface) return { method: "pointerdown", ...pointerSurface };
114
- throw new Error(
115
- "ChatGPT effort control did not expose its owned menu or structural slider after click and primary pointerdown",
116
- );
117
- }
118
-
119
- function safeIntegerAttribute(value: string | null): number | undefined {
120
- if (value === null || !/^-?\d+$/.test(value)) return undefined;
121
- const parsed = Number(value);
122
- return Number.isSafeInteger(parsed) ? parsed : undefined;
123
- }
124
-
125
- export function parseChatGptEffortSliderState(
126
- rawMin: string | null,
127
- rawMax: string | null,
128
- rawValue: string | null,
129
- ): ChatGptEffortSliderState | undefined {
130
- const min = safeIntegerAttribute(rawMin);
131
- const max = safeIntegerAttribute(rawMax);
132
- const value = safeIntegerAttribute(rawValue);
133
- if (min === undefined || max === undefined || value === undefined) return undefined;
134
- const optionCount = max - min + 1;
135
- if (optionCount < 1 || optionCount > CHATGPT_EFFORT_SLIDER_MAX_OPTIONS) return undefined;
136
- if (value < min || value > max) return undefined;
137
- return { min, max, value };
138
- }
139
-
140
- async function anyVisible(locator: Locator): Promise<boolean> {
141
- const count = await locator.count();
142
- for (let index = 0; index < count; index += 1) {
143
- if (await locator.nth(index).isVisible().catch(() => false)) return true;
144
- }
145
- return false;
146
- }
147
-
148
- export async function assertAuthenticatedChatGptPage(page: Page): Promise<void> {
149
- const composer = page.locator(
150
- CHATGPT_COMPOSER_SELECTOR,
151
- );
152
- if (!await anyVisible(composer)) {
153
- throw new Error("ChatGPT authentication could not be verified: no visible composer is present");
154
- }
155
- }
156
-
157
- export async function assertTemporaryChatPage(page: Page): Promise<void> {
158
- const url = new URL(page.url());
159
- const expected = new URL(CHATGPT_TEMPORARY_CHAT_URL);
160
- if (url.origin !== expected.origin || url.pathname !== expected.pathname || url.searchParams.get("temporary-chat") !== "true") {
161
- throw new Error(`ChatGPT left the isolated Temporary Chat surface (${page.url()})`);
162
- }
163
- }
164
-
165
- export async function detectChatGptAccountCapabilities(
166
- page: Page,
167
- options: { selectorTimeoutMs?: number; stableAbsenceMs?: number } = {},
168
- ): Promise<ChatGptWebAccountCapabilities> {
169
- const composers = page.locator(CHATGPT_COMPOSER_SELECTOR).filter({ visible: true });
170
- const composer = composers.last();
171
- const composerForm = composer.locator("xpath=ancestor::form[1]");
172
- const effortButton = composerForm.locator(CHATGPT_EFFORT_CONTROL_SELECTOR).last();
173
- const deadline = Date.now() + (options.selectorTimeoutMs ?? 30_000);
174
- const stableAbsenceMs = options.stableAbsenceMs ?? 3_000;
175
- let absenceSince: number | undefined;
176
- let presenceObservations = 0;
177
- while (true) {
178
- const effortVisible = await effortButton.isVisible().catch(() => false);
179
- if (effortVisible) {
180
- presenceObservations += 1;
181
- absenceSince = undefined;
182
- if (presenceObservations >= 2) break;
183
- await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
184
- continue;
185
- }
186
- presenceObservations = 0;
187
- const composerReady = await composers.count().then(count => count === 1).catch(() => false);
188
- const formReady = await composerForm.count().then(count => count === 1).catch(() => false);
189
- const documentReady = await page.evaluate(() => document.readyState === "complete").catch(() => false);
190
- if (composerReady && formReady && documentReady) {
191
- absenceSince ??= Date.now();
192
- if (Date.now() - absenceSince >= stableAbsenceMs) {
193
- return { solAvailable: false, proAvailable: false };
194
- }
195
- } else {
196
- absenceSince = undefined;
197
- }
198
- if (Date.now() >= deadline) {
199
- throw new Error("ChatGPT account capability probe did not reach a stable composer state");
200
- }
201
- await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
202
- }
203
- const menu = page.locator(CHATGPT_EFFORT_MENU_SELECTOR).last();
204
- const menuVisible = await menu.isVisible().catch(() => false);
205
- const menuExpanded = await effortButton.getAttribute("aria-expanded").catch(() => null);
206
- if (!menuVisible && menuExpanded !== "true") await effortButton.press("Enter");
207
- try {
208
- const efforts = menu.locator(CHATGPT_EFFORT_ITEM_SELECTOR);
209
- const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
210
- const waitAbort = new AbortController();
211
- try {
212
- const ready = await Promise.race([
213
- efforts.first().waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
214
- .then(() => "items" as const),
215
- slider.waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
216
- .then(() => "slider" as const),
217
- ]);
218
- const sliderVisible = ready === "slider" || await slider.isVisible().catch(() => false);
219
- if (!sliderVisible) {
220
- return { solAvailable: true, proAvailable: await efforts.count() >= 5 };
221
- }
222
- const state = parseChatGptEffortSliderState(
223
- await slider.getAttribute("aria-valuemin"),
224
- await slider.getAttribute("aria-valuemax"),
225
- await slider.getAttribute("aria-valuenow"),
226
- );
227
- if (!state) {
228
- throw new Error(
229
- "ChatGPT model controls are unavailable. Reload ChatGPT and run Repair again.",
230
- { cause: new Error("ChatGPT effort slider exposed an invalid ARIA range") },
231
- );
232
- }
233
- return { solAvailable: true, proAvailable: state.max - state.min + 1 >= 5 };
234
- } finally {
235
- waitAbort.abort();
236
- }
237
- } finally {
238
- await page.keyboard.press("Escape").catch(() => {});
239
- }
240
- }
1
+ import type { Locator, Page } from "playwright-core";
2
+ import type { ChatGptWebAccountCapabilities } from "./chatgpt-web-models";
3
+
4
+ export const CHATGPT_TEMPORARY_CHAT_URL = "https://chatgpt.com/?temporary-chat=true";
5
+ export const CHATGPT_COMPOSER_SELECTOR = [
6
+ '[data-testid="prompt-textarea"]',
7
+ "#prompt-textarea",
8
+ '[contenteditable="true"][data-lexical-editor="true"]',
9
+ ].join(", ");
10
+ export const CHATGPT_EFFORT_CONTROL_SELECTOR = [
11
+ 'button[aria-haspopup="menu"][data-tone="neutral"]',
12
+ 'button[data-testid="model-switcher-dropdown-button"][aria-haspopup="menu"]',
13
+ ].join(", ");
14
+ export const CHATGPT_EFFORT_MENU_SELECTOR = [
15
+ '[data-testid="composer-intelligence-picker-content"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
16
+ '[role="menu"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
17
+ '[role="group"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
18
+ ].join(", ");
19
+ export const CHATGPT_EFFORT_ITEM_SELECTOR = '[role="menuitemradio"]';
20
+ export const CHATGPT_EFFORT_SLIDER_SELECTOR = '[data-model-reasoning-effort-slider] [role="slider"]';
21
+ export const CHATGPT_EFFORT_SLIDER_MAX_OPTIONS = 5;
22
+ export const CHATGPT_STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
23
+ export const CHATGPT_COMPLETION_ACTION_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
24
+ export const CHATGPT_ASSISTANT_TURN_SELECTOR = [
25
+ '[data-testid^="conversation-turn-"][data-turn="assistant"]',
26
+ '[data-testid^="conversation-turn-"][data-message-author-role="assistant"]',
27
+ '[data-testid^="conversation-turn-"]:has([data-message-author-role="assistant"])',
28
+ ].join(", ");
29
+ export const CHATGPT_USER_TURN_SELECTOR = [
30
+ '[data-testid^="conversation-turn-"][data-turn="user"]',
31
+ '[data-testid^="conversation-turn-"][data-message-author-role="user"]',
32
+ '[data-testid^="conversation-turn-"]:has([data-message-author-role="user"])',
33
+ ].join(", ");
34
+
35
+ export interface ChatGptEffortSliderState {
36
+ min: number;
37
+ max: number;
38
+ value: number;
39
+ }
40
+
41
+ export interface ChatGptEffortActivation {
42
+ method: "already-open" | "click" | "pointerdown";
43
+ menu: Locator;
44
+ slider: Locator;
45
+ }
46
+
47
+ function effortMenuSelectorForId(menuId: string): string {
48
+ return `[id=${JSON.stringify(menuId)}]`;
49
+ }
50
+
51
+ export async function chatGptEffortMenuForControl(page: Page, control: Locator): Promise<Locator> {
52
+ const menuId = await control.getAttribute("aria-controls").catch(() => null);
53
+ if (menuId) return page.locator(effortMenuSelectorForId(menuId));
54
+ return page.locator(CHATGPT_EFFORT_MENU_SELECTOR).filter({ visible: true }).last();
55
+ }
56
+
57
+ async function visibleEffortSurface(
58
+ page: Page,
59
+ control: Locator,
60
+ ): Promise<{ menu: Locator; slider: Locator } | undefined> {
61
+ const menu = await chatGptEffortMenuForControl(page, control);
62
+ const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
63
+ if (await menu.isVisible().catch(() => false) || await slider.isVisible().catch(() => false)) {
64
+ return { menu, slider };
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ async function waitForEffortSurface(
70
+ page: Page,
71
+ control: Locator,
72
+ timeoutMs: number,
73
+ ): Promise<{ menu: Locator; slider: Locator } | undefined> {
74
+ const deadline = Date.now() + timeoutMs;
75
+ do {
76
+ const surface = await visibleEffortSurface(page, control);
77
+ if (surface) return surface;
78
+ if (Date.now() >= deadline) return undefined;
79
+ await new Promise(resolveSleep => setTimeout(resolveSleep, 50));
80
+ } while (true);
81
+ }
82
+
83
+ async function clearGhostEffortState(page: Page, control: Locator): Promise<void> {
84
+ const expanded = await control.getAttribute("aria-expanded").catch(() => null);
85
+ const state = await control.getAttribute("data-state").catch(() => null);
86
+ if (expanded === "true" || state === "open") {
87
+ await page.keyboard.press("Escape").catch(() => {});
88
+ }
89
+ }
90
+
91
+ export async function activateChatGptEffortMenu(
92
+ page: Page,
93
+ control: Locator,
94
+ options: { settleMs?: number } = {},
95
+ ): Promise<ChatGptEffortActivation> {
96
+ const openSurface = await visibleEffortSurface(page, control);
97
+ if (openSurface) return { method: "already-open", ...openSurface };
98
+
99
+ const settleMs = options.settleMs ?? 3_000;
100
+ await clearGhostEffortState(page, control);
101
+ await control.click({ force: true, timeout: Math.max(1, settleMs) });
102
+ const clickedSurface = await waitForEffortSurface(page, control, settleMs);
103
+ if (clickedSurface) return { method: "click", ...clickedSurface };
104
+
105
+ await clearGhostEffortState(page, control);
106
+ await control.dispatchEvent("pointerdown", {
107
+ button: 0,
108
+ buttons: 1,
109
+ pointerType: "mouse",
110
+ isPrimary: true,
111
+ });
112
+ const pointerSurface = await waitForEffortSurface(page, control, settleMs);
113
+ if (pointerSurface) return { method: "pointerdown", ...pointerSurface };
114
+ throw new Error(
115
+ "ChatGPT effort control did not expose its owned menu or structural slider after click and primary pointerdown",
116
+ );
117
+ }
118
+
119
+ function safeIntegerAttribute(value: string | null): number | undefined {
120
+ if (value === null || !/^-?\d+$/.test(value)) return undefined;
121
+ const parsed = Number(value);
122
+ return Number.isSafeInteger(parsed) ? parsed : undefined;
123
+ }
124
+
125
+ export function parseChatGptEffortSliderState(
126
+ rawMin: string | null,
127
+ rawMax: string | null,
128
+ rawValue: string | null,
129
+ ): ChatGptEffortSliderState | undefined {
130
+ const min = safeIntegerAttribute(rawMin);
131
+ const max = safeIntegerAttribute(rawMax);
132
+ const value = safeIntegerAttribute(rawValue);
133
+ if (min === undefined || max === undefined || value === undefined) return undefined;
134
+ const optionCount = max - min + 1;
135
+ if (optionCount < 1 || optionCount > CHATGPT_EFFORT_SLIDER_MAX_OPTIONS) return undefined;
136
+ if (value < min || value > max) return undefined;
137
+ return { min, max, value };
138
+ }
139
+
140
+ async function anyVisible(locator: Locator): Promise<boolean> {
141
+ const count = await locator.count();
142
+ for (let index = 0; index < count; index += 1) {
143
+ if (await locator.nth(index).isVisible().catch(() => false)) return true;
144
+ }
145
+ return false;
146
+ }
147
+
148
+ export async function assertAuthenticatedChatGptPage(page: Page): Promise<void> {
149
+ const composer = page.locator(
150
+ CHATGPT_COMPOSER_SELECTOR,
151
+ );
152
+ if (!await anyVisible(composer)) {
153
+ throw new Error("ChatGPT authentication could not be verified: no visible composer is present");
154
+ }
155
+ }
156
+
157
+ export async function assertTemporaryChatPage(page: Page): Promise<void> {
158
+ const url = new URL(page.url());
159
+ const expected = new URL(CHATGPT_TEMPORARY_CHAT_URL);
160
+ if (url.origin !== expected.origin || url.pathname !== expected.pathname || url.searchParams.get("temporary-chat") !== "true") {
161
+ throw new Error(`ChatGPT left the isolated Temporary Chat surface (${page.url()})`);
162
+ }
163
+ }
164
+
165
+ export async function detectChatGptAccountCapabilities(
166
+ page: Page,
167
+ options: { selectorTimeoutMs?: number; stableAbsenceMs?: number } = {},
168
+ ): Promise<ChatGptWebAccountCapabilities> {
169
+ const composers = page.locator(CHATGPT_COMPOSER_SELECTOR).filter({ visible: true });
170
+ const composer = composers.last();
171
+ const composerForm = composer.locator("xpath=ancestor::form[1]");
172
+ const effortButton = composerForm.locator(CHATGPT_EFFORT_CONTROL_SELECTOR).last();
173
+ const deadline = Date.now() + (options.selectorTimeoutMs ?? 30_000);
174
+ const stableAbsenceMs = options.stableAbsenceMs ?? 3_000;
175
+ let absenceSince: number | undefined;
176
+ let presenceObservations = 0;
177
+ while (true) {
178
+ const effortVisible = await effortButton.isVisible().catch(() => false);
179
+ if (effortVisible) {
180
+ presenceObservations += 1;
181
+ absenceSince = undefined;
182
+ if (presenceObservations >= 2) break;
183
+ await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
184
+ continue;
185
+ }
186
+ presenceObservations = 0;
187
+ const composerReady = await composers.count().then(count => count === 1).catch(() => false);
188
+ const formReady = await composerForm.count().then(count => count === 1).catch(() => false);
189
+ const documentReady = await page.evaluate(() => document.readyState === "complete").catch(() => false);
190
+ if (composerReady && formReady && documentReady) {
191
+ absenceSince ??= Date.now();
192
+ if (Date.now() - absenceSince >= stableAbsenceMs) {
193
+ return { solAvailable: false, proAvailable: false };
194
+ }
195
+ } else {
196
+ absenceSince = undefined;
197
+ }
198
+ if (Date.now() >= deadline) {
199
+ throw new Error("ChatGPT account capability probe did not reach a stable composer state");
200
+ }
201
+ await new Promise(resolveSleep => setTimeout(resolveSleep, 100));
202
+ }
203
+ const menu = page.locator(CHATGPT_EFFORT_MENU_SELECTOR).last();
204
+ const menuVisible = await menu.isVisible().catch(() => false);
205
+ const menuExpanded = await effortButton.getAttribute("aria-expanded").catch(() => null);
206
+ if (!menuVisible && menuExpanded !== "true") await effortButton.press("Enter");
207
+ try {
208
+ const efforts = menu.locator(CHATGPT_EFFORT_ITEM_SELECTOR);
209
+ const slider = page.locator(CHATGPT_EFFORT_SLIDER_SELECTOR).filter({ visible: true }).last();
210
+ const waitAbort = new AbortController();
211
+ try {
212
+ const ready = await Promise.race([
213
+ efforts.first().waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
214
+ .then(() => "items" as const),
215
+ slider.waitFor({ state: "visible", timeout: 70_000, signal: waitAbort.signal })
216
+ .then(() => "slider" as const),
217
+ ]);
218
+ const sliderVisible = ready === "slider" || await slider.isVisible().catch(() => false);
219
+ if (!sliderVisible) {
220
+ return { solAvailable: true, proAvailable: await efforts.count() >= 5 };
221
+ }
222
+ const state = parseChatGptEffortSliderState(
223
+ await slider.getAttribute("aria-valuemin"),
224
+ await slider.getAttribute("aria-valuemax"),
225
+ await slider.getAttribute("aria-valuenow"),
226
+ );
227
+ if (!state) {
228
+ throw new Error(
229
+ "ChatGPT model controls are unavailable. Reload ChatGPT and run Repair again.",
230
+ { cause: new Error("ChatGPT effort slider exposed an invalid ARIA range") },
231
+ );
232
+ }
233
+ return { solAvailable: true, proAvailable: state.max - state.min + 1 >= 5 };
234
+ } finally {
235
+ waitAbort.abort();
236
+ }
237
+ } finally {
238
+ await page.keyboard.press("Escape").catch(() => {});
239
+ }
240
+ }