@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.
- package/dist/bin/oracle-cli.js +10 -1
- package/dist/src/browser/actions/thinkingTime.js +149 -0
- package/dist/src/browser/config.js +2 -3
- package/dist/src/browser/index.js +3 -2
- package/dist/src/browser/policies.js +6 -2
- package/dist/src/browser/projectSourcesRunner.js +5 -2
- package/dist/src/browser/reattach.js +4 -1
- package/dist/src/cli/browserConfig.js +37 -14
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/dryRun.js +1 -0
- package/dist/src/cli/projectSources.js +5 -3
- package/dist/src/mcp/tools/consult.js +13 -4
- package/dist/src/remote/server.js +8 -3
- package/package.json +1 -1
package/dist/bin/oracle-cli.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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 ??
|
|
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
|
|
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
|
|
40
|
-
return {
|
|
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
|
|
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
|
|
56
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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:
|
|
165
|
-
?
|
|
166
|
-
: options.
|
|
167
|
-
?
|
|
168
|
-
:
|
|
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
|
}
|
package/dist/src/cli/dryRun.js
CHANGED
|
@@ -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
|
-
:
|
|
78
|
-
?
|
|
79
|
-
:
|
|
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:
|
|
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 ??
|
|
283
|
-
|
|
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
|
|
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 ||
|
|
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.
|
|
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",
|