ask-pro 0.1.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.
Files changed (55) hide show
  1. package/.codex-plugin/plugin.json +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/assets/ask-pro_logo.png +0 -0
  5. package/dist/bin/ask-pro-cli.js +507 -0
  6. package/dist/scripts/run-cli.js +27 -0
  7. package/dist/src/ask-pro/atomicWrite.js +26 -0
  8. package/dist/src/ask-pro/browserRunner.js +796 -0
  9. package/dist/src/ask-pro/responseZip.js +349 -0
  10. package/dist/src/ask-pro/session.js +662 -0
  11. package/dist/src/ask-pro/sessionControllerLease.js +64 -0
  12. package/dist/src/ask-pro/toon.js +26 -0
  13. package/dist/src/ask-pro/zip.js +85 -0
  14. package/dist/src/browser/actions/assistantResponse.js +1245 -0
  15. package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
  16. package/dist/src/browser/actions/attachments.js +1720 -0
  17. package/dist/src/browser/actions/composerSendReadiness.js +369 -0
  18. package/dist/src/browser/actions/domEvents.js +31 -0
  19. package/dist/src/browser/actions/inputGuard.js +52 -0
  20. package/dist/src/browser/actions/modelPickerDom.js +68 -0
  21. package/dist/src/browser/actions/modelSelection.js +576 -0
  22. package/dist/src/browser/actions/navigation.js +510 -0
  23. package/dist/src/browser/actions/promptComposer.js +824 -0
  24. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  25. package/dist/src/browser/actions/thinkingStatus.js +408 -0
  26. package/dist/src/browser/actions/thinkingTime.js +635 -0
  27. package/dist/src/browser/actions/windowState.js +47 -0
  28. package/dist/src/browser/attachRunning.js +31 -0
  29. package/dist/src/browser/chatgptModelCatalog.js +321 -0
  30. package/dist/src/browser/chromeLifecycle.js +807 -0
  31. package/dist/src/browser/config.js +110 -0
  32. package/dist/src/browser/constants.js +85 -0
  33. package/dist/src/browser/cookies.js +191 -0
  34. package/dist/src/browser/detect.js +337 -0
  35. package/dist/src/browser/domDebug.js +72 -0
  36. package/dist/src/browser/errors.js +20 -0
  37. package/dist/src/browser/format.js +16 -0
  38. package/dist/src/browser/index.js +2631 -0
  39. package/dist/src/browser/language.js +97 -0
  40. package/dist/src/browser/liveTabs.js +434 -0
  41. package/dist/src/browser/modelStrategy.js +13 -0
  42. package/dist/src/browser/pageActions.js +5 -0
  43. package/dist/src/browser/profilePaths.js +282 -0
  44. package/dist/src/browser/profileState.js +413 -0
  45. package/dist/src/browser/providerDomFlow.js +17 -0
  46. package/dist/src/browser/providers/chatgptDomProvider.js +50 -0
  47. package/dist/src/browser/reattach.js +534 -0
  48. package/dist/src/browser/reattachHelpers.js +387 -0
  49. package/dist/src/browser/utils.js +122 -0
  50. package/dist/src/browserMode.js +1 -0
  51. package/dist/src/version.js +39 -0
  52. package/package.json +114 -0
  53. package/scripts/refresh-local-plugin.mjs +179 -0
  54. package/scripts/refresh-local-plugin.ps1 +93 -0
  55. package/skills/ask-pro/SKILL.md +181 -0
