@steipete/oracle 0.14.1 → 0.15.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.
Files changed (85) hide show
  1. package/dist/bin/oracle-cli.js +16 -6
  2. package/dist/bin/oracle.js +569 -0
  3. package/dist/docs-site/.nojekyll +0 -0
  4. package/dist/docs-site/CNAME +1 -0
  5. package/dist/docs-site/RELEASING.html +410 -0
  6. package/dist/docs-site/agents.html +374 -0
  7. package/dist/docs-site/anthropic.html +368 -0
  8. package/dist/docs-site/bridge.html +416 -0
  9. package/dist/docs-site/browser-mode.html +594 -0
  10. package/dist/docs-site/chromium-forks.html +347 -0
  11. package/dist/docs-site/cli-reference.html +346 -0
  12. package/dist/docs-site/configuration.html +462 -0
  13. package/dist/docs-site/favicon.svg +14 -0
  14. package/dist/docs-site/followup.html +375 -0
  15. package/dist/docs-site/gemini.html +383 -0
  16. package/dist/docs-site/grok.html +325 -0
  17. package/dist/docs-site/index.html +360 -0
  18. package/dist/docs-site/install.html +335 -0
  19. package/dist/docs-site/linux.html +321 -0
  20. package/dist/docs-site/llms.txt +43 -0
  21. package/dist/docs-site/manual-tests.html +596 -0
  22. package/dist/docs-site/mcp.html +391 -0
  23. package/dist/docs-site/multimodel.html +364 -0
  24. package/dist/docs-site/mythical-pro-agents.html +360 -0
  25. package/dist/docs-site/notifier.html +338 -0
  26. package/dist/docs-site/openai-endpoints.html +399 -0
  27. package/dist/docs-site/openrouter.html +344 -0
  28. package/dist/docs-site/quickstart.html +369 -0
  29. package/dist/docs-site/refactor/ux.html +532 -0
  30. package/dist/docs-site/sessions.html +388 -0
  31. package/dist/docs-site/social-card.png +0 -0
  32. package/dist/docs-site/social-card.svg +79 -0
  33. package/dist/docs-site/spec.html +363 -0
  34. package/dist/docs-site/testing.html +320 -0
  35. package/dist/docs-site/tui-debug.html +326 -0
  36. package/dist/docs-site/windows-work.html +323 -0
  37. package/dist/docs-site/windows.html +320 -0
  38. package/dist/src/browser/actions/assistantResponse.js +40 -31
  39. package/dist/src/browser/actions/attachments.js +28 -1
  40. package/dist/src/browser/actions/deepResearch.js +344 -128
  41. package/dist/src/browser/actions/modelSelection.js +75 -9
  42. package/dist/src/browser/actions/promptComposer.js +71 -14
  43. package/dist/src/browser/actions/thinkingStatus.js +19 -1
  44. package/dist/src/browser/actions/thinkingTime.js +65 -20
  45. package/dist/src/browser/artifacts.js +193 -14
  46. package/dist/src/browser/chatgptFiles.js +674 -91
  47. package/dist/src/browser/chromeCookies.js +312 -0
  48. package/dist/src/browser/chromeLifecycle.js +35 -4
  49. package/dist/src/browser/constants.js +5 -0
  50. package/dist/src/browser/deepResearchResult.js +23 -0
  51. package/dist/src/browser/index.js +112 -41
  52. package/dist/src/browser/keytarShim.js +56 -0
  53. package/dist/src/browser/profileCopy.js +93 -0
  54. package/dist/src/browser/sessionRunner.js +9 -3
  55. package/dist/src/browser/windowsCookies.js +219 -0
  56. package/dist/src/cli/bridge/client.js +4 -1
  57. package/dist/src/cli/bridge/doctor.js +19 -0
  58. package/dist/src/cli/browserConfig.js +17 -1
  59. package/dist/src/cli/runOptions.js +11 -2
  60. package/dist/src/cli/sessionDisplay.js +6 -1
  61. package/dist/src/cli/sessionRunner.js +41 -17
  62. package/dist/src/config.js +3 -0
  63. package/dist/src/oracle/client.js +2 -0
  64. package/dist/src/oracle/modelResolver.js +85 -0
  65. package/dist/src/oracle/multiModelRunner.js +4 -1
  66. package/dist/src/oracle/run.js +4 -1
  67. package/dist/src/remote/client.js +253 -22
  68. package/dist/src/remote/health.js +27 -0
  69. package/dist/src/remote/server.js +239 -4
  70. package/dist/src/remote/types.js +1 -1
  71. package/dist/src/sessionManager.js +1 -0
  72. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  74. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  75. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  76. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  77. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  78. package/package.json +45 -43
  79. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  83. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  84. package/vendor/oracle-notifier/README.md +26 -0
  85. package/vendor/oracle-notifier/build-notifier.sh +0 -0
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import os from "node:os";
4
4
  import net from "node:net";
