@steipete/oracle 0.18.0 → 0.20.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 (169) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +76 -76
  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 +170 -45
  13. package/dist/src/browser/actions/modelSelection.js +91 -4
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +364 -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 +156 -16
  19. package/dist/src/browser/actions/webSearch.js +96 -0
  20. package/dist/src/browser/attachRunning.js +46 -2
  21. package/dist/src/browser/attachmentValidation.js +53 -0
  22. package/dist/src/browser/cancellation.js +105 -0
  23. package/dist/src/browser/chatgptImages.js +2 -0
  24. package/dist/src/browser/chatgptThrottle.js +104 -0
  25. package/dist/src/browser/chromeLifecycle.js +272 -47
  26. package/dist/src/browser/config.js +14 -1
  27. package/dist/src/browser/detect.js +1 -1
  28. package/dist/src/browser/index.js +525 -274
  29. package/dist/src/browser/liveTabs.js +3 -4
  30. package/dist/src/browser/modelDisplay.js +9 -0
  31. package/dist/src/browser/pageActions.js +2 -2
  32. package/dist/src/browser/profileState.js +114 -9
  33. package/dist/src/browser/projectSourcesRunner.js +83 -15
  34. package/dist/src/browser/prompt.js +272 -113
  35. package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
  36. package/dist/src/browser/reattach.js +31 -4
  37. package/dist/src/browser/recoveryTarget.js +131 -0
  38. package/dist/src/browser/sessionRunner.js +73 -11
  39. package/dist/src/browser/tabLeaseRegistry.js +434 -38
  40. package/dist/src/browser/targetClaim.js +54 -0
  41. package/dist/src/browser/utils.js +20 -2
  42. package/dist/src/cli/browserConfig.js +36 -4
  43. package/dist/src/cli/browserDefaults.js +10 -0
  44. package/dist/src/cli/browserTabs.js +15 -29
  45. package/dist/src/cli/detachedSession.js +69 -0
  46. package/dist/src/cli/dryRun.js +58 -48
  47. package/dist/src/cli/harvestIntegrity.js +140 -0
  48. package/dist/src/cli/options.js +15 -0
  49. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  50. package/dist/src/cli/runOptions.js +19 -4
  51. package/dist/src/cli/serveBrowserConfig.js +24 -0
  52. package/dist/src/cli/sessionDisplay.js +13 -7
  53. package/dist/src/cli/sessionRunner.js +95 -12
  54. package/dist/src/config.js +1 -0
  55. package/dist/src/gemini-web/client.js +67 -21
  56. package/dist/src/gemini-web/executor.js +4 -0
  57. package/dist/src/gemini-web/http.js +15 -0
  58. package/dist/src/gemini-web/models.js +15 -11
  59. package/dist/src/mcp/server.js +11 -14
  60. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  61. package/dist/src/mcp/tools/consult.js +86 -24
  62. package/dist/src/mcp/tools/projectSources.js +4 -6
  63. package/dist/src/mcp/tools/sessionResources.js +1 -1
  64. package/dist/src/mcp/tools/sessions.js +2 -2
  65. package/dist/src/mcp/tools/wait.js +182 -0
  66. package/dist/src/mcp/types.js +8 -1
  67. package/dist/src/oracle/config.js +14 -0
  68. package/dist/src/oracle/errors.js +11 -0
  69. package/dist/src/oracle/geminiModels.js +1 -0
  70. package/dist/src/oracle/providerRoutePlan.js +3 -2
  71. package/dist/src/oracle/run.js +27 -4
  72. package/dist/src/remote/client.js +73 -6
  73. package/dist/src/remote/health.js +6 -2
  74. package/dist/src/remote/runSlots.js +83 -0
  75. package/dist/src/remote/server.js +346 -71
  76. package/dist/src/sessionManager.js +36 -1
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  78. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  79. package/package.json +25 -23
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/dist/bin/oracle.js +0 -683
  83. package/dist/docs-site/.nojekyll +0 -0
  84. package/dist/docs-site/CNAME +0 -1
  85. package/dist/docs-site/RELEASING.html +0 -410
  86. package/dist/docs-site/agents.html +0 -374
  87. package/dist/docs-site/anthropic.html +0 -368
  88. package/dist/docs-site/bridge.html +0 -400
  89. package/dist/docs-site/browser-mode.html +0 -588
  90. package/dist/docs-site/chromium-forks.html +0 -347
  91. package/dist/docs-site/cli-reference.html +0 -346
  92. package/dist/docs-site/configuration.html +0 -436
  93. package/dist/docs-site/favicon.svg +0 -14
  94. package/dist/docs-site/followup.html +0 -368
  95. package/dist/docs-site/gemini.html +0 -379
  96. package/dist/docs-site/grok.html +0 -325
  97. package/dist/docs-site/index.html +0 -360
  98. package/dist/docs-site/install.html +0 -335
  99. package/dist/docs-site/linux.html +0 -321
  100. package/dist/docs-site/llms.txt +0 -43
  101. package/dist/docs-site/manual-tests.html +0 -595
  102. package/dist/docs-site/mcp.html +0 -368
  103. package/dist/docs-site/multimodel.html +0 -364
  104. package/dist/docs-site/mythical-pro-agents.html +0 -360
  105. package/dist/docs-site/notifier.html +0 -338
  106. package/dist/docs-site/openai-endpoints.html +0 -387
  107. package/dist/docs-site/openrouter.html +0 -344
  108. package/dist/docs-site/quickstart.html +0 -369
  109. package/dist/docs-site/refactor/ux.html +0 -532
  110. package/dist/docs-site/sessions.html +0 -388
  111. package/dist/docs-site/social-card.png +0 -0
  112. package/dist/docs-site/social-card.svg +0 -79
  113. package/dist/docs-site/spec.html +0 -363
  114. package/dist/docs-site/testing.html +0 -320
  115. package/dist/docs-site/tui-debug.html +0 -326
  116. package/dist/docs-site/windows-work.html +0 -323
  117. package/dist/docs-site/windows.html +0 -320
  118. package/dist/markdansi/types/index.js +0 -4
  119. package/dist/oracle/bin/oracle-cli.js +0 -472
  120. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  121. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  122. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  123. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  124. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  125. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  126. package/dist/oracle/src/browser/config.js +0 -33
  127. package/dist/oracle/src/browser/constants.js +0 -40
  128. package/dist/oracle/src/browser/cookies.js +0 -210
  129. package/dist/oracle/src/browser/domDebug.js +0 -36
  130. package/dist/oracle/src/browser/index.js +0 -331
  131. package/dist/oracle/src/browser/pageActions.js +0 -5
  132. package/dist/oracle/src/browser/prompt.js +0 -88
  133. package/dist/oracle/src/browser/promptSummary.js +0 -20
  134. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  135. package/dist/oracle/src/browser/types.js +0 -1
  136. package/dist/oracle/src/browser/utils.js +0 -62
  137. package/dist/oracle/src/browserMode.js +0 -1
  138. package/dist/oracle/src/cli/browserConfig.js +0 -44
  139. package/dist/oracle/src/cli/dryRun.js +0 -59
  140. package/dist/oracle/src/cli/engine.js +0 -17
  141. package/dist/oracle/src/cli/errorUtils.js +0 -9
  142. package/dist/oracle/src/cli/help.js +0 -70
  143. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  144. package/dist/oracle/src/cli/options.js +0 -103
  145. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  146. package/dist/oracle/src/cli/rootAlias.js +0 -30
  147. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  148. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  149. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  150. package/dist/oracle/src/heartbeat.js +0 -43
  151. package/dist/oracle/src/oracle/client.js +0 -48
  152. package/dist/oracle/src/oracle/config.js +0 -29
  153. package/dist/oracle/src/oracle/errors.js +0 -101
  154. package/dist/oracle/src/oracle/files.js +0 -220
  155. package/dist/oracle/src/oracle/format.js +0 -33
  156. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  157. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  158. package/dist/oracle/src/oracle/request.js +0 -48
  159. package/dist/oracle/src/oracle/run.js +0 -444
  160. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  161. package/dist/oracle/src/oracle/types.js +0 -1
  162. package/dist/oracle/src/oracle.js +0 -9
  163. package/dist/oracle/src/sessionManager.js +0 -205
  164. package/dist/oracle/src/version.js +0 -39
  165. package/dist/scripts/chrome/browser-tools.js +0 -295
  166. package/dist/src/browser/chromeCookies.js +0 -312
  167. package/dist/src/browser/keytarShim.js +0 -56
  168. package/dist/src/browser/profileSync.js +0 -141
  169. package/dist/src/browser/windowsCookies.js +0 -219
