@andrewting19/oracle 0.9.8 → 0.10.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.
@@ -123,14 +123,14 @@ export async function connectToChrome(port, logger, host) {
123
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, rootSession) {
127
127
  if (targetUrl) {
128
128
  const targetConnection = await connectToNewTarget(host, port, targetUrl, logger, {
129
- opened: () => `Opened dedicated remote Chrome tab targeting ${targetUrl}`,
129
+ opened: (targetId) => `Opened dedicated remote Chrome ${rootSession ? "hidden " : ""}target ${targetId}`,
130
130
  openFailed: (message) => `Failed to open dedicated remote Chrome tab (${message}); falling back to first target.`,
131
131
  attachFailed: (targetId, message) => `Failed to attach to dedicated remote Chrome tab ${targetId} (${message}); falling back to first target.`,
132
132
  closeFailed: (targetId, message) => `Failed to close unused remote Chrome tab ${targetId}: ${message}`,
133
- });
133
+ }, rootSession);
134
134
  if (targetConnection) {
135
135
  return { client: targetConnection.client, targetId: targetConnection.targetId };
136
136
  }
@@ -154,9 +154,35 @@ export async function closeRemoteChromeTarget(host, port, targetId, logger) {
154
154
  logger(`Failed to close remote Chrome tab ${targetId}: ${message}`);
155
155
  }
156
156
  }
