@steipete/oracle 0.17.3 → 0.19.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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +75 -64
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +267 -15
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +15 -3
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +447 -263
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/policies.js +6 -2
  31. package/dist/src/browser/profileState.js +114 -9
  32. package/dist/src/browser/projectSourcesRunner.js +88 -17
  33. package/dist/src/browser/prompt.js +272 -113
  34. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  35. package/dist/src/browser/reattach.js +14 -4
  36. package/dist/src/browser/sessionRunner.js +71 -11
  37. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  38. package/dist/src/browser/utils.js +20 -2
  39. package/dist/src/cli/browserConfig.js +40 -15
  40. package/dist/src/cli/browserDefaults.js +13 -0
  41. package/dist/src/cli/browserTabs.js +12 -29
  42. package/dist/src/cli/detachedSession.js +69 -0
  43. package/dist/src/cli/dryRun.js +58 -47
  44. package/dist/src/cli/harvestIntegrity.js +140 -0
  45. package/dist/src/cli/projectSources.js +5 -3
  46. package/dist/src/cli/serveBrowserConfig.js +24 -0
  47. package/dist/src/cli/sessionDisplay.js +9 -2
  48. package/dist/src/cli/sessionRunner.js +91 -7
  49. package/dist/src/config.js +1 -0
  50. package/dist/src/gemini-web/client.js +67 -21
  51. package/dist/src/gemini-web/executor.js +4 -0
  52. package/dist/src/gemini-web/http.js +15 -0
  53. package/dist/src/gemini-web/models.js +15 -11
  54. package/dist/src/mcp/server.js +11 -14
  55. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  56. package/dist/src/mcp/tools/consult.js +97 -26
  57. package/dist/src/mcp/tools/projectSources.js +4 -6
  58. package/dist/src/mcp/tools/sessionResources.js +1 -1
  59. package/dist/src/mcp/tools/sessions.js +2 -2
  60. package/dist/src/mcp/tools/wait.js +182 -0
  61. package/dist/src/mcp/types.js +7 -0
  62. package/dist/src/oracle/errors.js +11 -0
  63. package/dist/src/oracle/providerRoutePlan.js +3 -2
  64. package/dist/src/remote/client.js +70 -6
  65. package/dist/src/remote/health.js +6 -2
  66. package/dist/src/remote/runSlots.js +83 -0
  67. package/dist/src/remote/server.js +351 -72
  68. package/dist/src/sessionManager.js +36 -1
  69. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  71. package/package.json +25 -23
  72. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  74. package/dist/bin/oracle.js +0 -683
  75. package/dist/docs-site/.nojekyll +0 -0
  76. package/dist/docs-site/CNAME +0 -1
  77. package/dist/docs-site/RELEASING.html +0 -410
  78. package/dist/docs-site/agents.html +0 -374
  79. package/dist/docs-site/anthropic.html +0 -368
  80. package/dist/docs-site/bridge.html +0 -400
  81. package/dist/docs-site/browser-mode.html +0 -588
  82. package/dist/docs-site/chromium-forks.html +0 -347
  83. package/dist/docs-site/cli-reference.html +0 -346
  84. package/dist/docs-site/configuration.html +0 -436
  85. package/dist/docs-site/favicon.svg +0 -14
  86. package/dist/docs-site/followup.html +0 -368
  87. package/dist/docs-site/gemini.html +0 -379
  88. package/dist/docs-site/grok.html +0 -325
  89. package/dist/docs-site/index.html +0 -360
  90. package/dist/docs-site/install.html +0 -335
  91. package/dist/docs-site/linux.html +0 -321
  92. package/dist/docs-site/llms.txt +0 -43
  93. package/dist/docs-site/manual-tests.html +0 -595
  94. package/dist/docs-site/mcp.html +0 -368
  95. package/dist/docs-site/multimodel.html +0 -364
  96. package/dist/docs-site/mythical-pro-agents.html +0 -360
  97. package/dist/docs-site/notifier.html +0 -338
  98. package/dist/docs-site/openai-endpoints.html +0 -387
  99. package/dist/docs-site/openrouter.html +0 -344
  100. package/dist/docs-site/quickstart.html +0 -369
  101. package/dist/docs-site/refactor/ux.html +0 -532
  102. package/dist/docs-site/sessions.html +0 -388
  103. package/dist/docs-site/social-card.png +0 -0
  104. package/dist/docs-site/social-card.svg +0 -79
  105. package/dist/docs-site/spec.html +0 -363
  106. package/dist/docs-site/testing.html +0 -320
  107. package/dist/docs-site/tui-debug.html +0 -326
  108. package/dist/docs-site/windows-work.html +0 -323
  109. package/dist/docs-site/windows.html +0 -320
  110. package/dist/markdansi/types/index.js +0 -4
  111. package/dist/oracle/bin/oracle-cli.js +0 -472
  112. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  113. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  114. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  115. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  116. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  117. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  118. package/dist/oracle/src/browser/config.js +0 -33
  119. package/dist/oracle/src/browser/constants.js +0 -40
  120. package/dist/oracle/src/browser/cookies.js +0 -210
  121. package/dist/oracle/src/browser/domDebug.js +0 -36
  122. package/dist/oracle/src/browser/index.js +0 -331
  123. package/dist/oracle/src/browser/pageActions.js +0 -5
  124. package/dist/oracle/src/browser/prompt.js +0 -88
  125. package/dist/oracle/src/browser/promptSummary.js +0 -20
  126. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  127. package/dist/oracle/src/browser/types.js +0 -1
  128. package/dist/oracle/src/browser/utils.js +0 -62
  129. package/dist/oracle/src/browserMode.js +0 -1
  130. package/dist/oracle/src/cli/browserConfig.js +0 -44
  131. package/dist/oracle/src/cli/dryRun.js +0 -59
  132. package/dist/oracle/src/cli/engine.js +0 -17
  133. package/dist/oracle/src/cli/errorUtils.js +0 -9
  134. package/dist/oracle/src/cli/help.js +0 -70
  135. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  136. package/dist/oracle/src/cli/options.js +0 -103
  137. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  138. package/dist/oracle/src/cli/rootAlias.js +0 -30
  139. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  140. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  141. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  142. package/dist/oracle/src/heartbeat.js +0 -43
  143. package/dist/oracle/src/oracle/client.js +0 -48
  144. package/dist/oracle/src/oracle/config.js +0 -29
  145. package/dist/oracle/src/oracle/errors.js +0 -101
  146. package/dist/oracle/src/oracle/files.js +0 -220
  147. package/dist/oracle/src/oracle/format.js +0 -33
  148. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  149. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  150. package/dist/oracle/src/oracle/request.js +0 -48
  151. package/dist/oracle/src/oracle/run.js +0 -444
  152. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  153. package/dist/oracle/src/oracle/types.js +0 -1
  154. package/dist/oracle/src/oracle.js +0 -9
  155. package/dist/oracle/src/sessionManager.js +0 -205
  156. package/dist/oracle/src/version.js +0 -39
  157. package/dist/scripts/chrome/browser-tools.js +0 -295
  158. package/dist/src/browser/chromeCookies.js +0 -312
  159. package/dist/src/browser/keytarShim.js +0 -56
  160. package/dist/src/browser/profileSync.js +0 -141
  161. package/dist/src/browser/windowsCookies.js +0 -219
