@steipete/oracle 0.9.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 (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -1,26 +1,26 @@
1
- import { rm } from 'node:fs/promises';
2
- import { readFileSync } from 'node:fs';
3
- import os from 'node:os';
4
- import net from 'node:net';
5
- import { execFile } from 'node:child_process';
6
- import { promisify } from 'node:util';
7
- import CDP from 'chrome-remote-interface';
8
- import { launch, Launcher } from 'chrome-launcher';
9
- import { cleanupStaleProfileState } from './profileState.js';
10
- import { delay } from './utils.js';
1
+ import { rm } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import net from "node:net";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
7
+ import CDP from "chrome-remote-interface";
8
+ import { launch, Launcher } from "chrome-launcher";
9
+ import { cleanupStaleProfileState } from "./profileState.js";
10
+ import { delay } from "./utils.js";
11
11
  const execFileAsync = promisify(execFile);
12
12
  export async function launchChrome(config, userDataDir, logger) {
13
13
  const connectHost = resolveRemoteDebugHost();
14
- const debugBindAddress = connectHost && connectHost !== '127.0.0.1' ? '0.0.0.0' : connectHost;
14
+ const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost;
15
15
  const debugPort = config.debugPort ?? parseDebugPortEnv();
16
16
  const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress);
17
- const usePatchedLauncher = Boolean(connectHost && connectHost !== '127.0.0.1');
17
+ const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
18
18
  const launcher = usePatchedLauncher
19
19
  ? await launchWithCustomHost({
20
20
  chromeFlags,
21
21
  chromePath: config.chromePath ?? undefined,
22
22
  userDataDir,
23
- host: connectHost ?? '127.0.0.1',
23
+ host: connectHost ?? "127.0.0.1",
24
24
  requestedPort: debugPort ?? undefined,
25
25
  })
26
26
  : await launch({
@@ -30,13 +30,13 @@ export async function launchChrome(config, userDataDir, logger) {
30
30
  handleSIGINT: false,
31
31
  port: debugPort ?? undefined,
32
32
  });
33
- const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
34
- const hostLabel = connectHost ? ` on ${connectHost}` : '';
33
+ const pidLabel = typeof launcher.pid === "number" ? ` (pid ${launcher.pid})` : "";
34
+ const hostLabel = connectHost ? ` on ${connectHost}` : "";
35
35
  logger(`Launched Chrome${pidLabel} on port ${launcher.port}${hostLabel}`);
36
- return Object.assign(launcher, { host: connectHost ?? '127.0.0.1' });
36
+ return Object.assign(launcher, { host: connectHost ?? "127.0.0.1" });
37
37
  }
38
38
  export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logger, opts) {
39
- const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
39
+ const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
40
40
  let handling;
41
41
  const handleSignal = (signal) => {
42
42
  if (handling) {
@@ -46,7 +46,7 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
46
46
  const inFlight = opts?.isInFlight?.() ?? false;
47
47
  const leaveRunning = keepBrowser || inFlight;
48
48
  if (leaveRunning) {
49
- logger(`Received ${signal}; leaving Chrome running${inFlight ? ' (assistant response pending)' : ''}`);
49
+ logger(`Received ${signal}; leaving Chrome running${inFlight ? " (assistant response pending)" : ""}`);
50
50
  }
51
51
  else {
52
52
  logger(`Received ${signal}; terminating Chrome process`);
@@ -69,18 +69,18 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
69
69
  if (opts?.preserveUserDataDir) {
70
70
  // Preserve the profile directory (manual login), but clear reattach hints so we don't
71
71
  // try to reuse a dead DevTools port on the next run.
72
- await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
72
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
73
73
  }
74
74
  else {
75
75
  await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
76
76
  }
77
77
  }
78
78
  })().finally(() => {
79
- const exitCode = signal === 'SIGINT' ? 130 : 1;
79
+ const exitCode = signal === "SIGINT" ? 130 : 1;
80
80
  // Vitest treats any `process.exit()` call as an unhandled failure, even if mocked.
81
81
  // Keep production behavior (hard-exit on signals) while letting tests observe state changes.
82
82
  process.exitCode = exitCode;
83
- const isTestRun = process.env.VITEST === '1' || process.env.NODE_ENV === 'test';
83
+ const isTestRun = process.env.VITEST === "1" || process.env.NODE_ENV === "test";
84
84
  if (!isTestRun) {
85
85
  process.exit(exitCode);
86
86
  }
@@ -96,12 +96,12 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
96
96
  };
97
97
  }
98
98
  export async function hideChromeWindow(chrome, logger) {
99
- if (process.platform !== 'darwin') {
100
- logger('Window hiding is only supported on macOS');
99
+ if (process.platform !== "darwin") {
100
+ logger("Window hiding is only supported on macOS");
101
101
  return;
102
102
  }
103
103
  if (!chrome.pid) {
104
- logger('Unable to hide window: missing Chrome PID');
104
+ logger("Unable to hide window: missing Chrome PID");
105
105
  return;
106
106
  }
107
107
  const script = `tell application "System Events"
@@ -110,8 +110,8 @@ export async function hideChromeWindow(chrome, logger) {
110
110
  end try
111
111
  end tell`;
112
112
  try {
113
- await execFileAsync('osascript', ['-e', script]);
114
- logger('Chrome window hidden (Cmd-H)');
113
+ await execFileAsync("osascript", ["-e", script]);
114
+ logger("Chrome window hidden (Cmd-H)");
115
115
  }
116
116
  catch (error) {
117
117
  const message = error instanceof Error ? error.message : String(error);
@@ -120,10 +120,18 @@ export async function hideChromeWindow(chrome, logger) {
120
120
  }
121
121
  export async function connectToChrome(port, logger, host) {
122
122
  const client = await CDP({ port, host });
123
- logger('Connected to Chrome DevTools protocol');
123
+ logger("Connected to Chrome DevTools protocol");
124
124
  return client;
125
125
  }
126
- export async function connectToRemoteChrome(host, port, logger, targetUrl) {
126
+ export async function connectToRemoteChrome(host, port, logger, targetUrl, browserWSEndpoint, options) {
127
+ if (browserWSEndpoint) {
128
+ return await connectToRemoteChromeTarget(host, port, logger, {
129
+ browserWSEndpoint,
130
+ targetUrl: targetUrl ?? "about:blank",
131
+ closeTargetOnDispose: true,
132
+ approvalWaitMs: options?.approvalWaitMs,
133
+ });
134
+ }
127
135
  if (targetUrl) {
128
136
  const targetConnection = await connectToNewTarget(host, port, targetUrl, logger, {
129
137
  opened: () => `Opened dedicated remote Chrome tab targeting ${targetUrl}`,
@@ -132,12 +140,24 @@ export async function connectToRemoteChrome(host, port, logger, targetUrl) {
132
140
  closeFailed: (targetId, message) => `Failed to close unused remote Chrome tab ${targetId}: ${message}`,
133
141
  });
134
142
  if (targetConnection) {
135
- return { client: targetConnection.client, targetId: targetConnection.targetId };
143
+ return {
144
+ client: targetConnection.client,
145
+ targetId: targetConnection.targetId,
146
+ close: async () => {
147
+ await targetConnection.client.close().catch(() => undefined);
148
+ await closeRemoteChromeTarget(host, port, targetConnection.targetId, logger);
149
+ },
150
+ };
136
151
  }
137
152
  }
138
153
  const fallbackClient = await CDP({ host, port });
139
154
  logger(`Connected to remote Chrome DevTools protocol at ${host}:${port}`);
140
- return { client: fallbackClient };
155
+ return {
156
+ client: fallbackClient,
157
+ close: async () => {
158
+ await fallbackClient.close().catch(() => undefined);
159
+ },
160
+ };
141
161
  }
142
162
  export async function closeRemoteChromeTarget(host, port, targetId, logger) {
143
163
  if (!targetId) {
@@ -154,6 +174,111 @@ export async function closeRemoteChromeTarget(host, port, targetId, logger) {
154
174
  logger(`Failed to close remote Chrome tab ${targetId}: ${message}`);
155
175
  }
156
176
  }
177
+ export async function listRemoteChromeTargets(options) {
178
+ if (!options.browserWSEndpoint) {
179
+ const targets = await CDP.List({ host: options.host, port: options.port });
180
+ return targets;
181
+ }
182
+ const browser = await CDP({ target: options.browserWSEndpoint, local: true });
183
+ try {
184
+ const result = await browser.Target.getTargets();
185
+ return (result.targetInfos ?? []).map((target) => ({
186
+ targetId: target.targetId,
187
+ type: target.type,
188
+ url: target.url,
189
+ }));
190
+ }
191
+ finally {
192
+ await browser.close().catch(() => undefined);
193
+ }
194
+ }
195
+ export async function connectToRemoteChromeTarget(host, port, logger, options) {
196
+ if (!options.browserWSEndpoint) {
197
+ const client = await CDP({ host, port, target: options.targetId });
198
+ return {
199
+ client,
200
+ targetId: options.targetId,
201
+ close: async () => {
202
+ await client.close().catch(() => undefined);
203
+ },
204
+ };
205
+ }
206
+ const browser = await connectToBrowserWebSocket(host, port, options.browserWSEndpoint, logger, options.approvalWaitMs);
207
+ let targetId = options.targetId;
208
+ try {
209
+ if (!targetId) {
210
+ const created = await browser.Target.createTarget({
211
+ url: options.targetUrl ?? "about:blank",
212
+ });
213
+ targetId = created.targetId;
214
+ logger(`Opened dedicated remote Chrome tab targeting ${options.targetUrl ?? "about:blank"}`);
215
+ }
216
+ const attached = await browser.Target.attachToTarget({ targetId, flatten: true });
217
+ const client = createSessionBoundChromeClient(browser, attached.sessionId);
218
+ return {
219
+ client,
220
+ targetId,
221
+ browserWSEndpoint: options.browserWSEndpoint,
222
+ close: async () => {
223
+ await browser.Target.detachFromTarget({ sessionId: attached.sessionId }).catch(() => undefined);
224
+ if (options.closeTargetOnDispose && targetId) {
225
+ await browser.Target.closeTarget({ targetId }).catch(() => undefined);
226
+ }
227
+ await browser.close().catch(() => undefined);
228
+ },
229
+ };
230
+ }
231
+ catch (error) {
232
+ await browser.close().catch(() => undefined);
233
+ throw error;
234
+ }
235
+ }
236
+ async function connectToBrowserWebSocket(host, port, browserWSEndpoint, logger, approvalWaitMs) {
237
+ if (!approvalWaitMs || approvalWaitMs <= 0) {
238
+ return (await CDP({ target: browserWSEndpoint, local: true }));
239
+ }
240
+ logger(`Waiting for Chrome remote debugging approval for ${host}:${port}...`);
241
+ const deadline = Date.now() + approvalWaitMs;
242
+ let lastApprovalError;
243
+ while (Date.now() < deadline) {
244
+ const remainingMs = Math.max(1, deadline - Date.now());
245
+ try {
246
+ return await Promise.race([
247
+ CDP({ target: browserWSEndpoint, local: true }),
248
+ new Promise((_, reject) => {
249
+ setTimeout(() => {
250
+ reject(new Error("__oracle_remote_debugging_approval_timeout__"));
251
+ }, remainingMs);
252
+ }),
253
+ ]);
254
+ }
255
+ catch (error) {
256
+ if (error instanceof Error &&
257
+ error.message === "__oracle_remote_debugging_approval_timeout__") {
258
+ break;
259
+ }
260
+ if (!isRemoteDebuggingApprovalError(error)) {
261
+ throw error;
262
+ }
263
+ lastApprovalError = error;
264
+ await delay(Math.min(500, Math.max(0, deadline - Date.now())));
265
+ }
266
+ }
267
+ const suffix = lastApprovalError instanceof Error && lastApprovalError.message
268
+ ? ` Last Chrome response: ${lastApprovalError.message}`
269
+ : "";
270
+ throw new Error(`Oracle waited ${formatApprovalWait(approvalWaitMs)} for Chrome remote debugging approval at ${host}:${port}. Allow the Chrome prompt or retry after toggling remote debugging.${suffix}`);
271
+ }
272
+ function isRemoteDebuggingApprovalError(error) {
273
+ const message = error instanceof Error ? error.message : String(error ?? "");
274
+ return /unexpected server response:\s*403|remote debugging|forbidden/i.test(message);
275
+ }
276
+ function formatApprovalWait(waitMs) {
277
+ if (waitMs % 1000 === 0) {
278
+ return `${waitMs / 1000}s`;
279
+ }
280
+ return `${waitMs}ms`;
281
+ }
157
282
  async function connectToNewTarget(host, port, url, logger, messages) {
158
283
  try {
159
284
  const target = await CDP.New({ host, port, url });
@@ -182,13 +307,63 @@ async function connectToNewTarget(host, port, url, logger, messages) {
182
307
  }
183
308
  return null;
184
309
  }
310
+ function createSessionBoundChromeClient(browser, sessionId) {
311
+ const browserWithEvents = browser;
312
+ const bindDomain = (domainName) => {
313
+ const domain = browser[domainName];
314
+ const eventName = (name) => `${domainName}.${name}.${sessionId}`;
315
+ return new Proxy((domain ?? {}), {
316
+ get(target, prop, receiver) {
317
+ if (prop === "on") {
318
+ return (name, listener) => {
319
+ const domainEvent = target[name];
320
+ if (typeof domainEvent === "function") {
321
+ return domainEvent(sessionId, listener);
322
+ }
323
+ browserWithEvents.on(eventName(name), listener);
324
+ return () => browserWithEvents.removeListener(eventName(name), listener);
325
+ };
326
+ }
327
+ if (prop === "off" || prop === "removeListener") {
328
+ return (name, listener) => {
329
+ const off = browserWithEvents.off ?? browserWithEvents.removeListener.bind(browserWithEvents);
330
+ off(eventName(name), listener);
331
+ };
332
+ }
333
+ const value = Reflect.get(target, prop, receiver);
334
+ if (typeof value !== "function") {
335
+ return value;
336
+ }
337
+ return (...args) => value(...args, sessionId);
338
+ },
339
+ });
340
+ };
341
+ return {
342
+ ...browser,
343
+ Network: bindDomain("Network"),
344
+ Page: bindDomain("Page"),
345
+ Runtime: bindDomain("Runtime"),
346
+ Input: bindDomain("Input"),
347
+ DOM: bindDomain("DOM"),
348
+ on: browserWithEvents.on.bind(browserWithEvents),
349
+ once: browserWithEvents.once.bind(browserWithEvents),
350
+ off: browserWithEvents.off?.bind(browserWithEvents) ??
351
+ browserWithEvents.removeListener.bind(browserWithEvents),
352
+ removeListener: browserWithEvents.removeListener.bind(browserWithEvents),
353
+ close: async () => {
354
+ await browser.Target.detachFromTarget({ sessionId }).catch(() => undefined);
355
+ },
356
+ };
357
+ }
185
358
  export async function connectWithNewTab(port, logger, initialUrl, host, options) {
186
- const effectiveHost = host ?? '127.0.0.1';
187
- const url = initialUrl ?? 'about:blank';
359
+ const effectiveHost = host ?? "127.0.0.1";
360
+ const url = initialUrl ?? "about:blank";
188
361
  const fallbackToDefault = options?.fallbackToDefault ?? true;
189
362
  const retries = Math.max(0, options?.retries ?? 0);
190
363
  const retryDelayMs = Math.max(0, options?.retryDelayMs ?? 250);
191
- const fallbackLabel = fallbackToDefault ? 'falling back to default target.' : 'strict mode: not falling back.';
364
+ const fallbackLabel = fallbackToDefault
365
+ ? "falling back to default target."
366
+ : "strict mode: not falling back.";
192
367
  let attempt = 0;
193
368
  while (attempt <= retries) {
194
369
  const targetConnection = await connectToNewTarget(effectiveHost, port, url, logger, {
@@ -207,13 +382,13 @@ export async function connectWithNewTab(port, logger, initialUrl, host, options)
207
382
  await delay(retryDelayMs * attempt);
208
383
  }
209
384
  if (!fallbackToDefault) {
210
- throw new Error('Failed to open isolated browser tab; refusing to attach to default target.');
385
+ throw new Error("Failed to open isolated browser tab; refusing to attach to default target.");
211
386
  }
212
387
  const client = await connectToChrome(port, logger, effectiveHost);
213
388
  return { client };
214
389
  }
215
390
  export async function closeTab(port, targetId, logger, host) {
216
- const effectiveHost = host ?? '127.0.0.1';
391
+ const effectiveHost = host ?? "127.0.0.1";
217
392
  try {
218
393
  await CDP.Close({ host: effectiveHost, port, id: targetId });
219
394
  logger(`Closed isolated browser tab (target=${targetId})`);
@@ -223,35 +398,73 @@ export async function closeTab(port, targetId, logger, host) {
223
398
  logger(`Failed to close browser tab ${targetId}: ${message}`);
224
399
  }
225
400
  }
401
+ export async function closeBlankChromeTabs(port, logger, host, options) {
402
+ const effectiveHost = host ?? "127.0.0.1";
403
+ const excluded = new Set([...(options?.excludeTargetIds ?? [])].filter((targetId) => typeof targetId === "string" && targetId.length > 0));
404
+ let targets;
405
+ try {
406
+ targets = (await CDP.List({ host: effectiveHost, port }));
407
+ }
408
+ catch (error) {
409
+ const message = error instanceof Error ? error.message : String(error);
410
+ logger(`Failed to inspect blank Chrome tabs: ${message}`);
411
+ return;
412
+ }
413
+ let closed = 0;
414
+ for (const target of targets) {
415
+ const targetId = target.targetId ?? target.id;
416
+ if (!targetId || excluded.has(targetId) || !isBlankPageTarget(target)) {
417
+ continue;
418
+ }
419
+ try {
420
+ await CDP.Close({ host: effectiveHost, port, id: targetId });
421
+ closed += 1;
422
+ }
423
+ catch (error) {
424
+ const message = error instanceof Error ? error.message : String(error);
425
+ logger(`Failed to close blank Chrome tab ${targetId}: ${message}`);
426
+ }
427
+ }
428
+ if (closed > 0) {
429
+ logger(`Closed ${closed} blank Chrome tab${closed === 1 ? "" : "s"}.`);
430
+ }
431
+ }
432
+ function isBlankPageTarget(target) {
433
+ if (target.type && target.type !== "page") {
434
+ return false;
435
+ }
436
+ const url = (target.url ?? "").trim().toLowerCase();
437
+ return url === "about:blank" || url === "chrome://newtab/" || url === "chrome://new-tab-page/";
438
+ }
226
439
  function buildChromeFlags(headless, debugBindAddress) {
227
440
  const flags = [
228
- '--disable-background-networking',
229
- '--disable-background-timer-throttling',
230
- '--disable-breakpad',
231
- '--disable-client-side-phishing-detection',
232
- '--disable-default-apps',
233
- '--disable-hang-monitor',
234
- '--disable-popup-blocking',
235
- '--disable-prompt-on-repost',
236
- '--disable-sync',
237
- '--disable-translate',
238
- '--metrics-recording-only',
239
- '--no-first-run',
240
- '--safebrowsing-disable-auto-update',
241
- '--disable-features=TranslateUI,AutomationControlled',
242
- '--mute-audio',
243
- '--window-size=1280,720',
244
- '--lang=en-US',
245
- '--accept-lang=en-US,en',
441
+ "--disable-background-networking",
442
+ "--disable-background-timer-throttling",
443
+ "--disable-breakpad",
444
+ "--disable-client-side-phishing-detection",
445
+ "--disable-default-apps",
446
+ "--disable-hang-monitor",
447
+ "--disable-popup-blocking",
448
+ "--disable-prompt-on-repost",
449
+ "--disable-sync",
450
+ "--disable-translate",
451
+ "--metrics-recording-only",
452
+ "--no-first-run",
453
+ "--safebrowsing-disable-auto-update",
454
+ "--disable-features=TranslateUI,AutomationControlled",
455
+ "--mute-audio",
456
+ "--window-size=1280,720",
457
+ "--lang=en-US",
458
+ "--accept-lang=en-US,en",
246
459
  ];
247
- if (process.platform !== 'win32' && !isWsl()) {
248
- flags.push('--password-store=basic', '--use-mock-keychain');
460
+ if (process.platform !== "win32" && !isWsl()) {
461
+ flags.push("--password-store=basic", "--use-mock-keychain");
249
462
  }
250
463
  if (debugBindAddress) {
251
464
  flags.push(`--remote-debugging-address=${debugBindAddress}`);
252
465
  }
253
466
  if (headless) {
254
- flags.push('--headless=new');
467
+ flags.push("--headless=new");
255
468
  }
256
469
  return flags;
257
470
  }
@@ -274,8 +487,8 @@ function resolveRemoteDebugHost() {
274
487
  return null;
275
488
  }
276
489
  try {
277
- const resolv = readFileSync('/etc/resolv.conf', 'utf8');
278
- for (const line of resolv.split('\n')) {
490
+ const resolv = readFileSync("/etc/resolv.conf", "utf8");
491
+ for (const line of resolv.split("\n")) {
279
492
  const match = line.match(/^nameserver\s+([0-9.]+)/);
280
493
  if (match?.[1]) {
281
494
  return match[1];
@@ -288,14 +501,14 @@ function resolveRemoteDebugHost() {
288
501
  return null;
289
502
  }
290
503
  function isWsl() {
291
- if (process.platform !== 'linux') {
504
+ if (process.platform !== "linux") {
292
505
  return false;
293
506
  }
294
507
  if (process.env.WSL_DISTRO_NAME) {
295
508
  return true;
296
509
  }
297
510
  const release = os.release();
298
- return release.toLowerCase().includes('microsoft');
511
+ return release.toLowerCase().includes("microsoft");
299
512
  }
300
513
  async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
301
514
  const launcher = new Launcher({
@@ -310,7 +523,7 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
310
523
  patched.isDebuggerReady = function patchedIsDebuggerReady() {
311
524
  const debugPort = this.port ?? 0;
312
525
  if (!debugPort) {
313
- return Promise.reject(new Error('Missing Chrome debug port'));
526
+ return Promise.reject(new Error("Missing Chrome debug port"));
314
527
  }
315
528
  return new Promise((resolve, reject) => {
316
529
  const client = net.createConnection({ port: debugPort, host });
@@ -320,11 +533,11 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
320
533
  client.destroy();
321
534
  client.unref();
322
535
  };
323
- client.once('error', (err) => {
536
+ client.once("error", (err) => {
324
537
  cleanup();
325
538
  reject(err);
326
539
  });
327
- client.once('connect', () => {
540
+ client.once("connect", () => {
328
541
  cleanup();
329
542
  resolve();
330
543
  });
@@ -1,12 +1,26 @@
1
- import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from './constants.js';
2
- import { normalizeBrowserModelStrategy } from './modelStrategy.js';
3
- import { isTemporaryChatUrl, normalizeChatgptUrl } from './utils.js';
4
- import os from 'node:os';
5
- import path from 'node:path';
1
+ import { CHATGPT_URL, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, } from "./constants.js";
2
+ import { normalizeBrowserModelStrategy } from "./modelStrategy.js";
3
+ import { DEFAULT_MAX_CONCURRENT_CHATGPT_TABS, normalizeMaxConcurrentTabs, } from "./tabLeaseRegistry.js";
4
+ import { isTemporaryChatUrl, normalizeChatgptUrl } from "./utils.js";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ export const DEFAULT_CHATGPT_COOKIE_NAMES = [
8
+ "__Secure-next-auth.session-token",
9
+ "__Secure-next-auth.session-token.0",
10
+ "__Secure-next-auth.session-token.1",
11
+ "_account",
12
+ "cf_clearance",
13
+ "__cf_bm",
14
+ "_cfuvid",
15
+ "CF_Authorization",
16
+ "__cflb",
17
+ ];
6
18
  export const DEFAULT_BROWSER_CONFIG = {
7
19
  chromeProfile: null,
8
20
  chromePath: null,
9
21
  chromeCookiePath: null,
22
+ attachRunning: false,
23
+ browserTabRef: null,
10
24
  url: CHATGPT_URL,
11
25
  chatgptUrl: CHATGPT_URL,
12
26
  timeoutMs: 1_200_000,
@@ -16,11 +30,12 @@ export const DEFAULT_BROWSER_CONFIG = {
16
30
  assistantRecheckTimeoutMs: 120_000,
17
31
  reuseChromeWaitMs: 10_000,
18
32
  profileLockTimeoutMs: 300_000,
33
+ maxConcurrentTabs: DEFAULT_MAX_CONCURRENT_CHATGPT_TABS,
19
34
  autoReattachDelayMs: 0,
20
35
  autoReattachIntervalMs: 0,
21
36
  autoReattachTimeoutMs: 120_000,
22
37
  cookieSync: true,
23
- cookieNames: null,
38
+ cookieNames: DEFAULT_CHATGPT_COOKIE_NAMES,
24
39
  cookieSyncWaitMs: 0,
25
40
  inlineCookies: null,
26
41
  inlineCookiesSource: null,
@@ -32,42 +47,50 @@ export const DEFAULT_BROWSER_CONFIG = {
32
47
  debug: false,
33
48
  allowCookieErrors: false,
34
49
  remoteChrome: null,
50
+ remoteChromeBrowserWSEndpoint: null,
51
+ remoteChromeProfileRoot: null,
35
52
  manualLogin: false,
36
53
  manualLoginProfileDir: null,
37
54
  manualLoginCookieSync: false,
55
+ researchMode: "off",
56
+ archiveConversations: "auto",
38
57
  };
39
58
  export function resolveBrowserConfig(config) {
40
59
  const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
41
- const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim().toLowerCase() === 'true' ||
42
- (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? '').trim() === '1';
60
+ const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? "").trim().toLowerCase() === "true" ||
61
+ (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? "").trim() === "1";
43
62
  const rawUrl = config?.chatgptUrl ?? config?.url ?? DEFAULT_BROWSER_CONFIG.url;
44
63
  const normalizedUrl = normalizeChatgptUrl(rawUrl ?? DEFAULT_BROWSER_CONFIG.url, DEFAULT_BROWSER_CONFIG.url);
45
64
  const desiredModel = config?.desiredModel ?? DEFAULT_BROWSER_CONFIG.desiredModel ?? DEFAULT_MODEL_TARGET;
46
65
  const modelStrategy = normalizeBrowserModelStrategy(config?.modelStrategy) ??
47
66
  DEFAULT_BROWSER_CONFIG.modelStrategy ??
48
67
  DEFAULT_MODEL_STRATEGY;
49
- if (modelStrategy === 'select' && isTemporaryChatUrl(normalizedUrl) && /\bpro\b/i.test(desiredModel)) {
50
- throw new Error('Temporary Chat mode does not expose Pro models in the ChatGPT model picker. ' +
68
+ if (modelStrategy === "select" &&
69
+ isTemporaryChatUrl(normalizedUrl) &&
70
+ /\bpro\b/i.test(desiredModel)) {
71
+ throw new Error("Temporary Chat mode does not expose Pro models in the ChatGPT model picker. " +
51
72
  'Remove "temporary-chat=true" from your browser URL, or use a non-Pro model label (e.g. "GPT-5.2").');
52
73
  }
53
- const isWindows = process.platform === 'win32';
74
+ const isWindows = process.platform === "win32";
54
75
  const manualLogin = config?.manualLogin ?? (isWindows ? true : DEFAULT_BROWSER_CONFIG.manualLogin);
55
76
  const cookieSyncDefault = isWindows ? false : DEFAULT_BROWSER_CONFIG.cookieSync;
56
- const resolvedProfileDir = config?.manualLoginProfileDir ??
57
- process.env.ORACLE_BROWSER_PROFILE_DIR ??
58
- path.join(os.homedir(), '.oracle', 'browser-profile');
77
+ const resolvedProfileDir = resolveManualLoginProfileDir(config?.manualLoginProfileDir, process.env.ORACLE_BROWSER_PROFILE_DIR);
78
+ const researchMode = normalizeResearchMode(config?.researchMode);
79
+ const archiveConversations = normalizeArchiveMode(config?.archiveConversations);
80
+ const defaultTimeoutMs = researchMode === "deep" ? DEEP_RESEARCH_DEFAULT_TIMEOUT_MS : DEFAULT_BROWSER_CONFIG.timeoutMs;
59
81
  return {
60
82
  ...DEFAULT_BROWSER_CONFIG,
61
- ...(config ?? {}),
83
+ ...config,
62
84
  url: normalizedUrl,
63
85
  chatgptUrl: normalizedUrl,
64
- timeoutMs: config?.timeoutMs ?? DEFAULT_BROWSER_CONFIG.timeoutMs,
86
+ timeoutMs: config?.timeoutMs ?? defaultTimeoutMs,
65
87
  debugPort: config?.debugPort ?? debugPortEnv ?? DEFAULT_BROWSER_CONFIG.debugPort,
66
88
  inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
67
89
  assistantRecheckDelayMs: config?.assistantRecheckDelayMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckDelayMs,
68
90
  assistantRecheckTimeoutMs: config?.assistantRecheckTimeoutMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckTimeoutMs,
69
91
  reuseChromeWaitMs: config?.reuseChromeWaitMs ?? DEFAULT_BROWSER_CONFIG.reuseChromeWaitMs,
70
92
  profileLockTimeoutMs: config?.profileLockTimeoutMs ?? DEFAULT_BROWSER_CONFIG.profileLockTimeoutMs,
93
+ maxConcurrentTabs: normalizeMaxConcurrentTabs(config?.maxConcurrentTabs ?? DEFAULT_BROWSER_CONFIG.maxConcurrentTabs),
71
94
  autoReattachDelayMs: config?.autoReattachDelayMs ?? DEFAULT_BROWSER_CONFIG.autoReattachDelayMs,
72
95
  autoReattachIntervalMs: config?.autoReattachIntervalMs ?? DEFAULT_BROWSER_CONFIG.autoReattachIntervalMs,
73
96
  autoReattachTimeoutMs: config?.autoReattachTimeoutMs ?? DEFAULT_BROWSER_CONFIG.autoReattachTimeoutMs,
@@ -84,14 +107,26 @@ export function resolveBrowserConfig(config) {
84
107
  chromeProfile: config?.chromeProfile ?? DEFAULT_BROWSER_CONFIG.chromeProfile,
85
108
  chromePath: config?.chromePath ?? DEFAULT_BROWSER_CONFIG.chromePath,
86
109
  chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
110
+ attachRunning: config?.attachRunning ?? DEFAULT_BROWSER_CONFIG.attachRunning,
111
+ browserTabRef: config?.browserTabRef ?? DEFAULT_BROWSER_CONFIG.browserTabRef,
87
112
  debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
88
113
  allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
114
+ remoteChromeBrowserWSEndpoint: config?.remoteChromeBrowserWSEndpoint ?? DEFAULT_BROWSER_CONFIG.remoteChromeBrowserWSEndpoint,
115
+ remoteChromeProfileRoot: config?.remoteChromeProfileRoot ?? DEFAULT_BROWSER_CONFIG.remoteChromeProfileRoot,
89
116
  thinkingTime: config?.thinkingTime,
117
+ researchMode,
118
+ archiveConversations,
90
119
  manualLogin,
91
120
  manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
92
121
  manualLoginCookieSync: config?.manualLoginCookieSync ?? DEFAULT_BROWSER_CONFIG.manualLoginCookieSync,
93
122
  };
94
123
  }
124
+ function normalizeResearchMode(value) {
125
+ return value === "deep" ? "deep" : "off";
126
+ }
127
+ function normalizeArchiveMode(value) {
128
+ return value === "always" || value === "never" ? value : "auto";
129
+ }
95
130
  function parseDebugPort(raw) {
96
131
  if (!raw)
97
132
  return null;
@@ -101,3 +136,11 @@ function parseDebugPort(raw) {
101
136
  }
102
137
  return value;
103
138
  }
139
+ function resolveManualLoginProfileDir(...candidates) {
140
+ for (const candidate of candidates) {
141
+ const profileDir = candidate?.trim();
142
+ if (profileDir)
143
+ return profileDir;
144
+ }
145
+ return path.join(os.homedir(), ".oracle", "browser-profile");
146
+ }