@steipete/oracle 0.18.0 → 0.19.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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +119 -16
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. package/dist/src/browser/windowsCookies.js +0 -219
@@ -1,19 +1,29 @@
1
- import { rm } from "node:fs/promises";
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import * as childProcess from "node:child_process";
2
3
  import net from "node:net";
4
+ import path from "node:path";
3
5
  import CDP from "chrome-remote-interface";
4
- import { launch, Launcher } from "chrome-launcher";
6
+ import { launch, Launcher, } from "chrome-launcher";
5
7
  import { cleanupStaleProfileState } from "./profileState.js";
6
8
  import { delay } from "./utils.js";
7
9
  import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
8
10
  export async function launchChrome(config, userDataDir, logger) {
9
11
  const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
10
12
  const debugPort = config.debugPort ?? parseDebugPortEnv();
13
+ const usingCopiedProfile = Boolean(config.copyProfileSource);
14
+ const detachSharedChrome = shouldDetachSharedChrome(config);
15
+ const launchedProfileDirectory = usingCopiedProfile && config.chromeProfile ? config.chromeProfile : "Default";
16
+ await prepareChromeWindowStateForHiddenLaunch({
17
+ config,
18
+ userDataDir,
19
+ profileDirectory: launchedProfileDirectory,
20
+ logger,
21
+ });
11
22
  const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress, config.hideWindow ?? false);
12
23
  // copy-profile reuses a copied signed-in profile whose cookies are
13
24
  // Keychain-encrypted, so it must launch with the real Keychain (not mocked):
14
25
  // strip the keychain-mocking flags from both chrome-launcher's defaults and
15
26
  // Oracle's set, and ignore the defaults so they aren't re-added.
16
- const usingCopiedProfile = Boolean(config.copyProfileSource);
17
27
  if (usingCopiedProfile && config.chromeProfile) {
18
28
  chromeFlags.push(`--profile-directory=${config.chromeProfile}`);
19
29
  }
@@ -26,38 +36,231 @@ export async function launchChrome(config, userDataDir, logger) {
26
36
  host: connectHost ?? "127.0.0.1",
27
37
  requestedPort: debugPort ?? undefined,
28
38
  ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
39
+ detachSharedChrome,
29
40
  })
30
- : await launch({
41
+ : await launchWithStableProcessLifecycle({
31
42
  chromePath: config.chromePath ?? undefined,
32
43
  chromeFlags: launchOptions.chromeFlags,
33
44
  userDataDir,
34
45
  handleSIGINT: false,
35
46
  port: debugPort ?? undefined,
36
47
  ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
37
- });
48
+ }, detachSharedChrome);
38
49
  const pidLabel = typeof launcher.pid === "number" ? ` (pid ${launcher.pid})` : "";
39
50
  const hostLabel = connectHost ? ` on ${connectHost}` : "";
40
51
  logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
52
+ if (detachSharedChrome) {
53
+ logger("[browser] Browser control: Windows Chrome lifecycle detached=true; windowsHide=true.");
54
+ }
41
55
  return Object.assign(launcher, { host: connectHost ?? "127.0.0.1" });
42
56
  }
