@steipete/oracle 0.13.0 → 0.14.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.
Files changed (71) hide show
  1. package/README.md +22 -11
  2. package/dist/bin/oracle-cli.js +71 -71
  3. package/dist/src/browser/actions/assistantResponse.js +93 -10
  4. package/dist/src/browser/actions/deepResearch.js +180 -52
  5. package/dist/src/browser/actions/modelSelection.js +357 -40
  6. package/dist/src/browser/actions/navigation.js +294 -65
  7. package/dist/src/browser/actions/promptComposer.js +50 -4
  8. package/dist/src/browser/actions/thinkingTime.js +517 -105
  9. package/dist/src/browser/artifacts.js +19 -0
  10. package/dist/src/browser/chatgptFiles.js +797 -0
  11. package/dist/src/browser/chatgptImages.js +256 -27
  12. package/dist/src/browser/chromeLifecycle.js +4 -0
  13. package/dist/src/browser/config.js +2 -0
  14. package/dist/src/browser/index.js +404 -41
  15. package/dist/src/browser/pageActions.js +1 -1
  16. package/dist/src/browser/policies.js +2 -6
  17. package/dist/src/browser/projectSourcesRunner.js +16 -3
  18. package/dist/src/browser/prompt.js +161 -36
  19. package/dist/src/browser/reattach.js +6 -2
  20. package/dist/src/browser/reattachability.js +22 -10
  21. package/dist/src/browser/recoverConversation.js +73 -0
  22. package/dist/src/browser/sessionRunner.js +4 -1
  23. package/dist/src/cli/browserConfig.js +6 -2
  24. package/dist/src/cli/browserDefaults.js +2 -1
  25. package/dist/src/cli/browserTabs.js +129 -54
  26. package/dist/src/cli/followup.js +72 -0
  27. package/dist/src/cli/help.js +1 -1
  28. package/dist/src/cli/options.js +24 -0
  29. package/dist/src/cli/reattachGuidance.js +8 -0
  30. package/dist/src/cli/runOptions.js +2 -12
  31. package/dist/src/cli/sessionCommand.js +4 -0
  32. package/dist/src/cli/sessionDisplay.js +17 -3
  33. package/dist/src/cli/sessionLineage.js +7 -4
  34. package/dist/src/cli/sessionRunner.js +16 -1
  35. package/dist/src/gemini-web/client.js +5 -10
  36. package/dist/src/gemini-web/executor.js +1 -24
  37. package/dist/src/gemini-web/models.js +83 -0
  38. package/dist/src/mcp/server.js +2 -0
  39. package/dist/src/mcp/tools/chatgptImage.js +132 -0
  40. package/dist/src/mcp/tools/consult.js +270 -175
  41. package/dist/src/mcp/types.js +13 -2
  42. package/dist/src/mcp/utils.js +87 -1
  43. package/dist/src/oracle/config.js +27 -1
  44. package/dist/src/oracle/files.js +4 -6
  45. package/dist/src/oracle/geminiModels.js +2 -0
  46. package/dist/src/oracle/markdown.js +36 -3
  47. package/dist/src/oracle/modelResolver.js +21 -13
  48. package/dist/src/oracle/promptAssembly.js +2 -4
  49. package/dist/src/oracle/request.js +3 -5
  50. package/dist/src/oracle/thinkingTime.js +40 -0
  51. package/dist/src/oracle/tokenStats.js +5 -1
  52. package/dist/src/oracle.js +1 -1
  53. package/dist/src/sessionManager.js +1 -0
  54. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  55. package/package.json +47 -58
  56. package/vendor/oracle-notifier/build-notifier.sh +0 -0
  57. package/dist/bin/oracle.js +0 -569
  58. package/dist/src/browser/chromeCookies.js +0 -312
  59. package/dist/src/browser/keytarShim.js +0 -56
  60. package/dist/src/browser/windowsCookies.js +0 -219
  61. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  62. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  63. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  64. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  65. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  67. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  68. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  71. package/vendor/oracle-notifier/README.md +0 -26
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { getCliVersion } from "../../version.js";
3
- import { LoggingMessageNotificationParamsSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { LoggingMessageNotificationParamsSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { ensureBrowserAvailable, mapConsultToRunOptions } from "../utils.js";
5
5
  import { sessionStore } from "../../sessionStore.js";
6
6
  import { resolveRemoteServiceConfig } from "../../remote/remoteServiceConfig.js";
@@ -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
@@ -88,6 +88,14 @@ const consultInputShape = {
88
88
  .boolean()
89
89
  .optional()
90
90
  .describe("Browser-only: keep Chrome running after completion (useful for debugging)."),
91
+ generateImage: z
92
+ .string()
93
+ .optional()
94
+ .describe("Browser-only: save generated image(s) to this file path. For ChatGPT browser mode this enables the image-aware wait/download path used by CLI --generate-image."),
95
+ outputPath: z
96
+ .string()
97
+ .optional()
98
+ .describe("Browser-only image output fallback path, mirroring the CLI --output option for image operations."),
91
99
  dryRun: z
92
100
  .boolean()
93
101
  .optional()
@@ -130,6 +138,20 @@ const consultModelSummaryShape = z.object({
130
138
  .optional(),
131
139
  logPath: z.string().optional(),
132
140
  });
141
+ const consultArtifactSummaryShape = z.object({
142
+ kind: z.enum(["transcript", "deep-research-report", "image", "file"]),
143
+ path: z.string(),
144
+ label: z.string().optional(),
145
+ mimeType: z.string().optional(),
146
+ sizeBytes: z.number().optional(),
147
+ });
148
+ const consultImageSummaryShape = consultArtifactSummaryShape.extend({
149
+ kind: z.literal("image"),
150
+ alt: z.string().optional(),
151
+ width: z.number().optional(),
152
+ height: z.number().optional(),
153
+ fileId: z.string().optional(),
154
+ });
133
155
  const consultDryRunResolvedShape = z.object({
134
156
  resolvedEngine: z.enum(["api", "browser"]),
135
157
  model: z.string(),
@@ -144,22 +166,25 @@ const consultDryRunResolvedShape = z.object({
144
166
  researchMode: z.string().nullable().optional(),
145
167
  attachments: z.string().optional(),
146
168
  bundleFiles: z.boolean().optional(),
147
- bundleFormat: z.enum(["text", "zip"]).optional(),
169
+ bundleFormat: z.enum(["auto", "text", "zip"]).optional(),
148
170
  keepBrowser: z.boolean().optional(),
149
171
  manualLogin: z.boolean().optional(),
150
172
  profileDir: z.string().nullable().optional(),
151
173
  chatgptUrl: z.string().nullable().optional(),
174
+ imageOutputPath: z.string().nullable().optional(),
152
175
  })
153
176
  .optional(),
154
177
  guidance: z.array(z.string()),
155
178
  });
156
- const consultOutputShape = {
179
+ export const consultOutputShape = {
157
180
  sessionId: z.string().optional(),
158
181
  status: z.string(),
159
182
  output: z.string(),
160
183
  dryRun: z.boolean().optional(),
161
184
  resolved: consultDryRunResolvedShape.optional(),
162
185
  models: z.array(consultModelSummaryShape).optional(),
186
+ artifacts: z.array(consultArtifactSummaryShape).optional(),
187
+ images: z.array(consultImageSummaryShape).optional(),
163
188
  };
164
189
  export function summarizeModelRunsForConsult(runs) {
165
190
  if (!runs || runs.length === 0) {
@@ -191,6 +216,43 @@ export function summarizeModelRunsForConsult(runs) {
191
216
  };
192
217
  });
193
218
  }
219
+ function optionalString(value) {
220
+ return typeof value === "string" && value.length > 0 ? value : undefined;
221
+ }
222
+ function optionalNumber(value) {
223
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
224
+ }
225
+ export function summarizeArtifactsForConsult(artifacts) {
226
+ if (!artifacts || artifacts.length === 0) {
227
+ return undefined;
228
+ }
229
+ return artifacts.map((artifact) => ({
230
+ kind: artifact.kind,
231
+ path: artifact.path,
232
+ label: artifact.label,
233
+ mimeType: artifact.mimeType,
234
+ sizeBytes: artifact.sizeBytes,
235
+ }));
236
+ }
237
+ export function summarizeImageArtifactsForConsult(artifacts) {
238
+ const images = (artifacts ?? [])
239
+ .filter((artifact) => artifact.kind === "image")
240
+ .map((artifact) => {
241
+ const image = artifact;
242
+ return {
243
+ kind: "image",
244
+ path: artifact.path,
245
+ label: artifact.label,
246
+ mimeType: artifact.mimeType,
247
+ sizeBytes: artifact.sizeBytes,
248
+ alt: optionalString(image.alt),
249
+ width: optionalNumber(image.width),
250
+ height: optionalNumber(image.height),
251
+ fileId: optionalString(image.fileId),
252
+ };
253
+ });
254
+ return images.length > 0 ? images : undefined;
255
+ }
194
256
  export function buildConsultBrowserConfig({ userConfig, env, runModel, inputModel, browserModelLabel, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserKeepBrowser, }) {
195
257
  const configuredBrowser = userConfig.browser ?? {};
196
258
  const envProfileDir = (env.ORACLE_BROWSER_PROFILE_DIR ?? "").trim();
@@ -204,6 +266,7 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
204
266
  const manualLogin = hasProfileDir
205
267
  ? true
206
268
  : (configuredBrowser.manualLogin ?? process.platform === "win32");
269
+ const configuredThinkingTime = normalizeThinkingTimeLevel(configuredBrowser.thinkingTime);
207
270
  return {
208
271
  ...configuredBrowser,
209
272
  url: configuredUrl,
@@ -216,7 +279,7 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
216
279
  manualLoginProfileDir: manualLogin
217
280
  ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null)
218
281
  : null,
219
- thinkingTime: browserThinkingTime ?? configuredBrowser.thinkingTime,
282
+ thinkingTime: browserThinkingTime ?? configuredThinkingTime ?? undefined,
220
283
  modelStrategy: browserModelStrategy ?? configuredBrowser.modelStrategy,
221
284
  researchMode: browserResearchMode ?? configuredBrowser.researchMode,
222
285
  archiveConversations: browserArchive ?? configuredBrowser.archiveConversations,
@@ -250,6 +313,10 @@ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browser
250
313
  if (followUpCount > 0) {
251
314
  guidance.push("This is a multi-turn browser consult; all follow-ups stay in one ChatGPT conversation.");
252
315
  }
316
+ const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath ?? null;
317
+ if (resolvedEngine === "browser" && imageOutputPath) {
318
+ guidance.push("ChatGPT generated images will use the image-aware wait/download path and return saved files in structuredContent.images.");
319
+ }
253
320
  return {
254
321
  resolvedEngine,
255
322
  model: runOptions.model,
@@ -269,6 +336,7 @@ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browser
269
336
  manualLogin: browserConfig?.manualLogin,
270
337
  profileDir: browserConfig?.manualLoginProfileDir ?? null,
271
338
  chatgptUrl,
339
+ imageOutputPath,
272
340
  }
273
341
  : undefined,
274
342
  guidance,
@@ -291,7 +359,7 @@ export function formatConsultDryRunResolved(details) {
291
359
  lines.push(` browser research mode: ${details.browser.researchMode ?? "off"}`);
292
360
  lines.push(` browser attachments: ${details.browser.attachments ?? "auto"}`);
293
361
  lines.push(` browser bundle files: ${details.browser.bundleFiles ? "yes" : "no"}`);
294
- lines.push(` browser bundle format: ${details.browser.bundleFormat ?? "text"}`);
362
+ lines.push(` browser bundle format: ${details.browser.bundleFormat ?? "auto"}`);
295
363
  lines.push(` browser keep browser: ${details.browser.keepBrowser ? "yes" : "no"}`);
296
364
  lines.push(` browser manual login: ${details.browser.manualLogin ? "yes" : "no"}`);
297
365
  if (details.browser.profileDir) {
@@ -300,6 +368,9 @@ export function formatConsultDryRunResolved(details) {
300
368
  if (details.browser.chatgptUrl) {
301
369
  lines.push(` ChatGPT URL: ${details.browser.chatgptUrl}`);
302
370
  }
371
+ if (details.browser.imageOutputPath) {
372
+ lines.push(` image output: ${details.browser.imageOutputPath}`);
373
+ }
303
374
  }
304
375
  lines.push(` follow-ups: ${details.followUpCount}`);
305
376
  for (const guidance of details.guidance) {
@@ -307,28 +378,24 @@ export function formatConsultDryRunResolved(details) {
307
378
  }
308
379
  return lines;
309
380
  }
310
- export function registerConsultTool(server) {
311
- server.registerTool("consult", {
312
- title: "Run an oracle session",
313
- description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. Browser manual-login uses a private Oracle Chrome profile separate from the user\'s normal Chrome; dry-run output includes first-time setup guidance when that path is active. For browser-based image/file uploads, set `browserAttachments:"always"`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
314
- // Cast to any to satisfy SDK typings across differing Zod versions.
315
- inputSchema: consultInputShape,
316
- outputSchema: consultOutputShape,
317
- }, async (input) => {
318
- const textContent = (text) => [{ type: "text", text }];
319
- let parsedInput;
320
- try {
321
- parsedInput = applyConsultPreset(consultInputSchema.parse(input));
322
- }
323
- catch (error) {
324
- return {
325
- isError: true,
326
- content: textContent(error instanceof Error ? error.message : String(error)),
327
- };
328
- }
329
- const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, dryRun, slug, } = parsedInput;
330
- const { config: userConfig } = await loadUserConfig();
331
- const { runOptions, resolvedEngine } = mapConsultToRunOptions({
381
+ export async function runConsultTool(input, { server }) {
382
+ const textContent = (text) => [{ type: "text", text }];
383
+ let parsedInput;
384
+ try {
385
+ parsedInput = applyConsultPreset(consultInputSchema.parse(input));
386
+ }
387
+ catch (error) {
388
+ return {
389
+ isError: true,
390
+ content: textContent(error instanceof Error ? error.message : String(error)),
391
+ };
392
+ }
393
+ const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, generateImage, outputPath, dryRun, slug, } = parsedInput;
394
+ const { config: userConfig } = await loadUserConfig();
395
+ let runOptions;
396
+ let resolvedEngine;
397
+ try {
398
+ ({ runOptions, resolvedEngine } = mapConsultToRunOptions({
332
399
  prompt,
333
400
  files: files ?? [],
334
401
  model,
@@ -339,160 +406,188 @@ export function registerConsultTool(server) {
339
406
  browserBundleFiles,
340
407
  browserBundleFormat,
341
408
  browserFollowUps,
409
+ generateImage,
410
+ outputPath,
342
411
  userConfig,
343
412
  env: process.env,
413
+ }));
414
+ }
415
+ catch (error) {
416
+ return {
417
+ isError: true,
418
+ content: textContent(error instanceof Error ? error.message : String(error)),
419
+ };
420
+ }
421
+ const cwd = process.cwd();
422
+ const sendLog = (text, level = "info") => server
423
+ .sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
424
+ level,
425
+ data: { text, bytes: Buffer.byteLength(text, "utf8") },
426
+ }))
427
+ .catch(() => { });
428
+ const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
429
+ const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath;
430
+ if (resolvedEngine === "browser" && resolvedRemote.host && imageOutputPath) {
431
+ return {
432
+ isError: true,
433
+ content: textContent("ChatGPT image output is not supported with a remote browser service: generated files are not transferred back to the MCP caller. Unset ORACLE_REMOTE_HOST to generate images locally, or omit generateImage/outputPath."),
434
+ };
435
+ }
436
+ let browserConfig;
437
+ if (resolvedEngine === "browser") {
438
+ browserConfig = buildConsultBrowserConfig({
439
+ userConfig,
440
+ env: process.env,
441
+ runModel: runOptions.model,
442
+ inputModel: model,
443
+ browserModelLabel,
444
+ browserThinkingTime,
445
+ browserModelStrategy,
446
+ browserResearchMode,
447
+ browserArchive,
448
+ browserKeepBrowser,
344
449
  });
345
- const cwd = process.cwd();
346
- const sendLog = (text, level = "info") => server.server
347
- .sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
348
- level,
349
- data: { text, bytes: Buffer.byteLength(text, "utf8") },
350
- }))
351
- .catch(() => { });
352
- const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
353
- let browserConfig;
354
- if (resolvedEngine === "browser") {
355
- browserConfig = buildConsultBrowserConfig({
356
- userConfig,
357
- env: process.env,
358
- runModel: runOptions.model,
359
- inputModel: model,
360
- browserModelLabel,
361
- browserThinkingTime,
362
- browserModelStrategy,
363
- browserResearchMode,
364
- browserArchive,
365
- browserKeepBrowser,
366
- });
367
- }
368
- if (dryRun) {
369
- const lines = [];
370
- const log = (line) => {
371
- lines.push(line);
372
- sendLog(line);
373
- };
374
- const resolved = buildConsultDryRunResolved({
375
- resolvedEngine,
376
- runOptions,
377
- browserConfig,
378
- });
379
- await runDryRunSummary({
380
- engine: resolvedEngine,
381
- runOptions,
382
- cwd,
383
- version: getCliVersion(),
384
- log,
385
- browserConfig,
386
- });
387
- for (const line of formatConsultDryRunResolved(resolved)) {
388
- log(line);
389
- }
390
- const output = lines.join("\n").trim();
391
- return {
392
- content: textContent(output),
393
- structuredContent: {
394
- status: "dry-run",
395
- output,
396
- dryRun: true,
397
- resolved,
398
- },
399
- };
400
- }
401
- const browserGuard = ensureBrowserAvailable(resolvedEngine, {
402
- remoteHost: resolvedRemote.host,
450
+ }
451
+ if (dryRun) {
452
+ const lines = [];
453
+ const log = (line) => {
454
+ lines.push(line);
455
+ sendLog(line);
456
+ };
457
+ const resolved = buildConsultDryRunResolved({
458
+ resolvedEngine,
459
+ runOptions,
460
+ browserConfig,
403
461
  });
404
- if (resolvedEngine === "browser" && browserGuard) {
462
+ await runDryRunSummary({
463
+ engine: resolvedEngine,
464
+ runOptions,
465
+ cwd,
466
+ version: getCliVersion(),
467
+ log,
468
+ browserConfig,
469
+ });
470
+ for (const line of formatConsultDryRunResolved(resolved)) {
471
+ log(line);
472
+ }
473
+ const output = lines.join("\n").trim();
474
+ return {
475
+ content: textContent(output),
476
+ structuredContent: {
477
+ status: "dry-run",
478
+ output,
479
+ dryRun: true,
480
+ resolved,
481
+ },
482
+ };
483
+ }
484
+ const browserGuard = ensureBrowserAvailable(resolvedEngine, {
485
+ remoteHost: resolvedRemote.host,
486
+ });
487
+ if (resolvedEngine === "browser" && browserGuard) {
488
+ return {
489
+ isError: true,
490
+ content: textContent(browserGuard),
491
+ };
492
+ }
493
+ let browserDeps;
494
+ if (resolvedEngine === "browser" && resolvedRemote.host) {
495
+ if (!resolvedRemote.token) {
405
496
  return {
406
497
  isError: true,
407
- content: textContent(browserGuard),
498
+ content: textContent(`Remote host configured (${resolvedRemote.host}) but remote token is missing. Run \`oracle bridge client --connect <...>\` or set ORACLE_REMOTE_TOKEN.`),
408
499
  };
409
500
  }
410
- let browserDeps;
411
- if (resolvedEngine === "browser" && resolvedRemote.host) {
412
- if (!resolvedRemote.token) {
413
- return {
414
- isError: true,
415
- content: textContent(`Remote host configured (${resolvedRemote.host}) but remote token is missing. Run \`oracle bridge client --connect <...>\` or set ORACLE_REMOTE_TOKEN.`),
416
- };
417
- }
418
- browserDeps = {
419
- executeBrowser: createRemoteBrowserExecutor({
420
- host: resolvedRemote.host,
421
- token: resolvedRemote.token,
422
- }),
423
- };
501
+ browserDeps = {
502
+ executeBrowser: createRemoteBrowserExecutor({
503
+ host: resolvedRemote.host,
504
+ token: resolvedRemote.token,
505
+ }),
506
+ };
507
+ }
508
+ const notifications = resolveNotificationSettings({
509
+ cliNotify: undefined,
510
+ cliNotifySound: undefined,
511
+ env: process.env,
512
+ config: userConfig.notify,
513
+ });
514
+ const sessionMeta = await sessionStore.createSession({
515
+ ...runOptions,
516
+ mode: resolvedEngine,
517
+ slug,
518
+ browserConfig,
519
+ waitPreference: true,
520
+ }, cwd, notifications);
521
+ const logWriter = sessionStore.createLogWriter(sessionMeta.id);
522
+ // Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
523
+ const log = (line) => {
524
+ logWriter.logLine(line);
525
+ if (line !== undefined) {
526
+ sendLog(line);
424
527
  }
425
- const notifications = resolveNotificationSettings({
426
- cliNotify: undefined,
427
- cliNotifySound: undefined,
428
- env: process.env,
429
- config: userConfig.notify,
430
- });
431
- const sessionMeta = await sessionStore.createSession({
432
- ...runOptions,
528
+ };
529
+ const write = (chunk) => {
530
+ logWriter.writeChunk(chunk);
531
+ sendLog(chunk, "debug");
532
+ return true;
533
+ };
534
+ try {
535
+ await performSessionRun({
536
+ sessionMeta,
537
+ runOptions,
433
538
  mode: resolvedEngine,
434
- slug,
435
539
  browserConfig,
436
- waitPreference: true,
437
- }, cwd, notifications);
438
- const logWriter = sessionStore.createLogWriter(sessionMeta.id);
439
- // Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
440
- const log = (line) => {
441
- logWriter.logLine(line);
442
- if (line !== undefined) {
443
- sendLog(line);
444
- }
540
+ cwd,
541
+ log,
542
+ write,
543
+ version: getCliVersion(),
544
+ notifications,
545
+ muteStdout: true,
546
+ browserDeps,
547
+ });
548
+ }
549
+ catch (error) {
550
+ log(`Run failed: ${error instanceof Error ? error.message : String(error)}`);
551
+ return {
552
+ isError: true,
553
+ content: textContent(`Session ${sessionMeta.id} failed: ${error instanceof Error ? error.message : String(error)}`),
554
+ };
555
+ }
556
+ finally {
557
+ logWriter.stream.end();
558
+ }
559
+ try {
560
+ const finalMeta = (await sessionStore.readSession(sessionMeta.id)) ?? sessionMeta;
561
+ const summary = `Session ${sessionMeta.id} (${finalMeta.status})`;
562
+ const logTail = await readSessionLogTail(sessionMeta.id, 4000);
563
+ const modelsSummary = summarizeModelRunsForConsult(finalMeta.models);
564
+ const artifacts = summarizeArtifactsForConsult(finalMeta.artifacts);
565
+ const images = summarizeImageArtifactsForConsult(finalMeta.artifacts);
566
+ return {
567
+ content: textContent([summary, logTail || "(log empty)"].join("\n").trim()),
568
+ structuredContent: {
569
+ sessionId: sessionMeta.id,
570
+ status: finalMeta.status,
571
+ output: logTail ?? "",
572
+ models: modelsSummary,
573
+ artifacts,
574
+ images,
575
+ },
445
576
  };
446
- const write = (chunk) => {
447
- logWriter.writeChunk(chunk);
448
- sendLog(chunk, "debug");
449
- return true;
577
+ }
578
+ catch (error) {
579
+ return {
580
+ isError: true,
581
+ content: textContent(`Session completed but metadata fetch failed: ${error instanceof Error ? error.message : String(error)}`),
450
582
  };
451
- try {
452
- await performSessionRun({
453
- sessionMeta,
454
- runOptions,
455
- mode: resolvedEngine,
456
- browserConfig,
457
- cwd,
458
- log,
459
- write,
460
- version: getCliVersion(),
461
- notifications,
462
- muteStdout: true,
463
- browserDeps,
464
- });
465
- }
466
- catch (error) {
467
- log(`Run failed: ${error instanceof Error ? error.message : String(error)}`);
468
- return {
469
- isError: true,
470
- content: textContent(`Session ${sessionMeta.id} failed: ${error instanceof Error ? error.message : String(error)}`),
471
- };
472
- }
473
- finally {
474
- logWriter.stream.end();
475
- }
476
- try {
477
- const finalMeta = (await sessionStore.readSession(sessionMeta.id)) ?? sessionMeta;
478
- const summary = `Session ${sessionMeta.id} (${finalMeta.status})`;
479
- const logTail = await readSessionLogTail(sessionMeta.id, 4000);
480
- const modelsSummary = summarizeModelRunsForConsult(finalMeta.models);
481
- return {
482
- content: textContent([summary, logTail || "(log empty)"].join("\n").trim()),
483
- structuredContent: {
484
- sessionId: sessionMeta.id,
485
- status: finalMeta.status,
486
- output: logTail ?? "",
487
- models: modelsSummary,
488
- },
489
- };
490
- }
491
- catch (error) {
492
- return {
493
- isError: true,
494
- content: textContent(`Session completed but metadata fetch failed: ${error instanceof Error ? error.message : String(error)}`),
495
- };
496
- }
497
- });
583
+ }
584
+ }
585
+ export function registerConsultTool(server) {
586
+ server.registerTool("consult", {
587
+ title: "Run an oracle session",
588
+ description: 'Run an Oracle session (API or ChatGPT browser automation). Use `files` to attach project context. If `engine` is omitted, Oracle follows CLI defaults: config/ORACLE_ENGINE first, then API when OPENAI_API_KEY is set, otherwise browser. Browser GPT-5.5 Pro consults can take many minutes; use `dryRun:true` first when configuring an agent and inspect `sessions`/`oracle status` before retrying. Browser manual-login uses a private Oracle Chrome profile separate from the user\'s normal Chrome; dry-run output includes first-time setup guidance when that path is active. For browser-based image/file uploads, set `browserAttachments:"always"`. For ChatGPT image generation, set `generateImage` to enable the same image wait/download path as CLI --generate-image and read returned paths from `images`. Browser consults can include `browserFollowUps` for a multi-turn ChatGPT review in one conversation. Sessions are stored under `ORACLE_HOME_DIR` (shared with the CLI).',
589
+ // Cast to any to satisfy SDK typings across differing Zod versions.
590
+ inputSchema: consultInputShape,
591
+ outputSchema: consultOutputShape,
592
+ }, async (input) => runConsultTool(input, { server: server.server }));
498
593
  }
@@ -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,13 +20,15 @@ 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(),
19
28
  browserFollowUps: z.array(z.string()).optional(),
20
29
  browserKeepBrowser: z.boolean().optional(),
30
+ generateImage: z.string().optional(),
31
+ outputPath: z.string().optional(),
21
32
  dryRun: z.boolean().optional(),
22
33
  search: z.boolean().optional(),
23
34
  slug: z.string().optional(),