@steipete/oracle 0.17.3 → 0.18.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.
@@ -227,7 +227,7 @@ program
227
227
  .addOption(new Option("--copy-markdown", "Copy the assembled markdown bundle to the clipboard; pair with --render to print it too.").default(false))
228
228
  .addOption(new Option("--copy").hideHelp().default(false))
229
229
  .option("-s, --slug <words>", "Custom session slug (3-5 words).")
230
- .option("-m, --model <model>", "Model to target (gpt-5.5-pro default). GPT-5.6 aliases gpt-5.6 and gpt-5.6-sol work with the OpenAI API or ChatGPT browser. Browser mode also supports current GPT-5.5/GPT-5.4 targets and legacy Pro aliases; retired GPT-5.2 base/Instant/Thinking aliases are API-only. Other API targets include gpt-5.1-codex, gpt-5.2, gpt-5.2-instant, Gemini, Claude, and custom model IDs.", normalizeModelOption)
230
+ .option("-m, --model <model>", "Model to target (gpt-5.5-pro default). GPT-5.6 aliases gpt-5.6 and gpt-5.6-sol work with the OpenAI API or ChatGPT browser. In browser mode, generic Pro aliases follow the current GPT-5.6 Sol target; use explicit gpt-5.5-pro to pin GPT-5.5. Retired GPT-5.2 base/Instant/Thinking aliases are API-only. Other API targets include gpt-5.1-codex, gpt-5.2, gpt-5.2-instant, Gemini, Claude, and custom model IDs.", normalizeModelOption)
231
231
  .addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
232
232
  .argParser(collectModelList)
233
233
  .default([]))
@@ -331,6 +331,7 @@ program
331
331
  .addOption(new Option("--browser-cookie-names <names>", "Comma-separated cookie allowlist for sync.").hideHelp())
332
332
  .addOption(new Option("--browser-inline-cookies <jsonOrBase64>", "Inline cookies payload (JSON array or base64-encoded JSON).").hideHelp())
333
333
  .addOption(new Option("--browser-inline-cookies-file <path>", "Load inline cookies from file (JSON or base64 JSON).").hideHelp())
