@steipete/oracle 0.12.1 → 0.14.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 (97) hide show
  1. package/README.md +72 -61
  2. package/dist/bin/oracle-cli.js +77 -68
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/docs-site/.nojekyll +0 -0
  5. package/dist/docs-site/CNAME +1 -0
  6. package/dist/docs-site/RELEASING.html +410 -0
  7. package/dist/docs-site/agents.html +374 -0
  8. package/dist/docs-site/anthropic.html +368 -0
  9. package/dist/docs-site/bridge.html +400 -0
  10. package/dist/docs-site/browser-mode.html +593 -0
  11. package/dist/docs-site/chromium-forks.html +347 -0
  12. package/dist/docs-site/cli-reference.html +346 -0
  13. package/dist/docs-site/configuration.html +452 -0
  14. package/dist/docs-site/favicon.svg +14 -0
  15. package/dist/docs-site/followup.html +375 -0
  16. package/dist/docs-site/gemini.html +383 -0
  17. package/dist/docs-site/grok.html +325 -0
  18. package/dist/docs-site/index.html +360 -0
  19. package/dist/docs-site/install.html +335 -0
  20. package/dist/docs-site/linux.html +321 -0
  21. package/dist/docs-site/llms.txt +43 -0
  22. package/dist/docs-site/manual-tests.html +596 -0
  23. package/dist/docs-site/mcp.html +391 -0
  24. package/dist/docs-site/multimodel.html +364 -0
  25. package/dist/docs-site/mythical-pro-agents.html +360 -0
  26. package/dist/docs-site/notifier.html +338 -0
  27. package/dist/docs-site/openai-endpoints.html +387 -0
  28. package/dist/docs-site/openrouter.html +344 -0
  29. package/dist/docs-site/quickstart.html +369 -0
  30. package/dist/docs-site/refactor/ux.html +532 -0
  31. package/dist/docs-site/sessions.html +388 -0
  32. package/dist/docs-site/social-card.png +0 -0
  33. package/dist/docs-site/social-card.svg +79 -0
  34. package/dist/docs-site/spec.html +363 -0
  35. package/dist/docs-site/testing.html +320 -0
  36. package/dist/docs-site/tui-debug.html +326 -0
  37. package/dist/docs-site/windows-work.html +323 -0
  38. package/dist/docs-site/windows.html +320 -0
  39. package/dist/src/browser/actions/assistantResponse.js +91 -9
  40. package/dist/src/browser/actions/deepResearch.js +180 -52
  41. package/dist/src/browser/actions/modelSelection.js +111 -24
  42. package/dist/src/browser/actions/navigation.js +358 -67
  43. package/dist/src/browser/actions/promptComposer.js +241 -46
  44. package/dist/src/browser/actions/thinkingTime.js +308 -89
  45. package/dist/src/browser/artifacts.js +19 -0
  46. package/dist/src/browser/chatgptFiles.js +797 -0
  47. package/dist/src/browser/chatgptImages.js +130 -3
  48. package/dist/src/browser/chromeLifecycle.js +4 -0
  49. package/dist/src/browser/config.js +4 -0
  50. package/dist/src/browser/index.js +353 -41
  51. package/dist/src/browser/pageActions.js +1 -1
  52. package/dist/src/browser/policies.js +2 -6
  53. package/dist/src/browser/projectSourcesRunner.js +14 -2
  54. package/dist/src/browser/prompt.js +11 -15
  55. package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
  56. package/dist/src/browser/reattach.js +6 -2
  57. package/dist/src/browser/reattachability.js +22 -10
  58. package/dist/src/browser/recoverConversation.js +73 -0
  59. package/dist/src/browser/sessionRunner.js +4 -1
  60. package/dist/src/cli/bridge/doctor.js +7 -2
  61. package/dist/src/cli/browserConfig.js +13 -2
  62. package/dist/src/cli/browserDefaults.js +3 -0
  63. package/dist/src/cli/browserTabs.js +129 -54
  64. package/dist/src/cli/engine.js +6 -2
  65. package/dist/src/cli/followup.js +72 -0
  66. package/dist/src/cli/help.js +1 -1
  67. package/dist/src/cli/options.js +20 -0
  68. package/dist/src/cli/reattachGuidance.js +8 -0
  69. package/dist/src/cli/runOptions.js +10 -31
  70. package/dist/src/cli/sessionCommand.js +4 -0
  71. package/dist/src/cli/sessionDisplay.js +17 -3
  72. package/dist/src/cli/sessionLineage.js +7 -4
  73. package/dist/src/cli/sessionRunner.js +16 -1
  74. package/dist/src/config.js +164 -9
  75. package/dist/src/gemini-web/client.js +5 -10
  76. package/dist/src/gemini-web/executor.js +1 -24
  77. package/dist/src/gemini-web/models.js +83 -0
  78. package/dist/src/mcp/server.js +2 -0
  79. package/dist/src/mcp/tools/chatgptImage.js +127 -0
  80. package/dist/src/mcp/tools/consult.js +261 -167
  81. package/dist/src/mcp/types.js +2 -0
  82. package/dist/src/mcp/utils.js +87 -1
  83. package/dist/src/oracle/config.js +27 -1
  84. package/dist/src/oracle/files.js +4 -6
  85. package/dist/src/oracle/geminiModels.js +2 -0
  86. package/dist/src/oracle/markdown.js +36 -3
  87. package/dist/src/oracle/modelResolver.js +21 -13
  88. package/dist/src/oracle/promptAssembly.js +2 -4
  89. package/dist/src/oracle/request.js +3 -5
  90. package/dist/src/oracle/tokenStats.js +5 -1
  91. package/dist/src/oracle.js +1 -1
  92. package/dist/src/sessionManager.js +1 -0
  93. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  94. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  95. package/package.json +20 -29
  96. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  97. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -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";
