@andrewting19/oracle 0.9.9 → 0.10.1

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 });
@@ -269,12 +269,15 @@ export async function serveRemote(options = {}) {
269
269
  console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
270
270
  }
271
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).
272
273
  let sharedChrome;
274
+ let sharedChromeProcess = null;
275
+ let sharedRootSession = null;
273
276
  if (!preferManualLogin) {
274
277
  try {
275
278
  const userDataDir = await mkdtemp(path.join(os.tmpdir(), "oracle-serve-chrome-"));
276
- // Launch headed Chrome with --no-startup-window: no visible window, no focus stealing,
277
- // 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.
278
281
  const { launch: launchChromeRaw } = await import("chrome-launcher");
279
282
  const chrome = await launchChromeRaw({
280
283
  chromeFlags: [
@@ -295,16 +298,25 @@ export async function serveRemote(options = {}) {
295
298
  userDataDir,
296
299
  handleSIGINT: false,
297
300
  });
298
- sharedChrome = { host: "127.0.0.1", port: chrome.port };
299
- console.log(`Shared Chrome launched (pid ${chrome.pid}, port ${chrome.port}, no-startup-window). Concurrent runs will use isolated tabs.`);
300
- // Sync ChatGPT cookies into the shared Chrome so all tabs are authenticated.
301
- // With --no-startup-window, there are no targets. Create a temp tab to inject cookies.
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 at the browser level (not page level).
304
+ // With --no-startup-window there are no page targets, so we connect to the
305
+ // browser endpoint directly. Hidden target lifetimes are tied to the session
306
+ // that created them, so this must stay alive for the serve lifetime.
307
+ const CDP = (await import("chrome-remote-interface")).default;
308
+ const rootSession = await CDP({ host: "127.0.0.1", port: chrome.port, target: "browser" });
309
+ sharedRootSession = rootSession;
310
+ // Sync ChatGPT cookies into the shared Chrome via a hidden target.
302
311
  try {
303
312
  const { syncCookies } = await import("../browser/cookies.js");
304
- const CDP = (await import("chrome-remote-interface")).default;
305
- const tempTarget = await CDP.New({ host: "127.0.0.1", port: chrome.port, url: "about:blank" });
306
- const client = await CDP({ host: "127.0.0.1", port: chrome.port, target: tempTarget.id });
307
- const { Network } = client;
313
+ const { targetId } = await rootSession.Target.createTarget({
314
+ url: "about:blank",
315
+ hidden: true,
316
+ background: true,
317
+ });
318
+ const syncClient = await CDP({ host: "127.0.0.1", port: chrome.port, target: targetId });
319
+ const { Network } = syncClient;
308
320
  await Network.enable({});
309
321
  const syncLogger = ((msg) => { if (msg)
310
322
  console.log(`[shared-chrome] ${msg}`); });
@@ -313,13 +325,14 @@ export async function serveRemote(options = {}) {
313
325
  allowErrors: true,
314
326
  });
315
327
  console.log(`Synced ${count} ChatGPT cookies into shared Chrome.`);
316
- await client.close();
317
- await CDP.Close({ host: "127.0.0.1", port: chrome.port, id: tempTarget.id });
328
+ await syncClient.close();
329
+ await rootSession.Target.closeTarget({ targetId });
318
330
  }
319
331
  catch (cookieErr) {
320
332
  const msg = cookieErr instanceof Error ? cookieErr.message : String(cookieErr);
321
333
  console.log(`Warning: failed to sync cookies into shared Chrome (${msg}). Runs may need to authenticate.`);
322
334
  }
335
+ sharedChrome = { host: "127.0.0.1", port: chrome.port, rootSession };
323
336
  }
324
337
  catch (error) {
325
338
  const message = error instanceof Error ? error.message : String(error);
@@ -335,10 +348,27 @@ export async function serveRemote(options = {}) {
335
348
  await new Promise((resolve) => {
336
349
  const shutdown = () => {
337
350
  console.log("Shutting down remote service...");
338
- server
339
- .close()
340
- .catch((error) => console.error("Failed to close remote server:", error))
341
- .finally(() => resolve());
351
+ // Close root CDP session and kill shared Chrome before stopping the HTTP server.
352
+ const cleanupPromises = [];
353
+ if (sharedRootSession) {
354
+ cleanupPromises.push(sharedRootSession.close().catch((err) => {
355
+ const msg = err instanceof Error ? err.message : String(err);
356
+ console.error(`Failed to close root CDP session: ${msg}`);
357
+ }));
358
+ }
359
+ if (sharedChromeProcess) {
360
+ cleanupPromises.push(Promise.resolve(sharedChromeProcess.kill()).catch((err) => {
361
+ const msg = err instanceof Error ? err.message : String(err);
362
+ console.error(`Failed to kill shared Chrome: ${msg}`);
363
+ }));
364
+ }
365
+ Promise.all(cleanupPromises)
366
+ .finally(() => {
367
+ server
368
+ .close()
369
+ .catch((error) => console.error("Failed to close remote server:", error))
370
+ .finally(() => resolve());
371
+ });
342
372
  };
343
373
  process.on("SIGINT", shutdown);
344
374
  process.on("SIGTERM", shutdown);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrewting19/oracle",
3
- "version": "0.9.9",
3
+ "version": "0.10.1",
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",