@steipete/oracle 0.14.0 → 0.15.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 (31) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/oracle-cli.js +10 -8
  3. package/dist/docs-site/browser-mode.html +3 -2
  4. package/dist/docs-site/cli-reference.html +1 -1
  5. package/dist/docs-site/mcp.html +1 -1
  6. package/dist/src/browser/actions/assistantResponse.js +2 -1
  7. package/dist/src/browser/actions/deepResearch.js +132 -61
  8. package/dist/src/browser/actions/modelSelection.js +388 -30
  9. package/dist/src/browser/actions/thinkingTime.js +303 -65
  10. package/dist/src/browser/artifacts.js +2 -8
  11. package/dist/src/browser/chatgptFiles.js +198 -49
  12. package/dist/src/browser/chatgptImages.js +126 -24
  13. package/dist/src/browser/chromeLifecycle.js +35 -4
  14. package/dist/src/browser/deepResearchResult.js +23 -0
  15. package/dist/src/browser/index.js +145 -19
  16. package/dist/src/browser/profileCopy.js +93 -0
  17. package/dist/src/browser/projectSourcesRunner.js +2 -1
  18. package/dist/src/browser/prompt.js +151 -22
  19. package/dist/src/cli/browserConfig.js +19 -2
  20. package/dist/src/cli/browserDefaults.js +2 -1
  21. package/dist/src/cli/options.js +8 -0
  22. package/dist/src/cli/sessionRunner.js +13 -7
  23. package/dist/src/mcp/tools/chatgptImage.js +8 -3
  24. package/dist/src/mcp/tools/consult.js +9 -8
  25. package/dist/src/mcp/types.js +11 -2
  26. package/dist/src/oracle/thinkingTime.js +40 -0
  27. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  28. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  29. package/package.json +6 -6
  30. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  31. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,12 +1,14 @@
1
1
  import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSections, } from "../oracle.js";
4
+ import { readFiles, createFileSections, FileValidationError, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSections, } from "../oracle.js";
5
5
  import { isKnownModel } from "../oracle/modelResolver.js";
6
6
  import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
7
7
  import { buildAttachmentPlan } from "./policies.js";
8
8
  import { createStoredZip } from "./zipBundle.js";
9
9
  const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
