@steipete/oracle 0.14.0 → 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/README.md +2 -2
- package/dist/bin/oracle-cli.js +10 -8
- package/dist/docs-site/browser-mode.html +3 -2
- package/dist/docs-site/cli-reference.html +1 -1
- package/dist/docs-site/mcp.html +1 -1
- package/dist/src/browser/actions/assistantResponse.js +2 -1
- package/dist/src/browser/actions/deepResearch.js +132 -61
- package/dist/src/browser/actions/modelSelection.js +388 -30
- package/dist/src/browser/actions/thinkingTime.js +303 -65
- package/dist/src/browser/artifacts.js +2 -8
- package/dist/src/browser/chatgptFiles.js +198 -49
- package/dist/src/browser/chatgptImages.js +126 -24
- package/dist/src/browser/chromeLifecycle.js +35 -4
- package/dist/src/browser/deepResearchResult.js +23 -0
- package/dist/src/browser/index.js +145 -19
- package/dist/src/browser/profileCopy.js +93 -0
- package/dist/src/browser/projectSourcesRunner.js +2 -1
- package/dist/src/browser/prompt.js +151 -22
- package/dist/src/cli/browserConfig.js +19 -2
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/options.js +8 -0
- package/dist/src/cli/sessionRunner.js +13 -7
- package/dist/src/mcp/tools/chatgptImage.js +8 -3
- package/dist/src/mcp/tools/consult.js +9 -8
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/oracle/thinkingTime.js +40 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +6 -6
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +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
|
}
|
|
@@ -240,20 +246,39 @@ function formatChatGptUiWarningType(type) {
|
|
|
240
246
|
return "authentication/challenge";
|
|
241
247
|
}
|
|
242
248
|
}
|
|
243
|
-
async function
|
|
249
|
+
async function createChatGptUiWarningError(params) {
|
|
244
250
|
const [uiWarning] = await collectChatGptUiWarnings(params.Runtime);
|
|
245
|
-
if (!uiWarning)
|
|
246
|
-
return
|
|
247
|
-
}
|
|
251
|
+
if (!uiWarning)
|
|
252
|
+
return null;
|
|
248
253
|
params.logger(`[browser] ChatGPT UI warning detected (${uiWarning.type}): ${uiWarning.message}`);
|
|
249
|
-
return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for
|
|
250
|
-
stage:
|
|
254
|
+
return new BrowserAutomationError(`ChatGPT displayed a ${formatChatGptUiWarningType(uiWarning.type)} warning while waiting for ${params.waitTarget}: ${uiWarning.message}`, {
|
|
255
|
+
stage: params.stage,
|
|
251
256
|
code: "chatgpt-ui-warning",
|
|
252
257
|
uiWarning,
|
|
253
258
|
runtime: params.runtime,
|
|
254
259
|
diagnostics: params.diagnostics,
|
|
255
260
|
}, params.cause);
|
|
256
261
|
}
|
|
262
|
+
async function throwChatGptUiWarningIfPresent(params) {
|
|
263
|
+
const error = await createChatGptUiWarningError(params);
|
|
264
|
+
if (error)
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
async function createAssistantTimeoutError(params) {
|
|
268
|
+
const warningError = await createChatGptUiWarningError({
|
|
269
|
+
Runtime: params.Runtime,
|
|
270
|
+
logger: params.logger,
|
|
271
|
+
runtime: params.runtime,
|
|
272
|
+
stage: "assistant-timeout",
|
|
273
|
+
waitTarget: "the assistant",
|
|
274
|
+
diagnostics: params.diagnostics,
|
|
275
|
+
cause: params.cause,
|
|
276
|
+
});
|
|
277
|
+
if (!warningError) {
|
|
278
|
+
return new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime: params.runtime, diagnostics: params.diagnostics }, params.cause);
|
|
279
|
+
}
|
|
280
|
+
return warningError;
|
|
281
|
+
}
|
|
257
282
|
function listIgnoredRemoteChromeFlags(config) {
|
|
258
283
|
return [
|
|
259
284
|
config.headless ? "--browser-headless" : null,
|
|
@@ -334,7 +359,8 @@ function isImageOnlyUiChromeText(text) {
|
|
|
334
359
|
return (normalized.length === 0 ||
|
|
335
360
|
normalized === "edit" ||
|
|
336
361
|
normalized === "stopped thinking" ||
|
|
337
|
-
normalized === "stopped thinking edit"
|
|
362
|
+
normalized === "stopped thinking edit" ||
|
|
363
|
+
/^thought for \d+(?:\.\d+)?\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\s+edit$/.test(normalized));
|
|
338
364
|
}
|
|
339
365
|
function normalizeBrowserFollowUpPrompts(values) {
|
|
340
366
|
return (values ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -409,6 +435,15 @@ async function maybeArchiveCompletedConversation({ Runtime, logger, config, conv
|
|
|
409
435
|
export function maybeArchiveCompletedConversationForTest(args) {
|
|
410
436
|
return maybeArchiveCompletedConversation(args);
|
|
411
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
|
+
}
|
|
412
447
|
async function runSubmissionWithRecovery({ prompt, attachments, fallbackSubmission, submit, reloadPromptComposer, prepareFallbackSubmission, logger, }) {
|
|
413
448
|
let currentPrompt = prompt;
|
|
414
449
|
let currentAttachments = attachments;
|
|
@@ -500,6 +535,10 @@ export async function runBrowserMode(options) {
|
|
|
500
535
|
const attachments = options.attachments ?? [];
|
|
501
536
|
const fallbackSubmission = options.fallbackSubmission;
|
|
502
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
|
+
}
|
|
503
542
|
const isResumingConversation = Boolean(config.resumeConversationUrl);
|
|
504
543
|
const followUpPrompts = normalizeBrowserFollowUpPrompts(options.followUpPrompts);
|
|
505
544
|
if (config.researchMode === "deep" && followUpPrompts.length > 0) {
|
|
@@ -593,6 +632,12 @@ export async function runBrowserMode(options) {
|
|
|
593
632
|
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
594
633
|
}
|
|
595
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;
|
|
596
641
|
const manualProfileDir = config.manualLoginProfileDir
|
|
597
642
|
? path.resolve(config.manualLoginProfileDir)
|
|
598
643
|
: defaultManualLoginProfileDir();
|
|
@@ -609,6 +654,11 @@ export async function runBrowserMode(options) {
|
|
|
609
654
|
keepBrowser: effectiveKeepBrowser,
|
|
610
655
|
});
|
|
611
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
|
+
}
|
|
612
662
|
else {
|
|
613
663
|
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
614
664
|
}
|
|
@@ -638,6 +688,9 @@ export async function runBrowserMode(options) {
|
|
|
638
688
|
tabLease = null;
|
|
639
689
|
await handle.release().catch(() => undefined);
|
|
640
690
|
}
|
|
691
|
+
if (usingCopiedProfile) {
|
|
692
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
693
|
+
}
|
|
641
694
|
throw error;
|
|
642
695
|
}
|
|
643
696
|
const { chrome, reusedChrome } = acquiredChrome;
|
|
@@ -654,6 +707,8 @@ export async function runBrowserMode(options) {
|
|
|
654
707
|
isInFlight: () => runStatus !== "complete",
|
|
655
708
|
emitRuntimeHint,
|
|
656
709
|
preserveUserDataDir: manualLogin,
|
|
710
|
+
// copy-profile is a throwaway copy of a signed-in profile; never leave it on disk.
|
|
711
|
+
forceProfileCleanup: usingCopiedProfile,
|
|
657
712
|
});
|
|
658
713
|
}
|
|
659
714
|
catch {
|
|
@@ -690,9 +745,10 @@ export async function runBrowserMode(options) {
|
|
|
690
745
|
}
|
|
691
746
|
else {
|
|
692
747
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
748
|
+
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
693
749
|
const connection = await connectWithNewTab(chrome.port, logger, config.url, chromeHost, {
|
|
694
750
|
fallbackToDefault: !strictTabIsolation,
|
|
695
|
-
retries:
|
|
751
|
+
retries: devtoolsRetries,
|
|
696
752
|
retryDelayMs: 500,
|
|
697
753
|
});
|
|
698
754
|
client = connection.client;
|
|
@@ -732,11 +788,11 @@ export async function runBrowserMode(options) {
|
|
|
732
788
|
}
|
|
733
789
|
await Promise.all(domainEnablers);
|
|
734
790
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
735
|
-
if (!
|
|
791
|
+
if (!profileIsPreSigned) {
|
|
736
792
|
await Network.clearBrowserCookies();
|
|
737
793
|
}
|
|
738
794
|
const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
|
|
739
|
-
const cookieSyncEnabled = config.cookieSync && (!
|
|
795
|
+
const cookieSyncEnabled = config.cookieSync && (!profileIsPreSigned || manualLoginCookieSync);
|
|
740
796
|
if (cookieSyncEnabled) {
|
|
741
797
|
if (manualLoginCookieSync) {
|
|
742
798
|
logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
|
|
@@ -1039,8 +1095,8 @@ export async function runBrowserMode(options) {
|
|
|
1039
1095
|
attachmentNames: attachmentExpectations,
|
|
1040
1096
|
onPromptSubmitted: markPromptSubmitted,
|
|
1041
1097
|
};
|
|
1042
|
-
const
|
|
1043
|
-
? await
|
|
1098
|
+
const deepResearchTargetBaseline = deepResearch && client
|
|
1099
|
+
? await captureDeepResearchTargetBaseline(client, logger)
|
|
1044
1100
|
: undefined;
|
|
1045
1101
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
1046
1102
|
prompt,
|
|
@@ -1074,7 +1130,12 @@ export async function runBrowserMode(options) {
|
|
|
1074
1130
|
}
|
|
1075
1131
|
// Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
|
|
1076
1132
|
scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
|
|
1077
|
-
return {
|
|
1133
|
+
return {
|
|
1134
|
+
baselineTurns,
|
|
1135
|
+
baselineAssistantText,
|
|
1136
|
+
deepResearchTargetKeys: deepResearchTargetBaseline?.targetKeys,
|
|
1137
|
+
deepResearchTargetBaselineCaptured: deepResearchTargetBaseline?.captured,
|
|
1138
|
+
};
|
|
1078
1139
|
};
|
|
1079
1140
|
const reloadPromptComposer = async () => {
|
|
1080
1141
|
logger("[browser] Composer became unresponsive; reloading page and retrying once.");
|
|
@@ -1084,6 +1145,7 @@ export async function runBrowserMode(options) {
|
|
|
1084
1145
|
let baselineTurns = null;
|
|
1085
1146
|
let baselineAssistantText = null;
|
|
1086
1147
|
let deepResearchTargetKeys = [];
|
|
1148
|
+
let deepResearchTargetBaselineCaptured = false;
|
|
1087
1149
|
await acquireProfileLockIfNeeded();
|
|
1088
1150
|
try {
|
|
1089
1151
|
const submission = await runSubmissionWithRecovery({
|
|
@@ -1101,6 +1163,7 @@ export async function runBrowserMode(options) {
|
|
|
1101
1163
|
baselineTurns = submission.baselineTurns;
|
|
1102
1164
|
baselineAssistantText = submission.baselineAssistantText;
|
|
1103
1165
|
deepResearchTargetKeys = submission.deepResearchTargetKeys ?? [];
|
|
1166
|
+
deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
|
|
1104
1167
|
}
|
|
1105
1168
|
finally {
|
|
1106
1169
|
await releaseProfileLockIfHeld();
|
|
@@ -1108,7 +1171,10 @@ export async function runBrowserMode(options) {
|
|
|
1108
1171
|
const imageArtifactMinTurnIndex = baselineTurns;
|
|
1109
1172
|
if (deepResearch) {
|
|
1110
1173
|
await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger));
|
|
1111
|
-
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
|
+
}));
|
|
1112
1178
|
await updateConversationHint("post-deep-research", 15_000).catch(() => false);
|
|
1113
1179
|
runStatus = "complete";
|
|
1114
1180
|
const durationMs = Date.now() - startedAt;
|
|
@@ -1504,6 +1570,23 @@ export async function runBrowserMode(options) {
|
|
|
1504
1570
|
outputPath: options.outputPath,
|
|
1505
1571
|
answerText,
|
|
1506
1572
|
waitTimeoutMs: options.config?.timeoutMs,
|
|
1573
|
+
checkBlockingUiWarning: () => throwChatGptUiWarningIfPresent({
|
|
1574
|
+
Runtime,
|
|
1575
|
+
logger,
|
|
1576
|
+
stage: "image-artifact-wait",
|
|
1577
|
+
waitTarget: "generated image artifacts",
|
|
1578
|
+
runtime: {
|
|
1579
|
+
chromePid: chrome.pid,
|
|
1580
|
+
chromePort: chrome.port,
|
|
1581
|
+
chromeHost,
|
|
1582
|
+
userDataDir,
|
|
1583
|
+
chromeTargetId: lastTargetId,
|
|
1584
|
+
tabUrl: lastUrl,
|
|
1585
|
+
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1586
|
+
promptSubmitted,
|
|
1587
|
+
controllerPid: process.pid,
|
|
1588
|
+
},
|
|
1589
|
+
}),
|
|
1507
1590
|
});
|
|
1508
1591
|
answerText = imageArtifacts.answerText || answerText;
|
|
1509
1592
|
if (imageArtifacts.markdownSuffix) {
|
|
@@ -1576,6 +1659,10 @@ export async function runBrowserMode(options) {
|
|
|
1576
1659
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
1577
1660
|
const preservedErrorKind = classifyPreservedBrowserError(normalizedError, config.headless);
|
|
1578
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
|
+
}
|
|
1579
1666
|
preserveBrowserOnError = true;
|
|
1580
1667
|
const runtime = {
|
|
1581
1668
|
chromePid: chrome.pid,
|
|
@@ -1599,6 +1686,13 @@ export async function runBrowserMode(options) {
|
|
|
1599
1686
|
}, normalizedError);
|
|
1600
1687
|
}
|
|
1601
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
|
+
}
|
|
1602
1696
|
preserveBrowserOnError = true;
|
|
1603
1697
|
await emitRuntimeHint();
|
|
1604
1698
|
logger("Assistant capture incomplete; leaving browser open for reattach.");
|
|
@@ -1651,7 +1745,11 @@ export async function runBrowserMode(options) {
|
|
|
1651
1745
|
chrome?.port) {
|
|
1652
1746
|
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
1653
1747
|
}
|
|
1654
|
-
let keepBrowserOpen =
|
|
1748
|
+
let keepBrowserOpen = shouldKeepLocalBrowserOpen({
|
|
1749
|
+
effectiveKeepBrowser,
|
|
1750
|
+
preserveBrowserOnError,
|
|
1751
|
+
usingCopiedProfile,
|
|
1752
|
+
});
|
|
1655
1753
|
let cleanupProfileLock = null;
|
|
1656
1754
|
let terminatedRecordedChrome = false;
|
|
1657
1755
|
let otherActiveBrowserTabLeases = null;
|
|
@@ -2209,8 +2307,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2209
2307
|
attachmentNames: attachmentExpectations,
|
|
2210
2308
|
onPromptSubmitted: markPromptSubmitted,
|
|
2211
2309
|
};
|
|
2212
|
-
const
|
|
2213
|
-
? await
|
|
2310
|
+
const deepResearchTargetBaseline = deepResearch && client
|
|
2311
|
+
? await captureDeepResearchTargetBaseline(client, logger)
|
|
2214
2312
|
: undefined;
|
|
2215
2313
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
2216
2314
|
prompt,
|
|
@@ -2224,7 +2322,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2224
2322
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
2225
2323
|
baselineTurns = providerBaselineTurns;
|
|
2226
2324
|
}
|
|
2227
|
-
return {
|
|
2325
|
+
return {
|
|
2326
|
+
baselineTurns,
|
|
2327
|
+
baselineAssistantText,
|
|
2328
|
+
deepResearchTargetKeys: deepResearchTargetBaseline?.targetKeys,
|
|
2329
|
+
deepResearchTargetBaselineCaptured: deepResearchTargetBaseline?.captured,
|
|
2330
|
+
};
|
|
2228
2331
|
};
|
|
2229
2332
|
const reloadPromptComposer = async () => {
|
|
2230
2333
|
logger("[browser] Composer became unresponsive; reloading page and retrying once.");
|
|
@@ -2234,6 +2337,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2234
2337
|
let baselineTurns = null;
|
|
2235
2338
|
let baselineAssistantText = null;
|
|
2236
2339
|
let deepResearchTargetKeys = [];
|
|
2340
|
+
let deepResearchTargetBaselineCaptured = false;
|
|
2237
2341
|
const submission = await runSubmissionWithRecovery({
|
|
2238
2342
|
prompt: promptText,
|
|
2239
2343
|
attachments,
|
|
@@ -2249,10 +2353,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2249
2353
|
baselineTurns = submission.baselineTurns;
|
|
2250
2354
|
baselineAssistantText = submission.baselineAssistantText;
|
|
2251
2355
|
deepResearchTargetKeys = submission.deepResearchTargetKeys ?? [];
|
|
2356
|
+
deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
|
|
2252
2357
|
const imageArtifactMinTurnIndex = baselineTurns;
|
|
2253
2358
|
if (deepResearch) {
|
|
2254
2359
|
await waitForResearchPlanAutoConfirm(Runtime, logger);
|
|
2255
|
-
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
|
+
});
|
|
2256
2364
|
await emitRuntimeHint();
|
|
2257
2365
|
const durationMs = Date.now() - startedAt;
|
|
2258
2366
|
const tokens = estimateTokenCount(researchResult.text);
|
|
@@ -2616,6 +2724,23 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2616
2724
|
outputPath: options.outputPath,
|
|
2617
2725
|
answerText,
|
|
2618
2726
|
waitTimeoutMs: options.config?.timeoutMs,
|
|
2727
|
+
checkBlockingUiWarning: () => throwChatGptUiWarningIfPresent({
|
|
2728
|
+
Runtime,
|
|
2729
|
+
logger,
|
|
2730
|
+
stage: "image-artifact-wait",
|
|
2731
|
+
waitTarget: "generated image artifacts",
|
|
2732
|
+
runtime: {
|
|
2733
|
+
chromePort: port,
|
|
2734
|
+
chromeHost: host,
|
|
2735
|
+
chromeBrowserWSEndpoint: browserWSEndpoint,
|
|
2736
|
+
chromeProfileRoot,
|
|
2737
|
+
chromeTargetId: remoteTargetId ?? undefined,
|
|
2738
|
+
tabUrl: lastUrl,
|
|
2739
|
+
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2740
|
+
promptSubmitted,
|
|
2741
|
+
controllerPid: process.pid,
|
|
2742
|
+
},
|
|
2743
|
+
}),
|
|
2619
2744
|
});
|
|
2620
2745
|
answerText = imageArtifacts.answerText || answerText;
|
|
2621
2746
|
if (imageArtifacts.markdownSuffix) {
|
|
@@ -2757,6 +2882,7 @@ export const __test__ = {
|
|
|
2757
2882
|
listIgnoredRemoteChromeFlags,
|
|
2758
2883
|
resolveManualLoginWaitMs,
|
|
2759
2884
|
shouldCloseOwnedRunTargetAfterRun,
|
|
2885
|
+
shouldKeepLocalBrowserOpen,
|
|
2760
2886
|
};
|
|
2761
2887
|
export { syncCookies } from "./cookies.js";
|
|
2762
2888
|
export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from "./pageActions.js";
|
|
@@ -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
|
+
}
|
|
@@ -97,9 +97,10 @@ export async function runBrowserProjectSources(request) {
|
|
|
97
97
|
preserveUserDataDir: manualLogin,
|
|
98
98
|
});
|
|
99
99
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
100
|
+
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
100
101
|
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
101
102
|
fallbackToDefault: !strictTabIsolation,
|
|
102
|
-
retries:
|
|
103
|
+
retries: devtoolsRetries,
|
|
103
104
|
retryDelayMs: 500,
|
|
104
105
|
});
|
|
105
106
|
client = connection.client;
|