@@ -2,15 +2,16 @@
2
2
  import "dotenv/config";
3
3
  import process from "node:process";
4
4
  import { pathToFileURL } from "node:url";
5
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { McpServer } from "@modelcontextprotocol/server";
6
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
7
7
  import { getCliVersion } from "../version.js";
8
8
  import { registerChatGptImageTool } from "./tools/chatgptImage.js";
9
9
  import { registerConsultTool } from "./tools/consult.js";
10
10
  import { registerProjectSourcesTool } from "./tools/projectSources.js";
11
11
  import { registerSessionsTool } from "./tools/sessions.js";
12
+ import { registerWaitTool } from "./tools/wait.js";
12
13
  import { registerSessionResources } from "./tools/sessionResources.js";
13
- export async function startMcpServer() {
14
+ export function createMcpServer() {
14
15
  const server = new McpServer({
15
16
  name: "oracle-mcp",
16
17
  version: getCliVersion(),
@@ -23,19 +24,15 @@ export async function startMcpServer() {
23
24
  registerChatGptImageTool(server);
24
25
  registerProjectSourcesTool(server);
25
26
  registerSessionsTool(server);
27
+ registerWaitTool(server);
26
28
  registerSessionResources(server);
27
- const transport = new StdioServerTransport();
28
- transport.onerror = (error) => {
29
- console.error("MCP transport error:", error);
30
- };
31
- const closed = new Promise((resolve) => {
32
- transport.onclose = () => {
33
- resolve();
34
- };
29
+ return server;
30
+ }
31
+ export async function startMcpServer() {
32
+ serveStdio(createMcpServer, {
33
+ legacy: "serve",
34
+ onerror: (error) => console.error("MCP transport error:", error),
35
35
  });
36
- // Keep the process alive until the client closes the transport.
37
- await server.connect(transport);
38
- await closed;
39
36
  }
40
37
  export function shouldStartMcpServerFromModule(moduleUrl = import.meta.url, argv1 = process.argv[1]) {
41
38
  return argv1 ? moduleUrl === pathToFileURL(argv1).href : false;
@@ -101,9 +101,9 @@ export function registerChatGptImageTool(server) {
101
101
  server.registerTool("chatgpt_image", {
102
102
  title: "Generate an image with ChatGPT",
103
103
  description: "Agent-friendly wrapper for ChatGPT browser image generation. It selects browser mode, enables the image-aware wait/download path, uploads reference files when provided, and returns saved image paths in structuredContent.images.",
104
- inputSchema: chatGptImageInputShape,
105
- outputSchema: chatGptImageOutputShape,
106
- }, async (input) => {
104
+ inputSchema: z.object(chatGptImageInputShape),
105
+ outputSchema: z.object(chatGptImageOutputShape),
106
+ }, async (input, context) => {
107
107
  const textContent = (text) => [{ type: "text", text }];
108
108
  let parsed;
109
109
  try {
@@ -116,7 +116,7 @@ export function registerChatGptImageTool(server) {
116
116
  };
117
117
  }
118
118
  const consultInput = buildChatGptImageConsultInput(parsed);
119
- const result = await runConsultTool(consultInput, { server: server.server });
119
+ const result = await runConsultTool(consultInput, { log: context.mcpReq.log });
120
120
  if (result.isError || !result.structuredContent) {
121
121
  return result;
122
122
  }
@@ -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) {
@@ -26,7 +25,9 @@ import { loadUserConfig } from "../../config.js";
26
25
  import { resolveNotificationSettings } from "../../cli/notifier.js";
27
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."),
@@ -73,9 +74,9 @@ const consultInputShape = {
73
74
  .optional()
74
75
  .describe("Browser-only: model picker strategy. Mirrors the CLI --browser-model-strategy flag."),
75
76
  browserResearchMode: z
76
- .enum(["deep"])
77
+ .enum(["search", "deep"])
77
78
  .optional()
78
- .describe("Browser-only: activate ChatGPT Deep Research mode for broad web research."),
79
+ .describe("Browser-only: explicitly select ChatGPT Web Search or Deep Research."),
79
80
  browserArchive: z
80
81
  .enum(["auto", "always", "never"])
81
82
  .optional()
@@ -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) {
@@ -387,7 +393,7 @@ export function formatConsultDryRunResolved(details) {
387
393
  }
388
394
  return lines;
389
395
  }
390
- export async function runConsultTool(input, { server }) {
396
+ export async function runConsultTool(input, { log: requestLog, launchDetached = launchDetachedSession, }) {
391
397
  const textContent = (text) => [{ type: "text", text }];
392
398
  let parsedInput;
393
399
  try {
@@ -399,7 +405,7 @@ export async function runConsultTool(input, { server }) {
399
405
  content: textContent(error instanceof Error ? error.message : String(error)),
400
406
  };
401
407
  }
402
- 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;
403
409
  const { config: userConfig } = await loadUserConfig();
404
410
  let runOptions;
405
411
  let resolvedEngine;
@@ -428,12 +434,7 @@ export async function runConsultTool(input, { server }) {
428
434
  };
429
435
  }
430
436
  const cwd = process.cwd();
431
- const sendLog = (text, level = "info") => server
432
- .sendLoggingMessage(LoggingMessageNotificationParamsSchema.parse({
433
- level,
434
- data: { text, bytes: Buffer.byteLength(text, "utf8") },
435
- }))
436
- .catch(() => { });
437
+ const sendLog = (text, level = "info") => requestLog(level, { text, bytes: Buffer.byteLength(text, "utf8") }).catch(() => { });
437
438
  const resolvedRemote = resolveRemoteServiceConfig({ userConfig, env: process.env });
438
439
  const imageOutputPath = runOptions.generateImage ?? runOptions.outputPath;
439
440
  if (resolvedEngine === "browser" && resolvedRemote.host && imageOutputPath) {
@@ -490,6 +491,18 @@ export async function runConsultTool(input, { server }) {
490
491
  },
491
492
  };
492
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
+ }
493
506
  const browserGuard = ensureBrowserAvailable(resolvedEngine, {
494
507
  remoteHost: resolvedRemote.host,
495
508
  });
@@ -525,8 +538,58 @@ export async function runConsultTool(input, { server }) {
525
538
  mode: resolvedEngine,
526
539
  slug,
527
540
  browserConfig,
528
- waitPreference: true,
541
+ waitPreference: waitForCompletion,
529
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
+ }
530
593
  const logWriter = sessionStore.createLogWriter(sessionMeta.id);
531
594
  // Stream logs to both the session log and MCP logging notifications, but avoid buffering in memory
532
595
  const log = (line) => {
@@ -594,9 +657,8 @@ export async function runConsultTool(input, { server }) {
594
657
  export function registerConsultTool(server) {
595
658
  server.registerTool("consult", {
596
659
  title: "Run an oracle session",
597
- 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).',
598
- // Cast to any to satisfy SDK typings across differing Zod versions.
599
- inputSchema: consultInputShape,
600
- outputSchema: consultOutputShape,
601
- }, 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 }));
602
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
+ }
@@ -23,12 +23,13 @@ export const consultInputSchema = z
23
23
  browserBundleFormat: z.enum(["auto", "text", "zip"]).optional(),
24
24
  browserThinkingTime: browserThinkingTimeInputSchema.optional(),
25
25
  browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
26
- browserResearchMode: z.enum(["deep"]).optional(),
26
+ browserResearchMode: z.enum(["search", "deep"]).optional(),
27
27
  browserArchive: z.enum(["auto", "always", "never"]).optional(),
28
28
  browserFollowUps: z.array(z.string()).optional(),
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();
@@ -30,6 +30,20 @@ const countTokensAnthropic = (input) => {
30
30
  // supported limit at the base-rate boundary until cost estimation supports tiers.
31
31
  const GPT_5_6_BASE_RATE_INPUT_LIMIT = 272_000;
32
32
  export const MODEL_CONFIGS = {
33
+ "gpt-6-astra": {
34
+ model: "gpt-6-astra",
35
+ provider: "openai",
36
+ tokenizer: countTokensGpt5,
37
+ // Conservative base-rate budget, not Astra's maximum context window.
38
+ // https://developers.openai.com/api/docs/models/gpt-6-astra
39
+ inputLimit: 272_000,
40
+ pricing: {
41
+ inputPerToken: 10 / 1_000_000,
42
+ outputPerToken: 50 / 1_000_000,
43
+ },
44
+ reasoning: { effort: "xhigh" },
45
+ searchToolType: "web_search",
46
+ },
33
47
  "gpt-5.6": {
34
48
  model: "gpt-5.6",
35
49
  provider: "openai",
@@ -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);
@@ -1,4 +1,5 @@
1
1
  const MODEL_ID_MAP = {
2
+ "gpt-6-astra": "gpt-6-astra",
2
3
  "gemini-3.1-flash-lite": "gemini-3.1-flash-lite",
3
4
  "gemini-3.1-pro": "gemini-3.1-pro-preview",
4
5
  "gemini-3.5-flash": "gemini-3.5-flash",
@@ -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";