@steipete/oracle 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,6 +112,8 @@ npx -y @steipete/oracle oracle-mcp
112
112
  | `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
113
113
  | `--chatgpt-url <url>` | Target a ChatGPT workspace/folder (browser). |
114
114
  | `--browser-model-strategy <select\|current\|ignore>` | Control ChatGPT model selection in browser mode (current keeps the active model; ignore skips the picker). |
115
+ | `--browser-manual-login` | Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login. |
116
+ | `--browser-thinking-time <light\|standard\|extended\|heavy>` | Set ChatGPT thinking-time intensity (browser; Thinking/Pro models only). |
115
117
  | `--browser-port <port>` | Pin the Chrome DevTools port (WSL/Windows firewall helper). |
116
118
  | `--browser-inline-cookies[(-file)] <payload|path>` | Supply cookies without Chrome/Keychain (browser). |
117
119
  | `--browser-timeout`, `--browser-input-timeout` | Control overall/browser input timeouts (supports h/m/s/ms). |
@@ -147,7 +149,7 @@ Advanced flags
147
149
 
148
150
  | Area | Flags |
149
151
  | --- | --- |
150
- | Browser | `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
152
+ | Browser | `--browser-manual-login`, `--browser-thinking-time`, `--browser-timeout`, `--browser-input-timeout`, `--browser-inline-cookies[(-file)]`, `--browser-attachments`, `--browser-inline-files`, `--browser-bundle-files`, `--browser-keep-browser`, `--browser-headless`, `--browser-hide-window`, `--browser-no-cookie-sync`, `--browser-allow-cookie-errors`, `--browser-chrome-path`, `--browser-cookie-path`, `--chatgpt-url` |
151
153
  | Azure/OpenAI | `--azure-endpoint`, `--azure-deployment`, `--azure-api-version`, `--base-url` |
152
154
 
153
155
  Remote browser example
@@ -115,6 +115,11 @@ function buildThinkingTimeExpression(level) {
115
115
  if (aria.includes('thinking') || text.includes('thinking')) {
116
116
  return btn;
117
117
  }
118
+
119
+ // In some cases the pill is labeled "Pro".
120
+ if (aria.includes('pro') || text.includes('pro')) {
121
+ return btn;
122
+ }
118
123
  }
119
124
  }
120
125
  return null;
