@steipete/oracle 0.10.0 → 0.11.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 (52) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +224 -0
  4. package/dist/src/browser/actions/assistantResponse.js +26 -0
  5. package/dist/src/browser/actions/deepResearch.js +662 -0
  6. package/dist/src/browser/actions/modelSelection.js +78 -13
  7. package/dist/src/browser/actions/navigation.js +22 -0
  8. package/dist/src/browser/actions/projectSources.js +491 -0
  9. package/dist/src/browser/actions/promptComposer.js +52 -27
  10. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  11. package/dist/src/browser/artifacts.js +150 -0
  12. package/dist/src/browser/attachRunning.js +31 -0
  13. package/dist/src/browser/chatgptImages.js +315 -0
  14. package/dist/src/browser/chromeLifecycle.js +214 -3
  15. package/dist/src/browser/config.js +26 -2
  16. package/dist/src/browser/constants.js +8 -0
  17. package/dist/src/browser/controlPlan.js +81 -0
  18. package/dist/src/browser/detect.js +206 -33
  19. package/dist/src/browser/domDebug.js +49 -0
  20. package/dist/src/browser/index.js +1257 -485
  21. package/dist/src/browser/liveTabs.js +434 -0
  22. package/dist/src/browser/profileState.js +83 -3
  23. package/dist/src/browser/projectSourcesRunner.js +366 -0
  24. package/dist/src/browser/reattach.js +117 -45
  25. package/dist/src/browser/reattachHelpers.js +1 -1
  26. package/dist/src/browser/sessionRunner.js +53 -1
  27. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  28. package/dist/src/cli/bridge/claudeConfig.js +12 -8
  29. package/dist/src/cli/bridge/codexConfig.js +2 -2
  30. package/dist/src/cli/browserConfig.js +40 -0
  31. package/dist/src/cli/browserDefaults.js +31 -7
  32. package/dist/src/cli/browserTabs.js +228 -0
  33. package/dist/src/cli/dryRun.js +33 -1
  34. package/dist/src/cli/duplicatePromptGuard.js +10 -2
  35. package/dist/src/cli/help.js +1 -1
  36. package/dist/src/cli/options.js +4 -0
  37. package/dist/src/cli/projectSources.js +116 -0
  38. package/dist/src/cli/sessionCommand.js +51 -0
  39. package/dist/src/cli/sessionDisplay.js +121 -9
  40. package/dist/src/cli/sessionRunner.js +51 -7
  41. package/dist/src/mcp/consultPresets.js +19 -0
  42. package/dist/src/mcp/server.js +2 -0
  43. package/dist/src/mcp/tools/consult.js +201 -26
  44. package/dist/src/mcp/tools/projectSources.js +123 -0
  45. package/dist/src/mcp/types.js +7 -0
  46. package/dist/src/mcp/utils.js +6 -1
  47. package/dist/src/oracle/run.js +4 -1
  48. package/dist/src/projectSources/plan.js +27 -0
  49. package/dist/src/projectSources/types.js +1 -0
  50. package/dist/src/projectSources/url.js +23 -0
  51. package/dist/src/sessionManager.js +1 -0
  52. package/package.json +2 -1