5
5
  import { resolveBrowserConfig } from "./config.js";
6
+ import { copyChromeProfile } from "./profileCopy.js";
6
7
  import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, connectWithNewTab, closeTab, closeRemoteChromeTarget, closeBlankChromeTabs, } from "./chromeLifecycle.js";
7
8
  import { syncCookies } from "./cookies.js";
8
9
  import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
@@ -66,6 +67,11 @@ function classifyPreservedBrowserError(error, headless) {
66
67
  function shouldPreserveBrowserOnError(error, headless) {
67
68
  return classifyPreservedBrowserError(error, headless) !== null;
68
69
  }
70
+ function shouldKeepLocalBrowserOpen(options) {
71
+ if (options.usingCopiedProfile)
72
+ return false;
73
+ return options.effectiveKeepBrowser || options.preserveBrowserOnError;
74
+ }
69
75
  export function shouldPreserveBrowserOnErrorForTest(error, headless) {
70
76
  return shouldPreserveBrowserOnError(error, headless);
71
77
  }
@@ -429,6 +435,15 @@ async function maybeArchiveCompletedConversation({ Runtime, logger, config, conv
429
435
  export function maybeArchiveCompletedConversationForTest(args) {
430
436
  return maybeArchiveCompletedConversation(args);
431
437
  }
438
+ async function captureDeepResearchTargetBaseline(client, logger) {
439
+ try {
440
+ return { targetKeys: await captureDeepResearchTargetKeys(client), captured: true };
441
+ }
442
+ catch {
443
+ logger("[browser] Deep Research target baseline unavailable; retaining conversation-turn owner scoping.");
444
+ return { targetKeys: [], captured: false };
445
+ }
446
+ }
432
447
  async function runSubmissionWithRecovery({ prompt, attachments, fallbackSubmission, submit, reloadPromptComposer, prepareFallbackSubmission, logger, }) {
433
448
  let currentPrompt = prompt;
434
449
  let currentAttachments = attachments;
@@ -520,6 +535,10 @@ export async function runBrowserMode(options) {
520
535
  const attachments = options.attachments ?? [];
521
536
  const fallbackSubmission = options.fallbackSubmission;
522
537
  let config = resolveBrowserConfig(options.config);
538
+ const usingCopiedProfile = Boolean(config.copyProfileSource);
539
+ if (usingCopiedProfile && (config.attachRunning || config.remoteChrome)) {
540
+ throw new BrowserAutomationError("--copy-profile requires a locally launched Chrome instance and cannot be combined with attach-running or remote Chrome.", { stage: "profile-config" });
541
+ }
523
542
  const isResumingConversation = Boolean(config.resumeConversationUrl);
524
543
  const followUpPrompts = normalizeBrowserFollowUpPrompts(options.followUpPrompts);
525
544
  if (config.researchMode === "deep" && followUpPrompts.length > 0) {
@@ -539,6 +558,7 @@ export async function runBrowserMode(options) {
539
558
  let lastTargetId;
540
559
  let lastUrl;
541
560
  let promptSubmitted = false;
561
+ let modelSelectionEvidence;
542
562
  let tabLease = null;
543
563
  const emitRuntimeHint = async () => {
544
564
  if (!chrome?.port) {
@@ -557,7 +577,7 @@ export async function runBrowserMode(options) {
557
577
  controllerPid: process.pid,
558
578
  };
559
579
  try {
560
- await runtimeHintCb?.(hint);
580
+ await runtimeHintCb?.(hint, modelSelectionEvidence);
561
581
  await tabLease?.update({
562
582
  chromeHost,
563
583
  chromePort: chrome.port,
@@ -613,6 +633,12 @@ export async function runBrowserMode(options) {
613
633
  return runRemoteBrowserMode(promptText, attachments, config, logger, options);
614
634
  }
615
635
  const manualLogin = Boolean(config.manualLogin);
636
+ if (manualLogin && usingCopiedProfile) {
637
+ throw new BrowserAutomationError("--copy-profile cannot be combined with --browser-manual-login: choose either a throwaway copied profile or the persistent manual-login profile.", { stage: "profile-config" });
638
+ }
639
+ // Manual-login and copy-profile both start from an already-signed-in profile,
640
+ // so neither clears nor syncs cookies.
641
+ const profileIsPreSigned = manualLogin || usingCopiedProfile;
616
642
  const manualProfileDir = config.manualLoginProfileDir
617
643
  ? path.resolve(config.manualLoginProfileDir)
618
644
  : defaultManualLoginProfileDir();
@@ -629,6 +655,11 @@ export async function runBrowserMode(options) {
629
655
  keepBrowser: effectiveKeepBrowser,
630
656
  });
631
657
  }
658
+ else if (config.copyProfileSource) {
659
+ const copiedProfileDirectory = await copyChromeProfile(config.copyProfileSource, userDataDir, config.chromeProfile);
660
+ config = { ...config, chromeProfile: copiedProfileDirectory };
661
+ logger(`Seeded temporary Chrome profile ${copiedProfileDirectory} from ${config.copyProfileSource} (copy-profile mode; signed-in session reused without manual login)`);
662
+ }
632
663
  else {
633
664
  logger(`Created temporary Chrome profile at ${userDataDir}`);
634
665
  }
@@ -658,6 +689,9 @@ export async function runBrowserMode(options) {
658
689
  tabLease = null;
659
690
  await handle.release().catch(() => undefined);
660
691
  }
692
+ if (usingCopiedProfile) {
693
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
694
+ }
661
695
  throw error;
662
696
  }
663
697
  const { chrome, reusedChrome } = acquiredChrome;
@@ -674,6 +708,8 @@ export async function runBrowserMode(options) {
674
708
  isInFlight: () => runStatus !== "complete",
675
709
  emitRuntimeHint,
676
710
  preserveUserDataDir: manualLogin,
711
+ // copy-profile is a throwaway copy of a signed-in profile; never leave it on disk.
712
+ forceProfileCleanup: usingCopiedProfile,
677
713
  });
678
714
  }
679
715
  catch {
@@ -687,7 +723,6 @@ export async function runBrowserMode(options) {
687
723
  let answerMarkdown = "";
688
724
  let answerHtml = "";
689
725
  let runStatus = "attempted";
690
- let modelSelectionEvidence;
691
726
  let connectionClosedUnexpectedly = false;
692
727
  let stopThinkingMonitor = null;
693
728
  let removeDialogHandler = null;
@@ -753,11 +788,11 @@ export async function runBrowserMode(options) {
753
788
  }
754
789
  await Promise.all(domainEnablers);
755
790
  removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
756
- if (!manualLogin) {
791
+ if (!profileIsPreSigned) {
757
792
  await Network.clearBrowserCookies();
758
793
  }
759
794
  const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
760
- const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
795
+ const cookieSyncEnabled = config.cookieSync && (!profileIsPreSigned || manualLoginCookieSync);
761
796
  if (cookieSyncEnabled) {
762
797
  if (manualLoginCookieSync) {
763
798
  logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
@@ -984,19 +1019,6 @@ export async function runBrowserMode(options) {
984
1019
  },
985
1020
  }));
986
1021
  }
987
- if (deepResearch) {
988
- await raceWithDisconnect(withRetries(() => activateDeepResearch(Runtime, Input, logger), {
989
- retries: 2,
990
- delayMs: 500,
991
- onRetry: (attempt, error) => {
992
- if (options.verbose) {
993
- logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
994
- }
995
- },
996
- }));
997
- await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
998
- logger(`Prompt textarea ready (after Deep Research activation, ${promptText.length.toLocaleString()} chars queued)`);
999
- }
1000
1022
  const profileLockTimeoutMs = manualLogin ? (config.profileLockTimeoutMs ?? 0) : 0;
1001
1023
  let profileLock = null;
1002
1024
  const acquireProfileLockIfNeeded = async () => {
@@ -1047,6 +1069,19 @@ export async function runBrowserMode(options) {
1047
1069
  await waitForAttachmentCompletion(Runtime, attachmentWaitBudget, attachmentNames, logger);
1048
1070
  logger("All attachments uploaded");
1049
1071
  }
1072
+ if (deepResearch) {
1073
+ await raceWithDisconnect(withRetries(() => activateDeepResearch(Runtime, Input, logger), {
1074
+ retries: 2,
1075
+ delayMs: 500,
1076
+ onRetry: (attempt, error) => {
1077
+ if (options.verbose) {
1078
+ logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
1079
+ }
1080
+ },
1081
+ }));
1082
+ await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
1083
+ logger(`Prompt textarea ready (after Deep Research activation, ${prompt.length.toLocaleString()} chars queued)`);
1084
+ }
1050
1085
  let baselineTurns = await readConversationTurnCount(Runtime, logger);
1051
1086
  // Learned: return baselineTurns so assistant polling can ignore earlier content.
1052
1087
  const providerState = {
@@ -1060,8 +1095,8 @@ export async function runBrowserMode(options) {
1060
1095
  attachmentNames: attachmentExpectations,
1061
1096
  onPromptSubmitted: markPromptSubmitted,
1062
1097
  };
1063
- const deepResearchTargetKeys = deepResearch && client
1064
- ? await captureDeepResearchTargetKeys(client).catch(() => [])
1098
+ const deepResearchTargetBaseline = deepResearch && client
1099
+ ? await captureDeepResearchTargetBaseline(client, logger)
1065
1100
  : undefined;
1066
1101
  await runProviderSubmissionFlow(chatgptDomProvider, {
1067
1102
  prompt,
@@ -1095,7 +1130,12 @@ export async function runBrowserMode(options) {
1095
1130
  }
1096
1131
  // Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
1097
1132
  scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
1098
- return { baselineTurns, baselineAssistantText, deepResearchTargetKeys };
1133
+ return {
1134
+ baselineTurns,
1135
+ baselineAssistantText,
1136
+ deepResearchTargetKeys: deepResearchTargetBaseline?.targetKeys,
1137
+ deepResearchTargetBaselineCaptured: deepResearchTargetBaseline?.captured,
1138
+ };
1099
1139
  };
1100
1140
  const reloadPromptComposer = async () => {
1101
1141
  logger("[browser] Composer became unresponsive; reloading page and retrying once.");
@@ -1105,6 +1145,7 @@ export async function runBrowserMode(options) {
1105
1145
  let baselineTurns = null;
1106
1146
  let baselineAssistantText = null;
1107
1147
  let deepResearchTargetKeys = [];
1148
+ let deepResearchTargetBaselineCaptured = false;
1108
1149
  await acquireProfileLockIfNeeded();
1109
1150
  try {
1110
1151
  const submission = await runSubmissionWithRecovery({
@@ -1122,6 +1163,7 @@ export async function runBrowserMode(options) {
1122
1163
  baselineTurns = submission.baselineTurns;
1123
1164
  baselineAssistantText = submission.baselineAssistantText;
1124
1165
  deepResearchTargetKeys = submission.deepResearchTargetKeys ?? [];
1166
+ deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
1125
1167
  }
1126
1168
  finally {
1127
1169
  await releaseProfileLockIfHeld();
@@ -1129,7 +1171,10 @@ export async function runBrowserMode(options) {
1129
1171
  const imageArtifactMinTurnIndex = baselineTurns;
1130
1172
  if (deepResearch) {
1131
1173
  await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger));
1132
- const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, { ignoredTargetKeys: deepResearchTargetKeys }));
1174
+ const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
1175
+ ignoredTargetKeys: deepResearchTargetKeys,
1176
+ targetBaselineCaptured: deepResearchTargetBaselineCaptured,
1177
+ }));
1133
1178
  await updateConversationHint("post-deep-research", 15_000).catch(() => false);
1134
1179
  runStatus = "complete";
1135
1180
  const durationMs = Date.now() - startedAt;
@@ -1614,6 +1659,10 @@ export async function runBrowserMode(options) {
1614
1659
  connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
1615
1660
  const preservedErrorKind = classifyPreservedBrowserError(normalizedError, config.headless);
1616
1661
  if (preservedErrorKind === "cloudflare-challenge") {
1662
+ if (usingCopiedProfile) {
1663
+ logger("Cloudflare challenge detected; closing Chrome and removing the copied profile because copy-profile runs cannot be retained.");
1664
+ throw new BrowserAutomationError("Cloudflare challenge detected. Copy-profile runs cannot be retained; complete the check in the source Chrome profile, then rerun.", { stage: "cloudflare-challenge", reattachable: false }, normalizedError);
1665
+ }
1617
1666
  preserveBrowserOnError = true;
1618
1667
  const runtime = {
1619
1668
  chromePid: chrome.pid,
@@ -1637,6 +1686,13 @@ export async function runBrowserMode(options) {
1637
1686
  }, normalizedError);
1638
1687
  }
1639
1688
  if (preservedErrorKind === "reattachable-capture") {
1689
+ if (usingCopiedProfile) {
1690
+ logger("Assistant capture incomplete; closing Chrome and removing the copied profile because copy-profile runs cannot be reattached.");
1691
+ const details = normalizedError instanceof BrowserAutomationError
1692
+ ? { ...normalizedError.details, runtime: undefined, reattachable: false }
1693
+ : { stage: "assistant-recheck", reattachable: false };
1694
+ throw new BrowserAutomationError(normalizedError.message, details, normalizedError);
1695
+ }
1640
1696
  preserveBrowserOnError = true;
1641
1697
  await emitRuntimeHint();
1642
1698
  logger("Assistant capture incomplete; leaving browser open for reattach.");
@@ -1689,7 +1745,11 @@ export async function runBrowserMode(options) {
1689
1745
  chrome?.port) {
1690
1746
  await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
1691
1747
  }
1692
- let keepBrowserOpen = effectiveKeepBrowser || preserveBrowserOnError;
1748
+ let keepBrowserOpen = shouldKeepLocalBrowserOpen({
1749
+ effectiveKeepBrowser,
1750
+ preserveBrowserOnError,
1751
+ usingCopiedProfile,
1752
+ });
1693
1753
  let cleanupProfileLock = null;
1694
1754
  let terminatedRecordedChrome = false;
1695
1755
  let otherActiveBrowserTabLeases = null;
@@ -2023,6 +2083,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2023
2083
  let tabLease = null;
2024
2084
  let lastUrl;
2025
2085
  let promptSubmitted = false;
2086
+ let modelSelectionEvidence;
2026
2087
  let attachedExistingTab = false;
2027
2088
  let ownsTarget = true;
2028
2089
  const runtimeHintCb = options.runtimeHintCb;
@@ -2040,7 +2101,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2040
2101
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2041
2102
  promptSubmitted,
2042
2103
  controllerPid: process.pid,
2043
- });
2104
+ }, modelSelectionEvidence);
2044
2105
  await tabLease?.update({
2045
2106
  chromeHost: host,
2046
2107
  chromePort: port,
@@ -2066,7 +2127,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2066
2127
  let answerHtml = "";
2067
2128
  let connectionClosedUnexpectedly = false;
2068
2129
  let runStatus = "attempted";
2069
- let modelSelectionEvidence;
2070
2130
  let stopThinkingMonitor = null;
2071
2131
  let removeDialogHandler = null;
2072
2132
  let connection = null;
@@ -2193,19 +2253,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2193
2253
  },
2194
2254
  });
2195
2255
  }
2196
- if (deepResearch) {
2197
- await withRetries(() => activateDeepResearch(Runtime, Input, logger), {
2198
- retries: 2,
2199
- delayMs: 500,
2200
- onRetry: (attempt, error) => {
2201
- if (options.verbose) {
2202
- logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
2203
- }
2204
- },
2205
- });
2206
- await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
2207
- logger(`Prompt textarea ready (after Deep Research activation, ${promptText.length.toLocaleString()} chars queued)`);
2208
- }
2209
2256
  const submitOnce = async (prompt, submissionAttachments) => {
2210
2257
  const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
2211
2258
  const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
@@ -2235,6 +2282,19 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2235
2282
  await waitForAttachmentCompletion(Runtime, attachmentWaitBudget, attachmentNames, logger);
2236
2283
  logger("All attachments uploaded");
2237
2284
  }
2285
+ if (deepResearch) {
2286
+ await withRetries(() => activateDeepResearch(Runtime, Input, logger), {
2287
+ retries: 2,
2288
+ delayMs: 500,
2289
+ onRetry: (attempt, error) => {
2290
+ if (options.verbose) {
2291
+ logger(`[retry] Deep Research activation attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
2292
+ }
2293
+ },
2294
+ });
2295
+ await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
2296
+ logger(`Prompt textarea ready (after Deep Research activation, ${prompt.length.toLocaleString()} chars queued)`);
2297
+ }
2238
2298
  let baselineTurns = await readConversationTurnCount(Runtime, logger);
2239
2299
  const providerState = {
2240
2300
  runtime: Runtime,
@@ -2247,8 +2307,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2247
2307
  attachmentNames: attachmentExpectations,
2248
2308
  onPromptSubmitted: markPromptSubmitted,
2249
2309
  };
2250
- const deepResearchTargetKeys = deepResearch && client
2251
- ? await captureDeepResearchTargetKeys(client).catch(() => [])
2310
+ const deepResearchTargetBaseline = deepResearch && client
2311
+ ? await captureDeepResearchTargetBaseline(client, logger)
2252
2312
  : undefined;
2253
2313
  await runProviderSubmissionFlow(chatgptDomProvider, {
2254
2314
  prompt,
@@ -2262,7 +2322,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2262
2322
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
2263
2323
  baselineTurns = providerBaselineTurns;
2264
2324
  }
2265
- return { baselineTurns, baselineAssistantText, deepResearchTargetKeys };
2325
+ return {
2326
+ baselineTurns,
2327
+ baselineAssistantText,
2328
+ deepResearchTargetKeys: deepResearchTargetBaseline?.targetKeys,
2329
+ deepResearchTargetBaselineCaptured: deepResearchTargetBaseline?.captured,
2330
+ };
2266
2331
  };
2267
2332
  const reloadPromptComposer = async () => {
2268
2333
  logger("[browser] Composer became unresponsive; reloading page and retrying once.");
@@ -2272,6 +2337,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2272
2337
  let baselineTurns = null;
2273
2338
  let baselineAssistantText = null;
2274
2339
  let deepResearchTargetKeys = [];
2340
+ let deepResearchTargetBaselineCaptured = false;
2275
2341
  const submission = await runSubmissionWithRecovery({
2276
2342
  prompt: promptText,
2277
2343
  attachments,
@@ -2287,10 +2353,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2287
2353
  baselineTurns = submission.baselineTurns;
2288
2354
  baselineAssistantText = submission.baselineAssistantText;
2289
2355
  deepResearchTargetKeys = submission.deepResearchTargetKeys ?? [];
2356
+ deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
2290
2357
  const imageArtifactMinTurnIndex = baselineTurns;
2291
2358
  if (deepResearch) {
2292
2359
  await waitForResearchPlanAutoConfirm(Runtime, logger);
2293
- const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, { ignoredTargetKeys: deepResearchTargetKeys });
2360
+ const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
2361
+ ignoredTargetKeys: deepResearchTargetKeys,
2362
+ targetBaselineCaptured: deepResearchTargetBaselineCaptured,
2363
+ });
2294
2364
  await emitRuntimeHint();
2295
2365
  const durationMs = Date.now() - startedAt;
2296
2366
  const tokens = estimateTokenCount(researchResult.text);
@@ -2812,6 +2882,7 @@ export const __test__ = {
2812
2882
  listIgnoredRemoteChromeFlags,
2813
2883
  resolveManualLoginWaitMs,
2814
2884
  shouldCloseOwnedRunTargetAfterRun,
2885
+ shouldKeepLocalBrowserOpen,
2815
2886
  };
2816
2887
  export { syncCookies } from "./cookies.js";
2817
2888
  export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from "./pageActions.js";
@@ -0,0 +1,56 @@
1
+ const defaultLabels = [
2
+ { service: 'Chrome Safe Storage', account: 'Chrome' },
3
+ { service: 'Chromium Safe Storage', account: 'Chromium' },
4
+ { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
5
+ { service: 'Brave Safe Storage', account: 'Brave' },
6
+ { service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
7
+ ];
8
+ function loadEnvLabels() {
9
+ const raw = process.env.ORACLE_KEYCHAIN_LABELS;
10
+ if (!raw)
11
+ return [];
12
+ try {
13
+ const parsed = JSON.parse(raw);
14
+ if (Array.isArray(parsed)) {
15
+ return parsed
16
+ .map((entry) => (entry && typeof entry === 'object' ? entry : null))
17
+ .filter((entry) => Boolean(entry?.service && entry?.account));
18
+ }
19
+ }
20
+ catch {
21
+ // ignore invalid env payload
22
+ }
23
+ return [];
24
+ }
25
+ const fallbackLabels = [...loadEnvLabels(), ...defaultLabels];
26
+ const disableKeytar = process.env.ORACLE_DISABLE_KEYTAR === '1' || process.env.CI === 'true';
27
+ let keytar;
28
+ if (disableKeytar) {
29
+ keytar = {
30
+ getPassword: async () => null,
31
+ setPassword: async () => undefined,
32
+ deletePassword: async () => false,
33
+ };
34
+ }
35
+ else {
36
+ const keytarModule = await import('keytar');
37
+ keytar = (keytarModule.default ?? keytarModule);
38
+ const originalGetPassword = keytar.getPassword.bind(keytar);
39
+ keytar.getPassword = async (service, account) => {
40
+ const primary = await originalGetPassword(service, account);
41
+ if (primary) {
42
+ return primary;
43
+ }
44
+ for (const label of fallbackLabels) {
45
+ if (label.service === service && label.account === account) {
46
+ continue; // already tried
47
+ }
48
+ const value = await originalGetPassword(label.service, label.account);
49
+ if (value) {
50
+ return value;
51
+ }
52
+ }
53
+ return null;
54
+ };
55
+ }
56
+ export default keytar;
@@ -0,0 +1,93 @@
1
+ import { spawn } from "node:child_process";
2
+ import { cp, mkdir, readFile, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ /**
5
+ * Cache/derived subdirectories that bloat the copy and carry no signed-in-session
6
+ * signal, so they are skipped when seeding a copied Chrome profile.
7
+ */
8
+ const RSYNC_EXCLUDES = [
9
+ "Cache/",
10
+ "Code Cache/",
11
+ "GPUCache/",
12
+ "DawnGraphiteCache/",
13
+ "DawnWebGPUCache/",
14
+ "GrShaderCache/",
15
+ "ShaderCache/",
16
+ "Service Worker/CacheStorage/",
17
+ "Service Worker/ScriptCache/",
18
+ "Service Worker/Database/",
19
+ ];
20
+ /**
21
+ * Copy a signed-in Chrome user-data directory into `destDir` so a throwaway
22
+ * Chrome can launch on the copy and reuse the live session WITHOUT a manual
23
+ * sign-in. Copies the `Default/` profile (minus cache dirs) plus the top-level
24
+ * `Local State` file.
25
+ *
26
+ * `Local State` is required: on macOS it holds the Keychain-wrapped
27
+ * "Chrome Safe Storage" key that decrypts the profile's cookies — a cookies-only
28
+ * copy fails the logged-in check. Decryption only succeeds when the copy is
29
+ * launched by the real Chrome binary (the one on the Keychain ACL).
30
+ *
31
+ * Uses rsync (present on macOS/Linux) so a live, in-use source profile copies
32
+ * cleanly — rsync exit 24 ("source files vanished") is tolerated.
33
+ */
34
+ export async function copyChromeProfile(srcUserDataDir, destDir, requestedProfile) {
35
+ try {
36
+ const localStatePath = path.join(srcUserDataDir, "Local State");
37
+ const copiedLocalStatePath = path.join(destDir, "Local State");
38
+ await cp(localStatePath, copiedLocalStatePath).catch((err) => {
39
+ throw new Error(`--copy-profile: could not copy required "Local State" from ${srcUserDataDir} ` +
40
+ `(needed to select and decrypt the signed-in profile): ${err.message}`);
41
+ });
42
+ const localState = await readFile(copiedLocalStatePath, "utf8");
43
+ const profileDirectory = resolveChromeProfileDirectory(srcUserDataDir, localState, requestedProfile);
44
+ const srcProfile = path.join(srcUserDataDir, profileDirectory);
45
+ const destProfile = path.join(destDir, profileDirectory);
46
+ await mkdir(destProfile, { recursive: true });
47
+ // `Local State` is required (holds the Keychain-wrapped key that decrypts the
48
+ // cookies), so a copy failure must fail fast — otherwise the run continues with
49
+ // a profile that silently looks logged-out.
50
+ const args = ["-a"];
51
+ for (const exclude of RSYNC_EXCLUDES) {
52
+ args.push("--exclude", exclude);
53
+ }
54
+ args.push(`${srcProfile}/`, `${destProfile}/`);
55
+ await new Promise((resolve, reject) => {
56
+ const child = spawn("rsync", args, { stdio: "ignore" });
57
+ child.on("error", (err) => reject(new Error(`--copy-profile requires rsync on PATH (spawn failed): ${err.message}`)));
58
+ child.on("close", (code) => code === 0 || code === 24
59
+ ? resolve()
60
+ : reject(new Error(`rsync failed copying Chrome profile (exit ${code})`)));
61
+ });
62
+ return profileDirectory;
63
+ }
64
+ catch (error) {
65
+ // The destination is always a newly-created throwaway profile. Remove partial
66
+ // session-bearing copies before surfacing setup failures.
67
+ await rm(destDir, { recursive: true, force: true }).catch(() => undefined);
68
+ throw error;
69
+ }
70
+ }
71
+ function resolveChromeProfileDirectory(srcUserDataDir, localState, requestedProfile) {
72
+ let profile = requestedProfile?.trim();
73
+ if (!profile) {
74
+ try {
75
+ const parsed = JSON.parse(localState);
76
+ profile =
77
+ typeof parsed.profile?.last_used === "string" ? parsed.profile.last_used.trim() : "";
78
+ }
79
+ catch (error) {
80
+ throw new Error(`--copy-profile: could not parse "Local State" to select the active Chrome profile: ${error.message}`);
81
+ }
82
+ }
83
+ profile ||= "Default";
84
+ const root = path.resolve(srcUserDataDir);
85
+ const resolved = path.resolve(root, profile);
86
+ if (path.dirname(resolved) !== root) {
87
+ throw new Error(`--copy-profile: Chrome profile must be a direct child of the user-data directory; received ${JSON.stringify(profile)}.`);
88
+ }
89
+ return path.basename(resolved);
90
+ }
91
+ export function resolveChromeProfileDirectoryForTest(srcUserDataDir, localState, requestedProfile) {
92
+ return resolveChromeProfileDirectory(srcUserDataDir, localState, requestedProfile);
93
+ }
@@ -127,11 +127,17 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
127
127
  generateImagePath: runOptions.generateImage,
128
128
  outputPath: runOptions.outputPath,
129
129
  followUpPrompts: runOptions.browserFollowUps,
130
- runtimeHintCb: async (runtime) => {
131
- await persistRuntimeHint({
130
+ runtimeHintCb: async (runtime, modelSelection) => {
131
+ const runtimeWithController = {
132
132
  ...runtime,
133
133
  controllerPid: runtime.controllerPid ?? process.pid,
134
- });
134
+ };
135
+ if (modelSelection) {
136
+ await persistRuntimeHint(runtimeWithController, modelSelection);
137
+ }
138
+ else {
139
+ await persistRuntimeHint(runtimeWithController);
140
+ }
135
141
  },
136
142
  });
137
143
  }