@@ -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(),
@@ -149,17 +171,20 @@ const consultDryRunResolvedShape = z.object({
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();
@@ -250,6 +312,10 @@ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browser
250
312
  if (followUpCount > 0) {
251
313
  guidance.push("This is a multi-turn browser consult; all follow-ups stay in one ChatGPT conversation.");
252
314
  }
315
+ const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath ?? null;
316
+ if (resolvedEngine === "browser" && imageOutputPath) {
317
+ guidance.push("ChatGPT generated images will use the image-aware wait/download path and return saved files in structuredContent.images.");
318
+ }
253
319
  return {
254
320
  resolvedEngine,
255
321
  model: runOptions.model,
@@ -269,6 +335,7 @@ export function buildConsultDryRunResolved({ resolvedEngine, runOptions, browser
269
335
  manualLogin: browserConfig?.manualLogin,
270
336
  profileDir: browserConfig?.manualLoginProfileDir ?? null,
271
337
  chatgptUrl,
338
+ imageOutputPath,
272
339
  }
273
340
  : undefined,
274
341
  guidance,
@@ -300,6 +367,9 @@ export function formatConsultDryRunResolved(details) {
300
367
  if (details.browser.chatgptUrl) {
301
368
  lines.push(` ChatGPT URL: ${details.browser.chatgptUrl}`);
302
369
  }
370
+ if (details.browser.imageOutputPath) {
371
+ lines.push(` image output: ${details.browser.imageOutputPath}`);
372
+ }
303
373
  }
304
374
  lines.push(` follow-ups: ${details.followUpCount}`);
305
375
  for (const guidance of details.guidance) {
@@ -307,28 +377,24 @@ export function formatConsultDryRunResolved(details) {
307
377
  }
308
378
  return lines;
309
379
  }
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({
380
+ export async function runConsultTool(input, { server }) {
381
+ const textContent = (text) => [{ type: "text", text }];
382
+ let parsedInput;
383
+ try {
384
+ parsedInput = applyConsultPreset(consultInputSchema.parse(input));
385
+ }
386
+ catch (error) {
387
+ return {
388
+ isError: true,
389
+ content: textContent(error instanceof Error ? error.message : String(error)),
390
+ };
391
+ }
392
+ const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, generateImage, outputPath, dryRun, slug, } = parsedInput;
393
+ const { config: userConfig } = await loadUserConfig();
394
+ let runOptions;
395
+ let resolvedEngine;
396
+ try {
397
+ ({ runOptions, resolvedEngine } = mapConsultToRunOptions({
332
398
  prompt,
333
399
  files: files ?? [],
334
400
  model,
@@ -339,160 +405,188 @@ export function registerConsultTool(server) {
339
405
  browserBundleFiles,
340
406
  browserBundleFormat,
341
407
  browserFollowUps,
408
+ generateImage,
409
+ outputPath,
342
410
  userConfig,
343
411
  env: process.env,
412
+ }));
413
+ }
414
+ catch (error) {
415
+ return {
416
+ isError: true,
417
+ content: textContent(error instanceof Error ? error.message : String(error)),
418
+ };
419
+ }
420
+ const cwd = process.cwd();
421
+ const sendLog = (text, level = "info") => server
422
+ .sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
423
+ level,
424
+ data: { text, bytes: Buffer.byteLength(text, "utf8") },
425
+ }))
426
+ .catch(() => { });
427
+ const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
428
+ const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath;
429
+ if (resolvedEngine === "browser" && resolvedRemote.host && imageOutputPath) {
430
+ return {
431
+ isError: true,
432
+ 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."),
433
+ };
434
+ }
435
+ let browserConfig;
436
+ if (resolvedEngine === "browser") {
437
+ browserConfig = buildConsultBrowserConfig({
438
+ userConfig,
439
+ env: process.env,
440
+ runModel: runOptions.model,
441
+ inputModel: model,
442
+ browserModelLabel,
443
+ browserThinkingTime,
444
+ browserModelStrategy,
445
+ browserResearchMode,
446
+ browserArchive,
447
+ browserKeepBrowser,
344
448
  });
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,
449
+ }
450
+ if (dryRun) {
451
+ const lines = [];
452
+ const log = (line) => {
453
+ lines.push(line);
454
+ sendLog(line);
455
+ };
456
+ const resolved = buildConsultDryRunResolved({
457
+ resolvedEngine,
458
+ runOptions,
459
+ browserConfig,
403
460
  });
404
- if (resolvedEngine === "browser" && browserGuard) {
461
+ await runDryRunSummary({
462
+ engine: resolvedEngine,
463
+ runOptions,
464
+ cwd,
465
+ version: getCliVersion(),
466
+ log,
467
+ browserConfig,
468
+ });
469
+ for (const line of formatConsultDryRunResolved(resolved)) {
470
+ log(line);
471
+ }
472
+ const output = lines.join("\n").trim();
473
+ return {
474
+ content: textContent(output),
475
+ structuredContent: {
476
+ status: "dry-run",
477
+ output,
478
+ dryRun: true,
479
+ resolved,
480
+ },
481
+ };
482
+ }
483
+ const browserGuard = ensureBrowserAvailable(resolvedEngine, {
484
+ remoteHost: resolvedRemote.host,
485
+ });
486
+ if (resolvedEngine === "browser" && browserGuard) {
487
+ return {
488
+ isError: true,
489
+ content: textContent(browserGuard),
490
+ };
491
+ }
492
+ let browserDeps;
493
+ if (resolvedEngine === "browser" && resolvedRemote.host) {
494
+ if (!resolvedRemote.token) {
405
495
  return {
406
496
  isError: true,
407
- content: textContent(browserGuard),
497
+ content: textContent(`Remote host configured (${resolvedRemote.host}) but remote token is missing. Run \`oracle bridge client --connect <...>\` or set ORACLE_REMOTE_TOKEN.`),
408
498
  };
409
499
  }
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
- };
500
+ browserDeps = {
501
+ executeBrowser: createRemoteBrowserExecutor({
502
+ host: resolvedRemote.host,
503
+ token: resolvedRemote.token,
504
+ }),
505
+ };
506
+ }
507
+ const notifications = resolveNotificationSettings({
508
+ cliNotify: undefined,
509
+ cliNotifySound: undefined,
510
+ env: process.env,
511
+ config: userConfig.notify,
512
+ });
513
+ const sessionMeta = await sessionStore.createSession({
514
+ ...runOptions,
515
+ mode: resolvedEngine,
516
+ slug,
517
+ browserConfig,
518
+ waitPreference: true,
519
+ }, cwd, notifications);
520
+ const logWriter = sessionStore.createLogWriter(sessionMeta.id);
521
+ // Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
522
+ const log = (line) => {
523
+ logWriter.logLine(line);
524
+ if (line !== undefined) {
525
+ sendLog(line);
424
526
  }
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,
527
+ };
528
+ const write = (chunk) => {
529
+ logWriter.writeChunk(chunk);
530
+ sendLog(chunk, "debug");
531
+ return true;
532
+ };
533
+ try {
534
+ await performSessionRun({
535
+ sessionMeta,
536
+ runOptions,
433
537
  mode: resolvedEngine,
434
- slug,
435
538
  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
- }
539
+ cwd,
540
+ log,
541
+ write,
542
+ version: getCliVersion(),
543
+ notifications,
544
+ muteStdout: true,
545
+ browserDeps,
546
+ });
547
+ }
548
+ catch (error) {
549
+ log(`Run failed: ${error instanceof Error ? error.message : String(error)}`);
550
+ return {
551
+ isError: true,
552
+ content: textContent(`Session ${sessionMeta.id} failed: ${error instanceof Error ? error.message : String(error)}`),
553
+ };
554
+ }
555
+ finally {
556
+ logWriter.stream.end();
557
+ }
558
+ try {
559
+ const finalMeta = (await sessionStore.readSession(sessionMeta.id)) ?? sessionMeta;
560
+ const summary = `Session ${sessionMeta.id} (${finalMeta.status})`;
561
+ const logTail = await readSessionLogTail(sessionMeta.id, 4000);
562
+ const modelsSummary = summarizeModelRunsForConsult(finalMeta.models);
563
+ const artifacts = summarizeArtifactsForConsult(finalMeta.artifacts);
564
+ const images = summarizeImageArtifactsForConsult(finalMeta.artifacts);
565
+ return {
566
+ content: textContent([summary, logTail || "(log empty)"].join("\n").trim()),
567
+ structuredContent: {
568
+ sessionId: sessionMeta.id,
569
+ status: finalMeta.status,
570
+ output: logTail ?? "",
571
+ models: modelsSummary,
572
+ artifacts,
573
+ images,
574
+ },
445
575
  };
446
- const write = (chunk) => {
447
- logWriter.writeChunk(chunk);
448
- sendLog(chunk, "debug");
449
- return true;
576
+ }
577
+ catch (error) {
578
+ return {
579
+ isError: true,
580
+ content: textContent(`Session completed but metadata fetch failed: ${error instanceof Error ? error.message : String(error)}`),
450
581
  };
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
- });
582
+ }
583
+ }
584
+ export function registerConsultTool(server) {
585
+ server.registerTool("consult", {
586
+ title: "Run an oracle session",
587
+ 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).',
588
+ // Cast to any to satisfy SDK typings across differing Zod versions.
589
+ inputSchema: consultInputShape,
590
+ outputSchema: consultOutputShape,
591
+ }, async (input) => runConsultTool(input, { server: server.server }));
498
592
  }