@@ -0,0 +1,366 @@
1
+ import { mkdtemp, mkdir, rm } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { closeTab, connectWithNewTab, hideChromeWindow, launchChrome, registerTerminationHooks, } from "./chromeLifecycle.js";
5
+ import { resolveBrowserConfig } from "./config.js";
6
+ import { syncCookies } from "./cookies.js";
7
+ import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
8
+ import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
9
+ import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
10
+ import { CHATGPT_URL } from "./constants.js";
11
+ import { delay } from "./utils.js";
12
+ import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
13
+ import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
14
+ import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
15
+ export async function runBrowserProjectSources(request) {
16
+ const startedAt = Date.now();
17
+ const logger = ((message) => request.log?.(message));
18
+ const projectUrl = normalizeProjectSourcesUrl(request.chatgptUrl);
19
+ const operation = request.operation;
20
+ const files = request.files ?? [];
21
+ const plannedUploads = buildProjectSourcesUploadPlan(files);
22
+ const warnings = [];
23
+ if (operation === "add" && files.length === 0) {
24
+ throw new Error("Project Sources add requires at least one file.");
25
+ }
26
+ if (request.dryRun) {
27
+ return {
28
+ status: "dry-run",
29
+ operation,
30
+ projectUrl,
31
+ dryRun: true,
32
+ plannedUploads,
33
+ warnings,
34
+ tookMs: Date.now() - startedAt,
35
+ };
36
+ }
37
+ let config = resolveBrowserConfig({
38
+ ...request.config,
39
+ url: projectUrl,
40
+ chatgptUrl: projectUrl,
41
+ });
42
+ if (config.remoteChrome) {
43
+ throw new Error("Project Sources v1 uses local browser automation only. Run it on the signed-in browser host.");
44
+ }
45
+ const manualLogin = Boolean(config.manualLogin);
46
+ const manualProfileDir = config.manualLoginProfileDir
47
+ ? path.resolve(config.manualLoginProfileDir)
48
+ : path.join(os.homedir(), ".oracle", "browser-profile");
49
+ const userDataDir = manualLogin
50
+ ? manualProfileDir
51
+ : await mkdtemp(path.join(os.tmpdir(), "oracle-project-sources-"));
52
+ if (manualLogin) {
53
+ await mkdir(userDataDir, { recursive: true });
54
+ logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
55
+ }
56
+ else {
57
+ logger(`Created temporary Chrome profile at ${userDataDir}`);
58
+ }
59
+ let tabLease = null;
60
+ if (manualLogin) {
61
+ tabLease = await acquireBrowserTabLease(userDataDir, {
62
+ maxConcurrentTabs: config.maxConcurrentTabs,
63
+ timeoutMs: config.timeoutMs,
64
+ logger,
65
+ sessionId: "project-sources",
66
+ });
67
+ }
68
+ let chrome = null;
69
+ let reusedChrome = null;
70
+ let client = null;
71
+ let isolatedTargetId = null;
72
+ let removeTerminationHooks = null;
73
+ let removeDialogHandler = null;
74
+ let connectionClosedUnexpectedly = false;
75
+ let completed = false;
76
+ const effectiveKeepBrowser = Boolean(config.keepBrowser);
77
+ try {
78
+ const acquired = manualLogin
79
+ ? await acquireManualLoginChromeForProjectSources(userDataDir, config, logger)
80
+ : {
81
+ chrome: await launchChrome({ ...config, remoteChrome: null }, userDataDir, logger),
82
+ reusedChrome: null,
83
+ };
84
+ chrome = acquired.chrome;
85
+ reusedChrome = acquired.reusedChrome;
86
+ const chromeHost = chrome.host ?? "127.0.0.1";
87
+ if (tabLease) {
88
+ await tabLease.update({ chromeHost, chromePort: chrome.port });
89
+ }
90
+ removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
91
+ isInFlight: () => !completed,
92
+ preserveUserDataDir: manualLogin,
93
+ });
94
+ const strictTabIsolation = Boolean(manualLogin && reusedChrome);
95
+ const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
96
+ fallbackToDefault: !strictTabIsolation,
97
+ retries: strictTabIsolation ? 3 : 0,
98
+ retryDelayMs: 500,
99
+ });
100
+ client = connection.client;
101
+ isolatedTargetId = connection.targetId ?? null;
102
+ if (tabLease && isolatedTargetId) {
103
+ await tabLease.update({
104
+ chromeHost,
105
+ chromePort: chrome.port,
106
+ chromeTargetId: isolatedTargetId,
107
+ tabUrl: projectUrl,
108
+ });
109
+ }
110
+ const disconnectPromise = new Promise((_, reject) => {
111
+ client?.on("disconnect", () => {
112
+ connectionClosedUnexpectedly = true;
113
+ reject(new Error("Chrome window closed before Project Sources finished."));
114
+ });
115
+ });
116
+ const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
117
+ const { Network, Page, Runtime, Input, DOM } = client;
118
+ if (!config.headless && config.hideWindow) {
119
+ await hideChromeWindow(chrome, logger);
120
+ }
121
+ const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
122
+ if (DOM && typeof DOM.enable === "function") {
123
+ domainEnablers.push(DOM.enable());
124
+ }
125
+ await Promise.all(domainEnablers);
126
+ removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
127
+ if (!manualLogin) {
128
+ await Network.clearBrowserCookies();
129
+ }
130
+ const appliedCookies = await applyProjectSourcesCookies({
131
+ config,
132
+ network: Network,
133
+ manualLogin,
134
+ logger,
135
+ });
136
+ await raceWithDisconnect(navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger));
137
+ await raceWithDisconnect(waitForProjectSourcesLogin({
138
+ runtime: Runtime,
139
+ logger,
140
+ appliedCookies,
141
+ manualLogin,
142
+ timeoutMs: config.timeoutMs,
143
+ }));
144
+ await raceWithDisconnect(navigateToChatGPT(Page, Runtime, projectUrl, logger));
145
+ await raceWithDisconnect(openProjectSourcesTab(Runtime, Input, config.inputTimeoutMs, logger));
146
+ await raceWithDisconnect(waitForProjectSourcesReady(Runtime, config.inputTimeoutMs, logger));
147
+ const sourcesBefore = await raceWithDisconnect(waitForProjectSourcesListSettled(Runtime, config.inputTimeoutMs, logger));
148
+ let sourcesAfter = sourcesBefore;
149
+ if (operation === "add") {
150
+ sourcesAfter = await raceWithDisconnect(uploadProjectSources({ runtime: Runtime, dom: DOM, input: Input }, files, logger, config.timeoutMs));
151
+ }
152
+ const added = operation === "add" ? diffAddedProjectSources(sourcesBefore, sourcesAfter) : [];
153
+ completed = true;
154
+ return {
155
+ status: "ok",
156
+ operation,
157
+ projectUrl,
158
+ dryRun: false,
159
+ sourcesBefore,
160
+ sourcesAfter,
161
+ plannedUploads,
162
+ added,
163
+ warnings,
164
+ tookMs: Date.now() - startedAt,
165
+ };
166
+ }
167
+ finally {
168
+ removeDialogHandler?.();
169
+ removeTerminationHooks?.();
170
+ const chromeHost = chrome?.host ?? "127.0.0.1";
171
+ try {
172
+ await client?.close();
173
+ }
174
+ catch {
175
+ // ignore close failures
176
+ }
177
+ if (completed && isolatedTargetId && chrome?.port) {
178
+ await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
179
+ }
180
+ let keepBrowserOpen = effectiveKeepBrowser;
181
+ let cleanupProfileLock = null;
182
+ let terminatedRecordedChrome = false;
183
+ if (!keepBrowserOpen && manualLogin && tabLease) {
184
+ const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
185
+ if (cleanupLockTimeoutMs > 0) {
186
+ cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
187
+ timeoutMs: cleanupLockTimeoutMs,
188
+ logger,
189
+ sessionId: "project-sources",
190
+ }).catch(() => null);
191
+ }
192
+ keepBrowserOpen = await hasOtherActiveBrowserTabLeases(userDataDir, tabLease.id).catch(() => false);
193
+ if (keepBrowserOpen) {
194
+ logger("[browser] Other ChatGPT tab leases still active; leaving shared Chrome running.");
195
+ }
196
+ else if (reusedChrome && !connectionClosedUnexpectedly) {
197
+ keepBrowserOpen = true;
198
+ logger("[browser] Reused shared Chrome; leaving browser process running.");
199
+ }
200
+ }
201
+ if (tabLease) {
202
+ const handle = tabLease;
203
+ tabLease = null;
204
+ await handle.release().catch(() => undefined);
205
+ }
206
+ if (!keepBrowserOpen && chrome) {
207
+ if (!connectionClosedUnexpectedly) {
208
+ try {
209
+ if (!terminatedRecordedChrome) {
210
+ await chrome.kill();
211
+ }
212
+ }
213
+ catch {
214
+ // ignore kill failures
215
+ }
216
+ }
217
+ if (manualLogin) {
218
+ const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, { connectionClosedUnexpectedly, host: chrome.host ?? "127.0.0.1" });
219
+ if (shouldCleanup) {
220
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
221
+ }
222
+ }
223
+ else {
224
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
225
+ }
226
+ }
227
+ else if (chrome) {
228
+ try {
229
+ chrome.process?.unref();
230
+ }
231
+ catch {
232
+ // best effort
233
+ }
234
+ logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
235
+ }
236
+ if (cleanupProfileLock) {
237
+ await cleanupProfileLock.release().catch(() => undefined);
238
+ }
239
+ }
240
+ }
241
+ async function applyProjectSourcesCookies({ config, network, manualLogin, logger, }) {
242
+ const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
243
+ const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
244
+ if (!cookieSyncEnabled) {
245
+ logger(manualLogin
246
+ ? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
247
+ : "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
248
+ return 0;
249
+ }
250
+ const cookieCount = await syncCookies(network, config.url, config.chromeProfile, logger, {
251
+ allowErrors: config.allowCookieErrors ?? false,
252
+ filterNames: config.cookieNames ?? undefined,
253
+ inlineCookies: config.inlineCookies ?? undefined,
254
+ cookiePath: config.chromeCookiePath ?? undefined,
255
+ waitMs: config.cookieSyncWaitMs ?? 0,
256
+ });
257
+ logger(cookieCount > 0
258
+ ? config.inlineCookies
259
+ ? `Applied ${cookieCount} inline cookies`
260
+ : `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? "Default"}`
261
+ : "No Chrome cookies found; continuing without session reuse");
262
+ return cookieCount;
263
+ }
264
+ async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
265
+ if (!manualLogin) {
266
+ await ensureLoggedIn(runtime, logger, { appliedCookies });
267
+ return;
268
+ }
269
+ const deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
270
+ let lastNotice = 0;
271
+ while (Date.now() < deadline) {
272
+ try {
273
+ await ensureLoggedIn(runtime, logger, { appliedCookies });
274
+ return;
275
+ }
276
+ catch (error) {
277
+ const message = error instanceof Error ? error.message : String(error);
278
+ const retryable = message.toLowerCase().includes("login button") ||
279
+ message.toLowerCase().includes("session not detected");
280
+ if (!retryable) {
281
+ throw error;
282
+ }
283
+ const now = Date.now();
284
+ if (now - lastNotice > 5000) {
285
+ logger("Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...");
286
+ lastNotice = now;
287
+ }
288
+ await delay(1000);
289
+ }
290
+ }
291
+ throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
292
+ }
293
+ async function acquireManualLoginChromeForProjectSources(userDataDir, config, logger) {
294
+ const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
295
+ let launchLock = null;
296
+ if (lockTimeoutMs > 0) {
297
+ launchLock = await acquireProfileRunLock(userDataDir, {
298
+ timeoutMs: lockTimeoutMs,
299
+ logger,
300
+ sessionId: "project-sources",
301
+ });
302
+ }
303
+ try {
304
+ const reusedChrome = await maybeReuseProjectSourcesChrome(userDataDir, logger, {
305
+ waitForPortMs: config.reuseChromeWaitMs,
306
+ });
307
+ const chrome = reusedChrome ??
308
+ (await launchChrome({
309
+ ...config,
310
+ remoteChrome: null,
311
+ }, userDataDir, logger));
312
+ if (chrome.port) {
313
+ await writeDevToolsActivePort(userDataDir, chrome.port);
314
+ if (!reusedChrome && chrome.pid) {
315
+ await writeChromePid(userDataDir, chrome.pid);
316
+ }
317
+ }
318
+ return { chrome, reusedChrome };
319
+ }
320
+ finally {
321
+ await launchLock?.release().catch(() => undefined);
322
+ }
323
+ }
324
+ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {}) {
325
+ const waitForPortMs = Math.max(0, options.waitForPortMs ?? 0);
326
+ let port = await readDevToolsPort(userDataDir);
327
+ if (!port && waitForPortMs > 0) {
328
+ const deadline = Date.now() + waitForPortMs;
329
+ logger(`Waiting up to ${Math.round(waitForPortMs / 1000)}s for shared Chrome to appear...`);
330
+ while (!port && Date.now() < deadline) {
331
+ await delay(250);
332
+ port = await readDevToolsPort(userDataDir);
333
+ }
334
+ }
335
+ let pid = await readChromePid(userDataDir);
336
+ if (!port) {
337
+ const discovered = await findRunningChromeDebugTargetForProfile(userDataDir);
338
+ if (!discovered) {
339
+ return null;
340
+ }
341
+ const probe = await verifyDevToolsReachable({ port: discovered.port });
342
+ if (!probe.ok) {
343
+ logger(`Discovered Chrome for ${userDataDir} on port ${discovered.port} but it was unreachable (${probe.error}); launching new Chrome.`);
344
+ return null;
345
+ }
346
+ await writeDevToolsActivePort(userDataDir, discovered.port);
347
+ await writeChromePid(userDataDir, discovered.pid);
348
+ port = discovered.port;
349
+ pid = discovered.pid;
350
+ logger(`Discovered running Chrome for ${userDataDir}; reusing (DevTools port ${port}, pid ${pid})`);
351
+ return { port, pid, kill: async () => { }, process: undefined };
352
+ }
353
+ const probe = await verifyDevToolsReachable({ port });
354
+ if (!probe.ok) {
355
+ logger(`Recorded Chrome DevTools port ${port} is stale (${probe.error}); launching new Chrome.`);
356
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
357
+ return null;
358
+ }
359
+ logger(`Reusing running Chrome on port ${port} with profile ${userDataDir}`);
360
+ return {
361
+ port,
362
+ pid: pid ?? undefined,
363
+ kill: async () => { },
364
+ process: undefined,
365
+ };
366
+ }
@@ -3,41 +3,65 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { mkdtemp, mkdir, rm } from "node:fs/promises";
5
5
  import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