157
- async function connectToNewTarget(host, port, url, logger, messages) {
157
+ async function connectToNewTarget(host, port, url, logger, messages, rootSession) {
158
158
  try {
159
- const target = await CDP.New({ host, port, url });
159
+ let target;
160
+ if (rootSession) {
161
+ // Hidden target: not in tab strip, no window, no focus steal.
162
+ // Lifetime tied to rootSession (kept alive by the serve).
163
+ const result = await rootSession.Target.createTarget({
164
+ url,
165
+ hidden: true,
166
+ background: true,
167
+ });
168
+ target = { id: result.targetId };
169
+ }
170
+ else {
171
+ // No root session: try background + focus:false, fallback to CDP.New()
172
+ try {
173
+ const bgClient = await CDP({ host, port });
174
+ const result = await bgClient.Target.createTarget({
175
+ url,
176
+ background: true,
177
+ focus: false,
178
+ });
179
+ target = { id: result.targetId };
180
+ await bgClient.close();
181
+ }
182
+ catch (_bgErr) {
183
+ target = await CDP.New({ host, port, url });
184
+ }
185
+ }
160
186
  try {
161
187
  const client = await CDP({ host, port, target: target.id });
162
188
  if (messages.opened) {
@@ -1023,8 +1023,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1023
1023
  if (!remoteChromeConfig) {
1024
1024
  throw new Error("Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.");
1025
1025
  }
1026
- const { host, port } = remoteChromeConfig;
1027
- logger(`Connecting to remote Chrome at ${host}:${port}`);
1026
+ const { host, port, rootSession } = remoteChromeConfig;
1027
+ logger(`Connecting to remote Chrome at ${host}:${port}${rootSession ? " (hidden targets)" : ""}`);
1028
1028
  let client = null;
1029
1029
  let remoteTargetId = null;
1030
1030
  let lastUrl;
@@ -1054,7 +1054,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1054
1054
  let stopThinkingMonitor = null;
1055
1055
  let removeDialogHandler = null;
1056
1056
  try {
1057
- const connection = await connectToRemoteChrome(host, port, logger, config.url);
1057
+ const connection = await connectToRemoteChrome(host, port, logger, config.url, rootSession);
1058
1058
  client = connection.client;
1059
1059
  remoteTargetId = connection.targetId ?? null;
1060
1060
  await emitRuntimeHint();
@@ -1069,8 +1069,19 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1069
1069
  }
1070
1070
  await Promise.all(domainEnablers);
1071
1071
  removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
1072
- // Skip cookie sync for remote Chrome - it already has cookies
1073
- logger("Skipping cookie sync for remote Chrome (using existing session)");
1072
+ // Sync cookies into this tab for remote Chrome (needed for headed + Cloudflare).
1073
+ try {
1074
+ const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, {
1075
+ allowErrors: true,
1076
+ filterNames: config.cookieNames ?? undefined,
1077
+ cookiePath: config.chromeCookiePath ?? undefined,
1078
+ waitMs: config.cookieSyncWaitMs ?? 0,
1079
+ });
1080
+ logger(`Remote tab: synced ${cookieCount} cookies`);
1081
+ }
1082
+ catch (_syncErr) {
1083
+ logger("Remote tab: cookie sync failed, continuing with existing session");
1084
+ }
1074
1085
  await navigateToChatGPT(Page, Runtime, config.url, logger);
1075
1086
  await ensureNotBlocked(Runtime, config.headless, logger);
1076
1087
  await ensureLoggedIn(Runtime, logger, { remoteSession: true });
@@ -12,7 +12,6 @@ import { CHATGPT_URL } from "../browser/constants.js";
12
12
  import { getCliVersion } from "../version.js";
13
13
  import { cleanupStaleProfileState, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "../browser/profileState.js";
14
14
  import { normalizeChatgptUrl } from "../browser/utils.js";
15
- import { connectToChrome } from "../browser/chromeLifecycle.js";
16
15
  async function findAvailablePort() {
17
16
  return await new Promise((resolve, reject) => {
18
17
  const srv = net.createServer();
@@ -270,12 +269,15 @@ export async function serveRemote(options = {}) {
270
269
  console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
271
270
  }
272
271
  // Launch a shared Chrome instance for concurrent tab-based runs.
272
+ // Each run gets a hidden CDP target (no tab strip entry, no window, no focus stealing).
273
273
  let sharedChrome;
274
+ let sharedChromeProcess = null;
275
+ let sharedRootSession = null;
274
276
  if (!preferManualLogin) {
275
277
  try {
276
278
  const userDataDir = await mkdtemp(path.join(os.tmpdir(), "oracle-serve-chrome-"));
277
- // Launch headed Chrome with --no-startup-window: no visible window, no focus stealing,
278
- // but NOT headless (avoids Cloudflare fingerprinting). CDP creates tabs as needed.
279
+ // Launch headed Chrome with no initial window. Headed mode passes Cloudflare;
280
+ // hidden CDP targets prevent focus stealing on macOS.
279
281
  const { launch: launchChromeRaw } = await import("chrome-launcher");
280
282
  const chrome = await launchChromeRaw({
281
283
  chromeFlags: [
@@ -296,13 +298,23 @@ export async function serveRemote(options = {}) {
296
298
  userDataDir,
297
299
  handleSIGINT: false,
298
300
  });
299
- sharedChrome = { host: "127.0.0.1", port: chrome.port };
300
- console.log(`Shared Chrome launched (pid ${chrome.pid}, port ${chrome.port}, no-startup-window). Concurrent runs will use isolated tabs.`);
301
- // Sync ChatGPT cookies into the shared Chrome so all tabs are authenticated.
301
+ sharedChromeProcess = chrome;
302
+ console.log(`Shared Chrome launched (pid ${chrome.pid}, port ${chrome.port}, headed + no-startup-window). Runs will use hidden CDP targets.`);
303
+ // Create a persistent root CDP session. Hidden target lifetimes are tied to
304
+ // the session that created them, so this must stay alive for the serve lifetime.
305
+ const CDP = (await import("chrome-remote-interface")).default;
306
+ const rootSession = await CDP({ host: "127.0.0.1", port: chrome.port });
307
+ sharedRootSession = rootSession;
308
+ // Sync ChatGPT cookies into the shared Chrome via a hidden target.
302
309
  try {
303
310
  const { syncCookies } = await import("../browser/cookies.js");
304
- const client = await connectToChrome(chrome.port, console.log);
305
- const { Network } = client;
311
+ const { targetId } = await rootSession.Target.createTarget({
312
+ url: "about:blank",
313
+ hidden: true,
314
+ background: true,
315
+ });
316
+ const syncClient = await CDP({ host: "127.0.0.1", port: chrome.port, target: targetId });
317
+ const { Network } = syncClient;
306
318
  await Network.enable({});
307
319
  const syncLogger = ((msg) => { if (msg)
308
320
  console.log(`[shared-chrome] ${msg}`); });
@@ -311,12 +323,14 @@ export async function serveRemote(options = {}) {
311
323
  allowErrors: true,
312
324
  });
313
325
  console.log(`Synced ${count} ChatGPT cookies into shared Chrome.`);
314
- await client.close();
326
+ await syncClient.close();
327
+ await rootSession.Target.closeTarget({ targetId });
315
328
  }
316
329
  catch (cookieErr) {
317
330
  const msg = cookieErr instanceof Error ? cookieErr.message : String(cookieErr);
318
331
  console.log(`Warning: failed to sync cookies into shared Chrome (${msg}). Runs may need to authenticate.`);
319
332
  }
333
+ sharedChrome = { host: "127.0.0.1", port: chrome.port, rootSession };
320
334
  }
321
335
  catch (error) {
322
336
  const message = error instanceof Error ? error.message : String(error);
@@ -332,10 +346,27 @@ export async function serveRemote(options = {}) {
332
346
  await new Promise((resolve) => {
333
347
  const shutdown = () => {
334
348
  console.log("Shutting down remote service...");
335
- server
336
- .close()
337
- .catch((error) => console.error("Failed to close remote server:", error))
338
- .finally(() => resolve());
349
+ // Close root CDP session and kill shared Chrome before stopping the HTTP server.
350
+ const cleanupPromises = [];
351
+ if (sharedRootSession) {
352
+ cleanupPromises.push(sharedRootSession.close().catch((err) => {
353
+ const msg = err instanceof Error ? err.message : String(err);
354
+ console.error(`Failed to close root CDP session: ${msg}`);
355
+ }));
356
+ }
357
+ if (sharedChromeProcess) {
358
+ cleanupPromises.push(Promise.resolve(sharedChromeProcess.kill()).catch((err) => {
359
+ const msg = err instanceof Error ? err.message : String(err);
360
+ console.error(`Failed to kill shared Chrome: ${msg}`);
361
+ }));
362
+ }
363
+ Promise.all(cleanupPromises)
364
+ .finally(() => {
365
+ server
366
+ .close()
367
+ .catch((error) => console.error("Failed to close remote server:", error))
368
+ .finally(() => resolve());
369
+ });
339
370
  };
340
371
  process.on("SIGINT", shutdown);
341
372
  process.on("SIGTERM", shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewting19/oracle",
3
- "version": "0.9.8",
3
+ "version": "0.10.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.4 Pro, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/steipete/oracle#readme",