@steipete/oracle 0.8.5 → 0.9.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.
Files changed (49) hide show
  1. package/README.md +99 -5
  2. package/dist/bin/oracle-cli.js +376 -13
  3. package/dist/src/browser/actions/assistantResponse.js +72 -37
  4. package/dist/src/browser/actions/modelSelection.js +60 -8
  5. package/dist/src/browser/actions/navigation.js +2 -1
  6. package/dist/src/browser/actions/promptComposer.js +141 -32
  7. package/dist/src/browser/chromeLifecycle.js +25 -9
  8. package/dist/src/browser/config.js +14 -0
  9. package/dist/src/browser/constants.js +1 -1
  10. package/dist/src/browser/index.js +414 -43
  11. package/dist/src/browser/profileState.js +93 -0
  12. package/dist/src/browser/providerDomFlow.js +17 -0
  13. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  14. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +245 -0
  15. package/dist/src/browser/providers/index.js +2 -0
  16. package/dist/src/cli/browserConfig.js +33 -6
  17. package/dist/src/cli/browserDefaults.js +21 -0
  18. package/dist/src/cli/detach.js +5 -2
  19. package/dist/src/cli/fileSize.js +11 -0
  20. package/dist/src/cli/help.js +3 -3
  21. package/dist/src/cli/markdownBundle.js +5 -1
  22. package/dist/src/cli/options.js +40 -3
  23. package/dist/src/cli/runOptions.js +11 -3
  24. package/dist/src/cli/sessionDisplay.js +91 -2
  25. package/dist/src/cli/sessionLineage.js +56 -0
  26. package/dist/src/cli/sessionRunner.js +169 -2
  27. package/dist/src/cli/sessionTable.js +2 -1
  28. package/dist/src/cli/tui/index.js +3 -0
  29. package/dist/src/gemini-web/browserSessionManager.js +76 -0
  30. package/dist/src/gemini-web/client.js +16 -5
  31. package/dist/src/gemini-web/executionClients.js +1 -0
  32. package/dist/src/gemini-web/executionMode.js +18 -0
  33. package/dist/src/gemini-web/executor.js +273 -120
  34. package/dist/src/mcp/tools/consult.js +35 -21
  35. package/dist/src/oracle/client.js +42 -13
  36. package/dist/src/oracle/config.js +43 -7
  37. package/dist/src/oracle/errors.js +2 -2
  38. package/dist/src/oracle/files.js +20 -5
  39. package/dist/src/oracle/gemini.js +3 -0
  40. package/dist/src/oracle/modelResolver.js +33 -1
  41. package/dist/src/oracle/request.js +7 -2
  42. package/dist/src/oracle/run.js +22 -12
  43. package/dist/src/sessionManager.js +13 -2
  44. package/dist/src/sessionStore.js +2 -2
  45. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  46. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  47. package/package.json +24 -24
  48. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  49. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,34 +1,63 @@
1
- import OpenAI, { AzureOpenAI } from 'openai';
1
+ import OpenAI from 'openai';
2
2
  import path from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { createGeminiClient } from './gemini.js';
5
5
  import { createClaudeClient } from './claude.js';
6
6
  import { isOpenRouterBaseUrl } from './modelResolver.js';
