@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.
Files changed (49) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/modelSelection.js +74 -20
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +76 -18
  6. package/dist/src/browser/actions/thinkingTime.js +133 -19
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +78 -9
  9. package/dist/src/browser/manualLoginProfile.js +54 -0
  10. package/dist/src/browser/projectSourcesRunner.js +16 -5
  11. package/dist/src/browser/prompt.js +56 -37
  12. package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
  13. package/dist/src/browser/reattachability.js +22 -0
  14. package/dist/src/browser/sessionRunner.js +73 -1
  15. package/dist/src/browser/utils.js +1 -47
  16. package/dist/src/browser/zipBundle.js +152 -0
  17. package/dist/src/cli/browserConfig.js +13 -11
  18. package/dist/src/cli/browserDefaults.js +2 -1
  19. package/dist/src/cli/docsCheck.js +186 -0
  20. package/dist/src/cli/engine.js +11 -4
  21. package/dist/src/cli/options.js +12 -6
  22. package/dist/src/cli/perfTrace.js +242 -0
  23. package/dist/src/cli/promptRequirement.js +2 -0
  24. package/dist/src/cli/providerDoctor.js +85 -0
  25. package/dist/src/cli/runOptions.js +46 -16
  26. package/dist/src/cli/sessionDisplay.js +47 -4
  27. package/dist/src/cli/sessionLifecycle.js +38 -0
  28. package/dist/src/cli/sessionRunner.js +272 -3
  29. package/dist/src/cli/sessionTable.js +2 -1
  30. package/dist/src/duration.js +47 -0
  31. package/dist/src/mcp/tools/consult.js +19 -3
  32. package/dist/src/mcp/types.js +1 -0
  33. package/dist/src/mcp/utils.js +4 -1
  34. package/dist/src/oracle/baseUrl.js +17 -0
  35. package/dist/src/oracle/client.js +1 -22
  36. package/dist/src/oracle/config.js +17 -4
  37. package/dist/src/oracle/gemini.js +2 -22
  38. package/dist/src/oracle/geminiModels.js +21 -0
  39. package/dist/src/oracle/modelResolver.js +7 -1
  40. package/dist/src/oracle/multiModelRunner.js +20 -2
  41. package/dist/src/oracle/providerFailures.js +204 -0
  42. package/dist/src/oracle/providerRoutePlan.js +308 -0
  43. package/dist/src/oracle/providerRouting.js +92 -0
  44. package/dist/src/oracle/run.js +104 -107
  45. package/dist/src/oracle.js +1 -0
  46. package/dist/src/remote/client.js +8 -0
  47. package/dist/src/remote/server.js +26 -0
  48. package/dist/src/sessionManager.js +43 -23
  49. 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
- : path.join(os.homedir(), ".oracle", "browser-profile");
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
- await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
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 deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
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
- throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
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
- await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
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
- : path.join(os.homedir(), ".oracle", "browser-profile");
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 deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
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
- throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
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, { cwd });
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 bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
92
- const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
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 = { originalCount: sections.length, bundlePath };
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 bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
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 = { originalCount: sections.length, bundlePath };
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
+ }