6
- import { launchChrome, connectToChrome, hideChromeWindow } from "./chromeLifecycle.js";
6
+ import { launchChrome, connectToChrome, hideChromeWindow, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
7
7
  import { resolveBrowserConfig } from "./config.js";
8
8
  import { syncCookies } from "./cookies.js";
9
9
  import { CHATGPT_URL, CONVERSATION_TURN_SELECTOR } from "./constants.js";
10
10
  import { cleanupStaleProfileState } from "./profileState.js";
11
+ import { readDevToolsActivePortInfo } from "./detect.js";
11
12
  import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
13
+ import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
12
14
  export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
13
15
  const recoverSession = deps.recoverSession ??
14
16
  (async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
15
- if (!runtime.chromePort) {
17
+ if (!runtime.chromePort && !runtime.chromeBrowserWSEndpoint) {
16
18
  logger("No running Chrome detected; reopening browser to locate the session.");
17
19
  return recoverSession(runtime, config);
18
20
  }
19
- const host = runtime.chromeHost ?? "127.0.0.1";
20
21
  try {
22
+ const liveRuntime = (await refreshAttachRuntime(runtime).catch(() => runtime)) ?? runtime;
23
+ const host = liveRuntime.chromeHost ?? "127.0.0.1";
24
+ const port = liveRuntime.chromePort ?? inferPortFromBrowserWSEndpoint(liveRuntime.chromeBrowserWSEndpoint);
25
+ const browserWSEndpoint = liveRuntime.chromeBrowserWSEndpoint ?? undefined;
21
26
  const listTargets = deps.listTargets ??
22
- (async () => {
23
- const targets = await CDP.List({ host, port: runtime.chromePort });
24
- return targets;
25
- });
26
- const connect = deps.connect ?? ((options) => CDP(options));
27
+ (async () => (await listRemoteChromeTargets({
28
+ host,
29
+ port: port ?? 9222,
30
+ browserWSEndpoint,
31
+ })));
27
32
  const targetList = (await listTargets());
28
- const target = pickTarget(targetList, runtime);
29
- const client = (await connect({
30
- host,
31
- port: runtime.chromePort,
32
- target: target?.targetId,
33
- }));
34
- const { Runtime, DOM } = client;
33
+ const target = pickTarget(targetList, liveRuntime);
34
+ const connection = browserWSEndpoint && !deps.connect
35
+ ? await connectToRemoteChromeTarget(host, port ?? 9222, logger, {
36
+ browserWSEndpoint,
37
+ targetId: target?.targetId ?? target?.id,
38
+ closeTargetOnDispose: false,
39
+ })
40
+ : {
41
+ client: (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
42
+ ? {
43
+ target: browserWSEndpoint,
44
+ local: true,
45
+ targetId: target?.targetId ?? target?.id,
46
+ }
47
+ : {
48
+ host,
49
+ port,
50
+ target: target?.targetId ?? target?.id,
51
+ })),
52
+ close: async () => undefined,
53
+ };
54
+ const client = connection.client;
55
+ const { Runtime, DOM, Page } = client;
35
56
  if (Runtime?.enable) {
36
57
  await Runtime.enable();
37
58
  }
38
59
  if (DOM && typeof DOM.enable === "function") {
39
60
  await DOM.enable();
40
61
  }
62
+ if (Page && typeof Page.enable === "function") {
63
+ await Page.enable();
64
+ }
41
65
  const ensureConversationOpen = async () => {
42
66
  const { result } = await Runtime.evaluate({
43
67
  expression: "location.href",
@@ -68,19 +92,21 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
68
92
  await ensureConversationOpen();
69
93
  const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
70
94
  (deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
95
+ if (config?.researchMode === "deep") {
96
+ const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
97
+ const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client), timeoutMs + 5_000, "Reattach Deep Research response timed out");
98
+ await connection.close().catch(() => undefined);
99
+ return {
100
+ answerText: researchResult.text,
101
+ answerMarkdown: researchResult.text,
102
+ };
103
+ }
71
104
  const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
72
105
  const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined), timeoutMs + 5_000, "Reattach response timed out");
73
106
  const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
74
107
  const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
75
108
  const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
76
- if (client && typeof client.close === "function") {
77
- try {
78
- await client.close();
79
- }
80
- catch {
81
- // ignore
82
- }
83
- }
109
+ await connection.close().catch(() => undefined);
84
110
  return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
85
111
  }
86
112
  catch (error) {
@@ -89,6 +115,40 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
89
115
  return recoverSession(runtime, config);
90
116
  }
91
117
  }
118
+ async function refreshAttachRuntime(runtime) {
119
+ if (!runtime.chromeProfileRoot) {
120
+ return runtime;
121
+ }
122
+ const host = runtime.chromeHost ?? "127.0.0.1";
123
+ const activePort = await readDevToolsActivePortInfo(runtime.chromeProfileRoot, {
124
+ host,
125
+ });
126
+ if (!activePort) {
127
+ return runtime;
128
+ }
129
+ return {
130
+ ...runtime,
131
+ chromeHost: host,
132
+ chromePort: activePort.port,
133
+ chromeBrowserWSEndpoint: activePort.browserWSEndpoint,
134
+ };
135
+ }
136
+ function inferPortFromBrowserWSEndpoint(browserWSEndpoint) {
137
+ if (!browserWSEndpoint) {
138
+ return undefined;
139
+ }
140
+ try {
141
+ const parsed = new URL(browserWSEndpoint);
142
+ const port = Number.parseInt(parsed.port, 10);
143
+ if (Number.isFinite(port) && port > 0) {
144
+ return port;
145
+ }
146
+ }
147
+ catch {
148
+ // ignore malformed ws endpoints and fall back to caller defaults
149
+ }
150
+ return undefined;
151
+ }
92
152
  async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
93
153
  const resolved = resolveBrowserConfig(config ?? {});
94
154
  const manualLogin = Boolean(resolved.manualLogin);
@@ -151,35 +211,47 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
151
211
  const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
152
212
  const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
153
213
  const timeoutMs = resolved.timeoutMs ?? 120_000;
214
+ const cleanup = async () => {
215
+ if (client && typeof client.close === "function") {
216
+ try {
217
+ await client.close();
218
+ }
219
+ catch {
220
+ // ignore
221
+ }
222
+ }
223
+ if (!resolved.keepBrowser) {
224
+ try {
225
+ await chrome.kill();
226
+ }
227
+ catch {
228
+ // ignore
229
+ }
230
+ if (manualLogin) {
231
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
232
+ }
233
+ else {
234
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
235
+ }
236
+ }
237
+ };
154
238
  const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
155
239
  (deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
240
+ if (resolved.researchMode === "deep") {
241
+ const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
242
+ const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client);
243
+ await cleanup();
244
+ return {
245
+ answerText: researchResult.text,
246
+ answerMarkdown: researchResult.text,
247
+ };
248
+ }
156
249
  const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
157
250
  const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined);
158
251
  const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
159
252
  const markdown = (await captureMarkdown(Runtime, recovered.meta, logger)) ?? recovered.text;
160
253
  const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
161
- if (client && typeof client.close === "function") {
162
- try {
163
- await client.close();
164
- }
165
- catch {
166
- // ignore
167
- }
168
- }
169
- if (!resolved.keepBrowser) {
170
- try {
171
- await chrome.kill();
172
- }
173
- catch {
174
- // ignore
175
- }
176
- if (manualLogin) {
177
- await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
178
- }
179
- else {
180
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
181
- }
182
- }
254
+ await cleanup();
183
255
  return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
184
256
  }
185
257
  async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
@@ -6,7 +6,7 @@ export function pickTarget(targets, runtime) {
6
6
  return undefined;
7
7
  }
8
8
  if (runtime.chromeTargetId) {
9
- const byId = targets.find((t) => t.targetId === runtime.chromeTargetId);
9
+ const byId = targets.find((t) => (t.targetId ?? t.id) === runtime.chromeTargetId);
10
10
  if (byId)
11
11
  return byId;
12
12
  }