@steipete/oracle 0.17.1 → 0.17.3

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 (92) hide show
  1. package/dist/bin/oracle-cli.js +1 -1
  2. package/dist/bin/oracle-mcp.js +0 -0
  3. package/dist/bin/oracle.js +683 -0
  4. package/dist/docs-site/bridge.html +1 -17
  5. package/dist/docs-site/browser-mode.html +14 -20
  6. package/dist/docs-site/cli-reference.html +6 -6
  7. package/dist/docs-site/configuration.html +7 -33
  8. package/dist/docs-site/followup.html +9 -16
  9. package/dist/docs-site/gemini.html +6 -10
  10. package/dist/docs-site/index.html +7 -7
  11. package/dist/docs-site/install.html +1 -1
  12. package/dist/docs-site/manual-tests.html +1 -2
  13. package/dist/docs-site/mcp.html +5 -28
  14. package/dist/docs-site/mythical-pro-agents.html +10 -10
  15. package/dist/docs-site/openai-endpoints.html +1 -24
  16. package/dist/docs-site/quickstart.html +1 -1
  17. package/dist/docs-site/sessions.html +5 -6
  18. package/dist/docs-site/spec.html +1 -1
  19. package/dist/docs-site/windows-work.html +0 -1
  20. package/dist/markdansi/types/index.js +4 -0
  21. package/dist/oracle/bin/oracle-cli.js +472 -0
  22. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  23. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  24. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  25. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  26. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  27. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  28. package/dist/oracle/src/browser/config.js +33 -0
  29. package/dist/oracle/src/browser/constants.js +40 -0
  30. package/dist/oracle/src/browser/cookies.js +210 -0
  31. package/dist/oracle/src/browser/domDebug.js +36 -0
  32. package/dist/oracle/src/browser/index.js +331 -0
  33. package/dist/oracle/src/browser/pageActions.js +5 -0
  34. package/dist/oracle/src/browser/prompt.js +88 -0
  35. package/dist/oracle/src/browser/promptSummary.js +20 -0
  36. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  37. package/dist/oracle/src/browser/types.js +1 -0
  38. package/dist/oracle/src/browser/utils.js +62 -0
  39. package/dist/oracle/src/browserMode.js +1 -0
  40. package/dist/oracle/src/cli/browserConfig.js +44 -0
  41. package/dist/oracle/src/cli/dryRun.js +59 -0
  42. package/dist/oracle/src/cli/engine.js +17 -0
  43. package/dist/oracle/src/cli/errorUtils.js +9 -0
  44. package/dist/oracle/src/cli/help.js +70 -0
  45. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  46. package/dist/oracle/src/cli/options.js +103 -0
  47. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  48. package/dist/oracle/src/cli/rootAlias.js +30 -0
  49. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  50. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  51. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  52. package/dist/oracle/src/heartbeat.js +43 -0
  53. package/dist/oracle/src/oracle/client.js +48 -0
  54. package/dist/oracle/src/oracle/config.js +29 -0
  55. package/dist/oracle/src/oracle/errors.js +101 -0
  56. package/dist/oracle/src/oracle/files.js +220 -0
  57. package/dist/oracle/src/oracle/format.js +33 -0
  58. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  59. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  60. package/dist/oracle/src/oracle/request.js +48 -0
  61. package/dist/oracle/src/oracle/run.js +444 -0
  62. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  63. package/dist/oracle/src/oracle/types.js +1 -0
  64. package/dist/oracle/src/oracle.js +9 -0
  65. package/dist/oracle/src/sessionManager.js +205 -0
  66. package/dist/oracle/src/version.js +39 -0
  67. package/dist/scripts/chrome/browser-tools.js +295 -0
  68. package/dist/scripts/git-policy.js +0 -8
  69. package/dist/scripts/runner.js +1 -5
  70. package/dist/src/browser/actions/assistantResponse.js +71 -23
  71. package/dist/src/browser/actions/modelSelection.js +164 -5
  72. package/dist/src/browser/actions/thinkingTime.js +195 -18
  73. package/dist/src/browser/chromeCookies.js +312 -0
  74. package/dist/src/browser/index.js +5 -1
  75. package/dist/src/browser/keytarShim.js +56 -0
  76. package/dist/src/browser/policies.js +4 -0
  77. package/dist/src/browser/profileSync.js +141 -0
  78. package/dist/src/browser/projectSourcesRunner.js +2 -2
  79. package/dist/src/browser/reattach.js +47 -35
  80. package/dist/src/browser/windowsCookies.js +219 -0
  81. package/dist/src/cli/browserConfig.js +13 -4
  82. package/dist/src/cli/browserDefaults.js +6 -1
  83. package/dist/src/cli/options.js +1 -1
  84. package/dist/src/cli/projectSources.js +8 -1
  85. package/dist/src/mcp/tools/consult.js +1 -1
  86. package/dist/src/oracle/thinkingTime.js +6 -0
  87. package/dist/src/sessionManager.js +90 -9
  88. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  89. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  90. package/package.json +6 -6
  91. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  92. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -0,0 +1,219 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import { existsSync, promises as fsp } from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import sqlite3 from 'sqlite3';
