@steipete/oracle 0.11.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -10
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/modelSelection.js +74 -20
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +76 -18
- package/dist/src/browser/actions/thinkingTime.js +133 -19
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +78 -9
- 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/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattachability.js +22 -0
- package/dist/src/browser/sessionRunner.js +73 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -11
- 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 +47 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +272 -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 +1 -0
- 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 +308 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +104 -107
- 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 +43 -23
- package/package.json +15 -12
|
@@ -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";
|
|
@@ -290,6 +291,17 @@ async function closeRemoteConnectionAfterRun(options) {
|
|
|
290
291
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
291
292
|
return options.runStatus === "complete" && options.ownsTarget && !options.keepBrowser;
|
|
292
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
|
+
}
|
|
293
305
|
export async function runBrowserMode(options) {
|
|
294
306
|
const promptText = options.prompt?.trim();
|
|
295
307
|
if (!promptText) {
|
|
@@ -315,6 +327,7 @@ export async function runBrowserMode(options) {
|
|
|
315
327
|
const runtimeHintCb = options.runtimeHintCb;
|
|
316
328
|
let lastTargetId;
|
|
317
329
|
let lastUrl;
|
|
330
|
+
let promptSubmitted = false;
|
|
318
331
|
let tabLease = null;
|
|
319
332
|
const emitRuntimeHint = async () => {
|
|
320
333
|
if (!chrome?.port) {
|
|
@@ -328,6 +341,7 @@ export async function runBrowserMode(options) {
|
|
|
328
341
|
chromeTargetId: lastTargetId,
|
|
329
342
|
tabUrl: lastUrl,
|
|
330
343
|
conversationId,
|
|
344
|
+
promptSubmitted,
|
|
331
345
|
userDataDir,
|
|
332
346
|
controllerPid: process.pid,
|
|
333
347
|
};
|
|
@@ -345,6 +359,13 @@ export async function runBrowserMode(options) {
|
|
|
345
359
|
logger(`Failed to persist runtime hint: ${message}`);
|
|
346
360
|
}
|
|
347
361
|
};
|
|
362
|
+
const markPromptSubmitted = async () => {
|
|
363
|
+
if (promptSubmitted) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
promptSubmitted = true;
|
|
367
|
+
await emitRuntimeHint();
|
|
368
|
+
};
|
|
348
369
|
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
349
370
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
350
371
|
...redactBrowserConfigForDebugLog(config),
|
|
@@ -383,14 +404,19 @@ export async function runBrowserMode(options) {
|
|
|
383
404
|
const manualLogin = Boolean(config.manualLogin);
|
|
384
405
|
const manualProfileDir = config.manualLoginProfileDir
|
|
385
406
|
? path.resolve(config.manualLoginProfileDir)
|
|
386
|
-
:
|
|
407
|
+
: defaultManualLoginProfileDir();
|
|
387
408
|
const userDataDir = manualLogin
|
|
388
409
|
? manualProfileDir
|
|
389
410
|
: await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
|
|
411
|
+
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
390
412
|
if (manualLogin) {
|
|
391
413
|
// Learned: manual login reuses a persistent profile so cookies/SSO survive.
|
|
392
414
|
await mkdir(userDataDir, { recursive: true });
|
|
393
415
|
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
416
|
+
await assertManualLoginProfileReadyForRun({
|
|
417
|
+
userDataDir,
|
|
418
|
+
keepBrowser: effectiveKeepBrowser,
|
|
419
|
+
});
|
|
394
420
|
}
|
|
395
421
|
else {
|
|
396
422
|
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
@@ -403,7 +429,6 @@ export async function runBrowserMode(options) {
|
|
|
403
429
|
sessionId: options.sessionId,
|
|
404
430
|
});
|
|
405
431
|
}
|
|
406
|
-
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
407
432
|
let acquiredChrome;
|
|
408
433
|
try {
|
|
409
434
|
acquiredChrome = manualLogin
|
|
@@ -451,6 +476,7 @@ export async function runBrowserMode(options) {
|
|
|
451
476
|
let answerMarkdown = "";
|
|
452
477
|
let answerHtml = "";
|
|
453
478
|
let runStatus = "attempted";
|
|
479
|
+
let modelSelectionEvidence;
|
|
454
480
|
let connectionClosedUnexpectedly = false;
|
|
455
481
|
let stopThinkingMonitor = null;
|
|
456
482
|
let removeDialogHandler = null;
|
|
@@ -587,6 +613,8 @@ export async function runBrowserMode(options) {
|
|
|
587
613
|
appliedCookies,
|
|
588
614
|
manualLogin,
|
|
589
615
|
timeoutMs: config.timeoutMs,
|
|
616
|
+
profileDir: userDataDir,
|
|
617
|
+
keepBrowser: effectiveKeepBrowser,
|
|
590
618
|
}));
|
|
591
619
|
if (config.url !== baseUrl) {
|
|
592
620
|
await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
|
|
@@ -680,7 +708,7 @@ export async function runBrowserMode(options) {
|
|
|
680
708
|
await captureRuntimeSnapshot();
|
|
681
709
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
682
710
|
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
683
|
-
await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
711
|
+
modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
684
712
|
retries: 2,
|
|
685
713
|
delayMs: 300,
|
|
686
714
|
onRetry: (attempt, error) => {
|
|
@@ -699,13 +727,15 @@ export async function runBrowserMode(options) {
|
|
|
699
727
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
700
728
|
}
|
|
701
729
|
else if (modelStrategy === "ignore") {
|
|
730
|
+
modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
|
|
702
731
|
logger("Model picker: skipped (strategy=ignore)");
|
|
703
732
|
}
|
|
704
733
|
const deepResearch = config.researchMode === "deep";
|
|
705
734
|
// Handle thinking time selection if specified. Deep Research owns its own effort flow.
|
|
706
735
|
const thinkingTime = config.thinkingTime;
|
|
707
736
|
if (thinkingTime && !deepResearch) {
|
|
708
|
-
|
|
737
|
+
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
738
|
+
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
709
739
|
retries: 2,
|
|
710
740
|
delayMs: 300,
|
|
711
741
|
onRetry: (attempt, error) => {
|
|
@@ -783,6 +813,7 @@ export async function runBrowserMode(options) {
|
|
|
783
813
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
784
814
|
baselineTurns: baselineTurns ?? undefined,
|
|
785
815
|
attachmentNames,
|
|
816
|
+
onPromptSubmitted: markPromptSubmitted,
|
|
786
817
|
};
|
|
787
818
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
788
819
|
prompt,
|
|
@@ -791,6 +822,7 @@ export async function runBrowserMode(options) {
|
|
|
791
822
|
log: logger,
|
|
792
823
|
state: providerState,
|
|
793
824
|
});
|
|
825
|
+
await markPromptSubmitted();
|
|
794
826
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
795
827
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
796
828
|
baselineTurns = providerBaselineTurns;
|
|
@@ -881,6 +913,7 @@ export async function runBrowserMode(options) {
|
|
|
881
913
|
answerHtml: researchResult.html,
|
|
882
914
|
artifacts: savedArtifacts,
|
|
883
915
|
archive,
|
|
916
|
+
modelSelection: modelSelectionEvidence,
|
|
884
917
|
tookMs: durationMs,
|
|
885
918
|
answerTokens: tokens,
|
|
886
919
|
answerChars: researchResult.text.length,
|
|
@@ -891,6 +924,7 @@ export async function runBrowserMode(options) {
|
|
|
891
924
|
chromeTargetId: lastTargetId,
|
|
892
925
|
tabUrl: lastUrl,
|
|
893
926
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
927
|
+
promptSubmitted,
|
|
894
928
|
controllerPid: process.pid,
|
|
895
929
|
};
|
|
896
930
|
}
|
|
@@ -975,6 +1009,7 @@ export async function runBrowserMode(options) {
|
|
|
975
1009
|
chromeTargetId: lastTargetId,
|
|
976
1010
|
tabUrl: lastUrl,
|
|
977
1011
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1012
|
+
promptSubmitted,
|
|
978
1013
|
controllerPid: process.pid,
|
|
979
1014
|
},
|
|
980
1015
|
});
|
|
@@ -1030,6 +1065,7 @@ export async function runBrowserMode(options) {
|
|
|
1030
1065
|
chromeTargetId: lastTargetId,
|
|
1031
1066
|
tabUrl: lastUrl,
|
|
1032
1067
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1068
|
+
promptSubmitted,
|
|
1033
1069
|
controllerPid: process.pid,
|
|
1034
1070
|
};
|
|
1035
1071
|
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
|
|
@@ -1266,6 +1302,7 @@ export async function runBrowserMode(options) {
|
|
|
1266
1302
|
generatedImages: imageArtifacts.generatedImages,
|
|
1267
1303
|
savedImages: imageArtifacts.savedImages,
|
|
1268
1304
|
archive,
|
|
1305
|
+
modelSelection: modelSelectionEvidence,
|
|
1269
1306
|
tookMs: durationMs,
|
|
1270
1307
|
answerTokens,
|
|
1271
1308
|
answerChars,
|
|
@@ -1276,6 +1313,7 @@ export async function runBrowserMode(options) {
|
|
|
1276
1313
|
chromeTargetId: lastTargetId,
|
|
1277
1314
|
tabUrl: lastUrl,
|
|
1278
1315
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1316
|
+
promptSubmitted,
|
|
1279
1317
|
controllerPid: process.pid,
|
|
1280
1318
|
};
|
|
1281
1319
|
}
|
|
@@ -1293,6 +1331,7 @@ export async function runBrowserMode(options) {
|
|
|
1293
1331
|
userDataDir,
|
|
1294
1332
|
chromeTargetId: lastTargetId,
|
|
1295
1333
|
tabUrl: lastUrl,
|
|
1334
|
+
promptSubmitted,
|
|
1296
1335
|
controllerPid: process.pid,
|
|
1297
1336
|
};
|
|
1298
1337
|
const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
|
|
@@ -1333,6 +1372,7 @@ export async function runBrowserMode(options) {
|
|
|
1333
1372
|
userDataDir,
|
|
1334
1373
|
chromeTargetId: lastTargetId,
|
|
1335
1374
|
tabUrl: lastUrl,
|
|
1375
|
+
promptSubmitted,
|
|
1336
1376
|
controllerPid: process.pid,
|
|
1337
1377
|
},
|
|
1338
1378
|
}, normalizedError);
|
|
@@ -1491,12 +1531,13 @@ async function findEphemeralPort() {
|
|
|
1491
1531
|
});
|
|
1492
1532
|
});
|
|
1493
1533
|
}
|
|
1494
|
-
async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
|
|
1534
|
+
async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, profileDir, keepBrowser, }) {
|
|
1495
1535
|
if (!manualLogin) {
|
|
1496
1536
|
await ensureLoggedIn(runtime, logger, { appliedCookies });
|
|
1497
1537
|
return;
|
|
1498
1538
|
}
|
|
1499
|
-
const
|
|
1539
|
+
const waitMs = resolveManualLoginWaitMs(timeoutMs, Boolean(keepBrowser));
|
|
1540
|
+
const deadline = Date.now() + waitMs;
|
|
1500
1541
|
let lastNotice = 0;
|
|
1501
1542
|
while (Date.now() < deadline) {
|
|
1502
1543
|
try {
|
|
@@ -1518,7 +1559,10 @@ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, time
|
|
|
1518
1559
|
await delay(1000);
|
|
1519
1560
|
}
|
|
1520
1561
|
}
|
|
1521
|
-
|
|
1562
|
+
const setupCommand = formatManualLoginSetupCommand(profileDir ?? defaultManualLoginProfileDir());
|
|
1563
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session. " +
|
|
1564
|
+
`Browser mode is using Oracle's private Chrome profile at ${profileDir ?? "(default profile)"}, not your normal Chrome profile. ` +
|
|
1565
|
+
`Run first-time setup, sign in there, then retry: ${setupCommand}`);
|
|
1522
1566
|
}
|
|
1523
1567
|
async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answerText, answerMarkdown, logger, allowMarkdownUpdate, }) {
|
|
1524
1568
|
// Learned: long streaming responses can still be rendering after initial capture.
|
|
@@ -1677,6 +1721,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1677
1721
|
let remoteTargetId = null;
|
|
1678
1722
|
let tabLease = null;
|
|
1679
1723
|
let lastUrl;
|
|
1724
|
+
let promptSubmitted = false;
|
|
1680
1725
|
let attachedExistingTab = false;
|
|
1681
1726
|
let ownsTarget = true;
|
|
1682
1727
|
const runtimeHintCb = options.runtimeHintCb;
|
|
@@ -1692,6 +1737,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1692
1737
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
1693
1738
|
tabUrl: lastUrl,
|
|
1694
1739
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1740
|
+
promptSubmitted,
|
|
1695
1741
|
controllerPid: process.pid,
|
|
1696
1742
|
});
|
|
1697
1743
|
await tabLease?.update({
|
|
@@ -1706,12 +1752,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1706
1752
|
logger(`Failed to persist runtime hint: ${message}`);
|
|
1707
1753
|
}
|
|
1708
1754
|
};
|
|
1755
|
+
const markPromptSubmitted = async () => {
|
|
1756
|
+
if (promptSubmitted) {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
promptSubmitted = true;
|
|
1760
|
+
await emitRuntimeHint();
|
|
1761
|
+
};
|
|
1709
1762
|
const startedAt = Date.now();
|
|
1710
1763
|
let answerText = "";
|
|
1711
1764
|
let answerMarkdown = "";
|
|
1712
1765
|
let answerHtml = "";
|
|
1713
1766
|
let connectionClosedUnexpectedly = false;
|
|
1714
1767
|
let runStatus = "attempted";
|
|
1768
|
+
let modelSelectionEvidence;
|
|
1715
1769
|
let stopThinkingMonitor = null;
|
|
1716
1770
|
let removeDialogHandler = null;
|
|
1717
1771
|
let connection = null;
|
|
@@ -1801,7 +1855,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1801
1855
|
}
|
|
1802
1856
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
1803
1857
|
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
1804
|
-
await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
1858
|
+
modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
1805
1859
|
retries: 2,
|
|
1806
1860
|
delayMs: 300,
|
|
1807
1861
|
onRetry: (attempt, error) => {
|
|
@@ -1814,13 +1868,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1814
1868
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1815
1869
|
}
|
|
1816
1870
|
else if (modelStrategy === "ignore") {
|
|
1871
|
+
modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
|
|
1817
1872
|
logger("Model picker: skipped (strategy=ignore)");
|
|
1818
1873
|
}
|
|
1819
1874
|
const deepResearch = config.researchMode === "deep";
|
|
1820
1875
|
// Handle thinking time selection if specified. Deep Research owns its own effort flow.
|
|
1821
1876
|
const thinkingTime = config.thinkingTime;
|
|
1822
1877
|
if (thinkingTime && !deepResearch) {
|
|
1823
|
-
|
|
1878
|
+
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
1879
|
+
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
1824
1880
|
retries: 2,
|
|
1825
1881
|
delayMs: 300,
|
|
1826
1882
|
onRetry: (attempt, error) => {
|
|
@@ -1876,6 +1932,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1876
1932
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
1877
1933
|
baselineTurns: baselineTurns ?? undefined,
|
|
1878
1934
|
attachmentNames,
|
|
1935
|
+
onPromptSubmitted: markPromptSubmitted,
|
|
1879
1936
|
};
|
|
1880
1937
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
1881
1938
|
prompt,
|
|
@@ -1884,6 +1941,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1884
1941
|
log: logger,
|
|
1885
1942
|
state: providerState,
|
|
1886
1943
|
});
|
|
1944
|
+
await markPromptSubmitted();
|
|
1887
1945
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
1888
1946
|
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
1889
1947
|
baselineTurns = providerBaselineTurns;
|
|
@@ -1948,6 +2006,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1948
2006
|
answerHtml: researchResult.html,
|
|
1949
2007
|
artifacts: savedArtifacts,
|
|
1950
2008
|
archive,
|
|
2009
|
+
modelSelection: modelSelectionEvidence,
|
|
1951
2010
|
tookMs: durationMs,
|
|
1952
2011
|
answerTokens: tokens,
|
|
1953
2012
|
answerChars: researchResult.text.length,
|
|
@@ -1956,6 +2015,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1956
2015
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
1957
2016
|
tabUrl: lastUrl,
|
|
1958
2017
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2018
|
+
promptSubmitted,
|
|
1959
2019
|
controllerPid: process.pid,
|
|
1960
2020
|
};
|
|
1961
2021
|
}
|
|
@@ -2039,6 +2099,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2039
2099
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2040
2100
|
tabUrl: lastUrl,
|
|
2041
2101
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2102
|
+
promptSubmitted,
|
|
2042
2103
|
controllerPid: process.pid,
|
|
2043
2104
|
},
|
|
2044
2105
|
});
|
|
@@ -2107,6 +2168,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2107
2168
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2108
2169
|
tabUrl: lastUrl,
|
|
2109
2170
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2171
|
+
promptSubmitted,
|
|
2110
2172
|
controllerPid: process.pid,
|
|
2111
2173
|
};
|
|
2112
2174
|
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
|
|
@@ -2309,8 +2371,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2309
2371
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2310
2372
|
tabUrl: lastUrl,
|
|
2311
2373
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2374
|
+
promptSubmitted,
|
|
2312
2375
|
artifacts: savedArtifacts,
|
|
2313
2376
|
archive,
|
|
2377
|
+
modelSelection: modelSelectionEvidence,
|
|
2314
2378
|
controllerPid: process.pid,
|
|
2315
2379
|
};
|
|
2316
2380
|
}
|
|
@@ -2334,6 +2398,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2334
2398
|
chromeProfileRoot,
|
|
2335
2399
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2336
2400
|
tabUrl: lastUrl,
|
|
2401
|
+
promptSubmitted,
|
|
2337
2402
|
controllerPid: process.pid,
|
|
2338
2403
|
},
|
|
2339
2404
|
});
|
|
@@ -2372,10 +2437,14 @@ export { estimateTokenCount } from "./utils.js";
|
|
|
2372
2437
|
export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from "./config.js";
|
|
2373
2438
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
2374
2439
|
export const __test__ = {
|
|
2440
|
+
assertManualLoginProfileReadyForRun,
|
|
2375
2441
|
closeRemoteConnectionAfterRun,
|
|
2376
2442
|
detachKeptChromeProcess,
|
|
2443
|
+
formatManualLoginSetupCommand,
|
|
2444
|
+
isManualLoginProfileInitialized,
|
|
2377
2445
|
isImageOnlyUiChromeText,
|
|
2378
2446
|
listIgnoredRemoteChromeFlags,
|
|
2447
|
+
resolveManualLoginWaitMs,
|
|
2379
2448
|
shouldCloseOwnedRunTargetAfterRun,
|
|
2380
2449
|
};
|
|
2381
2450
|
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,
|
|
@@ -23,6 +23,7 @@ async function submitPromptViaAdapter(ctx) {
|
|
|
23
23
|
attachmentNames: state.attachmentNames ?? [],
|
|
24
24
|
baselineTurns: state.baselineTurns ?? undefined,
|
|
25
25
|
inputTimeoutMs: state.inputTimeoutMs ?? undefined,
|
|
26
|
+
onPromptSubmitted: state.onPromptSubmitted,
|
|
26
27
|
}, ctx.prompt, state.logger);
|
|
27
28
|
state.committedTurns =
|
|
28
29
|
typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function hasRecoverableChatGptConversation(runtime) {
|
|
2
|
+
if (!runtime) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (runtime.conversationId?.trim()) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
const tabUrl = runtime.tabUrl?.trim();
|
|
9
|
+
if (!tabUrl) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(tabUrl);
|
|
14
|
+
if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return /(?:^|\/)c\/[^/]+/.test(url.pathname);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|