@@ -18,6 +18,8 @@ export const consultInputSchema = z
18
18
  browserArchive: z.enum(["auto", "always", "never"]).optional(),
19
19
  browserFollowUps: z.array(z.string()).optional(),
20
20
  browserKeepBrowser: z.boolean().optional(),
21
+ generateImage: z.string().optional(),
22
+ outputPath: z.string().optional(),
21
23
  dryRun: z.boolean().optional(),
22
24
  search: z.boolean().optional(),
23
25
  slug: z.string().optional(),
@@ -1,6 +1,80 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import { resolveRunOptionsFromConfig } from "../cli/runOptions.js";
4
+ import { getOracleHomeDir } from "../oracleHome.js";
2
5
  import { Launcher } from "chrome-launcher";
3
- export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserBundleFormat, browserFollowUps, userConfig, env = process.env, }) {
6
+ const ALLOW_EXTERNAL_OUTPUT_ENV = "ORACLE_MCP_ALLOW_EXTERNAL_OUTPUT";
7
+ const MCP_OUTPUT_DIRNAME = "generated";
8
+ /**
9
+ * Whether MCP callers may write generated images outside the dedicated Oracle
10
+ * output directory. Off by default: MCP clients are less trusted than CLI users.
11
+ */
12
+ export function isExternalMcpOutputAllowed(env = process.env) {
13
+ const raw = env[ALLOW_EXTERNAL_OUTPUT_ENV]?.trim().toLowerCase();
14
+ return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
15
+ }
16
+ /**
17
+ * Resolve `target` through symlinks for the portion that exists on disk, then
18
+ * re-append the not-yet-created remainder. A lexical `path.resolve` is not
19
+ * enough: a symlinked parent under the Oracle home (e.g. `~/.oracle/generated`
20
+ * -> `/tmp/evil`) would pass a string-prefix check while the actual write lands
21
+ * outside the boundary. realpath-ing the deepest existing ancestor closes that.
22
+ */
23
+ function resolveThroughSymlinks(target) {
24
+ const resolved = path.resolve(target);
25
+ let current = resolved;
26
+ const tail = [];
27
+ // Bounded by the number of path segments; dirname() converges to the root.
28
+ for (;;) {
29
+ try {
30
+ const real = fs.realpathSync(current);
31
+ return tail.length ? path.join(real, ...tail) : real;
32
+ }
33
+ catch (error) {
34
+ if (error.code !== "ENOENT") {
35
+ throw error;
36
+ }
37
+ try {
38
+ if (fs.lstatSync(current).isSymbolicLink()) {
39
+ throw new Error(`MCP output path contains an unresolved symlink: ${current}`);
40
+ }
41
+ }
42
+ catch (lstatError) {
43
+ if (lstatError.code !== "ENOENT") {
44
+ throw lstatError;
45
+ }
46
+ }
47
+ const parent = path.dirname(current);
48
+ if (parent === current) {
49
+ return resolved;
50
+ }
51
+ tail.unshift(path.basename(current));
52
+ current = parent;
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Constrain an MCP-supplied output path to ORACLE_HOME_DIR/generated and return
58
+ * its canonical absolute form. This keeps agent writes away from Oracle config,
59
+ * session metadata, and browser profile state. Existing symlinks are resolved
60
+ * before the boundary check. Set ORACLE_MCP_ALLOW_EXTERNAL_OUTPUT to opt into
61
+ * arbitrary output paths as an explicit operator decision.
62
+ */
63
+ export function resolveMcpOutputPath(requestedPath, field, env = process.env) {
64
+ const resolved = path.resolve(requestedPath);
65
+ if (isExternalMcpOutputAllowed(env)) {
66
+ return resolved;
67
+ }
68
+ const oracleHome = resolveThroughSymlinks(getOracleHomeDir());
69
+ const root = path.join(oracleHome, MCP_OUTPUT_DIRNAME);
70
+ const realTarget = resolveThroughSymlinks(resolved);
71
+ if (realTarget.startsWith(`${root}${path.sep}`)) {
72
+ return realTarget;
73
+ }
74
+ throw new Error(`MCP "${field}" must resolve under the generated output directory (${root}); got "${realTarget}". ` +
75
+ `Use a path under that directory, or set ${ALLOW_EXTERNAL_OUTPUT_ENV}=1 to allow external output paths.`);
76
+ }
77
+ export function mapConsultToRunOptions({ prompt, files, model, models, engine, search, browserAttachments, browserBundleFiles, browserBundleFormat, browserFollowUps, generateImage, outputPath, userConfig, env = process.env, }) {
4
78
  // Normalize CLI-style inputs through the shared resolver so config/env defaults apply,
5
79
  // then overlay MCP-only overrides such as explicit search toggles.
6
80
  const mergedModels = Array.isArray(models) && models.length > 0
@@ -32,6 +106,18 @@ export function mapConsultToRunOptions({ prompt, files, model, models, engine, s
32
106
  .map((entry) => entry.trim())
33
107
  .filter(Boolean);
34
108
  }
109
+ const imageOutputPath = generateImage?.trim();
110
+ if (imageOutputPath) {
111
+ result.runOptions.generateImage = resolveMcpOutputPath(imageOutputPath, "generateImage", env);
112
+ }
113
+ const secondaryOutputPath = outputPath?.trim();
114
+ if (secondaryOutputPath) {
115
+ result.runOptions.outputPath = resolveMcpOutputPath(secondaryOutputPath, "outputPath", env);
116
+ }
117
+ if (result.resolvedEngine !== "browser" &&
118
+ (result.runOptions.generateImage || result.runOptions.outputPath)) {
119
+ throw new Error('MCP image output requires engine:"browser".');
120
+ }
35
121
  return result;
36
122
  }
37
123
  export function ensureBrowserAvailable(engine, options) {
@@ -164,6 +164,32 @@ export const MODEL_CONFIGS = {
164
164
  supportsBackground: false,
165
165
  supportsSearch: true,
166
166
  },
167
+ "gemini-3.5-flash": {
168
+ model: "gemini-3.5-flash",
169
+ provider: "google",
170
+ tokenizer: countTokensGpt5Pro,
171
+ inputLimit: 1_048_576,
172
+ pricing: {
173
+ inputPerToken: 1.5 / 1_000_000,
174
+ outputPerToken: 9 / 1_000_000,
175
+ },
176
+ reasoning: null,
177
+ supportsBackground: false,
178
+ supportsSearch: true,
179
+ },
180
+ "gemini-3.1-flash-lite": {
181
+ model: "gemini-3.1-flash-lite",
182
+ provider: "google",
183
+ tokenizer: countTokensGpt5Pro,
184
+ inputLimit: 1_048_576,
185
+ pricing: {
186
+ inputPerToken: 0.25 / 1_000_000,
187
+ outputPerToken: 1.5 / 1_000_000,
188
+ },
189
+ reasoning: null,
190
+ supportsBackground: false,
191
+ supportsSearch: true,
192
+ },
167
193
  "gemini-3-pro": {
168
194
  model: "gemini-3-pro",
169
195
  provider: "google",
@@ -223,6 +249,6 @@ export const MODEL_CONFIGS = {
223
249
  };
224
250
  export const DEFAULT_SYSTEM_PROMPT = [
225
251
  "You are Oracle, a focused one-shot problem solver.",
226
- "Emphasize direct answers and cite any files referenced.",
252
+ "Emphasize direct answers and cite referenced files as path:line or path:line-line when line numbers are available.",
227
253
  ].join(" ");
228
254
  export const TOKENIZER_OPTIONS = { allowedSpecial: "all" };