@@ -6,6 +6,7 @@ import { execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
7
  import CDP from 'chrome-remote-interface';
8
8
  import { launch, Launcher } from 'chrome-launcher';
9
+ import { cleanupStaleProfileState } from './profileState.js';
9
10
  const execFileAsync = promisify(execFile);
10
11
  export async function launchChrome(config, userDataDir, logger) {
11
12
  const connectHost = resolveRemoteDebugHost();
@@ -64,7 +65,14 @@ export function registerTerminationHooks(chrome, userDataDir, keepBrowser, logge
64
65
  catch {
65
66
  // ignore kill failures
66
67
  }
67
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
68
+ if (opts?.preserveUserDataDir) {
69
+ // Preserve the profile directory (manual login), but clear reattach hints so we don't
70
+ // try to reuse a dead DevTools port on the next run.
71
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
72
+ }
73
+ else {
74
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
75
+ }
68
76
  }
69
77
  })().finally(() => {
70
78
  const exitCode = signal === 'SIGINT' ? 130 : 1;
@@ -26,6 +26,7 @@ export const DEFAULT_BROWSER_CONFIG = {
26
26
  remoteChrome: null,
27
27
  manualLogin: false,
28
28
  manualLoginProfileDir: null,
29
+ manualLoginCookieSync: false,
29
30
  };
30
31
  export function resolveBrowserConfig(config) {
31
32
  const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
@@ -72,6 +73,7 @@ export function resolveBrowserConfig(config) {
72
73
  thinkingTime: config?.thinkingTime,
73
74
  manualLogin,
74
75
  manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
76
+ manualLoginCookieSync: config?.manualLoginCookieSync ?? DEFAULT_BROWSER_CONFIG.manualLoginCookieSync,
75
77
  };
76
78
  }
77
79
  function parseDebugPort(raw) {
@@ -13,7 +13,7 @@ import { formatElapsed } from '../oracle/format.js';
13
13
  import { CHATGPT_URL, CONVERSATION_TURN_SELECTOR, DEFAULT_MODEL_STRATEGY } from './constants.js';
14
14
  import { BrowserAutomationError } from '../oracle/errors.js';
15
15
  import { alignPromptEchoPair, buildPromptEchoMatcher } from './reattachHelpers.js';
16
- import { cleanupStaleProfileState, readChromePid, readDevToolsPort, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from './profileState.js';
16
+ import { cleanupStaleProfileState, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from './profileState.js';
17
17
  export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from './constants.js';
18
18
  export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from './utils.js';
19
19
  export async function runBrowserMode(options) {
@@ -95,7 +95,7 @@ export async function runBrowserMode(options) {
95
95
  else {
96
96
  logger(`Created temporary Chrome profile at ${userDataDir}`);
97
97
  }
98
- const effectiveKeepBrowser = config.keepBrowser || manualLogin;
98
+ const effectiveKeepBrowser = Boolean(config.keepBrowser);
99
99
  const reusedChrome = manualLogin ? await maybeReuseRunningChrome(userDataDir, logger) : null;
100
100
  const chrome = reusedChrome ??
101
101
  (await launchChrome({
@@ -115,6 +115,7 @@ export async function runBrowserMode(options) {
115
115
  removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
116
116
  isInFlight: () => runStatus !== 'complete',
117
117
  emitRuntimeHint,
118
+ preserveUserDataDir: manualLogin,
118
119
  });
119
120
  }
120
121
  catch {
@@ -160,8 +161,12 @@ export async function runBrowserMode(options) {
160
161
  if (!manualLogin) {
161
162
  await Network.clearBrowserCookies();
162
163
  }
163
- const cookieSyncEnabled = config.cookieSync && !manualLogin;
164
+ const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
165
+ const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
164
166
  if (cookieSyncEnabled) {
167
+ if (manualLoginCookieSync) {
168
+ logger('Manual login mode: seeding persistent profile with cookies from your Chrome profile.');
169
+ }
165
170
  if (!config.inlineCookies) {
166
171
  logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.');
167
172
  }
@@ -623,7 +628,19 @@ export async function runBrowserMode(options) {
623
628
  // ignore kill failures
624
629
  }
625
630
  }
626
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
631
+ if (manualLogin) {
632
+ const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, {
633
+ connectionClosedUnexpectedly,
634
+ host: chromeHost,
635
+ });
636
+ if (shouldCleanup) {
637
+ // Preserve the persistent manual-login profile, but clear stale reattach hints.
638
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
639
+ }
640
+ }
641
+ else {
642
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
643
+ }
627
644
  if (!connectionClosedUnexpectedly) {
628
645
  const totalSeconds = (Date.now() - startedAt) / 1000;
629
646
  logger(`Cleanup ${runStatus} • ${totalSeconds.toFixed(1)}s total`);
@@ -105,6 +105,22 @@ export async function verifyDevToolsReachable({ port, host = '127.0.0.1', attemp
105
105
  }
106
106
  return { ok: false, error: 'unreachable' };
107
107
  }
108
+ export async function shouldCleanupManualLoginProfileState(userDataDir, logger, options = {}) {
109
+ if (!options.connectionClosedUnexpectedly) {
110
+ return true;
111
+ }
112
+ const port = await readDevToolsPort(userDataDir);
113
+ if (!port) {
114
+ return true;
115
+ }
116
+ const probe = await (options.probe ?? verifyDevToolsReachable)({ port, host: options.host });
117
+ if (probe.ok) {
118
+ logger?.(`DevTools port ${port} still reachable; preserving manual-login profile state`);
119
+ return false;
120
+ }
121
+ logger?.(`DevTools port ${port} unreachable (${probe.error}); clearing stale profile state`);
122
+ return true;
123
+ }
108
124
  export async function cleanupStaleProfileState(userDataDir, logger, options = {}) {
109
125
  for (const candidate of getDevToolsActivePortPaths(userDataDir)) {
110
126
  try {
@@ -7,6 +7,7 @@ import { launchChrome, connectToChrome, hideChromeWindow } from './chromeLifecyc
7
7
  import { resolveBrowserConfig } from './config.js';
8
8
  import { syncCookies } from './cookies.js';
9
9
  import { CHATGPT_URL } from './constants.js';
10
+ import { cleanupStaleProfileState } from './profileState.js';
10
11
  import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from './reattachHelpers.js';
11
12
  export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
12
13
  const recoverSession = deps.recoverSession ??
@@ -159,14 +160,19 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
159
160
  // ignore
160
161
  }
161
162
  }
162
- if (!resolved.keepBrowser && !manualLogin) {
163
+ if (!resolved.keepBrowser) {
163
164
  try {
164
165
  await chrome.kill();
165
166
  }
166
167
  catch {
167
168
  // ignore
168
169
  }
169
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
170
+ if (manualLogin) {
171
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: 'never' }).catch(() => undefined);
172
+ }
173
+ else {
174
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
175
+ }
170
176
  }
171
177
  return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
172
178
  }
@@ -88,7 +88,8 @@ export async function buildBrowserConfig(options) {
88
88
  inlineCookiesSource: inline?.source ?? null,
89
89
  headless: undefined, // disable headless; Cloudflare blocks it
90
90
  keepBrowser: options.browserKeepBrowser ? true : undefined,
91
- manualLogin: options.browserManualLogin ? true : undefined,
91
+ manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
92
+ manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
92
93
  hideWindow: options.browserHideWindow ? true : undefined,
93
94
  desiredModel,
94
95
  modelStrategy,
@@ -45,4 +45,13 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
45
45
  if (isUnset('browserModelStrategy') && browser.modelStrategy !== undefined) {
46
46
  options.browserModelStrategy = browser.modelStrategy;
47
47
  }
48
+ if (isUnset('browserThinkingTime') && browser.thinkingTime !== undefined) {
49
+ options.browserThinkingTime = browser.thinkingTime;
50
+ }
51
+ if (isUnset('browserManualLogin') && browser.manualLogin !== undefined) {
52
+ options.browserManualLogin = browser.manualLogin;
53
+ }
54
+ if (isUnset('browserManualLoginProfileDir') && browser.manualLoginProfileDir !== undefined) {
55
+ options.browserManualLoginProfileDir = browser.manualLoginProfileDir;
56
+ }
48
57
  }
@@ -1,12 +1,19 @@
1
1
  import process from 'node:process';
2
2
  import { startOscProgress as startOscProgressShared, supportsOscProgress as supportsOscProgressShared, } from 'osc-progress';
3
3
  export function supportsOscProgress(env = process.env, isTty = process.stdout.isTTY) {
4
+ if (env.CODEX_MANAGED_BY_NPM === '1' && env.ORACLE_FORCE_OSC_PROGRESS !== '1') {
5
+ return false;
6
+ }
4
7
  return supportsOscProgressShared(env, isTty, {
5
8
  disableEnvVar: 'ORACLE_NO_OSC_PROGRESS',
6
9
  forceEnvVar: 'ORACLE_FORCE_OSC_PROGRESS',
7
10
  });
8
11
  }
9
12
  export function startOscProgress(options = {}) {
13
+ const env = options.env ?? process.env;
14
+ if (env.CODEX_MANAGED_BY_NPM === '1' && env.ORACLE_FORCE_OSC_PROGRESS !== '1') {
15
+ return () => { };
16
+ }
10
17
  return startOscProgressShared({
11
18
  ...options,
12
19
  // Preserve Oracle's previous default: progress emits to stdout.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.2 Pro (via gpt-5.1-pro alias), GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "type": "module",
6
6
  "main": "dist/bin/oracle-cli.js",