43
- export async function positionChromeWindowOffscreen(client, logger) {
57
+ function shouldDetachSharedChrome(config, platform = process.platform) {
58
+ return platform === "win32" && config.manualLogin === true && !config.copyProfileSource;
59
+ }
60
+ export const shouldDetachSharedChromeForTest = shouldDetachSharedChrome;
61
+ const spawnDetachedChromeOnWindows = ((command, args, options) => {
62
+ const child = childProcess.spawn(command, args, resolveChromeChildSpawnOptions(options, "win32"));
63
+ child.unref();
64
+ return child;
65
+ });
66
+ function resolveChromeChildSpawnOptions(options, platform = process.platform) {
67
+ return platform === "win32"
68
+ ? {
69
+ ...options,
70
+ detached: true,
71
+ windowsHide: true,
72
+ }
73
+ : options;
74
+ }
75
+ export function resolveChromeChildSpawnOptionsForTest(options, platform) {
76
+ return resolveChromeChildSpawnOptions(options, platform);
77
+ }
78
+ function chromeLauncherModuleOverrides(detachSharedChrome, platform = process.platform) {
79
+ return detachSharedChrome && platform === "win32"
80
+ ? { spawn: spawnDetachedChromeOnWindows }
81
+ : undefined;
82
+ }
83
+ async function launchWithStableProcessLifecycle(options, detachSharedChrome) {
84
+ if (!detachSharedChrome) {
85
+ return launch(options);
86
+ }
87
+ const launcher = new Launcher(options, chromeLauncherModuleOverrides(detachSharedChrome));
88
+ await launcher.launch();
89
+ return launchedChromeFromLauncher(launcher);
90
+ }
91
+ function launchedChromeFromLauncher(launcher) {
92
+ return {
93
+ pid: launcher.pid ?? 0,
94
+ port: launcher.port ?? 0,
95
+ process: launcher.chromeProcess,
96
+ kill: () => launcher.kill(),
97
+ remoteDebuggingPipes: launcher.remoteDebuggingPipes,
98
+ };
99
+ }
100
+ export async function positionChromeWindowOffscreen(client, userDataDir, logger) {
44
101
  if (process.platform !== "darwin") {
45
102
  logger("Window hiding is only supported on macOS");
46
103
  return;
47
104
  }
105
+ let savedState = false;
48
106
  try {
49
107
  const { windowId } = await client.Browser.getWindowForTarget();
108
+ if (!(await readSavedChromeWindowState(userDataDir))) {
109
+ const { bounds } = await client.Browser.getWindowBounds({ windowId });
110
+ await writeSavedChromeWindowState(userDataDir, bounds);
111
+ savedState = true;
112
+ }
50
113
  await client.Browser.setWindowBounds({
51
114
  windowId,
52
115
  bounds: { left: -32_000, top: -32_000, windowState: "normal" },
53
116
  });
54
- logger("Chrome window positioned off-screen");
55
117
  }
56
118
  catch (error) {
119
+ if (savedState) {
120
+ await rm(chromeWindowStatePath(userDataDir), { force: true }).catch(() => undefined);
121
+ }
57
122
  const message = error instanceof Error ? error.message : String(error);
58
123
  logger(`Failed to position Chrome window off-screen: ${message}`);
124
+ return;
125
+ }
126
+ logger("Chrome window positioned off-screen");
127
+ }
128
+ export async function positionChromeWindowOnscreen(client, userDataDir, logger) {
129
+ if (process.platform !== "darwin") {
130
+ return;
131
+ }
132
+ try {
133
+ const savedState = await readSavedChromeWindowState(userDataDir);
134
+ if (!savedState) {
135
+ return;
136
+ }
137
+ const { windowId } = await client.Browser.getWindowForTarget();
138
+ await client.Browser.setWindowBounds({
139
+ windowId,
140
+ bounds: restoreWindowBounds(savedState.bounds),
141
+ });
142
+ await rm(chromeWindowStatePath(userDataDir), { force: true });
143
+ logger("Chrome window restored to its pre-hide bounds");
144
+ }
145
+ catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ logger(`Failed to position Chrome window on-screen: ${message}`);
148
+ }
149
+ }
150
+ const CHROME_WINDOW_STATE_FILENAME = "oracle-window-state.json";
151
+ const DEFAULT_VISIBLE_WINDOW_BOUNDS = {
152
+ left: 80,
153
+ top: 80,
154
+ width: 1280,
155
+ height: 720,
156
+ windowState: "normal",
157
+ };
158
+ function chromeWindowStatePath(userDataDir) {
159
+ return path.join(userDataDir, CHROME_WINDOW_STATE_FILENAME);
160
+ }
161
+ async function readSavedChromeWindowState(userDataDir) {
162
+ try {
163
+ const parsed = JSON.parse(await readFile(chromeWindowStatePath(userDataDir), "utf8"));
164
+ const bounds = parseChromeWindowBounds(parsed.bounds);
165
+ if (parsed.version !== 1 || !bounds) {
166
+ return null;
167
+ }
168
+ return { version: 1, bounds };
169
+ }
170
+ catch {
171
+ return null;
59
172
  }
60
173
  }