334
+ .addOption(new Option("--browser-cookie-sync", "Copy cookies from live Chrome (opt-in; token rotation may invalidate that session)."))
334
335
  .addOption(new Option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.").hideHelp())
335
336
  .addOption(new Option("--browser-manual-login", "Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login.").hideHelp())
336
337
  .addOption(new Option("--browser-manual-login-profile-dir <path>", "Persistent Chrome profile directory for manual-login browser runs.").hideHelp())
@@ -393,6 +394,7 @@ program
393
394
  .option("--token <value>", "Access token clients must provide (random if omitted).")
394
395
  .option("--manual-login", "Use a dedicated Chrome profile for manual login (recommended when cookie sync is unavailable).", false)
395
396
  .option("--manual-login-profile-dir <path>", "Chrome profile directory for manual login (default ~/.oracle/browser-profile).")
397
+ .option("--browser-cookie-sync", "Copy cookies from this host's live Chrome profile instead of using the dedicated profile.", false)
396
398
  .action(async (commandOptions) => {
397
399
  const { serveRemote } = await import("../src/remote/server.js");
398
400
  await serveRemote({
@@ -401,6 +403,7 @@ program
401
403
  token: commandOptions.token,
402
404
  manualLoginDefault: commandOptions.manualLogin,
403
405
  manualLoginProfileDir: commandOptions.manualLoginProfileDir,
406
+ cookieSyncDefault: commandOptions.browserCookieSync,
404
407
  });
405
408
  });
406
409
  const projectSourcesCommand = program
@@ -422,6 +425,7 @@ function addProjectSourcesCommonOptions(command) {
422
425
  .option("--browser-cookie-path <path>", "Explicit Chrome cookie DB path.")
423
426
  .option("--browser-inline-cookies <json>", "Inline ChatGPT cookies JSON.")
424
427
  .option("--browser-inline-cookies-file <path>", "File containing ChatGPT cookies JSON.")
428
+ .option("--browser-cookie-sync", "Copy cookies from live Chrome (opt-in; token rotation may invalidate that session).")
425
429
  .option("--browser-no-cookie-sync", "Skip copying cookies from Chrome.")
426
430
  .option("--browser-keep-browser", "Keep Chrome running after completion.", false)
427
431
  .option("--browser-hide-window", "Hide Chrome window after launch on macOS.", false)
@@ -1334,6 +1338,7 @@ async function runRootCommand(options) {
1334
1338
  ...options,
1335
1339
  remoteHost: remoteHost ?? undefined,
1336
1340
  model: activeModel,
1341
+ browserRequestedModel: cliModelArg,
1337
1342
  browserModelLabel: resolveBrowserModelLabel(cliModelArg, activeModel),
1338
1343
  });
1339
1344
  return resolvedOptions.browserResumeConversationUrl
@@ -1981,6 +1986,10 @@ function printDebugHelp(cliName) {
1981
1986
  "--browser-cookie-wait <ms|s|m>",
1982
1987
  "Wait before retrying cookie sync when Chrome cookies are empty or locked.",
1983
1988
  ],
1989
+ [
1990
+ "--browser-cookie-sync",
1991
+ "Copy cookies from live Chrome (opt-in; token rotation may invalidate that session).",
1992
+ ],
1984
1993
  ["--browser-no-cookie-sync", "Skip copying cookies from your main profile."],
1985
1994
  [
1986
1995
  "--browser-manual-login",
@@ -1,6 +1,35 @@
1
1
  import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
2
2
  import { logDomFailure } from "../domDebug.js";
3
3
  import { buildClickDispatcher } from "./domEvents.js";
4
+ import { BrowserAutomationError } from "../../oracle/errors.js";
5
+ export class ThinkingTierUnavailableError extends BrowserAutomationError {
6
+ requestedLevel;
7
+ requestedLabel;
8
+ optionLabel;
9
+ notice;
10
+ confirmedTarget;
11
+ constructor(requestedLevel, requestedLabel, optionLabel, notice, confirmedTarget) {
12
+ const message = `Thinking time: ${optionLabel ?? requestedLabel} is unavailable on this account (${notice ?? "no reason given"}); refusing to submit without confirmed ${confirmedTarget}.`;
13
+ super(message, {
14
+ stage: "thinking-tier-unavailable",
15
+ requestedLevel,
16
+ requestedLabel,
17
+ optionLabel,
18
+ notice,
19
+ confirmedTarget,
20
+ });
21
+ this.name = "ThinkingTierUnavailableError";
22
+ this.requestedLevel = requestedLevel;
23
+ this.requestedLabel = requestedLabel;
24
+ this.optionLabel = optionLabel;
25
+ this.notice = notice;
26
+ this.confirmedTarget = confirmedTarget;
27
+ }
28
+ }
29
+ function confirmedThinkingTarget(level, capitalizedLevel, targetModelKind, observedModelKind) {
30
+ const strictModelKind = targetModelKind ?? observedModelKind;
31
+ return level === "pro" ? "Pro" : strictModelKind === "pro" ? "Pro Extended" : capitalizedLevel;
32
+ }
4
33
  const BROWSER_THINKING_LOG_PREFIX = "[browser] Thinking time:";
5
34
  function formatBrowserThinkingLog(message) {
6
35
  return `${BROWSER_THINKING_LOG_PREFIX} ${message.replace(/^Thinking time:\s*/, "")}`;
@@ -43,6 +72,18 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
43
72
  case "switched":
44
73
  logger(formatBrowserThinkingLog(result.label ?? capitalizedLevel));
45
74
  return;
75
+ case "option-disabled": {
76
+ await logDomFailure(Runtime, logger, "thinking-option-disabled");
77
+ logPickerDiagnostic(result, logger);
78
+ if (strictProEffort) {
79
+ throw new ThinkingTierUnavailableError(level, capitalizedLevel, result.label ?? null, result.notice ?? null, confirmedThinkingTarget(level, capitalizedLevel, targetModelKind, observedModelKind));
80
+ }
81
+ // A non-strict caller goes on to submit, so this log must not borrow the
82
+ // strict error's "refusing to submit" wording: the request really is sent,
83
+ // at whatever effort ChatGPT already had selected.
84
+ logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} is unavailable on this account (${result.notice ?? "no reason given"}); keeping the effort already selected in ChatGPT.`));
85
+ return;
86
+ }
46
87
  case "chip-not-found":
47
88
  case "menu-not-found":
48
89
  case "option-not-found":
@@ -97,6 +138,9 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desi
97
138
  case "switched":
98
139
  logger(formatBrowserThinkingLog(result.label ?? capitalizedLevel));
99
140
  return true;
141
+ case "option-disabled":
142
+ logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} is unavailable on this account (${result.notice ?? "no reason given"}); keeping the effort already selected in ChatGPT.`));
143
+ return false;
100
144
  case "chip-not-found":
101
145
  case "menu-not-found":
102
146
  case "option-not-found":
@@ -292,6 +336,21 @@ function buildThinkingTimeExpression(level, desiredModel) {
292
336
  .replace(/\\s+/g, ' ')
293
337
  .trim()
294
338
  .slice(0, maxLength);
339
+ const isOptionDisabled = (node) => {
340
+ if (!node || typeof node.getAttribute !== 'function') return false;
341
+ // data-disabled is Radix's valueless-presence convention, but an explicit
342
+ // "false" must not read as disabled: that would refuse a perfectly usable
343
+ // tier and report it as unavailable.
344
+ const dataDisabled = node.getAttribute('data-disabled');
345
+ const dataDisabledOn = dataDisabled !== null && String(dataDisabled).toLowerCase() !== 'false';
346
+ return (
347
+ node.getAttribute('aria-disabled') === 'true' ||
348
+ dataDisabledOn ||
349
+ (node.getAttribute('data-state') || '').toLowerCase() === 'disabled' ||
350
+ Boolean(node.disabled) ||
351
+ node.getAttribute('disabled') !== null
352
+ );
353
+ };
295
354
  const describeNode = (el) => {
296
355
  if (!el || typeof el.getAttribute !== 'function') return null;
297
356
  let rect = null;
@@ -314,7 +373,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
314
373
  ariaChecked: el.getAttribute('aria-checked'),
315
374
  ariaSelected: el.getAttribute('aria-selected'),
316
375
  ariaHaspopup: el.getAttribute('aria-haspopup'),
376
+ ariaDisabled: el.getAttribute('aria-disabled'),
377
+ dataDisabled: el.getAttribute('data-disabled'),
317
378
  dataState: el.getAttribute('data-state'),
379
+ disabled: isOptionDisabled(el),
318
380
  text: redactDiagnosticText(el.textContent, 80),
319
381
  rect,
320
382
  };
@@ -590,6 +652,77 @@ function buildThinkingTimeExpression(level, desiredModel) {
590
652
  }
591
653
  return matchesLevel(normalizedLabel);
592
654
  };
655
+ const nonEmptyNotice = (value) => {
656
+ const text = redactDiagnosticText(value, 160);
657
+ return text || null;
658
+ };
659
+ // The notice must be ROW-OWNED, and preferably this hover's own.
660
+ //
661
+ // NOTE: no backticks in this comment — it lives inside the injected template
662
+ // literal, where a backtick would terminate the string.
663
+ //
664
+ // A document-wide role=tooltip scan is what this must never become: novelty is
665
+ // not causality, and an unrelated control with an armed open-delay can mount its
666
+ // tooltip inside this hover's window. Every pass below is anchored on the row.
667
+ //
668
+ // Preference order, strongest provenance first:
669
+ // 1. an id this hover ADDED whose target is role=tooltip — Radix keeps an
670
+ // application-supplied description and APPENDS its tooltip id when opening,
671
+ // so the delta is what separates the real notice from a permanent blurb;
672
+ // 2. any other id this hover added;
673
+ // 3. an already-associated role=tooltip target, for a tooltip that was open
674
+ // before the probe arrived;
675
+ // 4. the row's static title, only once the poll has expired.
676
+ //
677
+ // Passes 3 and 4 are row-owned but NOT causal: a page that permanently points a
678
+ // disabled row at generic role=tooltip help, or gives it a generic title, will
679
+ // have that text reported. That is accepted deliberately — the value is an opaque
680
+ // notice for a human or a caller to interpret, not a parsed reset time — and the
681
+ // verified live target has neither at rest.
682
+ const describedIds = (option) =>
683
+ (option?.getAttribute?.('aria-describedby') || '').split(/\\s+/).filter(Boolean);
684
+ const isTooltipNode = (node) => node?.getAttribute?.('role') === 'tooltip';
685
+ const readDisabledNotice = (option, priorIds, allowTitle) => {
686
+ const ids = describedIds(option);
687
+ const prior = priorIds instanceof Set ? priorIds : new Set();
688
+ const fresh = ids.filter((id) => !prior.has(id));
689
+ for (const pass of [
690
+ fresh.filter((id) => isTooltipNode(document.getElementById?.(id))),
691
+ fresh,
692
+ ids.filter((id) => isTooltipNode(document.getElementById?.(id))),
693
+ ]) {
694
+ for (const id of pass) {
695
+ const notice = nonEmptyNotice(document.getElementById?.(id)?.textContent);
696
+ if (notice) return notice;
697
+ }
698
+ }
699
+ if (!allowTitle) return null;
700
+ return nonEmptyNotice(option?.getAttribute?.('title'));
701
+ };
702
+ const waitForDisabledNotice = async (option, priorIds) => {
703
+ const deadline = performance.now() + 400;
704
+ while (performance.now() < deadline) {
705
+ const notice = readDisabledNotice(option, priorIds, false);
706
+ if (notice) return notice;
707
+ await sleep(50);
708
+ }
709
+ // Only now may a static title speak: the association had its full window.
710
+ return readDisabledNotice(option, priorIds, true);
711
+ };
712
+ // Opening a tooltip stacks another Radix dismissable layer over the menu, and
713
+ // the topmost layer eats the Escape. One blind Escape therefore leaves the
714
+ // effort menu open, which matters for the non-strict caller that goes on to
715
+ // submit. Dismiss, let the layer unmount, and only Escape again while a menu is
716
+ // still there — never two unconditional Escapes, which could close an unrelated
717
+ // outer surface.
718
+ const closeMenusAfterTooltip = async () => {
719
+ closeOpenMenus();
720
+ for (let attempt = 0; attempt < 3; attempt += 1) {
721
+ await sleep(50);
722
+ if (!Array.from(document.querySelectorAll(MENU_CONTAINER_SELECTOR)).some(isVisible)) return;
723
+ closeOpenMenus();
724
+ }
725
+ };
593
726
  const selectAndVerify = async (trigger, findOption, modelKindOverride = null) => {
594
727
  const triggerModelKind =
595
728
  modelKindOverride ||
@@ -615,6 +748,22 @@ function buildThinkingTimeExpression(level, desiredModel) {
615
748
  }
616
749
  if (!option) return failure('option-not-found', { modelKind: triggerModelKind });
617
750
  const label = option.textContent?.trim?.() || null;
751
+ if (isOptionDisabled(option)) {
752
+ // Captured BEFORE the hover: the ids already here describe the row for other
753
+ // reasons and cannot be this hover's reason.
754
+ const priorIds = new Set(describedIds(option));
755
+ dispatchHoverSequence(option);
756
+ const notice = await waitForDisabledNotice(option, priorIds);
757
+ const result = failure('option-disabled', {
758
+ // The label is page text that reaches logs and diagnostics, so it is
759
+ // redacted like every other reported string.
760
+ label: redactDiagnosticText(option.textContent, 80) || null,
761
+ notice,
762
+ modelKind: triggerModelKind,
763
+ });
764
+ await closeMenusAfterTooltip();
765
+ return result;
766
+ }
618
767
  if (optionIsSelected(option)) {
619
768
  closeOpenMenus();
620
769
  return { status: 'already-selected', label };
@@ -35,7 +35,7 @@ export const DEFAULT_BROWSER_CONFIG = {
35
35
  autoReattachDelayMs: 0,
36
36
  autoReattachIntervalMs: 0,
37
37
  autoReattachTimeoutMs: 120_000,
38
- cookieSync: true,
38
+ cookieSync: false,
39
39
  cookieNames: DEFAULT_CHATGPT_COOKIE_NAMES,
40
40
  cookieSyncWaitMs: 0,
41
41
  inlineCookies: null,
@@ -70,7 +70,6 @@ export function resolveBrowserConfig(config) {
70
70
  DEFAULT_MODEL_STRATEGY;
71
71
  const isWindows = process.platform === "win32";
72
72
  const manualLogin = config?.manualLogin ?? (isWindows ? true : DEFAULT_BROWSER_CONFIG.manualLogin);
73
- const cookieSyncDefault = isWindows ? false : DEFAULT_BROWSER_CONFIG.cookieSync;
74
73
  const resolvedProfileDir = resolveManualLoginProfileDir(config?.manualLoginProfileDir, process.env.ORACLE_BROWSER_PROFILE_DIR);
75
74
  const researchMode = normalizeResearchMode(config?.researchMode);
76
75
  const archiveConversations = normalizeArchiveMode(config?.archiveConversations);
@@ -92,7 +91,7 @@ export function resolveBrowserConfig(config) {
92
91
  autoReattachDelayMs: config?.autoReattachDelayMs ?? DEFAULT_BROWSER_CONFIG.autoReattachDelayMs,
93
92
  autoReattachIntervalMs: config?.autoReattachIntervalMs ?? DEFAULT_BROWSER_CONFIG.autoReattachIntervalMs,
94
93
  autoReattachTimeoutMs: config?.autoReattachTimeoutMs ?? DEFAULT_BROWSER_CONFIG.autoReattachTimeoutMs,
95
- cookieSync: config?.cookieSync ?? cookieSyncDefault,
94
+ cookieSync: config?.cookieSync ?? DEFAULT_BROWSER_CONFIG.cookieSync,
96
95
  cookieNames: config?.cookieNames ?? DEFAULT_BROWSER_CONFIG.cookieNames,
97
96
  cookieSyncWaitMs: config?.cookieSyncWaitMs ?? DEFAULT_BROWSER_CONFIG.cookieSyncWaitMs,
98
97
  inlineCookies: config?.inlineCookies ?? DEFAULT_BROWSER_CONFIG.inlineCookies,
@@ -32,7 +32,7 @@ import { captureBrowserDiagnostics } from "./domDebug.js";
32
32
  import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./actions/archiveConversation.js";
33
33
  import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
34
34
  import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
35
- import { shouldSyncBrowserCookies } from "./policies.js";
35
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
36
36
  import { createConversationUrlMonitor, } from "./conversationUrlMonitor.js";
37
37
  import { extractStableConversationIdFromUrl as extractConversationIdFromUrl, isStableConversationUrl as isConversationUrl, } from "./conversationUrl.js";
38
38
  export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
@@ -874,6 +874,7 @@ export async function runBrowserMode(options) {
874
874
  logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
875
875
  }
876
876
  if (!config.inlineCookies) {
877
+ logger(CHROME_COOKIE_SYNC_WARNING);
877
878
  logger("Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.");
878
879
  }
879
880
  else {
@@ -902,7 +903,7 @@ export async function runBrowserMode(options) {
902
903
  else {
903
904
  logger(manualLogin
904
905
  ? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
905
- : "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
906
+ : "Skipping Chrome cookie copy (disabled by default; use --browser-cookie-sync to opt in).");
906
907
  }
907
908
  await clearStaleChatGptConversationCookies(Network, Target, logger, {
908
909
  preserveConversationIds: [
@@ -24,6 +24,7 @@ export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, ma
24
24
  shouldBundle,
25
25
  };
26
26
  }
27
+ export const CHROME_COOKIE_SYNC_WARNING = "Warning: copying cookies from a live Chrome profile can invalidate that browser's ChatGPT session when tokens rotate. Prefer --browser-manual-login or inline cookies when possible.";
27
28
  export function shouldSyncBrowserCookies(config, { manualLogin, profileIsPreSigned = manualLogin, }) {
28
29
  const explicitManualLoginSync = manualLogin && config.manualLoginCookieSync === true;
29
30
  return config.cookieSync && (!profileIsPreSigned || explicitManualLoginSync);
@@ -36,8 +37,11 @@ export function buildCookiePlan(config) {
36
37
  description: `Cookies: inline payload (${config.inlineCookies.length}) via ${source}.`,
37
38
  };
38
39
  }
39
- if (config?.cookieSync === false) {
40
- return { type: "disabled", description: "Cookies: sync disabled (--browser-no-cookie-sync)." };
40
+ if (config?.cookieSync !== true) {
41
+ return {
42
+ type: "disabled",
43
+ description: "Cookies: Chrome copy disabled (use --browser-manual-login, inline cookies, or --browser-cookie-sync).",
44
+ };
41
45
  }
42
46
  const allowlist = config?.cookieNames && config.cookieNames.length > 0
43
47
  ? config.cookieNames.join(", ")
@@ -13,7 +13,7 @@ import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, form
13
13
  import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
14
14
  import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
15
15
  import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
16
- import { shouldSyncBrowserCookies } from "./policies.js";
16
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
17
17
  export async function runBrowserProjectSources(request) {
18
18
  const startedAt = Date.now();
19
19
  const logger = ((message) => request.log?.(message));
@@ -253,9 +253,12 @@ async function applyProjectSourcesCookies({ config, network, manualLogin, logger
253
253
  if (!cookieSyncEnabled) {
254
254
  logger(manualLogin
255
255
  ? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
256
- : "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
256
+ : "Skipping Chrome cookie copy (disabled by default; use --browser-cookie-sync to opt in).");
257
257
  return 0;
258
258
  }
259
+ if (!config.inlineCookies) {
260
+ logger(CHROME_COOKIE_SYNC_WARNING);
261
+ }
259
262
  const cookieCount = await syncCookies(network, config.url, config.chromeProfile, logger, {
260
263
  allowErrors: config.allowCookieErrors ?? false,
261
264
  filterNames: config.cookieNames ?? undefined,
@@ -12,7 +12,7 @@ import { cleanupStaleProfileState } from "./profileState.js";
12
12
  import { readDevToolsActivePortInfo } from "./detect.js";
13
13
  import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
14
14
  import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
15
- import { shouldSyncBrowserCookies } from "./policies.js";
15
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
16
16
  export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
17
17
  const recoverSession = deps.recoverSession ??
18
18
  (async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
@@ -220,6 +220,9 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
220
220
  }
221
221
  let appliedCookies = 0;
222
222
  if (shouldSyncBrowserCookies(resolved, { manualLogin })) {
223
+ if (!resolved.inlineCookies) {
224
+ logger(CHROME_COOKIE_SYNC_WARNING);
225
+ }
223
226
  const sync = deps.syncCookies ?? syncCookies;
224
227
  try {
225
228
  appliedCookies = await sync(Network, resolved.url, resolved.chromeProfile, logger, {
@@ -13,6 +13,12 @@ const DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS = 45_000;
13
13
  const DEFAULT_BROWSER_RECHECK_TIMEOUT_MS = 120_000;
14
14
  const DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS = 120_000;
15
15
  const DEFAULT_CHROME_PROFILE = "Default";
16
+ const CURRENT_CHATGPT_PRO_ALIASES = new Set([
17
+ "gpt-5-pro",
18
+ "gpt-5.1-pro",
19
+ "gpt-5.2-pro",
20
+ "gpt-5.4-pro",
21
+ ]);
16
22
  // Ordered array: most specific models first to ensure correct selection.
17
23
  // The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
18
24
  const BROWSER_MODEL_LABELS = [
@@ -52,11 +58,8 @@ export function normalizeChatGptModelForBrowser(model) {
52
58
  return normalized;
53
59
  }
54
60
  // Pro variants: resolve to the latest Pro model in ChatGPT.
55
- if (normalized === "gpt-5-pro" ||
56
- normalized === "gpt-5.1-pro" ||
57
- normalized === "gpt-5.2-pro" ||
58
- normalized === "gpt-5.4-pro") {
59
- return "gpt-5.5-pro";
61
+ if (isCurrentChatGptProAlias(normalized)) {
62
+ return "gpt-5.6-sol";
60
63
  }
61
64
  // Explicit model variants: keep as-is (they have their own browser labels)
62
65
  if (normalized === "gpt-5.2-thinking" || normalized === "gpt-5.2-instant") {
@@ -68,6 +71,18 @@ export function normalizeChatGptModelForBrowser(model) {
68
71
  }
69
72
  return model;
70
73
  }
74
+ export function isCurrentChatGptProAlias(model) {
75
+ return CURRENT_CHATGPT_PRO_ALIASES.has(model?.trim().toLowerCase() ?? "");
76
+ }
77
+ export function resolveDefaultBrowserThinkingTime({ model, requestedModel, modelStrategy, }) {
78
+ const strategy = normalizeBrowserModelStrategy(modelStrategy) ?? DEFAULT_MODEL_STRATEGY;
79
+ if (strategy !== "select")
80
+ return undefined;
81
+ const normalizedModel = normalizeChatGptModelForBrowser(model);
82
+ return isCurrentChatGptProAlias(requestedModel ?? model) || normalizedModel === "gpt-5.5-pro"
83
+ ? "pro"
84
+ : undefined;
85
+ }
71
86
  export async function buildBrowserConfig(options) {
72
87
  if (options.copyProfile && options.browserKeepBrowser) {
73
88
  throw new Error("--copy-profile cannot be combined with --browser-keep-browser: the copied profile is a throwaway that is deleted after the run, so it must not be retained.");
@@ -85,11 +100,14 @@ export async function buildBrowserConfig(options) {
85
100
  const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
86
101
  const baseModel = options.model.toLowerCase();
87
102
  const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
88
- const normalizedBrowserModel = normalizeChatGptModelForBrowser(options.model);
89
103
  const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
90
104
  const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
91
105
  const thinkingTime = normalizeThinkingTimeLevel(options.browserThinkingTime) ??
92
- (modelStrategy === "select" && normalizedBrowserModel === "gpt-5.5-pro" ? "pro" : undefined);
106
+ resolveDefaultBrowserThinkingTime({
107
+ model: options.model,
108
+ requestedModel: options.browserRequestedModel,
109
+ modelStrategy,
110
+ });
93
111
  assertBrowserModelAvailable(options.model, modelStrategy);
94
112
  const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
95
113
  let inline = await resolveInlineCookies({
@@ -99,7 +117,9 @@ export async function buildBrowserConfig(options) {
99
117
  envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
100
118
  cwd: process.cwd(),
101
119
  });
102
- if (inline?.source?.startsWith("home:") && options.browserNoCookieSync !== true) {
120
+ const chromeCookieSyncRequested = options.browserNoCookieSync !== true &&
121
+ (options.browserCookieSync === true || options.browserManualLoginCookieSync === true);
122
+ if (inline?.source?.startsWith("home:") && chromeCookieSyncRequested) {
103
123
  inline = undefined;
104
124
  }
105
125
  let remoteChrome;
@@ -161,11 +181,13 @@ export async function buildBrowserConfig(options) {
161
181
  cookieSyncWaitMs: options.browserCookieWait
162
182
  ? parseBrowserDuration(options.browserCookieWait, "--browser-cookie-wait", 0)
163
183
  : undefined,
164
- cookieSync: options.browserNoCookieSync
165
- ? false
166
- : options.browserManualLoginCookieSync === true
167
- ? true
168
- : undefined,
184
+ cookieSync: inline?.cookies?.length
185
+ ? true
186
+ : options.browserNoCookieSync
187
+ ? false
188
+ : options.browserCookieSync === true || options.browserManualLoginCookieSync === true
189
+ ? true
190
+ : undefined,
169
191
  cookieNames,
170
192
  inlineCookies: inline?.cookies,
171
193
  inlineCookiesSource: inline?.source ?? null,
@@ -173,7 +195,7 @@ export async function buildBrowserConfig(options) {
173
195
  keepBrowser: options.browserKeepBrowser ? true : undefined,
174
196
  manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
175
197
  manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
176
- manualLoginCookieSync: options.browserManualLoginCookieSync,
198
+ manualLoginCookieSync: inline?.cookies?.length ? true : options.browserManualLoginCookieSync,
177
199
  copyProfileSource: options.copyProfile ?? undefined,
178
200
  hideWindow: options.browserHideWindow ? true : undefined,
179
201
  desiredModel,
@@ -206,6 +228,7 @@ function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies
206
228
  const conflicts = [
207
229
  options.browserChromeProfile ? "--browser-chrome-profile" : null,
208
230
  options.browserCookiePath ? "--browser-cookie-path" : null,
231
+ options.browserCookieSync ? "--browser-cookie-sync" : null,
209
232
  options.browserNoCookieSync ? "--browser-no-cookie-sync" : null,
210
233
  options.browserHeadless ? "--browser-headless" : null,
211
234
  options.browserHideWindow ? "--browser-hide-window" : null,
@@ -76,6 +76,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
76
76
  if (isUnset("browserCookieWait") && typeof browser.cookieSyncWaitMs === "number") {
77
77
  options.browserCookieWait = String(browser.cookieSyncWaitMs);
78
78
  }
79
+ if (!attachRunningRequested && isUnset("browserCookieSync") && browser.cookieSync !== undefined) {
80
+ options.browserCookieSync = browser.cookieSync;
81
+ }
79
82
  if (!attachRunningRequested && isUnset("browserHeadless") && browser.headless !== undefined) {
80
83
  options.browserHeadless = browser.headless;
81
84
  }
@@ -110,6 +110,7 @@ export async function runBrowserPreview({ runOptions, cwd, version, previewMode,
110
110
  log(chalk.cyan(headerLine));
111
111
  logBrowserControlPlan(browserConfig, log, "preview");
112
112
  logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
113
+ logBrowserCookieStrategy(browserConfig, log, "preview");
113
114
  logBrowserFileSummary(artifacts, log, "preview");
114
115
  if (previewMode === "json" || previewMode === "full") {
115
116
  const attachmentSummary = artifacts.attachments.map((attachment) => ({
@@ -74,9 +74,11 @@ export async function buildProjectSourcesBrowserConfig({ options, projectUrl, co
74
74
  const manualLoginCookieSync = flagConfig.manualLoginCookieSync ?? configuredBrowser.manualLoginCookieSync;
75
75
  const cookieSync = flagConfig.cookieSync === false
76
76
  ? false
77
- : manualLogin
78
- ? manualLoginCookieSync === true
79
- : configuredBrowser.cookieSync;
77
+ : flagConfig.cookieSync === true
78
+ ? true
79
+ : manualLogin
80
+ ? manualLoginCookieSync === true
81
+ : configuredBrowser.cookieSync === true;
80
82
  return {
81
83
  ...configuredBrowser,
82
84
  ...flagConfig,
@@ -24,7 +24,7 @@ import { CONSULT_PRESETS, browserThinkingTimeRawSchema, consultInputSchema } fro
24
24
  import { applyConsultPreset } from "../consultPresets.js";
25
25
  import { loadUserConfig } from "../../config.js";
26
26
  import { resolveNotificationSettings } from "../../cli/notifier.js";
27
- import { mapModelToBrowserLabel, resolveBrowserModelLabel } from "../../cli/browserConfig.js";
27
+ import { mapModelToBrowserLabel, resolveBrowserModelLabel, resolveDefaultBrowserThinkingTime, } from "../../cli/browserConfig.js";
28
28
  import { normalizeThinkingTimeLevel } from "../../oracle/thinkingTime.js";
29
29
  // Use raw shapes so the MCP SDK (with its bundled Zod) wraps them and emits valid JSON Schema.
30
30
  const consultInputShape = {
@@ -267,11 +267,14 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
267
267
  ? true
268
268
  : (configuredBrowser.manualLogin ?? process.platform === "win32");
269
269
  const configuredThinkingTime = normalizeThinkingTimeLevel(configuredBrowser.thinkingTime);
270
+ const modelStrategy = browserModelStrategy ?? configuredBrowser.modelStrategy;
270
271
  return {
271
272
  ...configuredBrowser,
272
273
  url: configuredUrl,
273
274
  chatgptUrl: configuredUrl,
274
- cookieSync: !manualLogin || configuredBrowser.manualLoginCookieSync === true,
275
+ cookieSync: manualLogin
276
+ ? configuredBrowser.manualLoginCookieSync === true
277
+ : configuredBrowser.cookieSync === true,
275
278
  headless: configuredBrowser.headless ?? false,
276
279
  hideWindow: configuredBrowser.hideWindow ?? false,
277
280
  keepBrowser: browserKeepBrowser ?? configuredBrowser.keepBrowser ?? false,
@@ -279,8 +282,14 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
279
282
  manualLoginProfileDir: manualLogin
280
283
  ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null)
281
284
  : null,
282
- thinkingTime: browserThinkingTime ?? configuredThinkingTime ?? undefined,
283
- modelStrategy: browserModelStrategy ?? configuredBrowser.modelStrategy,
285
+ thinkingTime: browserThinkingTime ??
286
+ configuredThinkingTime ??
287
+ resolveDefaultBrowserThinkingTime({
288
+ model: runModel,
289
+ requestedModel: inputModel,
290
+ modelStrategy,
291
+ }),
292
+ modelStrategy,
284
293
  researchMode: browserResearchMode ?? configuredBrowser.researchMode,
285
294
  archiveConversations: browserArchive ?? configuredBrowser.archiveConversations,
286
295
  desiredModel: desiredModelLabel || mapModelToBrowserLabel(runModel),
@@ -192,14 +192,15 @@ export async function createRemoteServer(options = {}, deps = {}) {
192
192
  // Preserve an explicit request to leave the completed conversation tab
193
193
  // open before the service forces `keepBrowser` for process lifetime.
194
194
  const clientRequestedKeepBrowser = payload.browserConfig?.keepBrowser === true;
195
- // Remote runs always rely on the host's own Chrome profile; ignore any inline cookie transfer.
195
+ // Remote runs rely on the host's authentication policy; never accept cookie payloads from clients.
196
196
  if (payload.browserConfig) {
197
197
  payload.browserConfig.inlineCookies = null;
198
198
  payload.browserConfig.inlineCookiesSource = null;
199
- payload.browserConfig.cookieSync = true;
199
+ payload.browserConfig.cookieSync = options.cookieSyncDefault === true;
200
200
  }
201
201
  else {
202
202
  payload.browserConfig = {};
203
+ payload.browserConfig.cookieSync = options.cookieSyncDefault === true;
203
204
  }
204
205
  // Enforce manual-login profile when cookie sync is unavailable (e.g., Windows/WSL).
205
206
  if (options.manualLoginDefault) {
@@ -293,7 +294,10 @@ export async function createRemoteServer(options = {}, deps = {}) {
293
294
  }
294
295
  export async function serveRemote(options = {}) {
295
296
  const manualProfileDir = options.manualLoginProfileDir ?? path.join(os.homedir(), ".oracle", "browser-profile");
296
- const preferManualLogin = options.manualLoginDefault || process.platform === "win32" || isWsl();
297
+ const preferManualLogin = options.manualLoginDefault ||
298
+ options.cookieSyncDefault !== true ||
299
+ process.platform === "win32" ||
300
+ isWsl();
297
301
  let cookies = null;
298
302
  let opened = false;
299
303
  if (isWsl() && process.env.ORACLE_ALLOW_WSL_SERVE !== "1") {
@@ -303,6 +307,7 @@ export async function serveRemote(options = {}) {
303
307
  return;
304
308
  }
305
309
  if (!preferManualLogin) {
310
+ console.log("Warning: Chrome cookie copying can invalidate an active ChatGPT session when tokens rotate. Prefer the default dedicated manual-login profile when possible.");
306
311
  // Warm-up: ensure this host has a ChatGPT login before accepting runs.
307
312
  const result = await loadLocalChatgptCookies(console.log, CHATGPT_URL);
308
313
  cookies = result.cookies;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.17.3",
3
+ "version": "0.18.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.6 Sol, GPT-5.6, GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://askoracle.sh",