@steipete/oracle 0.14.1 → 0.15.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.
- package/dist/bin/oracle-cli.js +2 -0
- 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 +400 -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 +452 -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 +387 -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/deepResearch.js +132 -61
- package/dist/src/browser/actions/modelSelection.js +45 -2
- package/dist/src/browser/actions/thinkingTime.js +65 -20
- package/dist/src/browser/artifacts.js +2 -8
- package/dist/src/browser/chatgptFiles.js +198 -49
- package/dist/src/browser/chromeCookies.js +312 -0
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +82 -11
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/windowsCookies.js +219 -0
- package/dist/src/cli/browserConfig.js +17 -1
- package/dist/src/cli/sessionRunner.js +13 -7
- 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 +33 -31
- 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
|
@@ -15,20 +15,31 @@ export async function launchChrome(config, userDataDir, logger) {
|
|
|
15
15
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
16
16
|
const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress);
|
|
17
17
|
const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
|
|
18
|
+
// copy-profile reuses a copied signed-in profile whose cookies are
|
|
19
|
+
// Keychain-encrypted, so it must launch with the real Keychain (not mocked):
|
|
20
|
+
// strip the keychain-mocking flags from both chrome-launcher's defaults and
|
|
21
|
+
// Oracle's set, and ignore the defaults so they aren't re-added.
|
|
22
|
+
const usingCopiedProfile = Boolean(config.copyProfileSource);
|
|
23
|
+
if (usingCopiedProfile && config.chromeProfile) {
|
|
24
|
+
chromeFlags.push(`--profile-directory=${config.chromeProfile}`);
|
|
25
|
+
}
|
|
26
|
+
const launchOptions = resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile);
|
|
18
27
|
const launcher = usePatchedLauncher
|
|
19
28
|
? await launchWithCustomHost({
|
|
20
|
-
chromeFlags,
|
|
29
|
+
chromeFlags: launchOptions.chromeFlags,
|
|
21
30
|
chromePath: config.chromePath ?? undefined,
|
|
22
31
|
userDataDir,
|
|
23
32
|
host: connectHost ?? "127.0.0.1",
|
|
24
33
|
requestedPort: debugPort ?? undefined,
|
|
34
|
+
ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
|
|
25
35
|
})
|
|
26
36
|
: await launch({
|
|
27
37
|
chromePath: config.chromePath ?? undefined,
|
|
28
|
-
chromeFlags,
|
|
38
|
+
chromeFlags: launchOptions.chromeFlags,
|
|
29
39
|
userDataDir,
|
|
30
40
|
handleSIGINT: false,
|
|
31
41
|
port: debugPort ?? undefined,
|
|
42
|
+
ignoreDefaultFlags: launchOptions.ignoreDefaultFlags,
|
|
32
43
|
});
|
|
33
44
|
const pidLabel = typeof launcher.pid === "number" ? ` (pid ${launcher.pid})` : "";
|
|
34
45
|
const hostLabel = connectHost ? ` on ${connectHost}` : "";
|
|
@@ -44,10 +55,14 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
|
|
|
44
55
|
}
|
|
45
56
|
handling = true;
|
|
46
57
|
const inFlight = opts?.isInFlight?.() ?? false;
|
|
47
|
-
const
|
|
58
|
+
const forceCleanup = opts?.forceProfileCleanup ?? false;
|
|
59
|
+
const leaveRunning = (keepBrowser || inFlight) && !forceCleanup;
|
|
48
60
|
if (leaveRunning) {
|
|
49
61
|
logger(`Received ${signal}; leaving Chrome running${inFlight ? " (assistant response pending)" : ""}`);
|
|
50
62
|
}
|
|
63
|
+
else if (forceCleanup && (keepBrowser || inFlight)) {
|
|
64
|
+
logger(`Received ${signal}; terminating Chrome and removing the copied profile (copy-profile is not retained)`);
|
|
65
|
+
}
|
|
51
66
|
else {
|
|
52
67
|
logger(`Received ${signal}; terminating Chrome process`);
|
|
53
68
|
}
|
|
@@ -343,6 +358,9 @@ function createSessionBoundChromeClient(browser, sessionId) {
|
|
|
343
358
|
// Raw `send` here is the browser-level send (not session-bound), so callers
|
|
344
359
|
// that issue Target.* via `send` must pass this page session id explicitly to
|
|
345
360
|
// stay scoped to this tab (e.g. Deep Research OOPIF auto-attach).
|
|
361
|
+
// chrome-remote-interface defines `send` on the client prototype, so object
|
|
362
|
+
// spread does not preserve it. Bind it explicitly for raw session commands.
|
|
363
|
+
send: typeof browser.send === "function" ? browser.send.bind(browser) : undefined,
|
|
346
364
|
oraclePageSessionId: sessionId,
|
|
347
365
|
Network: bindDomain("Network"),
|
|
348
366
|
Page: bindDomain("Page"),
|
|
@@ -472,6 +490,18 @@ function buildChromeFlags(headless, debugBindAddress) {
|
|
|
472
490
|
}
|
|
473
491
|
return flags;
|
|
474
492
|
}
|
|
493
|
+
function resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile) {
|
|
494
|
+
if (!usingCopiedProfile) {
|
|
495
|
+
return { chromeFlags, ignoreDefaultFlags: false };
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
chromeFlags: [...Launcher.defaultFlags(), ...chromeFlags].filter((flag) => flag !== "--use-mock-keychain" && flag !== "--password-store=basic"),
|
|
499
|
+
ignoreDefaultFlags: true,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
export function resolveChromeLaunchOptionsForTest(chromeFlags, usingCopiedProfile) {
|
|
503
|
+
return resolveChromeLaunchOptions(chromeFlags, usingCopiedProfile);
|
|
504
|
+
}
|
|
475
505
|
function parseDebugPortEnv() {
|
|
476
506
|
const raw = process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT;
|
|
477
507
|
if (!raw)
|
|
@@ -514,13 +544,14 @@ function isWsl() {
|
|
|
514
544
|
const release = os.release();
|
|
515
545
|
return release.toLowerCase().includes("microsoft");
|
|
516
546
|
}
|
|
517
|
-
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, }) {
|
|
547
|
+
async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
|
|
518
548
|
const launcher = new Launcher({
|
|
519
549
|
chromePath: chromePath ?? undefined,
|
|
520
550
|
chromeFlags,
|
|
521
551
|
userDataDir,
|
|
522
552
|
handleSIGINT: false,
|
|
523
553
|
port: requestedPort ?? undefined,
|
|
554
|
+
ignoreDefaultFlags,
|
|
524
555
|
});
|
|
525
556
|
if (host) {
|
|
526
557
|
const patched = launcher;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function isDeepResearchIncompleteText(text) {
|
|
2
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
3
|
+
const lines = text
|
|
4
|
+
.split(/\n+/)
|
|
5
|
+
.map((line) => line.trim())
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
const tailIsPlanningPanel = text.length <= 1_500 &&
|
|
8
|
+
lines.length >= 4 &&
|
|
9
|
+
lines.length <= 20 &&
|
|
10
|
+
/^update$/i.test(lines[1] ?? "") &&
|
|
11
|
+
/^stop research$/i.test(lines.at(-1) ?? "") &&
|
|
12
|
+
/^determining steps for creating a report(?:\.\.\.)?$/i.test(lines.at(-2) ?? "");
|
|
13
|
+
return (normalized === "called tool" ||
|
|
14
|
+
normalized === "used tool" ||
|
|
15
|
+
normalized === "użyto narzędzia" ||
|
|
16
|
+
normalized === "narzędzie wywołane" ||
|
|
17
|
+
normalized === "planning" ||
|
|
18
|
+
normalized === "researching" ||
|
|
19
|
+
normalized === "searching the web" ||
|
|
20
|
+
(text.trimStart().startsWith("<system-reminder>") &&
|
|
21
|
+
/<system-reminder>[\s\S]*#\s*plan mode\b/i.test(text)) ||
|
|
22
|
+
tailIsPlanningPanel);
|
|
23
|
+
}
|
|
@@ -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) {
|
|
@@ -613,6 +632,12 @@ export async function runBrowserMode(options) {
|
|
|
613
632
|
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
614
633
|
}
|
|
615
634
|
const manualLogin = Boolean(config.manualLogin);
|
|
635
|
+
if (manualLogin && usingCopiedProfile) {
|
|
636
|
+
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" });
|
|
637
|
+
}
|
|
638
|
+
// Manual-login and copy-profile both start from an already-signed-in profile,
|
|
639
|
+
// so neither clears nor syncs cookies.
|
|
640
|
+
const profileIsPreSigned = manualLogin || usingCopiedProfile;
|
|
616
641
|
const manualProfileDir = config.manualLoginProfileDir
|
|
617
642
|
? path.resolve(config.manualLoginProfileDir)
|
|
618
643
|
: defaultManualLoginProfileDir();
|
|
@@ -629,6 +654,11 @@ export async function runBrowserMode(options) {
|
|
|
629
654
|
keepBrowser: effectiveKeepBrowser,
|
|
630
655
|
});
|
|
631
656
|
}
|
|
657
|
+
else if (config.copyProfileSource) {
|
|
658
|
+
const copiedProfileDirectory = await copyChromeProfile(config.copyProfileSource, userDataDir, config.chromeProfile);
|
|
659
|
+
config = { ...config, chromeProfile: copiedProfileDirectory };
|
|
660
|
+
logger(`Seeded temporary Chrome profile ${copiedProfileDirectory} from ${config.copyProfileSource} (copy-profile mode; signed-in session reused without manual login)`);
|
|
661
|
+
}
|
|
632
662
|
else {
|
|
633
663
|
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
634
664
|
}
|
|
@@ -658,6 +688,9 @@ export async function runBrowserMode(options) {
|
|
|
658
688
|
tabLease = null;
|
|
659
689
|
await handle.release().catch(() => undefined);
|
|
660
690
|
}
|
|
691
|
+
if (usingCopiedProfile) {
|
|
692
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
693
|
+
}
|
|
661
694
|
throw error;
|
|
662
695
|
}
|
|
663
696
|
const { chrome, reusedChrome } = acquiredChrome;
|
|
@@ -674,6 +707,8 @@ export async function runBrowserMode(options) {
|
|
|
674
707
|
isInFlight: () => runStatus !== "complete",
|
|
675
708
|
emitRuntimeHint,
|
|
676
709
|
preserveUserDataDir: manualLogin,
|
|
710
|
+
// copy-profile is a throwaway copy of a signed-in profile; never leave it on disk.
|
|
711
|
+
forceProfileCleanup: usingCopiedProfile,
|
|
677
712
|
});
|
|
678
713
|
}
|
|
679
714
|
catch {
|
|
@@ -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.");
|
|
@@ -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;
|
|
@@ -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
|
+
}
|