7
+ /**
8
+ * Known native API base URLs that should still use their dedicated SDKs.
9
+ * Any other custom base URL is treated as an OpenAI-compatible proxy and
10
+ * all models are routed through the chat/completions adapter.
11
+ */
12
+ const NATIVE_API_HOSTS = [
13
+ 'api.openai.com',
14
+ 'api.anthropic.com',
15
+ 'generativelanguage.googleapis.com',
16
+ 'api.x.ai',
17
+ ];
18
+ export function isCustomBaseUrl(baseUrl) {
19
+ if (!baseUrl)
20
+ return false;
21
+ try {
22
+ const url = new URL(baseUrl);
23
+ return !NATIVE_API_HOSTS.some((host) => url.hostname === host || url.hostname.endsWith(`.${host}`));
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ export function buildAzureResponsesBaseUrl(endpoint) {
30
+ return `${endpoint.replace(/\/+$/, '')}/openai/v1`;
31
+ }
7
32
  export function createDefaultClientFactory() {
8
33
  const customFactory = loadCustomClientFactory();
9
34
  if (customFactory)
10
35
  return customFactory;
11
36
  return (key, options) => {
12
- if (options?.model?.startsWith('gemini')) {
13
- // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
14
- return createGeminiClient(key, options.model, options.resolvedModelId);
15
- }
16
- if (options?.model?.startsWith('claude')) {
17
- return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
37
+ const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
38
+ const customProxy = isCustomBaseUrl(options?.baseUrl);
39
+ // When using any custom/proxy base URL (OpenRouter, LiteLLM, vLLM, Together, etc.),
40
+ // route ALL models through the OpenAI chat/completions adapter instead of native SDKs
41
+ // which would reject the proxy's API key.
42
+ if (!openRouter && !customProxy) {
43
+ if (options?.model?.startsWith('gemini')) {
44
+ // Gemini client uses its own SDK; allow passing the already-resolved id for transparency/logging.
45
+ return createGeminiClient(key, options.model, options.resolvedModelId);
46
+ }
47
+ if (options?.model?.startsWith('claude')) {
48
+ return createClaudeClient(key, options.model, options.resolvedModelId, options.baseUrl);
49
+ }
18
50
  }
19
51
  let instance;
20
- const openRouter = isOpenRouterBaseUrl(options?.baseUrl);
21
52
  const defaultHeaders = openRouter ? buildOpenRouterHeaders() : undefined;
22
53
  const httpTimeoutMs = typeof options?.httpTimeoutMs === 'number' && Number.isFinite(options.httpTimeoutMs) && options.httpTimeoutMs > 0
23
54
  ? options.httpTimeoutMs
24
55
  : 20 * 60 * 1000;
25
56
  if (options?.azure?.endpoint) {
26
- instance = new AzureOpenAI({
57
+ instance = new OpenAI({
27
58
  apiKey: key,
28
- endpoint: options.azure.endpoint,
29
- apiVersion: options.azure.apiVersion,
30
- deployment: options.azure.deployment,
31
59
  timeout: httpTimeoutMs,
60
+ baseURL: buildAzureResponsesBaseUrl(options.azure.endpoint),
32
61
  });
33
62
  }
34
63
  else {
@@ -39,7 +68,7 @@ export function createDefaultClientFactory() {
39
68
  defaultHeaders,
40
69
  });
41
70
  }
42
- if (openRouter) {
71
+ if (openRouter || customProxy) {
43
72
  return buildOpenRouterCompletionClient(instance);
44
73
  }
45
74
  return {
@@ -2,19 +2,19 @@ import { countTokens as countTokensGpt5 } from 'gpt-tokenizer/model/gpt-5';
2
2
  import { countTokens as countTokensGpt5Pro } from 'gpt-tokenizer/model/gpt-5-pro';
3
3
  import { countTokens as countTokensAnthropicRaw } from '@anthropic-ai/tokenizer';
4
4
  import { stringifyTokenizerInput } from './tokenStringifier.js';
5
- export const DEFAULT_MODEL = 'gpt-5.2-pro';
6
- export const PRO_MODELS = new Set(['gpt-5.1-pro', 'gpt-5-pro', 'gpt-5.2-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
5
+ export const DEFAULT_MODEL = 'gpt-5.4-pro';
6
+ export const PRO_MODELS = new Set(['gpt-5.4-pro', 'gpt-5.1-pro', 'gpt-5-pro', 'gpt-5.2-pro', 'claude-4.5-sonnet', 'claude-4.1-opus']);
7
7
  const countTokensAnthropic = (input) => countTokensAnthropicRaw(stringifyTokenizerInput(input));
8
8
  export const MODEL_CONFIGS = {
9
9
  'gpt-5.1-pro': {
10
10
  model: 'gpt-5.1-pro',
11
- apiModel: 'gpt-5.2-pro',
11
+ apiModel: 'gpt-5.4-pro',
12
12
  provider: 'openai',
13
13
  tokenizer: countTokensGpt5Pro,
14
14
  inputLimit: 196000,
15
15
  pricing: {
16
- inputPerToken: 21 / 1_000_000,
17
- outputPerToken: 168 / 1_000_000,
16
+ inputPerToken: 30 / 1_000_000,
17
+ outputPerToken: 180 / 1_000_000,
18
18
  },
19
19
  reasoning: null,
20
20
  },
@@ -51,6 +51,28 @@ export const MODEL_CONFIGS = {
51
51
  },
52
52
  reasoning: { effort: 'high' },
53
53
  },
54
+ 'gpt-5.4': {
55
+ model: 'gpt-5.4',
56
+ provider: 'openai',
57
+ tokenizer: countTokensGpt5,
58
+ inputLimit: 196000,
59
+ pricing: {
60
+ inputPerToken: 2.5 / 1_000_000,
61
+ outputPerToken: 15 / 1_000_000,
62
+ },
63
+ reasoning: { effort: 'xhigh' },
64
+ },
65
+ 'gpt-5.4-pro': {
66
+ model: 'gpt-5.4-pro',
67
+ provider: 'openai',
68
+ tokenizer: countTokensGpt5Pro,
69
+ inputLimit: 196000,
70
+ pricing: {
71
+ inputPerToken: 30 / 1_000_000,
72
+ outputPerToken: 180 / 1_000_000,
73
+ },
74
+ reasoning: { effort: 'xhigh' },
75
+ },
54
76
  'gpt-5.2': {
55
77
  model: 'gpt-5.2',
56
78
  provider: 'openai',
@@ -76,15 +98,29 @@ export const MODEL_CONFIGS = {
76
98
  },
77
99
  'gpt-5.2-pro': {
78
100
  model: 'gpt-5.2-pro',
101
+ apiModel: 'gpt-5.4-pro',
79
102
  provider: 'openai',
80
103
  tokenizer: countTokensGpt5Pro,
81
104
  inputLimit: 196000,
82
105
  pricing: {
83
- inputPerToken: 21 / 1_000_000,
84
- outputPerToken: 168 / 1_000_000,
106
+ inputPerToken: 30 / 1_000_000,
107
+ outputPerToken: 180 / 1_000_000,
85
108
  },
86
109
  reasoning: { effort: 'xhigh' },
87
110
  },
111
+ 'gemini-3.1-pro': {
112
+ model: 'gemini-3.1-pro',
113
+ provider: 'google',
114
+ tokenizer: countTokensGpt5Pro,
115
+ inputLimit: 200000,
116
+ pricing: {
117
+ inputPerToken: 2 / 1_000_000,
118
+ outputPerToken: 12 / 1_000_000,
119
+ },
120
+ reasoning: null,
121
+ supportsBackground: false,
122
+ supportsSearch: true,
123
+ },
88
124
  'gemini-3-pro': {
89
125
  model: 'gemini-3-pro',
90
126
  provider: 'google',
@@ -96,12 +96,12 @@ export function toTransportError(error, model) {
96
96
  apiError.message ||
97
97
  (apiError.status ? `${apiError.status} OpenAI API error` : 'OpenAI API error');
98
98
  // Friendly guidance when a pro-tier model isn't available on this base URL / API key.
99
- if (model === 'gpt-5.2-pro' &&
99
+ if (model === 'gpt-5.4-pro' &&
100
100
  (code === 'model_not_found' ||
101
101
  messageText.includes('does not exist') ||
102
102
  messageText.includes('unknown model') ||
103
103
  messageText.includes('model_not_found'))) {
104
- return new OracleTransportError('model-unavailable', 'gpt-5.2-pro is not available on this API base/key. Try gpt-5-pro or gpt-5.2, or switch to the browser engine.', apiError);
104
+ return new OracleTransportError('model-unavailable', 'gpt-5.4-pro is not available on this API base/key. Try gpt-5-pro or gpt-5.4, or switch to the browser engine.', apiError);
105
105
  }
106
106
  if (apiError.status === 404 || apiError.status === 405) {
107
107
  return new OracleTransportError('unsupported-endpoint', 'HTTP 404/405 from the Responses API; this base URL or gateway likely does not expose /v1/responses. Set OPENAI_BASE_URL to api.openai.com/v1, update your Azure API version/deployment, or use the browser engine.', apiError);
@@ -2,10 +2,10 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import fg from 'fast-glob';
4
4
  import { FileValidationError } from './errors.js';
5
- const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
5
+ export const DEFAULT_MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024; // 1 MB
6
6
  const DEFAULT_FS = fs;
7
7
  const DEFAULT_IGNORED_DIRS = ['node_modules', 'dist', 'coverage', '.git', '.turbo', '.next', 'build', 'tmp'];
8
- export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = MAX_FILE_SIZE_BYTES, readContents = true, } = {}) {
8
+ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEFAULT_FS, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES, readContents = true, } = {}) {
9
9
  if (!filePaths || filePaths.length === 0) {
10
10
  return [];
11
11
  }
@@ -76,7 +76,7 @@ export async function readFiles(filePaths, { cwd = process.cwd(), fsModule = DEF
76
76
  accepted.push(filePath);
77
77
  }
78
78
  if (oversized.length > 0) {
79
- throw new FileValidationError(`The following files exceed the 1 MB limit:\n- ${oversized.join('\n- ')}`, {
79
+ throw new FileValidationError(`The following files exceed the ${formatBytes(maxFileSizeBytes)} limit:\n- ${oversized.join('\n- ')}`, {
80
80
  files: oversized,
81
81
  limitBytes: maxFileSizeBytes,
82
82
  });
@@ -347,13 +347,28 @@ function stripTrailingSlashes(value) {
347
347
  }
348
348
  function formatBytes(size) {
349
349
  if (size >= 1024 * 1024) {
350
- return `${(size / (1024 * 1024)).toFixed(1)} MB`;
350
+ return `${formatScaled(size / (1024 * 1024))} MB`;
351
351
  }
352
352
  if (size >= 1024) {
353
- return `${(size / 1024).toFixed(1)} KB`;
353
+ return `${formatScaled(size / 1024)} KB`;
354
354
  }
355
355
  return `${size} B`;
356
356
  }
357
+ function formatScaled(value) {
358
+ return value.toFixed(1).replace(/\.0$/, '');
359
+ }
360
+ export function normalizeMaxFileSizeBytes(value, source = 'max file size') {
361
+ if (value == null || value === '') {
362
+ return undefined;
363
+ }
364
+ const parsed = typeof value === 'number'
365
+ ? value
366
+ : Number.parseInt(typeof value === 'string' ? value.trim() : String(value), 10);
367
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
368
+ throw new Error(`${source} must be a positive integer number of bytes.`);
369
+ }
370
+ return parsed;
371
+ }
357
372
  function relativePath(targetPath, cwd) {
358
373
  const relative = path.relative(cwd, targetPath);
359
374
  return relative || targetPath;
@@ -1,6 +1,9 @@
1
1
  import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai';
2
2
  const MODEL_ID_MAP = {
3
+ 'gemini-3.1-pro': 'gemini-3.1-pro-preview',
3
4
  'gemini-3-pro': 'gemini-3-pro-preview',
5
+ 'gpt-5.4': 'gpt-5.4',
6
+ 'gpt-5.4-pro': 'gpt-5.4-pro',
4
7
  'gpt-5.1-pro': 'gpt-5.1-pro',
5
8
  'gpt-5-pro': 'gpt-5-pro',
6
9
  'gpt-5.1': 'gpt-5.1',
@@ -38,9 +38,30 @@ export function safeModelSlug(model) {
38
38
  }
39
39
  const catalogCache = new Map();
40
40
  const CACHE_TTL_MS = 5 * 60 * 1000;
41
+ const MAX_CACHE_ENTRIES = 20;
42
+ /**
43
+ * Prune stale entries from the catalog cache to prevent unbounded growth.
44
+ * Removes entries older than TTL and enforces a maximum cache size.
45
+ */
46
+ function pruneCatalogCache(now) {
47
+ // Remove stale entries first
48
+ for (const [key, entry] of catalogCache) {
49
+ if (now - entry.fetchedAt >= CACHE_TTL_MS) {
50
+ catalogCache.delete(key);
51
+ }
52
+ }
53
+ // If still over limit, evict oldest fetched entries (not true LRU; no last-access tracking).
54
+ if (catalogCache.size > MAX_CACHE_ENTRIES) {
55
+ const entries = [...catalogCache.entries()].sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
56
+ const toRemove = entries.slice(0, catalogCache.size - MAX_CACHE_ENTRIES);
57
+ for (const [key] of toRemove) {
58
+ catalogCache.delete(key);
59
+ }
60
+ }
61
+ }
41
62
  async function fetchOpenRouterCatalog(apiKey, fetcher) {
42
- const cached = catalogCache.get(apiKey);
43
63
  const now = Date.now();
64
+ const cached = catalogCache.get(apiKey);
44
65
  if (cached && now - cached.fetchedAt < CACHE_TTL_MS) {
45
66
  return cached.models;
46
67
  }
@@ -55,6 +76,8 @@ async function fetchOpenRouterCatalog(apiKey, fetcher) {
55
76
  const json = (await response.json());
56
77
  const models = json?.data ?? [];
57
78
  catalogCache.set(apiKey, { fetchedAt: now, models });
79
+ // Prune after insert so the max-size constraint is strictly enforced.
80
+ pruneCatalogCache(now);
58
81
  return models;
59
82
  }
60
83
  function mapToOpenRouterId(candidate, catalog, providerHint) {
@@ -149,3 +172,12 @@ export async function resolveModelConfig(model, options = {}) {
149
172
  export function isProModel(model) {
150
173
  return isKnownModel(model) && PRO_MODELS.has(model);
151
174
  }
175
+ export function resetOpenRouterCatalogCacheForTest() {
176
+ catalogCache.clear();
177
+ }
178
+ export function getOpenRouterCatalogCacheSizeForTest() {
179
+ return catalogCache.size;
180
+ }
181
+ export function getOpenRouterCatalogCacheMaxEntriesForTest() {
182
+ return MAX_CACHE_ENTRIES;
183
+ }
@@ -11,10 +11,11 @@ export function buildPrompt(basePrompt, files, cwd = process.cwd()) {
11
11
  const sectionText = sections.map((section) => section.sectionText).join('\n\n');
12
12
  return `${basePrompt.trim()}\n\n${sectionText}`;
13
13
  }
14
- export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, }) {
14
+ export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, previousResponseId, }) {
15
15
  const searchToolType = modelConfig.searchToolType ?? 'web_search_preview';
16
16
  return {
17
17
  model: modelConfig.apiModel ?? modelConfig.model,
18
+ previous_response_id: previousResponseId ? previousResponseId : undefined,
18
19
  instructions: systemPrompt,
19
20
  input: [
20
21
  {
@@ -37,7 +38,11 @@ export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, search
37
38
  export async function renderPromptMarkdown(options, deps = {}) {
38
39
  const cwd = deps.cwd ?? process.cwd();
39
40
  const fsModule = deps.fs ?? createFsAdapter(fs);
40
- const files = await readFiles(options.file ?? [], { cwd, fsModule });
41
+ const files = await readFiles(options.file ?? [], {
42
+ cwd,
43
+ fsModule,
44
+ maxFileSizeBytes: options.maxFileSizeBytes,
45
+ });
41
46
  const sections = createFileSections(files, cwd);
42
47
  const systemPrompt = options.system?.trim() || DEFAULT_SYSTEM_PROMPT;
43
48
  const userPrompt = (options.prompt ?? '').trim();
@@ -12,7 +12,7 @@ import { formatElapsed } from './format.js';
12
12
  import { formatFinishLine } from './finishLine.js';
13
13
  import { getFileTokenStats, printFileTokenStats } from './tokenStats.js';
14
14
  import { OracleResponseError, OracleTransportError, PromptValidationError, describeTransportError, toTransportError, } from './errors.js';
15
- import { createDefaultClientFactory } from './client.js';
15
+ import { createDefaultClientFactory, isCustomBaseUrl } from './client.js';
16
16
  import { formatBaseUrlForLog, maskApiKey } from './logging.js';
17
17
  import { startHeartbeat } from '../heartbeat.js';
18
18
  import { startOscProgress } from './oscProgress.js';
@@ -145,7 +145,7 @@ export async function runOracle(options, deps = {}) {
145
145
  const supportsBackground = modelConfig.supportsBackground !== false;
146
146
  const useBackground = supportsBackground ? options.background ?? isLongRunningModel : false;
147
147
  const inputTokenBudget = options.maxInput ?? modelConfig.inputLimit;
148
- const files = await readFiles(options.file ?? [], { cwd, fsModule });
148
+ const files = await readFiles(options.file ?? [], { cwd, fsModule, maxFileSizeBytes: options.maxFileSizeBytes });
149
149
  const searchEnabled = options.search !== false;
150
150
  logVerbose(`cwd: ${cwd}`);
151
151
  let pendingNoFilesTip = null;
@@ -189,11 +189,17 @@ export async function runOracle(options, deps = {}) {
189
189
  : DEFAULT_TIMEOUT_NON_PRO_MS / 1000
190
190
  : options.timeoutSeconds;
191
191
  const timeoutMs = timeoutSeconds * 1000;
192
+ const azureDeploymentName = isAzureOpenAI ? options.azure?.deployment?.trim() : undefined;
192
193
  // Track the concrete model id we dispatch to (especially for Gemini preview aliases)
193
194
  const effectiveModelId = options.effectiveModelId ??
194
- (options.model.startsWith('gemini')
195
- ? resolveGeminiModelId(options.model)
196
- : (modelConfig.apiModel ?? modelConfig.model));
195
+ (azureDeploymentName
196
+ ? azureDeploymentName
197
+ : options.model.startsWith('gemini')
198
+ ? resolveGeminiModelId(options.model)
199
+ : (modelConfig.apiModel ?? modelConfig.model));
200
+ if (!isPreview && options.previousResponseId) {
201
+ log(dim(`Continuing from response ${options.previousResponseId}`));
202
+ }
197
203
  const requestBody = buildRequestBody({
198
204
  modelConfig,
199
205
  systemPrompt,
@@ -201,8 +207,11 @@ export async function runOracle(options, deps = {}) {
201
207
  searchEnabled,
202
208
  maxOutputTokens: options.maxOutput,
203
209
  background: useBackground,
204
- storeResponse: useBackground,
210
+ // Storing makes follow-ups possible (Responses API chaining relies on stored response state).
211
+ storeResponse: useBackground || Boolean(options.previousResponseId),
212
+ previousResponseId: options.previousResponseId,
205
213
  });
214
+ requestBody.model = effectiveModelId;
206
215
  const estimatedInputTokens = estimateRequestTokens(requestBody, modelConfig);
207
216
  const tokenLabel = formatTokenEstimate(estimatedInputTokens, (text) => (richTty ? chalk.green(text) : text));
208
217
  const fileLabel = richTty ? chalk.magenta(fileCount.toString()) : fileCount.toString();
@@ -225,9 +234,9 @@ export async function runOracle(options, deps = {}) {
225
234
  log(dim(`Using ${envVar}=${maskedKey} for model ${modelConfig.model}${resolvedSuffix}`));
226
235
  }
227
236
  if (!options.suppressHeader &&
228
- modelConfig.model === 'gpt-5.1-pro' &&
229
- effectiveModelId === 'gpt-5.2-pro') {
230
- log(dim('Note: `gpt-5.1-pro` is a stable CLI alias; OpenAI API uses `gpt-5.2-pro`.'));
237
+ (modelConfig.model === 'gpt-5.1-pro' || modelConfig.model === 'gpt-5.2-pro') &&
238
+ effectiveModelId === 'gpt-5.4-pro') {
239
+ log(dim(`Note: \`${modelConfig.model}\` is a stable CLI alias; OpenAI API uses \`gpt-5.4-pro\`.`));
231
240
  }
232
241
  if (baseUrl) {
233
242
  log(dim(`Base URL: ${formatBaseUrlForLog(baseUrl)}`));
@@ -280,10 +289,11 @@ export async function runOracle(options, deps = {}) {
280
289
  inputTokenBudget,
281
290
  };
282
291
  }
292
+ const proxyCompatibleBaseUrl = baseUrl && (isOpenRouterBaseUrl(baseUrl) || isCustomBaseUrl(baseUrl)) ? baseUrl : undefined;
283
293
  const apiEndpoint = modelConfig.model.startsWith('gemini')
284
- ? undefined
285
- : isOpenRouterBaseUrl(baseUrl)
286
- ? baseUrl
294
+ ? proxyCompatibleBaseUrl
295
+ : proxyCompatibleBaseUrl
296
+ ? proxyCompatibleBaseUrl
287
297
  : modelConfig.model.startsWith('claude')
288
298
  ? process.env.ANTHROPIC_BASE_URL ?? baseUrl
289
299
  : baseUrl;
@@ -151,9 +151,9 @@ export async function updateModelRunMetadata(sessionId, model, updates) {
151
151
  export async function readModelRunMetadata(sessionId, model) {
152
152
  return readModelRunFile(sessionId, model);
153
153
  }
154
- export async function initializeSession(options, cwd, notifications) {
154
+ export async function initializeSession(options, cwd, notifications, baseSlugOverride) {
155
155
  await ensureSessionStorage();
156
- const baseSlug = createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
156
+ const baseSlug = baseSlugOverride || createSessionId(options.prompt || DEFAULT_SLUG, options.slug);
157
157
  const sessionId = await ensureUniqueSessionId(baseSlug);
158
158
  const dir = sessionDir(sessionId);
159
159
  await ensureDir(dir);
@@ -181,8 +181,12 @@ export async function initializeSession(options, cwd, notifications) {
181
181
  options: {
182
182
  prompt: options.prompt,
183
183
  file: options.file ?? [],
184
+ maxFileSizeBytes: options.maxFileSizeBytes,
184
185
  model: options.model,
185
186
  models: modelList,
187
+ previousResponseId: options.previousResponseId,
188
+ followupSessionId: options.followupSessionId,
189
+ followupModel: options.followupModel,
186
190
  effectiveModelId: options.effectiveModelId,
187
191
  maxInput: options.maxInput,
188
192
  system: options.system,
@@ -206,6 +210,13 @@ export async function initializeSession(options, cwd, notifications) {
206
210
  zombieTimeoutMs: options.zombieTimeoutMs,
207
211
  zombieUseLastActivity: options.zombieUseLastActivity,
208
212
  writeOutputPath: options.writeOutputPath,
213
+ waitPreference: options.waitPreference,
214
+ youtube: options.youtube,
215
+ generateImage: options.generateImage,
216
+ editImage: options.editImage,
217
+ outputPath: options.outputPath,
218
+ aspectRatio: options.aspectRatio,
219
+ geminiShowThoughts: options.geminiShowThoughts,
209
220
  },
210
221
  };
211
222
  await ensureDir(modelsDir(sessionId));
@@ -3,8 +3,8 @@ class FileSessionStore {
3
3
  ensureStorage() {
4
4
  return ensureSessionStorage();
5
5
  }
6
- createSession(options, cwd, notifications) {
7
- return initializeSession(options, cwd, notifications);
6
+ createSession(options, cwd, notifications, baseSlugOverride) {
7
+ return initializeSession(options, cwd, notifications, baseSlugOverride);
8
8
  }
9
9
  readSession(sessionId) {
10
10
  return readSessionMetadata(sessionId);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.8.5",
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.",
3
+ "version": "0.9.0",
4
+ "description": "CLI wrapper around OpenAI Responses API with GPT-5.4 Pro, GPT-5.4, 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",
7
7
  "bin": {
@@ -62,49 +62,49 @@
62
62
  "homepage": "https://github.com/steipete/oracle#readme",
63
63
  "dependencies": {
64
64
  "@anthropic-ai/tokenizer": "^0.0.4",
65
- "@google/genai": "^1.35.0",
65
+ "@google/genai": "^1.44.0",
66
66
  "@google/generative-ai": "^0.24.1",
67
- "@modelcontextprotocol/sdk": "^1.25.2",
68
- "@steipete/sweet-cookie": "^0.1.0",
67
+ "@modelcontextprotocol/sdk": "^1.27.1",
68
+ "@steipete/sweet-cookie": "^0.2.0",
69
69
  "chalk": "^5.6.2",
70
70
  "chrome-launcher": "^1.2.1",
71
- "chrome-remote-interface": "^0.33.3",
72
- "clipboardy": "^5.0.2",
73
- "commander": "^14.0.2",
74
- "dotenv": "^17.2.3",
71
+ "chrome-remote-interface": "^0.34.0",
72
+ "clipboardy": "^5.3.1",
73
+ "commander": "^14.0.3",
74
+ "dotenv": "^17.3.1",
75
75
  "fast-glob": "^3.3.3",
76
76
  "gpt-tokenizer": "^3.4.0",
77
- "inquirer": "13.2.0",
77
+ "inquirer": "13.3.0",
78
78
  "json5": "^2.2.3",
79
79
  "kleur": "^4.1.5",
80
80
  "markdansi": "0.2.1",
81
- "openai": "^6.16.0",
82
- "osc-progress": "^0.2.0",
83
- "qs": "^6.14.1",
84
- "shiki": "^3.21.0",
81
+ "openai": "^6.27.0",
82
+ "osc-progress": "^0.3.0",
83
+ "qs": "^6.15.0",
84
+ "shiki": "^4.0.1",
85
85
  "toasted-notifier": "^10.1.0",
86
86
  "tokentally": "^0.1.1",
87
- "zod": "^4.3.5"
87
+ "zod": "^4.3.6"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@anthropic-ai/tokenizer": "^0.0.4",
91
- "@biomejs/biome": "^2.3.11",
91
+ "@biomejs/biome": "^2.4.6",
92
92
  "@cdktf/node-pty-prebuilt-multiarch": "0.10.2",
93
93
  "@types/chrome-remote-interface": "^0.33.0",
94
94
  "@types/inquirer": "^9.0.9",
95
- "@types/node": "^25.0.6",
96
- "@vitest/coverage-v8": "4.0.17",
97
- "devtools-protocol": "0.0.1568893",
98
- "es-toolkit": "^1.43.0",
99
- "esbuild": "^0.27.2",
100
- "puppeteer-core": "^24.35.0",
95
+ "@types/node": "^25.3.5",
96
+ "@vitest/coverage-v8": "4.0.18",
97
+ "devtools-protocol": "0.0.1595872",
98
+ "es-toolkit": "^1.45.1",
99
+ "esbuild": "^0.27.3",
100
+ "puppeteer-core": "^24.38.0",
101
101
  "tsx": "^4.21.0",
102
102
  "typescript": "^5.9.3",
103
- "vitest": "^4.0.17"
103
+ "vitest": "^4.0.18"
104
104
  },
105
105
  "pnpm": {
106
106
  "overrides": {
107
- "devtools-protocol": "0.0.1568893"
107
+ "devtools-protocol": "0.0.1595872"
108
108
  },
109
109
  "onlyBuiltDependencies": [
110
110
  "@cdktf/node-pty-prebuilt-multiarch",