174
+ async function writeSavedChromeWindowState(userDataDir, bounds) {
175
+ await mkdir(userDataDir, { recursive: true });
176
+ await writeFile(chromeWindowStatePath(userDataDir), `${JSON.stringify({ version: 1, bounds })}\n`, "utf8");
177
+ }
178
+ async function prepareChromeWindowStateForHiddenLaunch({ config, userDataDir, profileDirectory, logger, }) {
179
+ if (process.platform !== "darwin" ||
180
+ config.headless ||
181
+ !config.hideWindow ||
182
+ (await readSavedChromeWindowState(userDataDir))) {
183
+ return;
184
+ }
185
+ const bounds = (await readPersistedChromeWindowBounds(userDataDir, profileDirectory)) ??
186
+ DEFAULT_VISIBLE_WINDOW_BOUNDS;
187
+ await writeSavedChromeWindowState(userDataDir, bounds);
188
+ logger("Recorded Chrome window placement before hidden launch");
189
+ }
190
+ async function readPersistedChromeWindowBounds(userDataDir, profileDirectory) {
191
+ const root = path.resolve(userDataDir);
192
+ const profile = path.resolve(root, profileDirectory);
193
+ if (path.dirname(profile) !== root) {
194
+ return null;
195
+ }
196
+ try {
197
+ const preferences = JSON.parse(await readFile(path.join(profile, "Preferences"), "utf8"));
198
+ return persistedPlacementToBounds(preferences.browser?.window_placement);
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ function persistedPlacementToBounds(placement) {
205
+ if (!placement) {
206
+ return null;
207
+ }
208
+ if (placement.maximized === true) {
209
+ return { windowState: "maximized" };
210
+ }
211
+ const left = finiteNumber(placement.left);
212
+ const top = finiteNumber(placement.top);
213
+ const right = finiteNumber(placement.right);
214
+ const bottom = finiteNumber(placement.bottom);
215
+ if (left === null || top === null || right === null || bottom === null) {
216
+ return null;
217
+ }
218
+ const width = right - left;
219
+ const height = bottom - top;
220
+ if (width <= 0 || height <= 0) {
221
+ return null;
222
+ }
223
+ return { left, top, width, height, windowState: "normal" };
224
+ }
225
+ function parseChromeWindowBounds(value) {
226
+ if (!value || typeof value !== "object") {
227
+ return null;
228
+ }
229
+ const bounds = value;
230
+ const windowState = bounds.windowState ?? "normal";
231
+ if (windowState !== "normal") {
232
+ return ["minimized", "maximized", "fullscreen"].includes(windowState) ? { windowState } : null;
233
+ }
234
+ const left = finiteNumber(bounds.left);
235
+ const top = finiteNumber(bounds.top);
236
+ const width = finiteNumber(bounds.width);
237
+ const height = finiteNumber(bounds.height);
238
+ if (left === null ||
239
+ top === null ||
240
+ width === null ||
241
+ height === null ||
242
+ width <= 0 ||
243
+ height <= 0) {
244
+ return null;
245
+ }
246
+ return { left, top, width, height, windowState: "normal" };
247
+ }
248
+ function finiteNumber(value) {
249
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
250
+ }
251
+ function restoreWindowBounds(bounds) {
252
+ const windowState = bounds.windowState ?? "normal";
253
+ if (windowState !== "normal") {
254
+ return { windowState };
255
+ }
256
+ return {
257
+ left: bounds.left ?? 80,
258
+ top: bounds.top ?? 80,
259
+ width: bounds.width,
260
+ height: bounds.height,
261
+ windowState: "normal",
262
+ };
263
+ }
61
264
  export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
62
265
  const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
63
266
  let handling;
@@ -68,7 +271,8 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
68
271
  handling = true;
69
272
  const inFlight = opts?.isInFlight?.() ?? false;
70
273
  const forceCleanup = opts?.forceProfileCleanup ?? false;
71
- const leaveRunning = (keepBrowser || inFlight) && !forceCleanup;
274
+ const preserveSharedChrome = opts?.preserveSharedChromeOnSignal ?? false;
275
+ const leaveRunning = (keepBrowser || inFlight || preserveSharedChrome) && !forceCleanup;
72
276
  if (leaveRunning) {
73
277
  logger(`Received ${signal}; leaving Chrome running${inFlight ? " (assistant response pending)" : ""}`);
74
278
  }
@@ -136,11 +340,12 @@ export async function connectToRemoteChrome(host, port, logger, targetUrl, brows
136
340
  approvalWaitMs: options?.approvalWaitMs,
137
341
  });
138
342
  }