@@ -1,11 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { getCliVersion } from "../../version.js";
3
- import { LoggingMessageNotificationParamsSchema, } from "@modelcontextprotocol/sdk/types.js";
4
3
  import { ensureBrowserAvailable, mapConsultToRunOptions } from "../utils.js";
5
4
  import { sessionStore } from "../../sessionStore.js";
6
5
  import { resolveRemoteServiceConfig } from "../../remote/remoteServiceConfig.js";
7
6
  import { createRemoteBrowserExecutor } from "../../remote/client.js";
8
- async function readSessionLogTail(sessionId, maxBytes) {
7
+ export async function readSessionLogTail(sessionId, maxBytes) {
9
8
  try {
10
9
  const log = await sessionStore.readLog(sessionId);
11
10
  if (log.length <= maxBytes) {
@@ -24,9 +23,11 @@ import { CONSULT_PRESETS, browserThinkingTimeRawSchema, consultInputSchema } fro
24
23
  import { applyConsultPreset } from "../consultPresets.js";
25
24
  import { loadUserConfig } from "../../config.js";
26
25
  import { resolveNotificationSettings } from "../../cli/notifier.js";
27
- import { mapModelToBrowserLabel, resolveBrowserModelLabel } from "../../cli/browserConfig.js";
26
+ import { mapModelToBrowserLabel, resolveBrowserModelLabel, resolveDefaultBrowserThinkingTime, } from "../../cli/browserConfig.js";
28
27
  import { normalizeThinkingTimeLevel } from "../../oracle/thinkingTime.js";
29
- // Use raw shapes so the MCP SDK (with its bundled Zod) wraps them and emits valid JSON Schema.
28
+ import { launchDetachedSession } from "../../cli/detachedSession.js";
29
+ import { buildSessionLifecycle } from "../../cli/sessionLifecycle.js";
30
+ // Shared fields are composed into explicit schemas when registering tools.
30
31
  const consultInputShape = {
31
32
  preset: z
32
33
  .enum(CONSULT_PRESETS)
@@ -60,11 +61,11 @@ const consultInputShape = {
60
61
  browserBundleFiles: z
61
62
  .boolean()
62
63
  .optional()
63
- .describe("Browser-only: bundle many files into a single upload (helps with upload limits)."),
64
+ .describe("Browser-only: force one upload bundle; auto/zip includes all resolved files. Multiple text/source uploads already bundle by default (flattened text unless ZIP is selected)."),
64
65
  browserBundleFormat: z
65
66
  .enum(["auto", "text", "zip"])
66
67
  .optional()
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
+ .describe('Browser-only: bundle upload format. Defaults to "auto", which keeps flattened text for text-only uploads and uses ZIP when raw files are present; "zip" forces a byte-preserving archive.'),
68
69
  browserThinkingTime: browserThinkingTimeRawSchema
69
70
  .optional()
70
71
  .describe("Browser-only: set ChatGPT thinking time when supported by the chosen model."),
@@ -96,6 +97,10 @@ const consultInputShape = {
96
97
  .string()
97
98
  .optional()
98
99
  .describe("Browser-only image output fallback path, mirroring the CLI --output option for image operations."),
100
+ waitForCompletion: z
101
+ .boolean()
102
+ .optional()
103
+ .describe("When false, start the Oracle run in a detached worker and return its sessionId immediately. Use the wait tool to block for completion without agent-side polling. Defaults to true for compatibility."),
99
104
  dryRun: z
100
105
  .boolean()
101
106
  .optional()
@@ -109,7 +114,7 @@ const consultInputShape = {
109
114
  .optional()
110
115
  .describe("Optional human-friendly session id (used for later `oracle sessions` lookups)."),
111
116
  };
112
- const consultModelSummaryShape = z.object({
117
+ export const consultModelSummaryShape = z.object({
113
118
  model: z.string(),
114
119
  status: z.string(),
115
120
  startedAt: z.string().optional(),
@@ -138,14 +143,14 @@ const consultModelSummaryShape = z.object({
138
143
  .optional(),
139
144
  logPath: z.string().optional(),
140
145
  });
141
- const consultArtifactSummaryShape = z.object({
146
+ export const consultArtifactSummaryShape = z.object({
142
147
  kind: z.enum(["transcript", "deep-research-report", "image", "file"]),
143
148
  path: z.string(),
144
149
  label: z.string().optional(),
145
150
  mimeType: z.string().optional(),
146
151
  sizeBytes: z.number().optional(),
147
152
  });
148
- const consultImageSummaryShape = consultArtifactSummaryShape.extend({
153
+ export const consultImageSummaryShape = consultArtifactSummaryShape.extend({
149
154
  kind: z.literal("image"),
150
155
  alt: z.string().optional(),
151
156
  width: z.number().optional(),
@@ -185,6 +190,7 @@ export const consultOutputShape = {
185
190
  models: z.array(consultModelSummaryShape).optional(),
186
191
  artifacts: z.array(consultArtifactSummaryShape).optional(),
187
192
  images: z.array(consultImageSummaryShape).optional(),
193
+ detached: z.boolean().optional(),
188
194
  };
189
195
  export function summarizeModelRunsForConsult(runs) {
190
196
  if (!runs || runs.length === 0) {
@@ -267,11 +273,14 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
267
273
  ? true
268
274
  : (configuredBrowser.manualLogin ?? process.platform === "win32");
269
275
  const configuredThinkingTime = normalizeThinkingTimeLevel(configuredBrowser.thinkingTime);
276
+ const modelStrategy = browserModelStrategy ?? configuredBrowser.modelStrategy;
270
277
  return {
271
278
  ...configuredBrowser,
272
279
  url: configuredUrl,
273
280
  chatgptUrl: configuredUrl,
274
- cookieSync: !manualLogin || configuredBrowser.manualLoginCookieSync === true,
281
+ cookieSync: manualLogin
282
+ ? configuredBrowser.manualLoginCookieSync === true
283
+ : configuredBrowser.cookieSync === true,
275
284
  headless: configuredBrowser.headless ?? false,
276
285
  hideWindow: configuredBrowser.hideWindow ?? false,
277
286
  keepBrowser: browserKeepBrowser ?? configuredBrowser.keepBrowser ?? false,
@@ -279,8 +288,14 @@ export function buildConsultBrowserConfig({ userConfig, env, runModel, inputMode
279
288
  manualLoginProfileDir: manualLogin
280
289
  ? ((envProfileDir || configuredBrowser.manualLoginProfileDir) ?? null)
281
290
  : null,
282
- thinkingTime: browserThinkingTime ?? configuredThinkingTime ?? undefined,
283
- modelStrategy: browserModelStrategy ?? configuredBrowser.modelStrategy,
291
+ thinkingTime: browserThinkingTime ??
292
+ configuredThinkingTime ??
293
+ resolveDefaultBrowserThinkingTime({
294
+ model: runModel,
295
+ requestedModel: inputModel,
296
+ modelStrategy,
297
+ }),
298
+ modelStrategy,
284
299
  researchMode: browserResearchMode ?? configuredBrowser.researchMode,
285
300
  archiveConversations: browserArchive ?? configuredBrowser.archiveConversations,
286
301
  desiredModel: desiredModelLabel || mapModelToBrowserLabel(runModel),
@@ -378,7 +393,7 @@ export function formatConsultDryRunResolved(details) {
378
393
  }
379
394
  return lines;
380
395
  }
381
- export async function runConsultTool(input, { server }) {
396
+ export async function runConsultTool(input, { log: requestLog, launchDetached = launchDetachedSession, }) {
382
397
  const textContent = (text) => [{ type: "text", text }];
383
398
  let parsedInput;
384
399
  try {
@@ -390,7 +405,7 @@ export async function runConsultTool(input, { server }) {
390
405
  content: textContent(error instanceof Error ? error.message : String(error)),
391
406
  };
392
407
  }
393
- const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, generateImage, outputPath, dryRun, slug, } = parsedInput;
408
+ const { prompt, files, model, models, engine, search, browserModelLabel, browserAttachments, browserBundleFiles, browserBundleFormat, browserThinkingTime, browserModelStrategy, browserResearchMode, browserArchive, browserFollowUps, browserKeepBrowser, generateImage, outputPath, waitForCompletion = true, dryRun, slug, } = parsedInput;
394
409
  const { config: userConfig } = await loadUserConfig();
395
410
  let runOptions;
396
411
  let resolvedEngine;
@@ -419,12 +434,7 @@ export async function runConsultTool(input, { server }) {
419
434
  };
420
435
  }
421
436
  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(() => { });
437
+ const sendLog = (text, level = "info") => requestLog(level, { text, bytes: Buffer.byteLength(text, "utf8") }).catch(() => { });
428
438
  const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
429
439
  const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath;
430
440
  if (resolvedEngine === "browser" && resolvedRemote.host && imageOutputPath) {
@@ -481,6 +491,18 @@ export async function runConsultTool(input, { server }) {
481
491
  },
482
492
  };
483
493
  }
494
+ if (!waitForCompletion && resolvedEngine === "browser" && resolvedRemote.host) {
495
+ return {
496
+ isError: true,
497
+ content: textContent("Detached MCP consults are not supported with a remote browser service yet. Keep waitForCompletion:true for this run."),
498
+ };
499
+ }
500
+ if (!waitForCompletion && process.env.ORACLE_NO_DETACH === "1") {
501
+ return {
502
+ isError: true,
503
+ content: textContent("Detached MCP consults are disabled by ORACLE_NO_DETACH=1. Remove it or keep waitForCompletion:true."),
504
+ };
505
+ }
484
506
  const browserGuard = ensureBrowserAvailable(resolvedEngine, {
485
507
  remoteHost: resolvedRemote.host,
486
508
  });
@@ -516,8 +538,58 @@ export async function runConsultTool(input, { server }) {
516
538
  mode: resolvedEngine,
517
539
  slug,
518
540
  browserConfig,
519
- waitPreference: true,
541
+ waitPreference: waitForCompletion,
520
542
  }, cwd, notifications);
543
+ if (!waitForCompletion) {
544
+ try {
545
+ await launchDetached({
546
+ sessionId: sessionMeta.id,
547
+ prepare: async (workerPid) => {
548
+ const lifecycle = buildSessionLifecycle({
549
+ engine: resolvedEngine,
550
+ detached: true,
551
+ workerPid,
552
+ reattachCommand: `oracle session ${sessionMeta.id}`,
553
+ });
554
+ await sessionStore.updateSession(sessionMeta.id, {
555
+ status: "running",
556
+ startedAt: new Date().toISOString(),
557
+ lifecycle,
558
+ });
559
+ },
560
+ });
561
+ const started = (await sessionStore.readSession(sessionMeta.id)) ?? sessionMeta;
562
+ const summary = `Session ${sessionMeta.id} (${started.status}; detached)`;
563
+ return {
564
+ content: textContent(`${summary}\nUse the wait tool with id=${JSON.stringify(sessionMeta.id)} to wait without polling.`),
565
+ structuredContent: {
566
+ sessionId: sessionMeta.id,
567
+ status: started.status,
568
+ output: "",
569
+ models: summarizeModelRunsForConsult(started.models),
570
+ artifacts: summarizeArtifactsForConsult(started.artifacts),
571
+ images: summarizeImageArtifactsForConsult(started.artifacts),
572
+ detached: true,
573
+ },
574
+ };
575
+ }
576
+ catch (error) {
577
+ const message = error instanceof Error ? error.message : String(error);
578
+ await sessionStore
579
+ .updateSession(sessionMeta.id, {
580
+ status: "error",
581
+ completedAt: new Date().toISOString(),
582
+ errorMessage: message,
583
+ response: { status: "error" },
584
+ error: { category: "internal", message },
585
+ })
586
+ .catch(() => undefined);
587
+ return {
588
+ isError: true,
589
+ content: textContent(`Unable to start detached session ${sessionMeta.id}: ${message}`),
590
+ };
591
+ }
592
+ }
521
593
  const logWriter = sessionStore.createLogWriter(sessionMeta.id);
522
594
  // Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
523
595
  const log = (line) => {
@@ -585,9 +657,8 @@ export async function runConsultTool(input, { server }) {
585
657
  export function registerConsultTool(server) {
586
658
  server.registerTool("consult", {
587
659
  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 }));
660
+ 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; set `waitForCompletion:false` to return a durable sessionId immediately, then use `wait` to block without agent-side polling. 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).',
661
+ inputSchema: z.object(consultInputShape),
662
+ outputSchema: z.object(consultOutputShape),
663
+ }, async (input, context) => runConsultTool(input, { log: context.mcpReq.log }));
593
664
  }
@@ -56,9 +56,9 @@ export function registerProjectSourcesTool(server) {
56
56
  server.registerTool("project_sources", {
57
57
  title: "Manage ChatGPT Project Sources",
58
58
  description: "List or append files to a ChatGPT Project's persistent Sources tab. This is useful for Developer Mode workflows where chats do not share memory, but explicit project sources provide shared context. Destructive delete/replace/sync operations are intentionally not included in v1.",
59
- inputSchema: projectSourcesInputShape,
60
- outputSchema: projectSourcesOutputShape,
61
- }, async (input) => {
59
+ inputSchema: z.object(projectSourcesInputShape),
60
+ outputSchema: z.object(projectSourcesOutputShape),
61
+ }, async (input, context) => {
62
62
  const textContent = (text) => [{ type: "text", text }];
63
63
  let parsed;
64
64
  try {
@@ -107,9 +107,7 @@ export function registerProjectSourcesTool(server) {
107
107
  dryRun: parsed.dryRun,
108
108
  config: browserConfig,
109
109
  log: (message) => {
110
- server.server
111
- .sendLoggingMessage({ level: "info", data: { text: message } })
112
- .catch(() => undefined);
110
+ context.mcpReq.log("info", { text: message }).catch(() => undefined);
113
111
  },
114
112
  });
115
113
  const output = result.status === "dry-run"
@@ -1,4 +1,4 @@
1
- import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { ResourceTemplate } from "@modelcontextprotocol/server";
2
2
  import fs from "node:fs/promises";
3
3
  import { sessionStore } from "../../sessionStore.js";
4
4
  // URIs:
@@ -44,8 +44,8 @@ export function registerSessionsTool(server) {
44
44
  server.registerTool("sessions", {
45
45
  title: "List or fetch oracle sessions",
46
46
  description: "Inspect Oracle session history stored under `ORACLE_HOME_DIR` (shared with the CLI). List recent sessions or fetch one by id/slug (optionally including metadata + request + log).",
47
- inputSchema: sessionsInputShape,
48
- outputSchema: sessionsOutputShape,
47
+ inputSchema: z.object(sessionsInputShape),
48
+ outputSchema: z.object(sessionsOutputShape),
49
49
  }, async (input) => {
50
50
  const textContent = (text) => [{ type: "text", text }];
51
51
  const { id, hours = 24, limit = 100, includeAll = false, detail = false, } = sessionsInputSchema.parse(input);
@@ -0,0 +1,182 @@
1
+ import { realpathSync, watch } from "node:fs";
2
+ import { z } from "zod";
3
+ import { sessionStore } from "../../sessionStore.js";
4
+ import { waitInputSchema } from "../types.js";
5
+ import { consultArtifactSummaryShape, consultImageSummaryShape, consultModelSummaryShape, readSessionLogTail, summarizeArtifactsForConsult, summarizeImageArtifactsForConsult, summarizeModelRunsForConsult, } from "./consult.js";
6
+ const TERMINAL_SESSION_STATUSES = new Set(["completed", "partial", "error", "cancelled"]);
7
+ const DEFAULT_FALLBACK_INTERVAL_MS = 1_000;
8
+ const waitInputShape = {
9
+ id: z.string().min(1, "Session id is required.").describe("Oracle session id or slug."),
10
+ timeoutMs: z
11
+ .number()
12
+ .int()
13
+ .nonnegative()
14
+ .optional()
15
+ .describe("How long to wait for a terminal session state. Omit to wait indefinitely; 0 returns an immediate snapshot. A wait timeout never cancels the Oracle session."),
16
+ };
17
+ const waitOutputShape = {
18
+ sessionId: z.string(),
19
+ status: z.string(),
20
+ waitStatus: z.enum(["terminal", "timed_out"]),
21
+ timedOut: z.boolean(),
22
+ cancelled: z.boolean(),
23
+ output: z.string(),
24
+ models: z.array(consultModelSummaryShape).optional(),
25
+ artifacts: z.array(consultArtifactSummaryShape).optional(),
26
+ images: z.array(consultImageSummaryShape).optional(),
27
+ };
28
+ function abortReason(signal) {
29
+ return signal.reason instanceof Error
30
+ ? signal.reason
31
+ : new DOMException("Oracle session wait was cancelled.", "AbortError");
32
+ }
33
+ function throwIfAborted(signal) {
34
+ if (signal?.aborted) {
35
+ throw abortReason(signal);
36
+ }
37
+ }
38
+ export function isTerminalSessionStatus(status) {
39
+ return TERMINAL_SESSION_STATUSES.has(status);
40
+ }
41
+ export function createSessionChangeSource(directory) {
42
+ let watcher;
43
+ let notified = false;
44
+ let pendingWake;
45
+ const onWake = () => {
46
+ if (!pendingWake) {
47
+ notified = true;
48
+ return;
49
+ }
50
+ const wake = pendingWake;
51
+ pendingWake = undefined;
52
+ wake();
53
+ };
54
+ const onError = () => {
55
+ const failedWatcher = watcher;
56
+ watcher = undefined;
57
+ failedWatcher?.off("change", onWake);
58
+ failedWatcher?.off("error", onError);
59
+ failedWatcher?.close();
60
+ onWake();
61
+ };
62
+ try {
63
+ // Windows short-path aliases can trigger a native libuv prefix assertion.
64
+ watcher = watch(realpathSync.native(directory));
65
+ watcher.on("change", onWake);
66
+ watcher.on("error", onError);
67
+ }
68
+ catch {
69
+ // A timer fallback below keeps waits correct when filesystem notifications
70
+ // are unavailable or the session directory is on an unsupported volume.
71
+ }
72
+ return {
73
+ wait(delayMs, signal) {
74
+ throwIfAborted(signal);
75
+ return new Promise((resolve, reject) => {
76
+ let settled = false;
77
+ const finish = (error) => {
78
+ if (settled)
79
+ return;
80
+ settled = true;
81
+ clearTimeout(timer);
82
+ if (pendingWake === wake)
83
+ pendingWake = undefined;
84
+ signal?.removeEventListener("abort", onAbort);
85
+ if (error)
86
+ reject(error);
87
+ else
88
+ resolve();
89
+ };
90
+ const wake = () => finish();
91
+ const onAbort = () => finish(abortReason(signal));
92
+ const timer = setTimeout(wake, delayMs);
93
+ pendingWake = wake;
94
+ signal?.addEventListener("abort", onAbort, { once: true });
95
+ if (signal?.aborted) {
96
+ onAbort();
97
+ return;
98
+ }
99
+ if (notified) {
100
+ notified = false;
101
+ queueMicrotask(wake);
102
+ }
103
+ });
104
+ },
105
+ close() {
106
+ watcher?.off("change", onWake);
107
+ watcher?.off("error", onError);
108
+ watcher?.close();
109
+ watcher = undefined;
110
+ },
111
+ };
112
+ }
113
+ const defaultWaitDeps = {
114
+ readSession: (id) => sessionStore.readSession(id),
115
+ getSessionDir: async (id) => (await sessionStore.getPaths(id)).dir,
116
+ createChangeSource: createSessionChangeSource,
117
+ now: Date.now,
118
+ };
119
+ export async function waitForSessionTerminal({ id, timeoutMs, signal, fallbackIntervalMs = DEFAULT_FALLBACK_INTERVAL_MS, }, deps = defaultWaitDeps) {
120
+ throwIfAborted(signal);
121
+ const deadline = timeoutMs === undefined ? undefined : deps.now() + timeoutMs;
122
+ let metadata = await deps.readSession(id);
123
+ if (!metadata) {
124
+ throw new Error(`Session "${id}" not found.`);
125
+ }
126
+ if (isTerminalSessionStatus(metadata.status)) {
127
+ return { metadata, waitStatus: "terminal", timedOut: false };
128
+ }
129
+ if (timeoutMs === 0) {
130
+ return { metadata, waitStatus: "timed_out", timedOut: true };
131
+ }
132
+ const directory = await deps.getSessionDir(id);
133
+ const changes = deps.createChangeSource(directory);
134
+ try {
135
+ // Close the read/watch race: the session may have completed while the
136
+ // filesystem watcher was being installed.
137
+ metadata = (await deps.readSession(id)) ?? metadata;
138
+ while (!isTerminalSessionStatus(metadata.status)) {
139
+ throwIfAborted(signal);
140
+ const remaining = deadline === undefined ? undefined : deadline - deps.now();
141
+ if (remaining !== undefined && remaining <= 0) {
142
+ return { metadata, waitStatus: "timed_out", timedOut: true };
143
+ }
144
+ const delayMs = Math.max(1, Math.min(fallbackIntervalMs, remaining ?? fallbackIntervalMs));
145
+ await changes.wait(delayMs, signal);
146
+ metadata = (await deps.readSession(id)) ?? metadata;
147
+ }
148
+ return { metadata, waitStatus: "terminal", timedOut: false };
149
+ }
150
+ finally {
151
+ changes.close();
152
+ }
153
+ }
154
+ export async function runWaitTool(input, extra) {
155
+ const { id, timeoutMs } = waitInputSchema.parse(input);
156
+ const result = await waitForSessionTerminal({ id, timeoutMs, signal: extra?.signal });
157
+ const { metadata } = result;
158
+ const logTail = (await readSessionLogTail(metadata.id, 4_000)) ?? "";
159
+ const summary = `Session ${metadata.id} (${metadata.status}; wait=${result.waitStatus})`;
160
+ return {
161
+ content: [{ type: "text", text: [summary, logTail || "(log empty)"].join("\n") }],
162
+ structuredContent: {
163
+ sessionId: metadata.id,
164
+ status: metadata.status,
165
+ waitStatus: result.waitStatus,
166
+ timedOut: result.timedOut,
167
+ cancelled: metadata.status === "cancelled",
168
+ output: logTail,
169
+ models: summarizeModelRunsForConsult(metadata.models),
170
+ artifacts: summarizeArtifactsForConsult(metadata.artifacts),
171
+ images: summarizeImageArtifactsForConsult(metadata.artifacts),
172
+ },
173
+ };
174
+ }
175
+ export function registerWaitTool(server) {
176
+ server.registerTool("wait", {
177
+ title: "Wait for an oracle session",
178
+ description: "Wait for an existing Oracle session to reach a terminal state without agent-side polling. Omit timeoutMs to wait indefinitely, or set a bounded caller wait. Wait timeout or request cancellation never cancels the Oracle session; call wait again with the same id to continue waiting.",
179
+ inputSchema: z.object(waitInputShape),
180
+ outputSchema: z.object(waitOutputShape),
181
+ }, async (input, context) => runWaitTool(input, { signal: context.mcpReq.signal }));
182
+ }
@@ -29,6 +29,7 @@ export const consultInputSchema = z
29
29
  browserKeepBrowser: z.boolean().optional(),
30
30
  generateImage: z.string().optional(),
31
31
  outputPath: z.string().optional(),
32
+ waitForCompletion: z.boolean().optional(),
32
33
  dryRun: z.boolean().optional(),
33
34
  search: z.boolean().optional(),
34
35
  slug: z.string().optional(),
@@ -41,3 +42,9 @@ export const sessionsInputSchema = z.object({
41
42
  includeAll: z.boolean().optional(),
42
43
  detail: z.boolean().optional(),
43
44
  });
45
+ export const waitInputSchema = z
46
+ .object({
47
+ id: z.string().min(1, "Session id is required."),
48
+ timeoutMs: z.number().int().nonnegative().optional(),
49
+ })
50
+ .strict();
@@ -26,6 +26,17 @@ export class BrowserAutomationError extends OracleUserError {
26
26
  this.name = "BrowserAutomationError";
27
27
  }
28
28
  }
29
+ /**
30
+ * The caller went away. Distinct from a failure: nothing was wrong with the run,
31
+ * it simply stopped being wanted, and a caller that reads this should not retry
32
+ * or report a problem.
33
+ */
34
+ export class BrowserRunCancelledError extends OracleUserError {
35
+ constructor(message = "Browser run cancelled: the caller disconnected.") {
36
+ super("browser-automation", message, { stage: "run-cancelled" });
37
+ this.name = "BrowserRunCancelledError";
38
+ }
39
+ }
29
40
  export class PromptValidationError extends OracleUserError {
30
41
  constructor(message, details, cause) {
31
42
  super("prompt-validation", message, details, cause);
@@ -75,7 +75,8 @@ function buildResolvedProviderRoute(input) {
75
75
  });
76
76
  const provider = state.provider;
77
77
  const isAzureOpenAI = state.isAzureOpenAI;
78
- let baseUrl = input.baseUrl?.trim();
78
+ // Azure owns the endpoint, including metadata lookups before request dispatch.
79
+ let baseUrl = isAzureOpenAI ? undefined : input.baseUrl?.trim();
79
80
  const providerQualifiedOpenRouterCandidate = !isAzureOpenAI && providerMode !== "openai" && input.model.includes("/");
80
81
  if (baseUrl &&
81
82
  providerQualifiedOpenRouterCandidate &&
@@ -83,7 +84,7 @@ function buildResolvedProviderRoute(input) {
83
84
  !isCustomBaseUrl(baseUrl)) {
84
85
  baseUrl = undefined;
85
86
  }
86
- if (!baseUrl) {
87
+ if (!baseUrl && !isAzureOpenAI) {
87
88
  let envBaseUrl;
88
89
  if (input.model.startsWith("grok")) {
89
90
  envBaseUrl = env.XAI_BASE_URL?.trim() || "https://api.x.ai/v1";