@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.
- package/dist/bin/oracle-cli.js +16 -6
- package/dist/bin/oracle.js +569 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +399 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +40 -31
- package/dist/src/browser/actions/attachments.js +28 -1
- package/dist/src/browser/actions/deepResearch.js +344 -128
- package/dist/src/browser/actions/modelSelection.js +75 -9
- package/dist/src/browser/actions/promptComposer.js +71 -14
- package/dist/src/browser/actions/thinkingStatus.js +19 -1
- package/dist/src/browser/actions/thinkingTime.js +65 -20
- package/dist/src/browser/artifacts.js +193 -14
- package/dist/src/browser/chatgptFiles.js +674 -91
- package/dist/src/browser/chromeCookies.js +312 -0
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/constants.js +5 -0
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +112 -41
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/sessionRunner.js +9 -3
- package/dist/src/browser/windowsCookies.js +219 -0
- package/dist/src/cli/bridge/client.js +4 -1
- package/dist/src/cli/bridge/doctor.js +19 -0
- package/dist/src/cli/browserConfig.js +17 -1
- package/dist/src/cli/runOptions.js +11 -2
- package/dist/src/cli/sessionDisplay.js +6 -1
- package/dist/src/cli/sessionRunner.js +41 -17
- package/dist/src/config.js +3 -0
- package/dist/src/oracle/client.js +2 -0
- package/dist/src/oracle/modelResolver.js +85 -0
- package/dist/src/oracle/multiModelRunner.js +4 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/remote/client.js +253 -22
- package/dist/src/remote/health.js +27 -0
- package/dist/src/remote/server.js +239 -4
- package/dist/src/remote/types.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
- package/package.json +45 -43
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
- package/vendor/oracle-notifier/README.md +26 -0
- 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 (!
|
|
791
|
+
if (!profileIsPreSigned) {
|
|
757
792
|
await Network.clearBrowserCookies();
|
|
758
793
|
}
|
|
759
794
|
const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
|
|
760
|
-
const cookieSyncEnabled = config.cookieSync && (!
|
|
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
|
|
1064
|
-
? await
|
|
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 {
|
|
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, {
|
|
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 =
|
|
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
|
|
2251
|
-
? await
|
|
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 {
|
|
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, {
|
|
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
|
-
|
|
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
|
}
|