139
- if (targetUrl) {
140
- const targetConnection = await connectToNewTarget(host, port, targetUrl, logger, {
141
- opened: () => `Opened dedicated remote Chrome tab targeting ${targetUrl}`,
142
- openFailed: (message) => `Failed to open dedicated remote Chrome tab (${message}); falling back to first target.`,
143
- attachFailed: (targetId, message) => `Failed to attach to dedicated remote Chrome tab ${targetId} (${message}); falling back to first target.`,
343
+ const newTargetUrl = targetUrl || (options?.fallbackToDefault === false ? "about:blank" : undefined);
344
+ if (newTargetUrl) {
345
+ const targetConnection = await connectToNewTarget(host, port, newTargetUrl, logger, {
346
+ opened: () => `Opened dedicated remote Chrome tab targeting ${newTargetUrl}`,
347
+ openFailed: (message) => `Failed to open dedicated remote Chrome tab (${message}); ${options?.fallbackToDefault === false ? "refusing to reuse an unrelated tab" : "falling back to first target"}.`,
348
+ attachFailed: (targetId, message) => `Failed to attach to dedicated remote Chrome tab ${targetId} (${message}); ${options?.fallbackToDefault === false ? "refusing to reuse an unrelated tab" : "falling back to first target"}.`,
144
349
  closeFailed: (targetId, message) => `Failed to close unused remote Chrome tab ${targetId}: ${message}`,
145
350
  });
146
351
  if (targetConnection) {
@@ -153,6 +358,9 @@ export async function connectToRemoteChrome(host, port, logger, targetUrl, brows
153
358
  },
154
359
  };
155
360
  }
361
+ if (options?.fallbackToDefault === false) {
362
+ throw new Error("Unable to create a dedicated remote Chrome tab; refusing to reuse an unrelated conversation.");
363
+ }
156
364
  }
157
365
  const fallbackClient = await CDP({ host, port });
158
366
  logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
@@ -183,7 +391,7 @@ export async function listRemoteChromeTargets(options) {
183
391
  const targets = await CDP.List({ host: options.host, port: options.port });
184
392
  return targets;
185
393
  }
186
- const browser = await CDP({ target: options.browserWSEndpoint, local: true });
394
+ const browser = await connectToBrowserWebSocket(options.host, options.port, options.browserWSEndpoint, options.logger ?? (() => { }), options.approvalWaitMs);
187
395
  try {
188
396
  const result = await browser.Target.getTargets();
189
397
  return (result.targetInfos ?? []).map((target) => ({
@@ -241,33 +449,54 @@ async function connectToBrowserWebSocket(host, port, browserWSEndpoint, logger,
241
449
  if (!approvalWaitMs || approvalWaitMs <= 0) {
242
450
  return (await CDP({ target: browserWSEndpoint, local: true }));
243
451
  }
244
- logger(`Waiting for Chrome remote debugging approval for ${host}:${port}...`);
245
- const deadline = Date.now() + approvalWaitMs;
452
+ logger(`[browser] Waiting for Chrome remote debugging approval for ${host}:${port}...`);
453
+ const startedAt = Date.now();
454
+ const deadline = startedAt + approvalWaitMs;
455
+ const progress = setInterval(() => {
456
+ logger(`[browser] Still waiting for Chrome remote debugging approval for ${host}:${port} (${formatApprovalWait(Date.now() - startedAt)} elapsed). Click Allow in an open Chrome window.`);
457
+ }, 15_000);
246
458
  let lastApprovalError;
247
- while (Date.now() < deadline) {
248
- const remainingMs = Math.max(1, deadline - Date.now());
249
- try {
250
- return await Promise.race([
251
- CDP({ target: browserWSEndpoint, local: true }),
252
- new Promise((_, reject) => {
253
- setTimeout(() => {
254
- reject(new Error("__oracle_remote_debugging_approval_timeout__"));
255
- }, remainingMs);
256
- }),
257
- ]);
258
- }
259
- catch (error) {
260
- if (error instanceof Error &&
261
- error.message === "__oracle_remote_debugging_approval_timeout__") {
262
- break;
459
+ try {
460
+ while (Date.now() < deadline) {
461
+ const remainingMs = Math.max(1, deadline - Date.now());
462
+ let timeout;
463
+ let expired = false;
464
+ try {
465
+ const connecting = CDP({ target: browserWSEndpoint, local: true }).then(async (client) => {
466
+ // An approval arriving after our deadline must not leak a connection.
467
+ if (expired)
468
+ await client.close().catch(() => undefined);
469
+ return client;
470
+ });
471
+ return await Promise.race([
472
+ connecting,
473
+ new Promise((_, reject) => {
474
+ timeout = setTimeout(() => {
475
+ expired = true;
476
+ reject(new Error("__oracle_remote_debugging_approval_timeout__"));
477
+ }, remainingMs);
478
+ }),
479
+ ]);
480
+ }
481
+ catch (error) {
482
+ if (error instanceof Error &&
483
+ error.message === "__oracle_remote_debugging_approval_timeout__") {
484
+ break;
485
+ }
486
+ if (!isRemoteDebuggingApprovalError(error)) {
487
+ throw error;
488
+ }
489
+ lastApprovalError = error;
263
490
  }
264
- if (!isRemoteDebuggingApprovalError(error)) {
265
- throw error;
491
+ finally {
492
+ clearTimeout(timeout);
266
493
  }
267
- lastApprovalError = error;
268
494
  await delay(Math.min(500, Math.max(0, deadline - Date.now())));
269
495
  }
270
496
  }
497
+ finally {
498
+ clearInterval(progress);
499
+ }
271
500
  const suffix = lastApprovalError instanceof Error && lastApprovalError.message
272
501
  ? ` Last Chrome response: ${lastApprovalError.message}`
273
502
  : "";
@@ -598,7 +827,7 @@ function parseDebugPortEnv() {
598
827
  }
599
828
  return value;
600
829
  }
601
- async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
830
+ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, detachSharedChrome, }) {
602
831
  const launcher = new Launcher({
603
832
  chromePath: chromePath ?? undefined,
604
833
  chromeFlags,
@@ -606,7 +835,7 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
606
835
  handleSIGINT: false,
607
836
  port: requestedPort ?? undefined,
608
837
  ignoreDefaultFlags,
609
- });
838
+ }, chromeLauncherModuleOverrides(detachSharedChrome));
610
839
  if (host) {
611
840
  const patched = launcher;
612
841
  patched.isDebuggerReady = function patchedIsDebuggerReady() {
@@ -634,13 +863,8 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
634
863
  };
635
864
  }
636
865
  await launcher.launch();
637
- const kill = async () => launcher.kill();
638
866
  return {
639
- pid: launcher.pid ?? undefined,
640
- port: launcher.port ?? 0,
641
- process: launcher.chromeProcess,
642
- kill,
867
+ ...launchedChromeFromLauncher(launcher),
643
868
  host: host ?? undefined,
644
- remoteDebuggingPipes: launcher.remoteDebuggingPipes,
645
869
  };
646
870
  }
@@ -2,6 +2,7 @@ import { CHATGPT_URL, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, DEFAULT_MODEL_STRATEGY,
2
2
  import { normalizeBrowserModelStrategy } from "./modelStrategy.js";
3
3
  import { DEFAULT_MAX_CONCURRENT_CHATGPT_TABS, normalizeMaxConcurrentTabs, } from "./tabLeaseRegistry.js";
4
4
  import { normalizeChatgptUrl } from "./utils.js";
5
+ import { parseDuration } from "../duration.js";
5
6
  import os from "node:os";
6
7
  import path from "node:path";
7
8
  export const DEFAULT_CHATGPT_COOKIE_NAMES = [
@@ -26,6 +27,7 @@ export const DEFAULT_BROWSER_CONFIG = {
26
27
  timeoutMs: 1_200_000,
27
28
  debugPort: null,
28
29
  inputTimeoutMs: 60_000,
30
+ approvalWaitMs: 20_000,
29
31
  attachmentTimeoutMs: 45_000,
30
32
  assistantRecheckDelayMs: 0,
31
33
  assistantRecheckTimeoutMs: 120_000,
@@ -82,6 +84,7 @@ export function resolveBrowserConfig(config) {
82
84
  timeoutMs: config?.timeoutMs ?? defaultTimeoutMs,
83
85
  debugPort: config?.debugPort ?? debugPortEnv ?? DEFAULT_BROWSER_CONFIG.debugPort,
84
86
  inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
87
+ approvalWaitMs: resolveBrowserApprovalWait(config?.approvalWaitMs),
85
88
  attachmentTimeoutMs: config?.attachmentTimeoutMs ?? DEFAULT_BROWSER_CONFIG.attachmentTimeoutMs,
86
89
  assistantRecheckDelayMs: config?.assistantRecheckDelayMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckDelayMs,
87
90
  assistantRecheckTimeoutMs: config?.assistantRecheckTimeoutMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckTimeoutMs,
@@ -154,3 +157,13 @@ function resolveManualLoginProfileDir(...candidates) {
154
157
  }
155
158
  return path.join(os.homedir(), ".oracle", "browser-profile");
156
159
  }
160
+ export function resolveBrowserApprovalWait(value) {
161
+ const raw = value ?? process.env.ORACLE_BROWSER_APPROVAL_WAIT?.trim();
162
+ if (raw === undefined || raw === "")
163
+ return DEFAULT_BROWSER_CONFIG.approvalWaitMs;
164
+ const parsed = typeof raw === "number" ? raw : parseDuration(raw, Number.NaN);
165
+ if (!Number.isSafeInteger(parsed) || parsed <= 0 || parsed > 2_147_483_647) {
166
+ throw new Error("Invalid browser approval wait: use a positive duration up to 2147483647ms (for example --browser-approval-wait 5m, browser.approvalWaitMs: 300000, or ORACLE_BROWSER_APPROVAL_WAIT=5m).");
167
+ }
168
+ return parsed;
169
+ }
@@ -291,7 +291,7 @@ function deriveDevToolsProfileRoot(activePortPath) {
291
291
  }
292
292
  return parentDir;
293
293
  }
294
- function formatWebSocketHost(host) {
294
+ export function formatWebSocketHost(host) {
295
295
  if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) {
296
296
  return `[${host}]`;
297
297
  }