7
+ import { createRequire } from 'node:module';
8
+ import { spawnSync } from 'node:child_process';
9
+ let cachedUnprotect = null;
10
+ function getUnprotectData() {
11
+ if (cachedUnprotect) {
12
+ return cachedUnprotect;
13
+ }
14
+ try {
15
+ // win-dpapi is CommonJS; require it explicitly and support both named/default shapes.
16
+ const dpapiModule = createRequire(import.meta.url)('win-dpapi');
17
+ const unprotect = dpapiModule?.Dpapi?.unprotectData ??
18
+ dpapiModule?.default?.unprotectData ??
19
+ ((_) => {
20
+ throw new Error('win-dpapi unprotectData not available');
21
+ });
22
+ cachedUnprotect = unprotect;
23
+ return unprotect;
24
+ }
25
+ catch (error) {
26
+ if (process.platform !== 'win32') {
27
+ // On macOS/Linux we don't need DPAPI; return a function that makes misuse obvious.
28
+ cachedUnprotect = () => {
29
+ throw new Error('win-dpapi is unavailable on non-Windows platforms');
30
+ };
31
+ return cachedUnprotect;
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ export async function loadWindowsCookies(dbPath, filterNames) {
37
+ if (process.platform !== 'win32') {
38
+ throw new Error('loadWindowsCookies is only supported on Windows');
39
+ }
40
+ const localStatePath = await locateLocalState(dbPath);
41
+ const cookiesCopy = await copyLockedFile(dbPath, 'Cookies'); // name the copy exactly "Cookies"
42
+ const cookiesDirForFallback = await ensureCookiesDirForFallback(cookiesCopy);
43
+ const localStateCopy = await copyLockedFile(localStatePath, 'Local State');
44
+ const aesKey = await extractWindowsAesKey(localStateCopy);
45
+ const rows = await readChromeCookiesDb(cookiesCopy, filterNames);
46
+ const cookies = [];
47
+ for (const row of rows) {
48
+ const enc = row.encrypted_value && row.encrypted_value.length > 0 ? row.encrypted_value : Buffer.from(row.value ?? '', 'utf8');
49
+ const decrypted = enc.length > 0 ? decryptCookie(enc, aesKey) : '';
50
+ cookies.push({
51
+ name: row.name,
52
+ value: decrypted,
53
+ domain: row.host_key?.startsWith('.') ? row.host_key.slice(1) : row.host_key,
54
+ path: row.path ?? '/',
55
+ expires: normalizeExpiration(row.expires_utc),
56
+ secure: Boolean(row.is_secure),
57
+ httpOnly: Boolean(row.is_httponly),
58
+ });
59
+ }
60
+ if (process.env.ORACLE_DEBUG_COOKIES === '1') {
61
+ // eslint-disable-next-line no-console
62
+ console.log(`[cookies] windows decrypt decoded ${cookies.length} cookies from ${cookiesCopy} (fallback dir ${cookiesDirForFallback})`);
63
+ }
64
+ return cookies.filter((c) => c.value);
65
+ }
66
+ function decryptCookie(value, aesKey) {
67
+ const prefix = value.slice(0, 3).toString();
68
+ // Chrome prefixes AES-GCM encrypted cookies with version markers like v10/v11/v20; treat all v** the same.
69
+ if (/^v\d{2}$/u.test(prefix)) {
70
+ const iv = value.slice(3, 15);
71
+ const tag = value.slice(value.length - 16);
72
+ const data = value.slice(15, value.length - 16);
73
+ const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
74
+ decipher.setAuthTag(tag);
75
+ const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
76
+ return decrypted.toString('utf8');
77
+ }
78
+ const unprotectData = getUnprotectData();
79
+ const unprotected = unprotectData(value, undefined, 'CurrentUser');
80
+ return Buffer.from(unprotected).toString('utf8');
81
+ }
82
+ async function locateLocalState(dbPath) {
83
+ // Prefer sibling Local State near the profile path; fall back to default location.
84
+ const guess = path.resolve(path.join(path.dirname(dbPath), '..', 'Local State'));
85
+ if (existsSync(guess))
86
+ return guess;
87
+ const localState = path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Local State');
88
+ if (existsSync(localState))
89
+ return localState;
90
+ throw new Error('Chrome Local State not found for AES key');
91
+ }
92
+ async function extractWindowsAesKey(localStatePath) {
93
+ const raw = await fs.readFile(localStatePath, 'utf8');
94
+ const state = JSON.parse(raw);
95
+ const encKeyB64 = state?.os_crypt?.encrypted_key;
96
+ if (!encKeyB64)
97
+ throw new Error('encrypted_key missing in Local State');
98
+ const encKey = Buffer.from(encKeyB64, 'base64');
99
+ const dpapiBlob = encKey.slice(5); // strip "DPAPI"
100
+ const unprotectData = getUnprotectData();
101
+ const unprotected = unprotectData(dpapiBlob, undefined, 'CurrentUser');
102
+ return Buffer.from(unprotected);
103
+ }
104
+ async function readChromeCookiesDb(dbPath, filterNames) {
105
+ const db = await openSqlite(dbPath);
106
+ const placeholders = filterNames && filterNames.size > 0 ? `AND name IN (${Array.from(filterNames).map(() => '?').join(',')})` : '';
107
+ const params = filterNames ? Array.from(filterNames) : [];
108
+ const sql = `SELECT name,value,encrypted_value,host_key,path,expires_utc,is_secure,is_httponly FROM cookies WHERE host_key LIKE '%chatgpt.com%' ${placeholders}`;
109
+ try {
110
+ return await allSqlite(db, sql, params);
111
+ }
112
+ finally {
113
+ db.close();
114
+ }
115
+ }
116
+ function openSqlite(dbPath) {
117
+ return new Promise((resolve, reject) => {
118
+ const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
119
+ if (err)
120
+ reject(err);
121
+ else
122
+ resolve(db);
123
+ });
124
+ });
125
+ }
126
+ async function copyLockedFile(sourcePath, targetName) {
127
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookie-'));
128
+ const tempPath = targetName ? path.join(tempDir, targetName) : path.join(tempDir, path.basename(sourcePath));
129
+ const psScript = `
130
+ $src = '${sourcePath.replace(/'/g, "''")}'
131
+ $dst = '${tempPath.replace(/'/g, "''")}'
132
+ try {
133
+ $fs = [System.IO.File]::Open($src, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
134
+ $bytes = New-Object byte[] $fs.Length
135
+ $null = $fs.Read($bytes, 0, $fs.Length)
136
+ $fs.Close()
137
+ [System.IO.File]::WriteAllBytes($dst, $bytes)
138
+ exit 0
139
+ } catch {
140
+ exit 1
141
+ }
142
+ `;
143
+ const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
144
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
145
+ timeout: 15000,
146
+ encoding: 'utf8',
147
+ });
148
+ if (result.status !== 0) {
149
+ // Fallback: best-effort normal copy
150
+ await fs.copyFile(sourcePath, tempPath);
151
+ }
152
+ if (process.env.ORACLE_DEBUG_COOKIES === '1') {
153
+ // eslint-disable-next-line no-console
154
+ console.log(`[cookies] copied locked file ${sourcePath} -> ${tempPath}`);
155
+ }
156
+ return tempPath;
157
+ }
158
+ // Create a directory containing a file named "Cookies" for chrome-cookies-secure fallback
159
+ export async function ensureCookiesDirForFallback(cookieFile) {
160
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-secure-'));
161
+ const target = path.join(dir, 'Cookies');
162
+ await fs.copyFile(cookieFile, target);
163
+ return dir;
164
+ }
165
+ function allSqlite(db, sql, params) {
166
+ return new Promise((resolve, reject) => {
167
+ db.all(sql, params, (err, rows) => {
168
+ if (err)
169
+ reject(err);
170
+ else
171
+ resolve((rows ?? []));
172
+ });
173
+ });
174
+ }
175
+ function normalizeExpiration(expires) {
176
+ if (!expires || Number.isNaN(expires)) {
177
+ return undefined;
178
+ }
179
+ const value = Number(expires);
180
+ if (value <= 0)
181
+ return undefined;
182
+ if (value > 1_000_000_000_000) {
183
+ return Math.round(value / 1_000_000 - 11644473600);
184
+ }
185
+ if (value > 1_000_000_000) {
186
+ return Math.round(value / 1000);
187
+ }
188
+ return Math.round(value);
189
+ }
190
+ export async function materializeCookieFile(sourcePath) {
191
+ if (process.platform !== 'win32')
192
+ return sourcePath;
193
+ const resolved = await resolveDirectoryCandidate(sourcePath);
194
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-'));
195
+ const tempPath = path.join(tempDir, 'Cookies');
196
+ await fs.copyFile(resolved, tempPath);
197
+ return tempPath;
198
+ }
199
+ async function resolveDirectoryCandidate(inputPath) {
200
+ const stat = await fs.stat(inputPath).catch(() => null);
201
+ if (!stat?.isDirectory())
202
+ return inputPath;
203
+ const network = path.join(inputPath, 'Network', 'Cookies');
204
+ const direct = path.join(inputPath, 'Cookies');
205
+ if (await fileExists(network))
206
+ return network;
207
+ if (await fileExists(direct))
208
+ return direct;
209
+ return inputPath;
210
+ }
211
+ async function fileExists(candidate) {
212
+ try {
213
+ const stat = await fsp.stat(candidate);
214
+ return stat.isFile();
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ }
@@ -19,7 +19,7 @@ const BROWSER_MODEL_LABELS = [
19
19
  // Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
20
20
  ["gpt-5.6-sol", "GPT-5.6 Sol"],
21
21
  ["gpt-5.6", "GPT-5.6 Sol"],
22
- ["gpt-5.5-pro", "Pro"],
22
+ ["gpt-5.5-pro", "GPT-5.5"],
23
23
  ["gpt-5.5-instant", "GPT-5.5 Instant"],
24
24
  ["gpt-5.5", "Thinking 5.5"],
25
25
  ["gpt-5.4-pro", "Pro"],
@@ -85,8 +85,11 @@ export async function buildBrowserConfig(options) {
85
85
  const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
86
86
  const baseModel = options.model.toLowerCase();
87
87
  const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
88
+ const normalizedBrowserModel = normalizeChatGptModelForBrowser(options.model);
88
89
  const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
89
90
  const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
91
+ const thinkingTime = normalizeThinkingTimeLevel(options.browserThinkingTime) ??
92
+ (modelStrategy === "select" && normalizedBrowserModel === "gpt-5.5-pro" ? "pro" : undefined);
90
93
  assertBrowserModelAvailable(options.model, modelStrategy);
91
94
  const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
92
95
  let inline = await resolveInlineCookies({
@@ -158,14 +161,19 @@ export async function buildBrowserConfig(options) {
158
161
  cookieSyncWaitMs: options.browserCookieWait
159
162
  ? parseBrowserDuration(options.browserCookieWait, "--browser-cookie-wait", 0)
160
163
  : undefined,
161
- cookieSync: options.browserNoCookieSync ? false : undefined,
164
+ cookieSync: options.browserNoCookieSync
165
+ ? false
166
+ : options.browserManualLoginCookieSync === true
167
+ ? true
168
+ : undefined,
162
169
  cookieNames,
163
170
  inlineCookies: inline?.cookies,
164
171
  inlineCookiesSource: inline?.source ?? null,
165
- headless: undefined, // disable headless; Cloudflare blocks it
172
+ headless: options.browserHeadless === true ? true : undefined,
166
173
  keepBrowser: options.browserKeepBrowser ? true : undefined,
167
174
  manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
168
175
  manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
176
+ manualLoginCookieSync: options.browserManualLoginCookieSync,
169
177
  copyProfileSource: options.copyProfile ?? undefined,
170
178
  hideWindow: options.browserHideWindow ? true : undefined,
171
179
  desiredModel,
@@ -175,7 +183,7 @@ export async function buildBrowserConfig(options) {
175
183
  allowCookieErrors: options.browserAllowCookieErrors ?? true,
176
184
  remoteChrome,
177
185
  browserTabRef: options.browserTab ?? undefined,
178
- thinkingTime: normalizeThinkingTimeLevel(options.browserThinkingTime) ?? undefined,
186
+ thinkingTime,
179
187
  researchMode: options.browserResearch === "deep" ? "deep" : "off",
180
188
  archiveConversations: options.browserArchive,
181
189
  };
@@ -199,6 +207,7 @@ function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies
199
207
  options.browserChromeProfile ? "--browser-chrome-profile" : null,
200
208
  options.browserCookiePath ? "--browser-cookie-path" : null,
201
209
  options.browserNoCookieSync ? "--browser-no-cookie-sync" : null,
210
+ options.browserHeadless ? "--browser-headless" : null,
202
211
  options.browserHideWindow ? "--browser-hide-window" : null,
203
212
  options.browserKeepBrowser ? "--browser-keep-browser" : null,
204
213
  options.browserManualLogin ? "--browser-manual-login" : null,
@@ -76,7 +76,7 @@ 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 (isUnset("browserHeadless") && browser.headless !== undefined) {
79
+ if (!attachRunningRequested && isUnset("browserHeadless") && browser.headless !== undefined) {
80
80
  options.browserHeadless = browser.headless;
81
81
  }
82
82
  if (!attachRunningRequested && isUnset("browserHideWindow") && browser.hideWindow !== undefined) {
@@ -111,4 +111,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
111
111
  browser.manualLoginProfileDir !== undefined) {
112
112
  options.browserManualLoginProfileDir = browser.manualLoginProfileDir;
113
113
  }
114
+ if (!attachRunningRequested &&
115
+ isUnset("browserManualLoginCookieSync") &&
116
+ browser.manualLoginCookieSync !== undefined) {
117
+ options.browserManualLoginCookieSync = browser.manualLoginCookieSync;
118
+ }
114
119
  }
@@ -133,7 +133,7 @@ export function parseThinkingTimeOption(value) {
133
133
  if (normalized) {
134
134
  return normalized;
135
135
  }
136
- throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "extra-high", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "xhigh".');
136
+ throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "extra-high", "pro", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "xhigh".');
137
137
  }
138
138
  export function normalizeModelOption(value) {
139
139
  return (value ?? "").trim();
@@ -71,14 +71,21 @@ export async function buildProjectSourcesBrowserConfig({ options, projectUrl, co
71
71
  envProfileDir ??
72
72
  null)
73
73
  : null;
74
+ const manualLoginCookieSync = flagConfig.manualLoginCookieSync ?? configuredBrowser.manualLoginCookieSync;
75
+ const cookieSync = flagConfig.cookieSync === false
76
+ ? false
77
+ : manualLogin
78
+ ? manualLoginCookieSync === true
79
+ : configuredBrowser.cookieSync;
74
80
  return {
75
81
  ...configuredBrowser,
76
82
  ...flagConfig,
77
83
  url: projectUrl,
78
84
  chatgptUrl: projectUrl,
79
- cookieSync: manualLogin ? false : (flagConfig.cookieSync ?? configuredBrowser.cookieSync),
85
+ cookieSync,
80
86
  manualLogin,
81
87
  manualLoginProfileDir,
88
+ manualLoginCookieSync,
82
89
  desiredModel: null,
83
90
  modelStrategy: "ignore",
84
91
  researchMode: "off",
@@ -271,7 +271,7 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
271
271
  ...configuredBrowser,
272
272
  url: configuredUrl,
273
273
  chatgptUrl: configuredUrl,
274
- cookieSync: !manualLogin,
274
+ cookieSync: !manualLogin || configuredBrowser.manualLoginCookieSync === true,
275
275
  headless: configuredBrowser.headless ?? false,
276
276
  hideWindow: configuredBrowser.hideWindow ?? false,
277
277
  keepBrowser: browserKeepBrowser ?? configuredBrowser.keepBrowser ?? false,
@@ -3,6 +3,10 @@ export const THINKING_TIME_LEVELS = [
3
3
  "standard",
4
4
  "extended",
5
5
  "extra-high",
6
+ // ChatGPT's unified Intelligence picker exposes Pro as the top effort tier of
7
+ // the active model rather than a separate model row, so it is a level here.
8
+ // Kept distinct from "heavy": requesting Pro must be deliberate.
9
+ "pro",
6
10
  "heavy",
7
11
  ];
8
12
  export const THINKING_TIME_ALIASES = [
@@ -38,6 +42,8 @@ export function normalizeThinkingTimeLevel(value) {
38
42
  case "extrahigh":
39
43
  case "xhigh":
40
44
  return "extra-high";
45
+ case "pro":
46
+ return "pro";
41
47
  case "heavy":
42
48
  return "heavy";
43
49
  default:
@@ -23,11 +23,86 @@ const DEFAULT_SLUG = "session";
23
23
  const MAX_SLUG_WORDS = 5;
24
24
  const MIN_CUSTOM_SLUG_WORDS = 3;
25
25
  const MAX_SLUG_WORD_LENGTH = 10;
26
+ // Session artifacts (prompt, attached file contents, model responses) are sensitive.
27
+ // Keep them owner-only, matching the meta.json / bridge-config posture (0o600/0o700).
28
+ const SESSION_DIR_MODE = 0o700;
29
+ const SESSION_FILE_MODE = 0o600;
30
+ const sessionStorageHardening = new Map();
26
31
  async function ensureDir(dirPath) {
27
- await fs.mkdir(dirPath, { recursive: true });
32
+ await fs.mkdir(dirPath, { recursive: true, mode: SESSION_DIR_MODE });
33
+ }
34
+ function isMissingPathError(error) {
35
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
36
+ }
37
+ async function chmodIfPresent(targetPath, mode) {
38
+ try {
39
+ await fs.chmod(targetPath, mode);
40
+ return true;
41
+ }
42
+ catch (error) {
43
+ if (isMissingPathError(error)) {
44
+ return false;
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ async function hardenSessionStorageEntry(targetPath) {
50
+ let stats;
51
+ try {
52
+ stats = await fs.lstat(targetPath);
53
+ }
54
+ catch (error) {
55
+ if (isMissingPathError(error)) {
56
+ return;
57
+ }
58
+ throw error;
59
+ }
60
+ if (stats.isSymbolicLink()) {
61
+ return;
62
+ }
63
+ if (stats.isDirectory()) {
64
+ if (!(await chmodIfPresent(targetPath, SESSION_DIR_MODE))) {
65
+ return;
66
+ }
67
+ let entries;
68
+ try {
69
+ entries = await fs.readdir(targetPath);
70
+ }
71
+ catch (error) {
72
+ if (isMissingPathError(error)) {
73
+ return;
74
+ }
75
+ throw error;
76
+ }
77
+ for (const entry of entries) {
78
+ await hardenSessionStorageEntry(path.join(targetPath, entry));
79
+ }
80
+ return;
81
+ }
82
+ if (stats.isFile()) {
83
+ await chmodIfPresent(targetPath, SESSION_FILE_MODE);
84
+ }
28
85
  }
29
86
  export async function ensureSessionStorage() {
30
- await ensureDir(getSessionsDir());
87
+ const sessionsDir = getSessionsDir();
88
+ await ensureDir(sessionsDir);
89
+ if (process.platform === "win32") {
90
+ return;
91
+ }
92
+ const stats = await fs.lstat(sessionsDir);
93
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
94
+ return;
95
+ }
96
+ const identity = `${sessionsDir}:${stats.dev}:${stats.ino}`;
97
+ let hardening = sessionStorageHardening.get(identity);
98
+ if (!hardening) {
99
+ hardening = hardenSessionStorageEntry(sessionsDir).catch((error) => {
100
+ sessionStorageHardening.delete(identity);
101
+ throw error;
102
+ });
103
+ sessionStorageHardening.set(identity, hardening);
104
+ }
105
+ await hardening;
31
106
  }
32
107
  function slugify(text, maxWords = MAX_SLUG_WORDS) {
33
108
  const normalized = text?.toLowerCase() ?? "";
@@ -110,7 +185,7 @@ async function reserveUniqueSessionDir(baseSlug) {
110
185
  for (;;) {
111
186
  const dir = sessionDir(candidate);
112
187
  try {
113
- await fs.mkdir(dir, { recursive: false });
188
+ await fs.mkdir(dir, { recursive: false, mode: SESSION_DIR_MODE });
114
189
  return candidate;
115
190
  }
116
191
  catch (error) {
@@ -171,7 +246,10 @@ export async function updateModelRunMetadata(sessionId, model, updates) {
171
246
  ...updates,
172
247
  model,
173
248
  });
174
- await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2), "utf8");
249
+ await fs.writeFile(modelJsonPath(sessionId, model), JSON.stringify(next, null, 2), {
250
+ encoding: "utf8",
251
+ mode: SESSION_FILE_MODE,
252
+ });
175
253
  return next;
176
254
  }
177
255
  export async function readModelRunMetadata(sessionId, model) {
@@ -261,10 +339,13 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
261
339
  status: "pending",
262
340
  log: { path: path.relative(sessionDir(sessionId), logFilePath) },
263
341
  };
264
- await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2), "utf8");
265
- await fs.writeFile(logFilePath, "", "utf8");
342
+ await fs.writeFile(jsonPath, JSON.stringify(modelRecord, null, 2), {
343
+ encoding: "utf8",
344
+ mode: SESSION_FILE_MODE,
345
+ });
346
+ await fs.writeFile(logFilePath, "", { encoding: "utf8", mode: SESSION_FILE_MODE });
266
347
  }));
267
- await fs.writeFile(logPath(sessionId), "", "utf8");
348
+ await fs.writeFile(logPath(sessionId), "", { encoding: "utf8", mode: SESSION_FILE_MODE });
268
349
  return metadata;
269
350
  }
270
351
  export async function readSessionMetadata(sessionId) {
@@ -350,9 +431,9 @@ async function attachModelRuns(meta, sessionId) {
350
431
  export function createSessionLogWriter(sessionId, model) {
351
432
  const targetPath = model ? modelLogPath(sessionId, model) : logPath(sessionId);
352
433
  if (model) {
353
- mkdirSync(modelsDir(sessionId), { recursive: true });
434
+ mkdirSync(modelsDir(sessionId), { recursive: true, mode: SESSION_DIR_MODE });
354
435
  }
355
- const stream = createWriteStream(targetPath, { flags: "a" });
436
+ const stream = createWriteStream(targetPath, { flags: "a", mode: SESSION_FILE_MODE });
356
437
  const logLine = (line = "") => {
357
438
  stream.write(`${line}\n`);
358
439
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
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",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@anthropic-ai/tokenizer": "^0.0.4",
61
- "@google/genai": "^2.15.0",
61
+ "@google/genai": "^2.16.0",
62
62
  "@google/generative-ai": "^0.24.1",
63
63
  "@modelcontextprotocol/sdk": "^1.30.0",
64
64
  "@steipete/sweet-cookie": "^0.4.1",
@@ -86,15 +86,15 @@
86
86
  "@anthropic-ai/tokenizer": "^0.0.4",
87
87
  "@types/chrome-remote-interface": "^0.34.0",
88
88
  "@types/inquirer": "^9.0.10",
89
- "@types/node": "^26.1.2",
89
+ "@types/node": "^26.2.0",
90
90
  "@vitest/coverage-v8": "4.1.10",
91
- "devtools-protocol": "0.0.1673900",
91
+ "devtools-protocol": "0.0.1676105",
92
92
  "es-toolkit": "^1.50.0",
93
93
  "esbuild": "^0.28.1",
94
94
  "oxfmt": "0.62.0",
95
95
  "oxlint": "^1.77.0",
96
96
  "puppeteer-core": "^25.5.0",
97
- "tsx": "^4.23.7",
97
+ "tsx": "^4.23.11",
98
98
  "typescript": "^7.0.2",
99
99
  "vitest": "^4.1.10"
100
100
  },
@@ -109,5 +109,5 @@
109
109
  "engines": {
110
110
  "node": ">=24"
111
111
  },
112
- "packageManager": "pnpm@10.34.5"
112
+ "packageManager": "pnpm@11.20.0"
113
113
  }