10
+ const MAX_BROWSER_ATTACHMENTS = 10;
11
+ const MAX_BROWSER_ZIP_BUNDLE_BYTES = 128 * 1024 * 1024;
10
12
  const MEDIA_EXTENSIONS = new Set([
11
13
  ".mp4",
12
14
  ".mov",
@@ -31,27 +33,95 @@ const MEDIA_EXTENSIONS = new Set([
31
33
  ".heif",
32
34
  ".pdf",
33
35
  ]);
36
+ const ARCHIVE_EXTENSIONS = new Set([
37
+ ".7z",
38
+ ".aab",
39
+ ".apk",
40
+ ".br",
41
+ ".bz2",
42
+ ".cab",
43
+ ".crx",
44
+ ".deb",
45
+ ".dmg",
46
+ ".doc",
47
+ ".docx",
48
+ ".ear",
49
+ ".epub",
50
+ ".gz",
51
+ ".ipa",
52
+ ".iso",
53
+ ".jar",
54
+ ".lz",
55
+ ".lz4",
56
+ ".msi",
57
+ ".odp",
58
+ ".ods",
59
+ ".odt",
60
+ ".pkg",
61
+ ".ppt",
62
+ ".pptx",
63
+ ".rar",
64
+ ".rpm",
65
+ ".tar",
66
+ ".tgz",
67
+ ".war",
68
+ ".whl",
69
+ ".xls",
70
+ ".xlsx",
71
+ ".xz",
72
+ ".xpi",
73
+ ".zip",
74
+ ".zipx",
75
+ ".zst",
76
+ ]);
34
77
  export function isMediaFile(filePath) {
35
78
  const ext = path.extname(filePath).toLowerCase();
36
79
  return MEDIA_EXTENSIONS.has(ext);
37
80
  }
81
+ export function isRawUploadFile(filePath) {
82
+ const ext = path.extname(filePath).toLowerCase();
83
+ return MEDIA_EXTENSIONS.has(ext) || ARCHIVE_EXTENSIONS.has(ext);
84
+ }
38
85
  function formatSectionsForBundle(sections, options = {}) {
39
86
  return formatFileSections(sections, {
40
87
  lineNumbers: options.lineNumbers ?? true,
41
88
  trailingNewline: true,
42
89
  });
43
90
  }
44
- async function writeBrowserBundle(sections, format) {
91
+ function resolveBrowserBundleFormat(format, sources) {
92
+ if (format !== "auto") {
93
+ return format;
94
+ }
95
+ return sources.hasRawUploadFiles ? "zip" : "text";
96
+ }
97
+ function shouldWriteBrowserBundle(format, { attachmentCount, bundleRequested, textSourceCount, textPlanShouldBundle, }) {
98
+ if (format === "zip") {
99
+ return (textPlanShouldBundle ||
100
+ (bundleRequested && attachmentCount > 0) ||
101
+ attachmentCount > MAX_BROWSER_ATTACHMENTS);
102
+ }
103
+ return textSourceCount > 0 && (textPlanShouldBundle || attachmentCount > MAX_BROWSER_ATTACHMENTS);
104
+ }
105
+ function assertAttachmentCount(attachments, format) {
106
+ if (attachments.length <= MAX_BROWSER_ATTACHMENTS)
107
+ return;
108
+ throw new Error(`Browser upload has ${attachments.length} attachments after applying bundle format "${format}". Use --browser-bundle-format auto or zip to stay within the ${MAX_BROWSER_ATTACHMENTS}-attachment limit.`);
109
+ }
110
+ async function writeBrowserBundle(sections, sources, format) {
45
111
  const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
46
112
  const tokenEstimateText = formatSectionsForBundle(sections, {
47
113
  lineNumbers: format === "text",
48
114
  });
49
115
  if (format === "zip") {
116
+ const totalSourceBytes = sources.reduce((total, source) => total + source.sizeBytes, 0);
117
+ if (totalSourceBytes > MAX_BROWSER_ZIP_BUNDLE_BYTES) {
118
+ throw new Error(`Browser ZIP bundle inputs exceed the ${MAX_BROWSER_ZIP_BUNDLE_BYTES}-byte in-memory limit.`);
119
+ }
50
120
  const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
51
- const buffer = createStoredZip(sections.map((section) => ({
52
- path: section.displayPath,
53
- content: section.content,
54
- })));
121
+ const buffer = createStoredZip(await Promise.all(sources.map(async (source) => ({
122
+ path: source.displayPath,
123
+ content: await fs.readFile(source.absolutePath),
124
+ }))));
55
125
  await fs.writeFile(bundlePath, buffer);
56
126
  return {
57
127
  attachment: {
@@ -60,7 +130,7 @@ async function writeBrowserBundle(sections, format) {
60
130
  sizeBytes: buffer.length,
61
131
  generatedBundle: true,
62
132
  },
63
- metadata: { originalCount: sections.length, bundlePath, format },
133
+ metadata: { originalCount: sources.length, bundlePath, format },
64
134
  tokenEstimateText,
65
135
  };
66
136
  }
@@ -81,11 +151,27 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
81
151
  const cwd = deps.cwd ?? process.cwd();
82
152
  const readFilesFn = deps.readFilesImpl ?? readFiles;
83
153
  const allFilePaths = runOptions.file ?? [];
84
- const textFilePaths = allFilePaths.filter((f) => !isMediaFile(f));
85
- const mediaFilePaths = allFilePaths.filter((f) => isMediaFile(f));
86
- const mediaAttachments = await Promise.all(mediaFilePaths.map(async (filePath) => {
154
+ const discoveredFiles = allFilePaths.length > 0
155
+ ? await readFilesFn(allFilePaths, {
156
+ cwd,
157
+ maxFileSizeBytes: 0,
158
+ readContents: false,
159
+ })
160
+ : [];
161
+ const textFilePaths = discoveredFiles
162
+ .filter((file) => !isRawUploadFile(file.path))
163
+ .map((file) => file.path);
164
+ const rawUploadFiles = discoveredFiles.filter((file) => isRawUploadFile(file.path));
165
+ const maxFileSizeBytes = runOptions.maxFileSizeBytes;
166
+ const rawUploadAttachments = await Promise.all(rawUploadFiles.map(async ({ path: filePath }) => {
87
167
  const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
88
168
  const stats = await fs.stat(resolvedPath);
169
+ if (maxFileSizeBytes && stats.size > maxFileSizeBytes) {
170
+ throw new FileValidationError(`The following file exceeds the ${maxFileSizeBytes}-byte limit:\n- ${path.relative(cwd, resolvedPath) || resolvedPath} (${stats.size} bytes)`, {
171
+ files: [resolvedPath],
172
+ limitBytes: maxFileSizeBytes,
173
+ });
174
+ }
89
175
  return {
90
176
  path: resolvedPath,
91
177
  displayPath: path.relative(cwd, resolvedPath) || path.basename(resolvedPath),
@@ -105,7 +191,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
105
191
  ? "never"
106
192
  : (runOptions.browserAttachments ?? "auto");
107
193
  const bundleRequested = Boolean(runOptions.browserBundleFiles);
108
- const bundleFormat = runOptions.browserBundleFormat ?? "text";
194
+ const bundleFormat = runOptions.browserBundleFormat ?? "auto";
195
+ if (attachmentsPolicy === "never" && rawUploadAttachments.length > 0) {
196
+ throw new FileValidationError("Raw or binary files cannot be pasted inline when browser attachments are disabled. Use --browser-attachments auto or always.", { files: rawUploadAttachments.map((attachment) => attachment.displayPath) });
197
+ }
109
198
  const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
110
199
  const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
111
200
  const baseComposerSections = [];
@@ -124,25 +213,47 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
124
213
  : inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
125
214
  ? inlinePlan
126
215
  : uploadPlan;
127
- const composerText = (selectedPlan.inlineBlock
216
+ const textBundleSources = sections.map((section) => ({
217
+ absolutePath: section.absolutePath,
218
+ displayPath: section.displayPath,
219
+ sizeBytes: Buffer.byteLength(section.content, "utf8"),
220
+ }));
221
+ const rawUploadBundleSources = rawUploadAttachments.map((attachment) => ({
222
+ absolutePath: attachment.path,
223
+ displayPath: attachment.displayPath,
224
+ sizeBytes: attachment.sizeBytes ?? 0,
225
+ }));
226
+ const allBundleSources = [...textBundleSources, ...rawUploadBundleSources];
227
+ const attachments = [...selectedPlan.attachments, ...rawUploadAttachments];
228
+ const resolvedBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
229
+ hasRawUploadFiles: rawUploadAttachments.length > 0,
230
+ });
231
+ const shouldBundle = shouldWriteBrowserBundle(resolvedBundleFormat, {
232
+ attachmentCount: attachments.length,
233
+ bundleRequested,
234
+ textSourceCount: textBundleSources.length,
235
+ textPlanShouldBundle: selectedPlan.shouldBundle,
236
+ });
237
+ const composerText = (!shouldBundle && selectedPlan.inlineBlock
128
238
  ? [...baseComposerSections, selectedPlan.inlineBlock]
129
239
  : baseComposerSections)
130
240
  .filter(Boolean)
131
241
  .join("\n\n")
132
242
  .trim();
133
- const attachments = [...selectedPlan.attachments, ...mediaAttachments];
134
- const shouldBundle = selectedPlan.shouldBundle;
135
243
  let bundleText = null;
136
244
  let bundled = null;
137
245
  if (shouldBundle) {
138
- const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
246
+ const writtenBundle = await writeBrowserBundle(sections, resolvedBundleFormat === "zip" ? allBundleSources : textBundleSources, resolvedBundleFormat);
139
247
  bundleText = writtenBundle.tokenEstimateText;
140
248
  attachments.length = 0;
141
249
  attachments.push(writtenBundle.attachment);
142
- attachments.push(...mediaAttachments);
250
+ if (resolvedBundleFormat === "text") {
251
+ attachments.push(...rawUploadAttachments);
252
+ }
143
253
  bundled = writtenBundle.metadata;
144
254
  }
145
- const inlineFileCount = selectedPlan.inlineFileCount;
255
+ assertAttachmentCount(attachments, resolvedBundleFormat);
256
+ const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
146
257
  const modelConfig = isKnownModel(runOptions.model)
147
258
  ? MODEL_CONFIGS[runOptions.model]
148
259
  : MODEL_CONFIGS["gpt-5.1"];
@@ -167,15 +278,27 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
167
278
  let fallback = null;
168
279
  if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
169
280
  const fallbackComposerText = baseComposerSections.join("\n\n").trim();
170
- const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
281
+ const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
171
282
  let fallbackBundled = null;
172
- if (uploadPlan.shouldBundle) {
173
- const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
283
+ const fallbackBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
284
+ hasRawUploadFiles: rawUploadAttachments.length > 0,
285
+ });
286
+ const fallbackShouldBundle = shouldWriteBrowserBundle(fallbackBundleFormat, {
287
+ attachmentCount: fallbackAttachments.length,
288
+ bundleRequested,
289
+ textSourceCount: textBundleSources.length,
290
+ textPlanShouldBundle: uploadPlan.shouldBundle,
291
+ });
292
+ if (fallbackShouldBundle) {
293
+ const writtenBundle = await writeBrowserBundle(sections, fallbackBundleFormat === "zip" ? allBundleSources : textBundleSources, fallbackBundleFormat);
174
294
  fallbackAttachments.length = 0;
175
295
  fallbackAttachments.push(writtenBundle.attachment);
176
- fallbackAttachments.push(...mediaAttachments);
296
+ if (fallbackBundleFormat === "text") {
297
+ fallbackAttachments.push(...rawUploadAttachments);
298
+ }
177
299
  fallbackBundled = writtenBundle.metadata;
178
300
  }
301
+ assertAttachmentCount(fallbackAttachments, fallbackBundleFormat);
179
302
  fallback = {
180
303
  composerText: fallbackComposerText,
181
304
  attachments: fallbackAttachments,
@@ -190,7 +313,13 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
190
313
  inlineFileCount,
191
314
  tokenEstimateIncludesInlineFiles,
192
315
  attachmentsPolicy,
193
- attachmentMode: selectedPlan.mode,
316
+ attachmentMode: shouldBundle
317
+ ? "bundle"
318
+ : attachments.length > 0
319
+ ? "upload"
320
+ : selectedPlan.mode === "bundle"
321
+ ? "inline"
322
+ : selectedPlan.mode,
194
323
  fallback,
195
324
  bundled,
196
325
  };
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
3
4
  import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
4
5
  import { normalizeChatgptUrl } from "../browser/utils.js";
5
6
  import { parseDuration } from "../duration.js";
@@ -63,6 +64,18 @@ export function normalizeChatGptModelForBrowser(model) {
63
64
  return model;
64
65
  }
65
66
  export async function buildBrowserConfig(options) {
67
+ if (options.copyProfile && options.browserKeepBrowser) {
68
+ 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.");
69
+ }
70
+ if (options.copyProfile && options.browserManualLogin) {
71
+ throw new Error("--copy-profile cannot be combined with --browser-manual-login: choose either a throwaway copied profile or the persistent manual-login profile.");
72
+ }
73
+ if (options.copyProfile && options.remoteChrome) {
74
+ throw new Error("--copy-profile cannot be combined with --remote-chrome: copied profiles require a locally launched Chrome instance.");
75
+ }
76
+ if (options.copyProfile && options.remoteHost) {
77
+ throw new Error("--copy-profile cannot be combined with --remote-host: the local profile source is not available to the remote browser service.");
78
+ }
66
79
  const desiredModelOverride = options.browserModelLabel?.trim();
67
80
  const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
68
81
  const baseModel = options.model.toLowerCase();
@@ -97,7 +110,9 @@ export async function buildBrowserConfig(options) {
97
110
  ? desiredModelOverride
98
111
  : mapModelToBrowserLabel(options.model);
99
112
  return {
100
- chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
113
+ chromeProfile: options.copyProfile
114
+ ? (options.browserChromeProfile ?? null)
115
+ : (options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE),
101
116
  chromePath: options.browserChromePath ?? null,
102
117
  chromeCookiePath: options.browserCookiePath ?? null,
103
118
  attachRunning,
@@ -145,6 +160,7 @@ export async function buildBrowserConfig(options) {
145
160
  keepBrowser: options.browserKeepBrowser ? true : undefined,
146
161
  manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
147
162
  manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
163
+ copyProfileSource: options.copyProfile ?? undefined,
148
164
  hideWindow: options.browserHideWindow ? true : undefined,
149
165
  desiredModel,
150
166
  modelStrategy,
@@ -153,7 +169,7 @@ export async function buildBrowserConfig(options) {
153
169
  allowCookieErrors: options.browserAllowCookieErrors ?? true,
154
170
  remoteChrome,
155
171
  browserTabRef: options.browserTab ?? undefined,
156
- thinkingTime: options.browserThinkingTime,
172
+ thinkingTime: normalizeThinkingTimeLevel(options.browserThinkingTime) ?? undefined,
157
173
  researchMode: options.browserResearch === "deep" ? "deep" : "off",
158
174
  archiveConversations: options.browserArchive,
159
175
  };
@@ -170,6 +186,7 @@ function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies
170
186
  options.browserKeepBrowser ? "--browser-keep-browser" : null,
171
187
  options.browserManualLogin ? "--browser-manual-login" : null,
172
188
  options.browserManualLoginProfileDir ? "--browser-manual-login-profile-dir" : null,
189
+ options.copyProfile ? "--copy-profile" : null,
173
190
  hasInlineCookies ? "--browser-inline-cookies/--browser-inline-cookies-file" : null,
174
191
  options.browserPort != null || options.browserDebugPort != null
175
192
  ? "--browser-port/--browser-debug-port"
@@ -1,5 +1,6 @@
1
1
  import { CHATGPT_URL } from "../browser/constants.js";
2
2
  import { normalizeChatgptUrl } from "../browser/utils.js";
3
+ import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
3
4
  export function applyBrowserDefaultsFromConfig(options, config, getSource) {
4
5
  const browser = config.browser;
5
6
  if (!browser)
@@ -89,7 +90,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
89
90
  options.browserModelStrategy = browser.modelStrategy;
90
91
  }
91
92
  if (isUnset("browserThinkingTime") && browser.thinkingTime !== undefined) {
92
- options.browserThinkingTime = browser.thinkingTime;
93
+ options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
93
94
  }
94
95
  if (isUnset("browserResearch") && browser.researchMode !== undefined) {
95
96
  options.browserResearch = browser.researchMode;
@@ -3,6 +3,7 @@ import { parseDuration } from "../duration.js";
3
3
  import path from "node:path";
4
4
  import fg from "fast-glob";
5
5
  import { DEFAULT_MODEL, MODEL_CONFIGS } from "../oracle/config.js";
6
+ import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
6
7
  export function collectPaths(value, previous = []) {
7
8
  if (!value) {
8
9
  return previous;
@@ -127,6 +128,13 @@ export function parseSearchOption(value) {
127
128
  }
128
129
  throw new InvalidArgumentError('Search mode must be "on" or "off".');
129
130
  }
131
+ export function parseThinkingTimeOption(value) {
132
+ const normalized = normalizeThinkingTimeLevel(value);
133
+ if (normalized) {
134
+ return normalized;
135
+ }
136
+ throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "extra-high".');
137
+ }
130
138
  export function normalizeModelOption(value) {
131
139
  return (value ?? "").trim();
132
140
  }
@@ -389,6 +389,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
389
389
  userError.details?.stage === "assistant-timeout";
390
390
  const cloudflareChallenge = userError?.category === "browser-automation" &&
391
391
  userError.details?.stage === "cloudflare-challenge";
392
+ const browserCanReattach = !browserConfig?.copyProfileSource;
392
393
  let reattachGuidanceLogged = false;
393
394
  const logBrowserReattachGuidance = (runtime) => {
394
395
  if (reattachGuidanceLogged || mode !== "browser")
@@ -399,7 +400,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
399
400
  reattachGuidanceLogged = true;
400
401
  log(formatBrowserReattachGuidance(sessionMeta.id));
401
402
  };
402
- if (connectionLost && mode === "browser") {
403
+ if (connectionLost && mode === "browser" && browserCanReattach) {
403
404
  const runtime = userError.details
404
405
  ?.runtime;
405
406
  const recoverableRuntime = runtime ?? sessionMeta.browser?.runtime;
@@ -456,7 +457,7 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
456
457
  logBrowserReattachGuidance(recoverableRuntime);
457
458
  return;
458
459
  }
459
- if (assistantTimeout && mode === "browser") {
460
+ if (assistantTimeout && mode === "browser" && browserCanReattach) {
460
461
  const runtime = userError.details
461
462
  ?.runtime;
462
463
  log(dim("Assistant response timed out; marking capture incomplete for reattach."));
@@ -509,9 +510,14 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
509
510
  }
510
511
  if (cloudflareChallenge && mode === "browser") {
511
512
  const details = userError.details;
512
- log(dim("Cloudflare challenge detected; browser left running so you can complete the check."));
513
- if (details?.reuseProfileHint) {
514
- log(dim(`Reuse this browser profile with: ${details.reuseProfileHint}`));
513
+ if (browserCanReattach) {
514
+ log(dim("Cloudflare challenge detected; browser left running so you can complete the check."));
515
+ if (details?.reuseProfileHint) {
516
+ log(dim(`Reuse this browser profile with: ${details.reuseProfileHint}`));
517
+ }
518
+ }
519
+ else {
520
+ log(dim("Cloudflare challenge detected; copied profile closed and removed."));
515
521
  }
516
522
  }
517
523
  if (userError) {
@@ -527,10 +533,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
527
533
  if (transportLine) {
528
534
  log(dim(`Transport: ${transportLine}`));
529
535
  }
530
- const browserRuntime = mode === "browser"
536
+ const browserRuntime = mode === "browser" && browserCanReattach
531
537
  ? userError?.details?.runtime
532
538
  : undefined;
533
- if (!cloudflareChallenge) {
539
+ if (!cloudflareChallenge && browserCanReattach) {
534
540
  logBrowserReattachGuidance(browserRuntime ?? sessionMeta.browser?.runtime);
535
541
  }
536
542
  await sessionStore.updateSession(sessionMeta.id, {
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { z } from "zod";
4
4
  import { getOracleHomeDir } from "../../oracleHome.js";
5
+ import { browserThinkingTimeInputSchema, browserThinkingTimeRawSchema, } from "../types.js";
5
6
  import { consultOutputShape, runConsultTool } from "./consult.js";
6
7
  const chatGptImageInputShape = {
7
8
  prompt: z.string().min(1, "Prompt is required.").describe("Image generation prompt."),
@@ -29,8 +30,7 @@ const chatGptImageInputShape = {
29
30
  .enum(["auto", "never", "always"])
30
31
  .optional()
31
32
  .describe('How to deliver files. Defaults to "always" when files are present so reference images are uploaded.'),
32
- browserThinkingTime: z
33
- .enum(["light", "standard", "extended", "heavy"])
33
+ browserThinkingTime: browserThinkingTimeRawSchema
34
34
  .optional()
35
35
  .describe("Set ChatGPT thinking time when supported by the chosen model."),
36
36
  browserModelStrategy: z
@@ -58,7 +58,12 @@ const chatGptImageOutputShape = {
58
58
  ...consultOutputShape,
59
59
  requestedOutputPath: z.string(),
60
60
  };
61
- const chatGptImageInputSchema = z.object(chatGptImageInputShape).strict();
61
+ const chatGptImageInputSchema = z
62
+ .object({
63
+ ...chatGptImageInputShape,
64
+ browserThinkingTime: browserThinkingTimeInputSchema.optional(),
65
+ })
66
+ .strict();
62
67
  function resolveDefaultImageOutputPath() {
63
68
  // Include a random token so concurrent agent calls in the same millisecond do
64
69
  // not resolve to the same default path and overwrite each other.
@@ -20,11 +20,12 @@ async function readSessionLogTail(sessionId, maxBytes) {
20
20
  import { performSessionRun } from "../../cli/sessionRunner.js";
21
21
  import { runDryRunSummary } from "../../cli/dryRun.js";
22
22
  import { CHATGPT_URL } from "../../browser/constants.js";
23
- import { CONSULT_PRESETS, consultInputSchema } from "../types.js";
23
+ import { CONSULT_PRESETS, browserThinkingTimeRawSchema, consultInputSchema } from "../types.js";
24
24
  import { applyConsultPreset } from "../consultPresets.js";
25
25
  import { loadUserConfig } from "../../config.js";
26
26
  import { resolveNotificationSettings } from "../../cli/notifier.js";
27
27
  import { mapModelToBrowserLabel, resolveBrowserModelLabel } from "../../cli/browserConfig.js";
28
+ import { normalizeThinkingTimeLevel } from "../../oracle/thinkingTime.js";
28
29
  // Use raw shapes so the MCP SDK (with its bundled Zod) wraps them and emits valid JSON Schema.
29
30
  const consultInputShape = {
30
31
  preset: z
@@ -61,11 +62,10 @@ const consultInputShape = {
61
62
  .optional()
62
63
  .describe("Browser-only: bundle many files into a single upload (helps with upload limits)."),
63
64
  browserBundleFormat: z
64
- .enum(["text", "zip"])
65
+ .enum(["auto", "text", "zip"])
65
66
  .optional()
66
- .describe('Browser-only: bundle upload format when browserBundleFiles is true or auto-bundling is needed. Defaults to "text"; "zip" preserves individual file names in one uploaded archive.'),
67
- browserThinkingTime: z
68
- .enum(["light", "standard", "extended", "heavy"])
67
+ .describe('Browser-only: bundle upload format when browserBundleFiles is true or auto-bundling is needed. Defaults to "auto"; "auto" uses ZIP when bundled inputs include raw/binary files.'),
68
+ browserThinkingTime: browserThinkingTimeRawSchema
69
69
  .optional()
70
70
  .describe("Browser-only: set ChatGPT thinking time when supported by the chosen model."),
71
71
  browserModelStrategy: z
@@ -166,7 +166,7 @@ const consultDryRunResolvedShape = z.object({
166
166
  researchMode: z.string().nullable().optional(),
167
167
  attachments: z.string().optional(),
168
168
  bundleFiles: z.boolean().optional(),
169
- bundleFormat: z.enum(["text", "zip"]).optional(),
169
+ bundleFormat: z.enum(["auto", "text", "zip"]).optional(),
170
170
  keepBrowser: z.boolean().optional(),
171
171
  manualLogin: z.boolean().optional(),
172
172
  profileDir: z.string().nullable().optional(),
@@ -266,6 +266,7 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
266
266
  const manualLogin = hasProfileDir
267
267
  ? true
268
268
  : (configuredBrowser.manualLogin ?? process.platform === "win32");
269
+ const configuredThinkingTime = normalizeThinkingTimeLevel(configuredBrowser.thinkingTime);
269
270
  return {
270
271
  ...configuredBrowser,
271
272
  url: configuredUrl,
@@ -278,7 +279,7 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
278
279
  manualLoginProfileDir: manualLogin
279
280
  ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null)
280
281
  : null,
281
- thinkingTime: browserThinkingTime ?? configuredBrowser.thinkingTime,
282
+ thinkingTime: browserThinkingTime ?? configuredThinkingTime ?? undefined,
282
283
  modelStrategy: browserModelStrategy ?? configuredBrowser.modelStrategy,
283
284
  researchMode: browserResearchMode ?? configuredBrowser.researchMode,
284
285
  archiveConversations: browserArchive ?? configuredBrowser.archiveConversations,
@@ -358,7 +359,7 @@ export function formatConsultDryRunResolved(details) {
358
359
  lines.push(` browser research mode: ${details.browser.researchMode ?? "off"}`);
359
360
  lines.push(` browser attachments: ${details.browser.attachments ?? "auto"}`);
360
361
  lines.push(` browser bundle files: ${details.browser.bundleFiles ? "yes" : "no"}`);
361
- lines.push(` browser bundle format: ${details.browser.bundleFormat ?? "text"}`);
362
+ lines.push(` browser bundle format: ${details.browser.bundleFormat ?? "auto"}`);
362
363
  lines.push(` browser keep browser: ${details.browser.keepBrowser ? "yes" : "no"}`);
363
364
  lines.push(` browser manual login: ${details.browser.manualLogin ? "yes" : "no"}`);
364
365
  if (details.browser.profileDir) {
@@ -1,5 +1,14 @@
1
1
  import { z } from "zod";
2
+ import { THINKING_TIME_INPUT_VALUES, normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
2
3
  export const CONSULT_PRESETS = ["chatgpt-pro-heavy"];
4
+ export const browserThinkingTimeRawSchema = z.enum(THINKING_TIME_INPUT_VALUES);
5
+ export const browserThinkingTimeInputSchema = browserThinkingTimeRawSchema.transform((value) => {
6
+ const normalized = normalizeThinkingTimeLevel(value);
7
+ if (!normalized) {
8
+ throw new Error(`Unknown browserThinkingTime value: ${value}`);
9
+ }
10
+ return normalized;
11
+ });
3
12
  export const consultInputSchema = z
4
13
  .object({
5
14
  preset: z.enum(CONSULT_PRESETS).optional(),
@@ -11,8 +20,8 @@ export const consultInputSchema = z
11
20
  browserModelLabel: z.string().optional(),
12
21
  browserAttachments: z.enum(["auto", "never", "always"]).optional(),
13
22
  browserBundleFiles: z.boolean().optional(),
14
- browserBundleFormat: z.enum(["text", "zip"]).optional(),
15
- browserThinkingTime: z.enum(["light", "standard", "extended", "heavy"]).optional(),
23
+ browserBundleFormat: z.enum(["auto", "text", "zip"]).optional(),
24
+ browserThinkingTime: browserThinkingTimeInputSchema.optional(),
16
25
  browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
17
26
  browserResearchMode: z.enum(["deep"]).optional(),
18
27
  browserArchive: z.enum(["auto", "always", "never"]).optional(),
@@ -0,0 +1,40 @@
1
+ export const THINKING_TIME_LEVELS = ["light", "standard", "extended", "heavy"];
2
+ export const THINKING_TIME_ALIASES = [
3
+ "instant",
4
+ "low",
5
+ "medium",
6
+ "high",
7
+ "extra-high",
8
+ "extra high",
9
+ "extrahigh",
10
+ "xhigh",
11
+ ];
12
+ export const THINKING_TIME_INPUT_VALUES = [
13
+ ...THINKING_TIME_LEVELS,
14
+ ...THINKING_TIME_ALIASES,
15
+ ];
16
+ export function normalizeThinkingTimeLevel(value) {
17
+ const normalized = (value ?? "")
18
+ .trim()
19
+ .toLowerCase()
20
+ .replace(/[_\s]+/g, "-");
21
+ switch (normalized) {
22
+ case "light":
23
+ case "instant":
24
+ case "low":
25
+ return "light";
26
+ case "standard":
27
+ case "medium":
28
+ return "standard";
29
+ case "extended":
30
+ case "high":
31
+ return "extended";
32
+ case "heavy":
33
+ case "extra-high":
34
+ case "extrahigh":
35
+ case "xhigh":
36
+ return "heavy";
37
+ default:
38
+ return null;
39
+ }
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "CLI wrapper around OpenAI Responses API with 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",
@@ -61,7 +61,7 @@
61
61
  "@google/genai": "^2.8.0",
62
62
  "@google/generative-ai": "^0.24.1",
63
63
  "@modelcontextprotocol/sdk": "^1.29.0",
64
- "@steipete/sweet-cookie": "^0.3.0",
64
+ "@steipete/sweet-cookie": "^0.4.0",
65
65
  "chalk": "^5.6.2",
66
66
  "chrome-launcher": "^1.2.1",
67
67
  "chrome-remote-interface": "^0.34.0",
@@ -73,7 +73,7 @@
73
73
  "inquirer": "14.0.2",
74
74
  "json5": "^2.2.3",
75
75
  "kleur": "^4.1.5",
76
- "markdansi": "0.2.1",
76
+ "markdansi": "0.3.1",
77
77
  "openai": "^6.42.0",
78
78
  "osc-progress": "^0.3.0",
79
79
  "qs": "^6.15.2",
@@ -87,8 +87,8 @@
87
87
  "@types/chrome-remote-interface": "^0.34.0",
88
88
  "@types/inquirer": "^9.0.9",
89
89
  "@types/node": "^25.9.2",
90
- "@typescript/native-preview": "7.0.0-dev.20260609.1",
91
- "@vitest/coverage-v8": "4.1.8",
90
+ "@typescript/native-preview": "7.0.0-dev.20260612.1",
91
+ "@vitest/coverage-v8": "4.1.9",
92
92
  "devtools-protocol": "0.0.1643702",
93
93
  "es-toolkit": "^1.47.0",
94
94
  "esbuild": "^0.28.0",
@@ -97,7 +97,7 @@
97
97
  "puppeteer-core": "^25.1.0",
98
98
  "tsx": "^4.22.4",
99
99
  "typescript": "^6.0.3",
100
- "vitest": "^4.1.8"
100
+ "vitest": "^4.1.9"
101
101
  },
102
102
  "devEngines": {
103
103
  "runtime": [