@@ -0,0 +1,17 @@
1
+ export async function runProviderSubmissionFlow(adapter, ctx) {
2
+ await adapter.waitForUi(ctx);
3
+ if (adapter.selectMode) {
4
+ await adapter.selectMode(ctx);
5
+ }
6
+ await adapter.typePrompt(ctx);
7
+ await adapter.submitPrompt(ctx);
8
+ }
9
+ export async function runProviderDomFlow(adapter, ctx) {
10
+ await runProviderSubmissionFlow(adapter, ctx);
11
+ const response = await adapter.waitForResponse(ctx);
12
+ const thoughts = adapter.extractThoughts ? await adapter.extractThoughts(ctx) : null;
13
+ return { ...response, thoughts };
14
+ }
15
+ export function joinSelectors(selectors) {
16
+ return selectors.join(", ");
17
+ }
@@ -0,0 +1,50 @@
1
+ import { ensurePromptReady } from "../actions/navigation.js";
2
+ import { submitPrompt } from "../actions/promptComposer.js";
3
+ import { waitForAssistantResponse } from "../actions/assistantResponse.js";
4
+ function requireState(ctx) {
5
+ const state = ctx.state;
6
+ if (!state?.runtime || !state?.input || !state?.logger) {
7
+ throw new Error("chatgptDomProvider requires runtime/input/logger in context.state.");
8
+ }
9
+ return state;
10
+ }
11
+ async function waitForUi(ctx) {
12
+ const state = requireState(ctx);
13
+ await ensurePromptReady(state.runtime, state.inputTimeoutMs ?? 30_000, state.logger);
14
+ }
15
+ async function typePrompt(_ctx) {
16
+ // submitPrompt() handles typing + send for ChatGPT.
17
+ }
18
+ async function submitPromptViaAdapter(ctx) {
19
+ const state = requireState(ctx);
20
+ const committedTurns = await submitPrompt({
21
+ runtime: state.runtime,
22
+ input: state.input,
23
+ attachmentNames: state.attachmentNames ?? [],
24
+ baselineTurns: state.baselineTurns ?? undefined,
25
+ inputTimeoutMs: state.inputTimeoutMs ?? undefined,
26
+ afterSubmit: state.afterSubmit,
27
+ }, ctx.prompt, state.logger);
28
+ state.committedTurns =
29
+ typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;
30
+ if (state.committedTurns != null &&
31
+ (state.baselineTurns == null || state.committedTurns > state.baselineTurns)) {
32
+ state.baselineTurns = Math.max(0, state.committedTurns - 1);
33
+ }
34
+ }
35
+ async function waitForResponse(ctx) {
36
+ const state = requireState(ctx);
37
+ const answer = await waitForAssistantResponse(state.runtime, state.timeoutMs, state.logger, state.baselineTurns ?? undefined);
38
+ return {
39
+ text: answer.text,
40
+ html: answer.html,
41
+ meta: answer.meta,
42
+ };
43
+ }
44
+ export const chatgptDomProvider = {
45
+ providerName: "chatgpt-web",
46
+ waitForUi,
47
+ typePrompt,
48
+ submitPrompt: submitPromptViaAdapter,
49
+ waitForResponse,
50
+ };
@@ -0,0 +1,534 @@
1
+ import { AssistantStoppedError } from "./errors.js";
2
+ import { createAssistantContinuation } from "./actions/assistantResponse.js";
3
+ import CDP from "chrome-remote-interface";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { mkdtemp, mkdir, rm } from "node:fs/promises";
7
+ import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
8
+ import { launchChrome, connectToChrome, hideChromeWindow, connectToRemoteChromeTarget, listRemoteChromeTargets, closeRemoteChromeTarget, closeChromeGracefully, connectWithNewTab, closeTab, maybeReuseRunningChrome, releaseChromeProcessHandle, restoreChromeWindowByPid, shouldLaunchChromeMinimized, } from "./chromeLifecycle.js";
9
+ import { DEFAULT_BROWSER_CONFIG, resolveBrowserConfig } from "./config.js";
10
+ import { defaultAskProBrowserProfileDir } from "./profilePaths.js";
11
+ import { applyPageLanguageOverrides, seedChromeProfileLanguage } from "./language.js";
12
+ import { syncCookies } from "./cookies.js";
13
+ import { CHATGPT_URL } from "./constants.js";
14
+ import { setChromeWindowState } from "./actions/windowState.js";
15
+ import { acquireProfileRunLock, cleanupStaleProfileState, createManagedChromeRunLease, releaseManagedChromeRunLeaseAndCountPeers, verifyDevToolsReachable, } from "./profileState.js";
16
+ import { readDevToolsActivePortInfo } from "./detect.js";
17
+ import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
18
+ function lifecycleLockTimeout(timeoutMs) {
19
+ return timeoutMs && timeoutMs > 0
20
+ ? timeoutMs
21
+ : (DEFAULT_BROWSER_CONFIG.profileLockTimeoutMs ?? 300_000);
22
+ }
23
+ async function acquireReattachRunLease(userDataDir, timeoutMs, logger) {
24
+ const effectiveTimeoutMs = lifecycleLockTimeout(timeoutMs);
25
+ const lock = await acquireProfileRunLock(userDataDir, { timeoutMs: effectiveTimeoutMs, logger });
26
+ try {
27
+ return {
28
+ userDataDir,
29
+ timeoutMs: effectiveTimeoutMs,
30
+ lease: await createManagedChromeRunLease(userDataDir),
31
+ };
32
+ }
33
+ finally {
34
+ await lock?.release();
35
+ }
36
+ }
37
+ async function releaseReattachRunLease(runLease, logger, onLastRun) {
38
+ if (!runLease)
39
+ return 0;
40
+ try {
41
+ const lock = await acquireProfileRunLock(runLease.userDataDir, {
42
+ timeoutMs: runLease.timeoutMs,
43
+ logger,
44
+ });
45
+ try {
46
+ const peerRunCount = await releaseManagedChromeRunLeaseAndCountPeers(runLease.userDataDir, runLease.lease, logger);
47
+ if (peerRunCount === 0)
48
+ await onLastRun?.();
49
+ return peerRunCount;
50
+ }
51
+ finally {
52
+ await lock?.release();
53
+ }
54
+ }
55
+ catch (error) {
56
+ const message = error instanceof Error ? error.message : String(error);
57
+ logger(`Failed to resolve shared Chrome ownership; retaining browser (${message})`);
58
+ return null;
59
+ }
60
+ }
61
+ async function closeManagedChromeIfUnused(chrome, userDataDir, inspectRemainingPages, logger) {
62
+ if (inspectRemainingPages) {
63
+ try {
64
+ const targets = await listRemoteChromeTargets({
65
+ host: chrome.host ?? "127.0.0.1",
66
+ port: chrome.port,
67
+ });
68
+ if (targets.some((target) => target.type === "page")) {
69
+ releaseChromeProcessHandle(chrome);
70
+ return;
71
+ }
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ logger(`Failed to inspect remaining Chrome tabs; retaining browser (${message})`);
76
+ releaseChromeProcessHandle(chrome);
77
+ return;
78
+ }
79
+ }
80
+ try {
81
+ await closeChromeGracefully(chrome, logger);
82
+ }
83
+ catch (error) {
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ logger(`Failed to close managed Chrome; retaining browser (${message})`);
86
+ releaseChromeProcessHandle(chrome);
87
+ return;
88
+ }
89
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
90
+ }
91
+ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
92
+ deps = { ...deps, continuationState: deps.continuationState ?? { used: false } };
93
+ const recoverSession = deps.recoverSession ??
94
+ (async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
95
+ const recoverWithRelaunchMode = async (runtimeMeta, configMeta) => {
96
+ await deps.chromeModeCb?.("relaunched");
97
+ return recoverSession(runtimeMeta, configMeta);
98
+ };
99
+ if (!runtime.chromePort && !runtime.chromeBrowserWSEndpoint) {
100
+ logger("No running Chrome detected; reopening browser to locate the session.");
101
+ return recoverWithRelaunchMode(runtime, config);
102
+ }
103
+ const resolvedConfig = resolveBrowserConfig(config ?? {});
104
+ const directUserDataDir = resolvedConfig.manualLogin
105
+ ? !resolvedConfig.remoteChrome && runtime.userDataDir
106
+ ? runtime.userDataDir
107
+ : null
108
+ : null;
109
+ let reattachRunLease = directUserDataDir
110
+ ? await acquireReattachRunLease(directUserDataDir, resolvedConfig.profileLockTimeoutMs, logger)
111
+ : null;
112
+ let closeConnection = null;
113
+ try {
114
+ const liveRuntime = await refreshAttachRuntime(runtime, logger).catch(() => runtime);
115
+ if (!liveRuntime.chromePort && !liveRuntime.chromeBrowserWSEndpoint) {
116
+ logger("Saved Chrome runtime metadata is stale; reopening browser to locate the session.");
117
+ await releaseReattachRunLease(reattachRunLease, logger);
118
+ reattachRunLease = null;
119
+ return recoverWithRelaunchMode(runtime, config);
120
+ }
121
+ const host = liveRuntime.chromeHost ?? "127.0.0.1";
122
+ const port = liveRuntime.chromePort ?? inferPortFromBrowserWSEndpoint(liveRuntime.chromeBrowserWSEndpoint);
123
+ const browserWSEndpoint = liveRuntime.chromeBrowserWSEndpoint ?? undefined;
124
+ const listTargets = deps.listTargets ??
125
+ (async () => (await listRemoteChromeTargets({
126
+ host,
127
+ port: port ?? 9222,
128
+ browserWSEndpoint,
129
+ })));
130
+ const targetList = (await listTargets());
131
+ const target = pickTarget(targetList, liveRuntime);
132
+ const connection = browserWSEndpoint && !deps.connect
133
+ ? await connectToRemoteChromeTarget(host, port ?? 9222, logger, {
134
+ browserWSEndpoint,
135
+ targetId: target?.targetId,
136
+ closeTargetOnDispose: false,
137
+ })
138
+ : {
139
+ client: (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
140
+ ? {
141
+ target: browserWSEndpoint,
142
+ local: true,
143
+ targetId: target?.targetId,
144
+ }
145
+ : {
146
+ host,
147
+ port,
148
+ target: target?.targetId,
149
+ })),
150
+ close: async () => undefined,
151
+ };
152
+ closeConnection = connection.close;
153
+ const connectedTargetId = "targetId" in connection ? connection.targetId : target?.targetId;
154
+ const ownsConnectedTarget = Boolean(!resolvedConfig.browserTabRef &&
155
+ liveRuntime.chromeTargetId &&
156
+ connectedTargetId === liveRuntime.chromeTargetId);
157
+ const client = connection.client;
158
+ const windowTransitionLock = directUserDataDir && shouldLaunchChromeMinimized({ ...resolvedConfig, startMinimized: true })
159
+ ? await acquireProfileRunLock(directUserDataDir, {
160
+ timeoutMs: lifecycleLockTimeout(resolvedConfig.profileLockTimeoutMs),
161
+ logger,
162
+ })
163
+ : null;
164
+ try {
165
+ if (windowTransitionLock) {
166
+ const restored = await restoreChromeWindowByPid(liveRuntime.chromePid, logger);
167
+ if (!restored) {
168
+ await setChromeWindowState(client, "normal", logger, {
169
+ targetId: connectedTargetId,
170
+ reason: "resume",
171
+ });
172
+ }
173
+ }
174
+ }
175
+ finally {
176
+ await windowTransitionLock?.release();
177
+ }
178
+ const { Runtime, DOM, Page, Input } = client;
179
+ if (Runtime?.enable) {
180
+ await Runtime.enable();
181
+ }
182
+ if (DOM && typeof DOM.enable === "function") {
183
+ await DOM.enable();
184
+ }
185
+ const ensureConversationOpen = async () => {
186
+ const { result } = await Runtime.evaluate({
187
+ expression: "location.href",
188
+ returnByValue: true,
189
+ });
190
+ const href = typeof result?.value === "string" ? result.value : "";
191
+ if (href.includes("/c/")) {
192
+ const currentId = extractConversationIdFromUrl(href);
193
+ if (!runtime.conversationId || (currentId && currentId === runtime.conversationId)) {
194
+ return;
195
+ }
196
+ }
197
+ const opened = await openConversationFromSidebarWithRetry(Runtime, {
198
+ conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
199
+ preferProjects: true,
200
+ promptPreview: deps.promptPreview,
201
+ }, 15_000);
202
+ if (!opened) {
203
+ throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
204
+ }
205
+ await waitForLocationChange(Runtime, 15_000);
206
+ };
207
+ const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
208
+ const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
209
+ const timeoutMs = config?.timeoutMs ?? 120_000;
210
+ const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
211
+ await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
212
+ await ensureConversationOpen();
213
+ const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
214
+ const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
215
+ const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined, runtime.conversationId, createAssistantContinuation(Runtime, Input, logger, undefined, deps.continuationState)), timeoutMs + 5_000, "Reattach response timed out");
216
+ const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
217
+ const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
218
+ const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
219
+ const afterAnswerResult = await deps.afterAnswerCb?.({
220
+ Runtime,
221
+ Page,
222
+ Input,
223
+ answer: {
224
+ text: aligned.answerText,
225
+ markdown: aligned.answerMarkdown,
226
+ },
227
+ });
228
+ await connection.close().catch(() => undefined);
229
+ closeConnection = null;
230
+ const keepBrowserOpen = Boolean(resolvedConfig.keepBrowser ||
231
+ resolvedConfig.browserTabRef ||
232
+ afterAnswerResult?.keepBrowserOpen);
233
+ if (reattachRunLease && !keepBrowserOpen && ownsConnectedTarget) {
234
+ await closeRemoteChromeTarget(host, port ?? 9222, connectedTargetId, logger);
235
+ }
236
+ if (reattachRunLease) {
237
+ const completedLease = reattachRunLease;
238
+ reattachRunLease = null;
239
+ const chrome = {
240
+ port: port ?? 9222,
241
+ pid: liveRuntime.chromePid,
242
+ host,
243
+ kill: async () => undefined,
244
+ process: undefined,
245
+ };
246
+ await releaseReattachRunLease(completedLease, logger, keepBrowserOpen
247
+ ? undefined
248
+ : async () => closeManagedChromeIfUnused(chrome, completedLease.userDataDir, true, logger));
249
+ }
250
+ return {
251
+ answerText: aligned.answerText,
252
+ answerMarkdown: aligned.answerMarkdown,
253
+ chromeMode: "reused_devtools",
254
+ keepBrowserOpen: afterAnswerResult?.keepBrowserOpen,
255
+ };
256
+ }
257
+ catch (error) {
258
+ await closeConnection?.().catch(() => undefined);
259
+ await releaseReattachRunLease(reattachRunLease, logger);
260
+ reattachRunLease = null;
261
+ if (error instanceof AssistantStoppedError)
262
+ throw error;
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
265
+ return recoverWithRelaunchMode(runtime, config);
266
+ }
267
+ }
268
+ async function refreshAttachRuntime(runtime, logger) {
269
+ if (!runtime.chromeProfileRoot) {
270
+ return runtime;
271
+ }
272
+ const host = runtime.chromeHost ?? "127.0.0.1";
273
+ const activePort = await readDevToolsActivePortInfo(runtime.chromeProfileRoot, {
274
+ host,
275
+ });
276
+ if (!activePort) {
277
+ return runtime;
278
+ }
279
+ const probe = await verifyDevToolsReachable({
280
+ port: activePort.port,
281
+ host,
282
+ attempts: 1,
283
+ timeoutMs: 750,
284
+ });
285
+ if (!probe.ok) {
286
+ logger(`DevTools port ${activePort.port} unreachable (${probe.error}); ignoring stale profile runtime metadata.`);
287
+ await cleanupStaleProfileState(runtime.chromeProfileRoot, logger, {
288
+ lockRemovalMode: "never",
289
+ }).catch(() => undefined);
290
+ return {
291
+ ...runtime,
292
+ chromePort: undefined,
293
+ chromeBrowserWSEndpoint: undefined,
294
+ };
295
+ }
296
+ return {
297
+ ...runtime,
298
+ chromeHost: host,
299
+ chromePort: activePort.port,
300
+ chromeBrowserWSEndpoint: activePort.browserWSEndpoint,
301
+ };
302
+ }
303
+ function inferPortFromBrowserWSEndpoint(browserWSEndpoint) {
304
+ if (!browserWSEndpoint) {
305
+ return undefined;
306
+ }
307
+ try {
308
+ const parsed = new URL(browserWSEndpoint);
309
+ const port = Number.parseInt(parsed.port, 10);
310
+ if (Number.isFinite(port) && port > 0) {
311
+ return port;
312
+ }
313
+ }
314
+ catch {
315
+ // ignore malformed ws endpoints and fall back to caller defaults
316
+ }
317
+ return undefined;
318
+ }
319
+ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
320
+ const resolved = resolveBrowserConfig(config ?? {});
321
+ const manualLogin = Boolean(resolved.manualLogin);
322
+ const userDataDir = manualLogin
323
+ ? (runtime.userDataDir ?? resolved.manualLoginProfileDir ?? defaultAskProBrowserProfileDir())
324
+ : await mkdtemp(path.join(os.tmpdir(), "ask-pro-reattach-"));
325
+ if (manualLogin) {
326
+ await mkdir(userDataDir, { recursive: true });
327
+ }
328
+ const launchLock = manualLogin
329
+ ? await acquireProfileRunLock(userDataDir, {
330
+ timeoutMs: lifecycleLockTimeout(resolved.profileLockTimeoutMs),
331
+ logger,
332
+ })
333
+ : null;
334
+ let reattachRunLease = null;
335
+ let reusedChrome = null;
336
+ let chrome = null;
337
+ try {
338
+ reusedChrome = manualLogin ? await maybeReuseRunningChrome(userDataDir, logger) : null;
339
+ if (!reusedChrome) {
340
+ await seedChromeProfileLanguage(userDataDir, resolved.acceptLanguage, logger);
341
+ }
342
+ chrome = reusedChrome ?? (await launchChrome(resolved, userDataDir, logger));
343
+ if (manualLogin) {
344
+ reattachRunLease = {
345
+ userDataDir,
346
+ timeoutMs: lifecycleLockTimeout(resolved.profileLockTimeoutMs),
347
+ lease: await createManagedChromeRunLease(userDataDir),
348
+ };
349
+ }
350
+ }
351
+ catch (error) {
352
+ if (chrome && !reusedChrome) {
353
+ const closed = await closeChromeGracefully(chrome, logger).then(() => true, () => {
354
+ releaseChromeProcessHandle(chrome);
355
+ return false;
356
+ });
357
+ if (closed) {
358
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
359
+ }
360
+ }
361
+ else {
362
+ releaseChromeProcessHandle(chrome);
363
+ }
364
+ throw error;
365
+ }
366
+ finally {
367
+ await launchLock?.release();
368
+ }
369
+ if (!chrome)
370
+ throw new Error("Failed to start or reuse managed Chrome.");
371
+ const chromeHost = chrome.host ?? "127.0.0.1";
372
+ let client = null;
373
+ let isolatedTargetId;
374
+ let completed = false;
375
+ let keepBrowserOpen = false;
376
+ try {
377
+ const manageManagedWindowState = manualLogin && shouldLaunchChromeMinimized({ ...resolved, startMinimized: true });
378
+ const windowTransitionLock = manageManagedWindowState
379
+ ? await acquireProfileRunLock(userDataDir, {
380
+ timeoutMs: lifecycleLockTimeout(resolved.profileLockTimeoutMs),
381
+ logger,
382
+ })
383
+ : null;
384
+ let tabSetupFailed = false;
385
+ try {
386
+ const isolatedConnection = manualLogin
387
+ ? await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
388
+ fallbackToDefault: false,
389
+ retries: 2,
390
+ })
391
+ : { client: await connectToChrome(chrome.port, logger, chromeHost) };
392
+ client = isolatedConnection.client;
393
+ isolatedTargetId = isolatedConnection.targetId;
394
+ if (manageManagedWindowState) {
395
+ const restored = await restoreChromeWindowByPid(chrome.pid, logger);
396
+ if (!restored) {
397
+ await setChromeWindowState(client, "normal", logger, {
398
+ targetId: isolatedTargetId,
399
+ reason: "resume-relaunch",
400
+ });
401
+ }
402
+ }
403
+ }
404
+ catch (error) {
405
+ tabSetupFailed = true;
406
+ throw error;
407
+ }
408
+ finally {
409
+ if (tabSetupFailed && windowTransitionLock) {
410
+ const restoreClient = client ??
411
+ (await CDP({ host: chromeHost, port: chrome.port }).catch(() => null));
412
+ let restored = false;
413
+ if (restoreClient) {
414
+ restored = await setChromeWindowState(restoreClient, "normal", logger, {
415
+ targetId: isolatedTargetId,
416
+ reason: "resume-tab-setup-failed",
417
+ });
418
+ if (restoreClient !== client)
419
+ await restoreClient.close().catch(() => undefined);
420
+ }
421
+ if (!restored)
422
+ await restoreChromeWindowByPid(chrome.pid, logger);
423
+ }
424
+ await windowTransitionLock?.release();
425
+ }
426
+ if (!client)
427
+ throw new Error("Failed to connect to managed Chrome.");
428
+ const { Network, Page, Runtime, DOM } = client;
429
+ if (Runtime?.enable) {
430
+ await Runtime.enable();
431
+ }
432
+ if (DOM && typeof DOM.enable === "function") {
433
+ await DOM.enable();
434
+ }
435
+ if (resolved.acceptLanguage) {
436
+ await applyPageLanguageOverrides(client, resolved.acceptLanguage, logger);
437
+ }
438
+ if (!resolved.headless && resolved.hideWindow) {
439
+ await hideChromeWindow(chrome, logger);
440
+ }
441
+ let appliedCookies = 0;
442
+ if (!manualLogin && resolved.cookieSync) {
443
+ appliedCookies = await syncCookies(Network, resolved.url, resolved.chromeProfile, logger, {
444
+ allowErrors: resolved.allowCookieErrors,
445
+ filterNames: resolved.cookieNames ?? undefined,
446
+ inlineCookies: resolved.inlineCookies ?? undefined,
447
+ cookiePath: resolved.chromeCookiePath ?? undefined,
448
+ waitMs: resolved.cookieSyncWaitMs ?? 0,
449
+ });
450
+ }
451
+ await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
452
+ await ensureNotBlocked(Runtime, resolved.headless, logger);
453
+ await ensureLoggedIn(Runtime, logger, { appliedCookies });
454
+ if (resolved.url !== CHATGPT_URL) {
455
+ await navigateToChatGPT(Page, Runtime, resolved.url, logger);
456
+ await ensureNotBlocked(Runtime, resolved.headless, logger);
457
+ }
458
+ await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
459
+ const conversationUrl = buildConversationUrl(runtime, resolved.url);
460
+ if (conversationUrl) {
461
+ logger(`Reopening conversation at ${conversationUrl}`);
462
+ await navigateToChatGPT(Page, Runtime, conversationUrl, logger);
463
+ await ensureNotBlocked(Runtime, resolved.headless, logger);
464
+ await ensurePromptReady(Runtime, resolved.inputTimeoutMs, logger);
465
+ }
466
+ else {
467
+ const opened = await openConversationFromSidebarWithRetry(Runtime, {
468
+ conversationId: runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? ""),
469
+ preferProjects: resolved.url !== CHATGPT_URL ||
470
+ Boolean(runtime.tabUrl &&
471
+ (/\/g\//.test(runtime.tabUrl) || runtime.tabUrl.includes("/project"))),
472
+ promptPreview: deps.promptPreview,
473
+ }, 15_000);
474
+ if (!opened) {
475
+ throw new Error("Unable to locate prior ChatGPT conversation in sidebar.");
476
+ }
477
+ await waitForLocationChange(Runtime, 15_000);
478
+ }
479
+ const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
480
+ const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
481
+ const timeoutMs = resolved.timeoutMs ?? 120_000;
482
+ const minTurnIndex = await readConversationTurnIndex(Runtime, logger);
483
+ const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
484
+ const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined, runtime.conversationId, createAssistantContinuation(Runtime, client.Input, logger, undefined, deps.continuationState));
485
+ const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
486
+ const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
487
+ const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
488
+ const afterAnswerResult = await deps.afterAnswerCb?.({
489
+ Runtime,
490
+ Page,
491
+ Input: client.Input,
492
+ answer: {
493
+ text: aligned.answerText,
494
+ markdown: aligned.answerMarkdown,
495
+ },
496
+ });
497
+ keepBrowserOpen = Boolean(resolved.keepBrowser || afterAnswerResult?.keepBrowserOpen);
498
+ completed = true;
499
+ return {
500
+ answerText: aligned.answerText,
501
+ answerMarkdown: aligned.answerMarkdown,
502
+ chromeMode: "relaunched",
503
+ keepBrowserOpen: afterAnswerResult?.keepBrowserOpen,
504
+ };
505
+ }
506
+ finally {
507
+ await client?.close().catch(() => undefined);
508
+ if (manualLogin && completed && !keepBrowserOpen && isolatedTargetId) {
509
+ await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
510
+ }
511
+ if (reattachRunLease) {
512
+ const completedLease = reattachRunLease;
513
+ reattachRunLease = null;
514
+ await releaseReattachRunLease(completedLease, logger, completed && !keepBrowserOpen
515
+ ? async () => closeManagedChromeIfUnused(chrome, completedLease.userDataDir, Boolean(reusedChrome), logger)
516
+ : undefined);
517
+ releaseChromeProcessHandle(chrome);
518
+ }
519
+ else if (!manualLogin && completed && !keepBrowserOpen) {
520
+ await closeChromeGracefully(chrome, logger).catch(() => undefined);
521
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
522
+ }
523
+ else {
524
+ releaseChromeProcessHandle(chrome);
525
+ }
526
+ }
527
+ }
528
+ // biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
529
+ export const __test__ = {
530
+ pickTarget,
531
+ extractConversationIdFromUrl,
532
+ buildConversationUrl,
533
+ openConversationFromSidebar,
534
+ };