@steipete/oracle 0.11.0 → 0.12.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 +56 -11
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/archiveConversation.js +12 -0
- package/dist/src/browser/actions/modelSelection.js +61 -18
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/config.js +1 -7
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +65 -48
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/sessionRunner.js +72 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -18
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +5 -2
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +5 -1
- package/package.json +8 -6
|
@@ -26,6 +26,7 @@ import { resolveAttachRunningConnection } from "./attachRunning.js";
|
|
|
26
26
|
import { connectToExistingChatGptTab } from "./liveTabs.js";
|
|
27
27
|
import { captureBrowserDiagnostics } from "./domDebug.js";
|
|
28
28
|
import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./actions/archiveConversation.js";
|
|
29
|
+
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
29
30
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
|
|
30
31
|
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
|
|
31
32
|
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
|
|
@@ -70,19 +71,12 @@ export function shouldPreserveBrowserOnErrorForTest(error, headless) {
|
|
|
70
71
|
export function classifyPreservedBrowserErrorForTest(error, headless) {
|
|
71
72
|
return classifyPreservedBrowserError(error, headless);
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
normalized.includes("gpt-5.5 pro") ||
|
|
80
|
-
normalized.includes("gpt 5.5 pro") ||
|
|
81
|
-
normalized.includes("gpt 5 5 pro"));
|
|
82
|
-
}
|
|
83
|
-
export function shouldSkipThinkingTimeSelectionForTest(desiredModel, thinkingTime) {
|
|
84
|
-
return shouldSkipThinkingTimeSelection(desiredModel, thinkingTime);
|
|
85
|
-
}
|
|
74
|
+
// NOTE: Previously, shouldSkipThinkingTimeSelection() would skip the thinking
|
|
75
|
+
// time UI step when desiredModel was gpt-5.5-pro and thinkingTime was "extended",
|
|
76
|
+
// assuming that selecting "Pro Extended" in the old UI already implied Extended
|
|
77
|
+
// effort. This is wrong for lower-tier plans ($100/mo Pro) where selecting "Pro"
|
|
78
|
+
// defaults to Standard effort. ensureThinkingTime() already handles the
|
|
79
|
+
// "already-selected" case as a no-op, so always attempting it is safe.
|
|
86
80
|
function listIgnoredRemoteChromeFlags(config) {
|
|
87
81
|
return [
|
|
88
82
|
config.headless ? "--browser-headless" : null,
|
|
@@ -297,6 +291,17 @@ async function closeRemoteConnectionAfterRun(options) {
|
|
|
297
291
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
298
292
|
return options.runStatus === "complete" && options.ownsTarget && !options.keepBrowser;
|
|
299
293
|
}
|
|
294
|
+
function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
|
|
295
|
+
return {
|
|
296
|
+
requestedModel: desiredModel ?? null,
|
|
297
|
+
resolvedLabel: null,
|
|
298
|
+
strategy,
|
|
299
|
+
status: "skipped",
|
|
300
|
+
verified: false,
|
|
301
|
+
source: "config",
|
|
302
|
+
capturedAt: new Date().toISOString(),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
300
305
|
export async function runBrowserMode(options) {
|
|
301
306
|
const promptText = options.prompt?.trim();
|
|
302
307
|
if (!promptText) {
|
|
@@ -390,14 +395,19 @@ export async function runBrowserMode(options) {
|
|
|
390
395
|
const manualLogin = Boolean(config.manualLogin);
|
|
391
396
|
const manualProfileDir = config.manualLoginProfileDir
|
|
392
397
|
? path.resolve(config.manualLoginProfileDir)
|
|
393
|
-
:
|
|
398
|
+
: defaultManualLoginProfileDir();
|
|
394
399
|
const userDataDir = manualLogin
|
|
395
400
|
? manualProfileDir
|
|
396
401
|
: await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
|
|
402
|
+
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
397
403
|
if (manualLogin) {
|
|
398
404
|
// Learned: manual login reuses a persistent profile so cookies/SSO survive.
|
|
399
405
|
await mkdir(userDataDir, { recursive: true });
|
|
400
406
|
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
407
|
+
await assertManualLoginProfileReadyForRun({
|
|
408
|
+
userDataDir,
|
|
409
|
+
keepBrowser: effectiveKeepBrowser,
|
|
410
|
+
});
|
|
401
411
|
}
|
|
402
412
|
else {
|
|
403
413
|
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
@@ -410,7 +420,6 @@ export async function runBrowserMode(options) {
|
|
|
410
420
|
sessionId: options.sessionId,
|
|
411
421
|
});
|
|
412
422
|
}
|
|
413
|
-
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
414
423
|
let acquiredChrome;
|
|
415
424
|
try {
|
|
416
425
|
acquiredChrome = manualLogin
|
|
@@ -458,6 +467,7 @@ export async function runBrowserMode(options) {
|
|
|
458
467
|
let answerMarkdown = "";
|
|
459
468
|
let answerHtml = "";
|
|
460
469
|
let runStatus = "attempted";
|
|
470
|
+
let modelSelectionEvidence;
|
|
461
471
|
let connectionClosedUnexpectedly = false;
|
|
462
472
|
let stopThinkingMonitor = null;
|
|
463
473
|
let removeDialogHandler = null;
|
|
@@ -594,6 +604,8 @@ export async function runBrowserMode(options) {
|
|
|
594
604
|
appliedCookies,
|
|
595
605
|
manualLogin,
|
|
596
606
|
timeoutMs: config.timeoutMs,
|
|
607
|
+
profileDir: userDataDir,
|
|
608
|
+
keepBrowser: effectiveKeepBrowser,
|
|
597
609
|
}));
|
|
598
610
|
if (config.url !== baseUrl) {
|
|
599
611
|
await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
|
|
@@ -687,7 +699,7 @@ export async function runBrowserMode(options) {
|
|
|
687
699
|
await captureRuntimeSnapshot();
|
|
688
700
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
689
701
|
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
690
|
-
await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
702
|
+
modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
691
703
|
retries: 2,
|
|
692
704
|
delayMs: 300,
|
|
693
705
|
onRetry: (attempt, error) => {
|
|
@@ -706,26 +718,22 @@ export async function runBrowserMode(options) {
|
|
|
706
718
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
707
719
|
}
|
|
708
720
|
else if (modelStrategy === "ignore") {
|
|
721
|
+
modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
|
|
709
722
|
logger("Model picker: skipped (strategy=ignore)");
|
|
710
723
|
}
|
|
711
724
|
const deepResearch = config.researchMode === "deep";
|
|
712
725
|
// Handle thinking time selection if specified. Deep Research owns its own effort flow.
|
|
713
726
|
const thinkingTime = config.thinkingTime;
|
|
714
727
|
if (thinkingTime && !deepResearch) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
725
|
-
}
|
|
726
|
-
},
|
|
727
|
-
}));
|
|
728
|
-
}
|
|
728
|
+
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
|
|
729
|
+
retries: 2,
|
|
730
|
+
delayMs: 300,
|
|
731
|
+
onRetry: (attempt, error) => {
|
|
732
|
+
if (options.verbose) {
|
|
733
|
+
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
}));
|
|
729
737
|
}
|
|
730
738
|
if (deepResearch) {
|
|
731
739
|
await raceWithDisconnect(withRetries(() => activateDeepResearch(Runtime, Input, logger), {
|
|
@@ -893,6 +901,7 @@ export async function runBrowserMode(options) {
|
|
|
893
901
|
answerHtml: researchResult.html,
|
|
894
902
|
artifacts: savedArtifacts,
|
|
895
903
|
archive,
|
|
904
|
+
modelSelection: modelSelectionEvidence,
|
|
896
905
|
tookMs: durationMs,
|
|
897
906
|
answerTokens: tokens,
|
|
898
907
|
answerChars: researchResult.text.length,
|
|
@@ -1278,6 +1287,7 @@ export async function runBrowserMode(options) {
|
|
|
1278
1287
|
generatedImages: imageArtifacts.generatedImages,
|
|
1279
1288
|
savedImages: imageArtifacts.savedImages,
|
|
1280
1289
|
archive,
|
|
1290
|
+
modelSelection: modelSelectionEvidence,
|
|
1281
1291
|
tookMs: durationMs,
|
|
1282
1292
|
answerTokens,
|
|
1283
1293
|
answerChars,
|
|
@@ -1503,12 +1513,13 @@ async function findEphemeralPort() {
|
|
|
1503
1513
|
});
|
|
1504
1514
|
});
|
|
1505
1515
|
}
|
|
1506
|
-
async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
|
|
1516
|
+
async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, profileDir, keepBrowser, }) {
|
|
1507
1517
|
if (!manualLogin) {
|
|
1508
1518
|
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
1509
1519
|
return;
|
|
1510
1520
|
}
|
|
1511
|
-
const
|
|
1521
|
+
const waitMs = resolveManualLoginWaitMs(timeoutMs, Boolean(keepBrowser));
|
|
1522
|
+
const deadline = Date.now() + waitMs;
|
|
1512
1523
|
let lastNotice = 0;
|
|
1513
1524
|
while (Date.now() < deadline) {
|
|
1514
1525
|
try {
|
|
@@ -1530,7 +1541,10 @@ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, time
|
|
|
1530
1541
|
await delay(1000);
|
|
1531
1542
|
}
|
|
1532
1543
|
}
|
|
1533
|
-
|
|
1544
|
+
const setupCommand = formatManualLoginSetupCommand(profileDir ?? defaultManualLoginProfileDir());
|
|
1545
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session. " +
|
|
1546
|
+
`Browser mode is using Oracle's private Chrome profile at ${profileDir ?? "(default profile)"}, not your normal Chrome profile. ` +
|
|
1547
|
+
`Run first-time setup, sign in there, then retry: ${setupCommand}`);
|
|
1534
1548
|
}
|
|
1535
1549
|
async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answerText, answerMarkdown, logger, allowMarkdownUpdate, }) {
|
|
1536
1550
|
// Learned: long streaming responses can still be rendering after initial capture.
|
|
@@ -1724,6 +1738,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1724
1738
|
let answerHtml = "";
|
|
1725
1739
|
let connectionClosedUnexpectedly = false;
|
|
1726
1740
|
let runStatus = "attempted";
|
|
1741
|
+
let modelSelectionEvidence;
|
|
1727
1742
|
let stopThinkingMonitor = null;
|
|
1728
1743
|
let removeDialogHandler = null;
|
|
1729
1744
|
let connection = null;
|
|
@@ -1813,7 +1828,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1813
1828
|
}
|
|
1814
1829
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
1815
1830
|
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
1816
|
-
await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
1831
|
+
modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
1817
1832
|
retries: 2,
|
|
1818
1833
|
delayMs: 300,
|
|
1819
1834
|
onRetry: (attempt, error) => {
|
|
@@ -1826,26 +1841,22 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1826
1841
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1827
1842
|
}
|
|
1828
1843
|
else if (modelStrategy === "ignore") {
|
|
1844
|
+
modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
|
|
1829
1845
|
logger("Model picker: skipped (strategy=ignore)");
|
|
1830
1846
|
}
|
|
1831
1847
|
const deepResearch = config.researchMode === "deep";
|
|
1832
1848
|
// Handle thinking time selection if specified. Deep Research owns its own effort flow.
|
|
1833
1849
|
const thinkingTime = config.thinkingTime;
|
|
1834
1850
|
if (thinkingTime && !deepResearch) {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
1845
|
-
}
|
|
1846
|
-
},
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1851
|
+
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
|
|
1852
|
+
retries: 2,
|
|
1853
|
+
delayMs: 300,
|
|
1854
|
+
onRetry: (attempt, error) => {
|
|
1855
|
+
if (options.verbose) {
|
|
1856
|
+
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
1857
|
+
}
|
|
1858
|
+
},
|
|
1859
|
+
});
|
|
1849
1860
|
}
|
|
1850
1861
|
if (deepResearch) {
|
|
1851
1862
|
await withRetries(() => activateDeepResearch(Runtime, Input, logger), {
|
|
@@ -1965,6 +1976,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1965
1976
|
answerHtml: researchResult.html,
|
|
1966
1977
|
artifacts: savedArtifacts,
|
|
1967
1978
|
archive,
|
|
1979
|
+
modelSelection: modelSelectionEvidence,
|
|
1968
1980
|
tookMs: durationMs,
|
|
1969
1981
|
answerTokens: tokens,
|
|
1970
1982
|
answerChars: researchResult.text.length,
|
|
@@ -2328,6 +2340,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2328
2340
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2329
2341
|
artifacts: savedArtifacts,
|
|
2330
2342
|
archive,
|
|
2343
|
+
modelSelection: modelSelectionEvidence,
|
|
2331
2344
|
controllerPid: process.pid,
|
|
2332
2345
|
};
|
|
2333
2346
|
}
|
|
@@ -2389,10 +2402,14 @@ export { estimateTokenCount } from "./utils.js";
|
|
|
2389
2402
|
export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from "./config.js";
|
|
2390
2403
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
2391
2404
|
export const __test__ = {
|
|
2405
|
+
assertManualLoginProfileReadyForRun,
|
|
2392
2406
|
closeRemoteConnectionAfterRun,
|
|
2393
2407
|
detachKeptChromeProcess,
|
|
2408
|
+
formatManualLoginSetupCommand,
|
|
2409
|
+
isManualLoginProfileInitialized,
|
|
2394
2410
|
isImageOnlyUiChromeText,
|
|
2395
2411
|
listIgnoredRemoteChromeFlags,
|
|
2412
|
+
resolveManualLoginWaitMs,
|
|
2396
2413
|
shouldCloseOwnedRunTargetAfterRun,
|
|
2397
2414
|
};
|
|
2398
2415
|
export { syncCookies } from "./cookies.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
5
|
+
export function resolveManualLoginWaitMs(timeoutMs, keepBrowser) {
|
|
6
|
+
const configured = Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
|
|
7
|
+
if (keepBrowser) {
|
|
8
|
+
return configured;
|
|
9
|
+
}
|
|
10
|
+
return Math.min(configured, 30_000);
|
|
11
|
+
}
|
|
12
|
+
export async function assertManualLoginProfileReadyForRun({ userDataDir, keepBrowser, }) {
|
|
13
|
+
if (keepBrowser) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (await isManualLoginProfileInitialized(userDataDir)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const setupCommand = formatManualLoginSetupCommand(userDataDir);
|
|
20
|
+
throw new BrowserAutomationError("ChatGPT browser manual-login profile is not initialized. " +
|
|
21
|
+
`Browser mode is using Oracle's private Chrome profile at ${userDataDir}, separate from your normal Chrome profile. ` +
|
|
22
|
+
`Run first-time setup, sign in there, then retry: ${setupCommand}. ` +
|
|
23
|
+
"If you want to reuse an already signed-in Chrome instead, use --browser-attach-running.", {
|
|
24
|
+
stage: "browser-login-setup",
|
|
25
|
+
details: {
|
|
26
|
+
profileDir: userDataDir,
|
|
27
|
+
setupCommand,
|
|
28
|
+
sessionStatus: "needs_login",
|
|
29
|
+
},
|
|
30
|
+
reuseProfileHint: setupCommand,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function isManualLoginProfileInitialized(profileDir) {
|
|
34
|
+
const entries = await readdir(profileDir, { withFileTypes: true }).catch(() => []);
|
|
35
|
+
return entries.some((entry) => {
|
|
36
|
+
if (!entry.name)
|
|
37
|
+
return false;
|
|
38
|
+
if (entry.name === "Default" || entry.name === "Local State")
|
|
39
|
+
return true;
|
|
40
|
+
if (entry.name.startsWith("Profile "))
|
|
41
|
+
return true;
|
|
42
|
+
return false;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function formatManualLoginSetupCommand(profileDir) {
|
|
46
|
+
return [
|
|
47
|
+
"oracle --engine browser --browser-manual-login --browser-keep-browser",
|
|
48
|
+
`--browser-manual-login-profile-dir ${JSON.stringify(profileDir)}`,
|
|
49
|
+
'-p "HI"',
|
|
50
|
+
].join(" ");
|
|
51
|
+
}
|
|
52
|
+
export function defaultManualLoginProfileDir() {
|
|
53
|
+
return path.join(os.homedir(), ".oracle", "browser-profile");
|
|
54
|
+
}
|
|
@@ -9,6 +9,7 @@ import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLe
|
|
|
9
9
|
import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
10
10
|
import { CHATGPT_URL } from "./constants.js";
|
|
11
11
|
import { delay } from "./utils.js";
|
|
12
|
+
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
12
13
|
import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
|
|
13
14
|
import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
|
|
14
15
|
import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
|
|
@@ -45,13 +46,18 @@ export async function runBrowserProjectSources(request) {
|
|
|
45
46
|
const manualLogin = Boolean(config.manualLogin);
|
|
46
47
|
const manualProfileDir = config.manualLoginProfileDir
|
|
47
48
|
? path.resolve(config.manualLoginProfileDir)
|
|
48
|
-
:
|
|
49
|
+
: defaultManualLoginProfileDir();
|
|
49
50
|
const userDataDir = manualLogin
|
|
50
51
|
? manualProfileDir
|
|
51
52
|
: await mkdtemp(path.join(os.tmpdir(), "oracle-project-sources-"));
|
|
53
|
+
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
52
54
|
if (manualLogin) {
|
|
53
55
|
await mkdir(userDataDir, { recursive: true });
|
|
54
56
|
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
57
|
+
await assertManualLoginProfileReadyForRun({
|
|
58
|
+
userDataDir,
|
|
59
|
+
keepBrowser: effectiveKeepBrowser,
|
|
60
|
+
});
|
|
55
61
|
}
|
|
56
62
|
else {
|
|
57
63
|
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
@@ -73,7 +79,6 @@ export async function runBrowserProjectSources(request) {
|
|
|
73
79
|
let removeDialogHandler = null;
|
|
74
80
|
let connectionClosedUnexpectedly = false;
|
|
75
81
|
let completed = false;
|
|
76
|
-
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
77
82
|
try {
|
|
78
83
|
const acquired = manualLogin
|
|
79
84
|
? await acquireManualLoginChromeForProjectSources(userDataDir, config, logger)
|
|
@@ -140,6 +145,8 @@ export async function runBrowserProjectSources(request) {
|
|
|
140
145
|
appliedCookies,
|
|
141
146
|
manualLogin,
|
|
142
147
|
timeoutMs: config.timeoutMs,
|
|
148
|
+
profileDir: userDataDir,
|
|
149
|
+
keepBrowser: effectiveKeepBrowser,
|
|
143
150
|
}));
|
|
144
151
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, projectUrl, logger));
|
|
145
152
|
await raceWithDisconnect(openProjectSourcesTab(Runtime, Input, config.inputTimeoutMs, logger));
|
|
@@ -261,12 +268,13 @@ async function applyProjectSourcesCookies({ config, network, manualLogin, logger
|
|
|
261
268
|
: "No Chrome cookies found; continuing without session reuse");
|
|
262
269
|
return cookieCount;
|
|
263
270
|
}
|
|
264
|
-
async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
|
|
271
|
+
async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, profileDir, keepBrowser, }) {
|
|
265
272
|
if (!manualLogin) {
|
|
266
273
|
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
267
274
|
return;
|
|
268
275
|
}
|
|
269
|
-
const
|
|
276
|
+
const waitMs = resolveManualLoginWaitMs(timeoutMs, Boolean(keepBrowser));
|
|
277
|
+
const deadline = Date.now() + waitMs;
|
|
270
278
|
let lastNotice = 0;
|
|
271
279
|
while (Date.now() < deadline) {
|
|
272
280
|
try {
|
|
@@ -288,7 +296,10 @@ async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, man
|
|
|
288
296
|
await delay(1000);
|
|
289
297
|
}
|
|
290
298
|
}
|
|
291
|
-
|
|
299
|
+
const setupCommand = formatManualLoginSetupCommand(profileDir ?? defaultManualLoginProfileDir());
|
|
300
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session. " +
|
|
301
|
+
`Browser mode is using Oracle's private Chrome profile at ${profileDir ?? "(default profile)"}, not your normal Chrome profile. ` +
|
|
302
|
+
`Run first-time setup, sign in there, then retry: ${setupCommand}`);
|
|
292
303
|
}
|
|
293
304
|
async function acquireManualLoginChromeForProjectSources(userDataDir, config, logger) {
|
|
294
305
|
const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
@@ -5,6 +5,7 @@ import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, format
|
|
|
5
5
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
6
6
|
import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
|
|
7
7
|
import { buildAttachmentPlan } from "./policies.js";
|
|
8
|
+
import { createStoredZip } from "./zipBundle.js";
|
|
8
9
|
const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
|
|
9
10
|
const MEDIA_EXTENSIONS = new Set([
|
|
10
11
|
".mp4",
|
|
@@ -34,6 +35,49 @@ export function isMediaFile(filePath) {
|
|
|
34
35
|
const ext = path.extname(filePath).toLowerCase();
|
|
35
36
|
return MEDIA_EXTENSIONS.has(ext);
|
|
36
37
|
}
|
|
38
|
+
function formatSectionsForBundle(sections) {
|
|
39
|
+
const bundleLines = [];
|
|
40
|
+
sections.forEach((section) => {
|
|
41
|
+
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
42
|
+
bundleLines.push("");
|
|
43
|
+
});
|
|
44
|
+
return `${bundleLines
|
|
45
|
+
.join("\n")
|
|
46
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
47
|
+
.trimEnd()}\n`;
|
|
48
|
+
}
|
|
49
|
+
async function writeBrowserBundle(sections, format) {
|
|
50
|
+
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
|
|
51
|
+
const tokenEstimateText = formatSectionsForBundle(sections);
|
|
52
|
+
if (format === "zip") {
|
|
53
|
+
const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
|
|
54
|
+
const buffer = createStoredZip(sections.map((section) => ({
|
|
55
|
+
path: section.displayPath,
|
|
56
|
+
content: section.content,
|
|
57
|
+
})));
|
|
58
|
+
await fs.writeFile(bundlePath, buffer);
|
|
59
|
+
return {
|
|
60
|
+
attachment: {
|
|
61
|
+
path: bundlePath,
|
|
62
|
+
displayPath: bundlePath,
|
|
63
|
+
sizeBytes: buffer.length,
|
|
64
|
+
},
|
|
65
|
+
metadata: { originalCount: sections.length, bundlePath, format },
|
|
66
|
+
tokenEstimateText,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
|
|
70
|
+
await fs.writeFile(bundlePath, tokenEstimateText, "utf8");
|
|
71
|
+
return {
|
|
72
|
+
attachment: {
|
|
73
|
+
path: bundlePath,
|
|
74
|
+
displayPath: bundlePath,
|
|
75
|
+
sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
|
|
76
|
+
},
|
|
77
|
+
metadata: { originalCount: sections.length, bundlePath, format },
|
|
78
|
+
tokenEstimateText,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
37
81
|
export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
38
82
|
const cwd = deps.cwd ?? process.cwd();
|
|
39
83
|
const readFilesFn = deps.readFilesImpl ?? readFiles;
|
|
@@ -49,7 +93,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
49
93
|
sizeBytes: stats.size,
|
|
50
94
|
};
|
|
51
95
|
}));
|
|
52
|
-
const files = await readFilesFn(textFilePaths, {
|
|
96
|
+
const files = await readFilesFn(textFilePaths, {
|
|
97
|
+
cwd,
|
|
98
|
+
maxFileSizeBytes: runOptions.maxFileSizeBytes,
|
|
99
|
+
});
|
|
53
100
|
const basePrompt = (runOptions.prompt ?? "").trim();
|
|
54
101
|
const userPrompt = basePrompt;
|
|
55
102
|
const systemPrompt = runOptions.system?.trim() || "";
|
|
@@ -59,6 +106,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
59
106
|
? "never"
|
|
60
107
|
: (runOptions.browserAttachments ?? "auto");
|
|
61
108
|
const bundleRequested = Boolean(runOptions.browserBundleFiles);
|
|
109
|
+
const bundleFormat = runOptions.browserBundleFormat ?? "text";
|
|
62
110
|
const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
|
|
63
111
|
const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
|
|
64
112
|
const baseComposerSections = [];
|
|
@@ -88,26 +136,12 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
88
136
|
let bundleText = null;
|
|
89
137
|
let bundled = null;
|
|
90
138
|
if (shouldBundle) {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const bundleLines = [];
|
|
94
|
-
sections.forEach((section) => {
|
|
95
|
-
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
96
|
-
bundleLines.push("");
|
|
97
|
-
});
|
|
98
|
-
bundleText = `${bundleLines
|
|
99
|
-
.join("\n")
|
|
100
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
101
|
-
.trimEnd()}\n`;
|
|
102
|
-
await fs.writeFile(bundlePath, bundleText, "utf8");
|
|
139
|
+
const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
|
|
140
|
+
bundleText = writtenBundle.tokenEstimateText;
|
|
103
141
|
attachments.length = 0;
|
|
104
|
-
attachments.push(
|
|
105
|
-
path: bundlePath,
|
|
106
|
-
displayPath: bundlePath,
|
|
107
|
-
sizeBytes: Buffer.byteLength(bundleText, "utf8"),
|
|
108
|
-
});
|
|
142
|
+
attachments.push(writtenBundle.attachment);
|
|
109
143
|
attachments.push(...mediaAttachments);
|
|
110
|
-
bundled =
|
|
144
|
+
bundled = writtenBundle.metadata;
|
|
111
145
|
}
|
|
112
146
|
const inlineFileCount = selectedPlan.inlineFileCount;
|
|
113
147
|
const modelConfig = isKnownModel(runOptions.model)
|
|
@@ -140,26 +174,11 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
140
174
|
const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
|
|
141
175
|
let fallbackBundled = null;
|
|
142
176
|
if (uploadPlan.shouldBundle) {
|
|
143
|
-
const
|
|
144
|
-
const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
|
|
145
|
-
const bundleLines = [];
|
|
146
|
-
sections.forEach((section) => {
|
|
147
|
-
bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
|
|
148
|
-
bundleLines.push("");
|
|
149
|
-
});
|
|
150
|
-
const fallbackBundleText = `${bundleLines
|
|
151
|
-
.join("\n")
|
|
152
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
153
|
-
.trimEnd()}\n`;
|
|
154
|
-
await fs.writeFile(bundlePath, fallbackBundleText, "utf8");
|
|
177
|
+
const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
|
|
155
178
|
fallbackAttachments.length = 0;
|
|
156
|
-
fallbackAttachments.push(
|
|
157
|
-
path: bundlePath,
|
|
158
|
-
displayPath: bundlePath,
|
|
159
|
-
sizeBytes: Buffer.byteLength(fallbackBundleText, "utf8"),
|
|
160
|
-
});
|
|
179
|
+
fallbackAttachments.push(writtenBundle.attachment);
|
|
161
180
|
fallbackAttachments.push(...mediaAttachments);
|
|
162
|
-
fallbackBundled =
|
|
181
|
+
fallbackBundled = writtenBundle.metadata;
|
|
163
182
|
}
|
|
164
183
|
fallback = {
|
|
165
184
|
composerText: fallbackComposerText,
|
|
@@ -5,6 +5,61 @@ import { runBrowserMode } from "../browserMode.js";
|
|
|
5
5
|
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
6
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
7
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
8
|
+
const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
|
|
9
|
+
const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
|
|
10
|
+
function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
11
|
+
if (!browserConfig.desiredModel) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
requestedModel: browserConfig.desiredModel,
|
|
16
|
+
resolvedLabel: null,
|
|
17
|
+
strategy: browserConfig.modelStrategy,
|
|
18
|
+
status: "unavailable",
|
|
19
|
+
verified: false,
|
|
20
|
+
source: "config",
|
|
21
|
+
capturedAt: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function formatModelSelectionEvidence(evidence) {
|
|
25
|
+
const requested = evidence.requestedModel ?? "(none)";
|
|
26
|
+
const resolved = evidence.resolvedLabel ?? "(unavailable)";
|
|
27
|
+
const strategy = evidence.strategy ?? "(default)";
|
|
28
|
+
const verified = evidence.verified ? "yes" : "no";
|
|
29
|
+
return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
|
|
30
|
+
}
|
|
31
|
+
function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
|
|
32
|
+
const candidates = [
|
|
33
|
+
runOptions.model,
|
|
34
|
+
browserConfig.desiredModel,
|
|
35
|
+
evidence?.requestedModel,
|
|
36
|
+
evidence?.resolvedLabel,
|
|
37
|
+
];
|
|
38
|
+
return candidates.some((value) => typeof value === "string" && /\bpro\b/i.test(value));
|
|
39
|
+
}
|
|
40
|
+
export function buildBrowserRunWarningsForTest(args) {
|
|
41
|
+
return buildBrowserRunWarnings(args);
|
|
42
|
+
}
|
|
43
|
+
function buildBrowserRunWarnings(args) {
|
|
44
|
+
if (!isRequestedProBrowserRun(args.runOptions, args.browserConfig, args.modelSelection) ||
|
|
45
|
+
args.inputTokens < LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD ||
|
|
46
|
+
args.elapsedMs >= LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
code: "browser-pro-fast-large-run",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message: `Large browser Pro run completed quickly (${(args.elapsedMs / 1000).toFixed(0)}s for ~${args.inputTokens.toLocaleString()} input tokens); verify the stored model selection evidence before claiming Pro Extended output.`,
|
|
54
|
+
details: {
|
|
55
|
+
inputTokens: args.inputTokens,
|
|
56
|
+
elapsedMs: args.elapsedMs,
|
|
57
|
+
requestedModel: args.modelSelection?.requestedModel ?? args.browserConfig.desiredModel,
|
|
58
|
+
resolvedLabel: args.modelSelection?.resolvedLabel ?? null,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
}
|
|
8
63
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
9
64
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
10
65
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
@@ -37,7 +92,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
37
92
|
if (typeof message !== "string")
|
|
38
93
|
return;
|
|
39
94
|
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
40
|
-
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance/i.test(message);
|
|
95
|
+
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
|
|
41
96
|
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
42
97
|
return;
|
|
43
98
|
log(message);
|
|
@@ -84,6 +139,20 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
84
139
|
const message = error instanceof Error ? error.message : "Browser automation failed.";
|
|
85
140
|
throw new BrowserAutomationError(message, { stage: "execute-browser" }, error);
|
|
86
141
|
}
|
|
142
|
+
const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
|
|
143
|
+
if (modelSelection) {
|
|
144
|
+
log(formatModelSelectionEvidence(modelSelection));
|
|
145
|
+
}
|
|
146
|
+
const warnings = buildBrowserRunWarnings({
|
|
147
|
+
runOptions,
|
|
148
|
+
browserConfig,
|
|
149
|
+
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
150
|
+
elapsedMs: browserResult.tookMs,
|
|
151
|
+
modelSelection,
|
|
152
|
+
});
|
|
153
|
+
for (const warning of warnings) {
|
|
154
|
+
log(chalk.yellow(`[browser] ${warning.message}`));
|
|
155
|
+
}
|
|
87
156
|
if (!runOptions.silent) {
|
|
88
157
|
log(chalk.bold("Answer:"));
|
|
89
158
|
log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim("(no text output)"));
|
|
@@ -148,6 +217,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
148
217
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
149
218
|
},
|
|
150
219
|
archive: browserResult.archive,
|
|
220
|
+
modelSelection,
|
|
221
|
+
warnings,
|
|
151
222
|
answerText,
|
|
152
223
|
artifacts: savedArtifacts,
|
|
153
224
|
};
|