@steipete/oracle 0.18.0 → 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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  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 +119 -16
  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 +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. package/dist/src/browser/windowsCookies.js +0 -219
@@ -1,7 +1,9 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, } from "./models.js";
4
- const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
4
+ import { fetchGeminiWebResource } from "./http.js";
5
+ import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, getGeminiWebModelSelection, } from "./models.js";
6
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
5
7
  const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
6
8
  const GEMINI_APP_URL = "https://gemini.google.com/app";
7
9
  const GEMINI_STREAM_GENERATE_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate";
@@ -61,7 +63,7 @@ function buildCookieHeader(cookieMap) {
61
63
  }
62
64
  export async function fetchGeminiAccessToken(cookieMap, signal) {
63
65
  const cookieHeader = buildCookieHeader(cookieMap);
64
- const res = await fetch(GEMINI_APP_URL, {
66
+ const res = await fetchGeminiWebResource(GEMINI_APP_URL, {
65
67
  redirect: "follow",
66
68
  signal,
67
69
  headers: {
@@ -112,7 +114,7 @@ function ensureFullSizeImageUrl(url) {
112
114
  async function fetchWithCookiePreservingRedirects(url, init, signal, maxRedirects = 10) {
113
115
  let current = url;
114
116
  for (let i = 0; i <= maxRedirects; i += 1) {
115
- const res = await fetch(current, { ...init, redirect: "manual", signal });
117
+ const res = await fetchGeminiWebResource(current, { ...init, redirect: "manual", signal });
116
118
  if (res.status >= 300 && res.status < 400) {
117
119
  const location = res.headers.get("location");
118
120
  if (!location)
@@ -146,7 +148,7 @@ async function uploadGeminiFile(filePath, signal) {
146
148
  const mimeType = resolveGeminiUploadMimeType(absPath);
147
149
  const form = new FormData();
148
150
  form.append("file", new Blob([data], { type: mimeType }), fileName);
149
- const res = await fetch(GEMINI_UPLOAD_URL, {
151
+ const res = await fetchGeminiWebResource(GEMINI_UPLOAD_URL, {
150
152
  method: "POST",
151
153
  redirect: "follow",
152
154
  signal,
@@ -162,18 +164,38 @@ async function uploadGeminiFile(filePath, signal) {
162
164
  }
163
165
  return { id: text, name: fileName, mimeType };
164
166
  }
165
- function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
166
- const promptPayload = uploaded.length > 0
167
- ? [
168
- prompt,
169
- 0,
170
- null,
171
- // Format: [[[fileId, 1, null, "mimeType"], "filename", ...]]
172
- uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
173
- ]
174
- : [prompt];
175
- const innerList = [promptPayload, null, chatMetadata ?? null];
176
- return JSON.stringify([null, JSON.stringify(innerList)]);
167
+ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata, model, requestId) {
168
+ const request = Array(97).fill(null);
169
+ request[0] = [
170
+ prompt,
171
+ 0,
172
+ null,
173
+ uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
174
+ null,
175
+ null,
176
+ 0,
177
+ ];
178
+ request[1] = ["en"];
179
+ request[2] = chatMetadata ?? ["", "", "", null, null, null, null, null, null, ""];
180
+ request[6] = [1];
181
+ request[7] = 1;
182
+ request[10] = 1;
183
+ request[11] = 0;
184
+ request[17] = [[0]];
185
+ request[18] = 0;
186
+ request[27] = 1;
187
+ request[30] = [4];
188
+ request[41] = [1];
189
+ request[53] = 0;
190
+ request[59] = requestId;
191
+ request[61] = [];
192
+ request[68] = 2;
193
+ const selection = getGeminiWebModelSelection(model);
194
+ request[79] = selection.modelCode;
195
+ request[80] = selection.thinkingCode;
196
+ request[91] = 0;
197
+ request[96] = chatMetadata == null ? 1 : 0;
198
+ return JSON.stringify([null, JSON.stringify(request)]);
177
199
  }
178
200
  export function parseGeminiStreamGenerateResponse(rawText) {
179
201
  const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
@@ -274,11 +296,12 @@ export async function runGeminiWebOnce(input) {
274
296
  }
275
297
  uploaded.push(await uploadGeminiFile(file, input.signal));
276
298
  }
277
- const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
299
+ const requestId = randomUUID().toUpperCase();
300
+ const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null, input.model, requestId);
278
301
  const params = new URLSearchParams();
279
302
  params.set("at", at);
280
303
  params.set("f.req", fReq);
281
- const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
304
+ const res = await fetchGeminiWebResource(GEMINI_STREAM_GENERATE_URL, {
282
305
  method: "POST",
283
306
  redirect: "follow",
284
307
  signal: input.signal,
@@ -290,6 +313,9 @@ export async function runGeminiWebOnce(input) {
290
313
  "user-agent": USER_AGENT,
291
314
  cookie: cookieHeader,
292
315
  [MODEL_HEADER_NAME]: buildGeminiWebModelHeader(input.model),
316
+ "x-goog-ext-73010989-jspb": "[0]",
317
+ "x-goog-ext-73010990-jspb": "[0]",
318
+ "x-goog-ext-525005358-jspb": JSON.stringify([requestId, 1]),
293
319
  },
294
320
  body: params.toString(),
295
321
  });
@@ -338,10 +364,30 @@ export async function runGeminiWebOnce(input) {
338
364
  export async function runGeminiWebWithFallback(input) {
339
365
  const attempt = await runGeminiWebOnce(input);
340
366
  if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== FALLBACK_GEMINI_WEB_MODEL) {
367
+ if (input.allowModelFallback === false) {
368
+ throw new Error(`Requested Gemini web model ${input.model} is unavailable and model fallback is disabled.`);
369
+ }
341
370
  const fallback = await runGeminiWebOnce({ ...input, model: FALLBACK_GEMINI_WEB_MODEL });
342
- return { ...fallback, effectiveModel: FALLBACK_GEMINI_WEB_MODEL };
371
+ return {
372
+ ...assertGeminiWebRunSucceeded(fallback),
373
+ effectiveModel: FALLBACK_GEMINI_WEB_MODEL,
374
+ };
375
+ }
376
+ return { ...assertGeminiWebRunSucceeded(attempt), effectiveModel: input.model };
377
+ }
378
+ function assertGeminiWebRunSucceeded(output) {
379
+ if (output.errorCode !== undefined) {
380
+ throw new Error(`Gemini web request failed with error code ${output.errorCode}.`);
381
+ }
382
+ if (output.errorMessage) {
383
+ throw new Error(`Gemini web request failed: ${output.errorMessage}`);
384
+ }
385
+ if (!output.text.trim() &&
386
+ output.images.length === 0 &&
387
+ extractGgdlUrls(output.rawResponseText).length === 0) {
388
+ throw new Error("Gemini web request ended without a response.");
343
389
  }
344
- return { ...attempt, effectiveModel: input.model };
390
+ return output;
345
391
  }
346
392
  export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath, signal) {
347
393
  const generatedOrWeb = output.images.find((img) => img.kind === "generated") ?? output.images[0];
@@ -325,6 +325,7 @@ export function createGeminiWebExecutor(geminiOptions) {
325
325
  cookieMap: cookieResult.cookieMap,
326
326
  chatMetadata: null,
327
327
  signal: controller.signal,
328
+ allowModelFallback: geminiOptions.allowModelFallback,
328
329
  });
329
330
  const editPrompt = `Use image generation tool to ${prompt}`;
330
331
  const out = await runGeminiWebWithFallback({
@@ -334,6 +335,7 @@ export function createGeminiWebExecutor(geminiOptions) {
334
335
  cookieMap: cookieResult.cookieMap,
335
336
  chatMetadata: intro.metadata,
336
337
  signal: controller.signal,
338
+ allowModelFallback: geminiOptions.allowModelFallback,
337
339
  });
338
340
  response = {
339
341
  text: out.text ?? null,
@@ -357,6 +359,7 @@ export function createGeminiWebExecutor(geminiOptions) {
357
359
  cookieMap: cookieResult.cookieMap,
358
360
  chatMetadata: null,
359
361
  signal: controller.signal,
362
+ allowModelFallback: geminiOptions.allowModelFallback,
360
363
  });
361
364
  response = {
362
365
  text: out.text ?? null,
@@ -379,6 +382,7 @@ export function createGeminiWebExecutor(geminiOptions) {
379
382
  cookieMap: cookieResult.cookieMap,
380
383
  chatMetadata: null,
381
384
  signal: controller.signal,
385
+ allowModelFallback: geminiOptions.allowModelFallback,
382
386
  });
383
387
  response = {
384
388
  text: out.text ?? null,
@@ -0,0 +1,15 @@
1
+ export async function fetchGeminiWebResource(url, init = {}) {
2
+ try {
3
+ return await fetch(url, init);
4
+ }
5
+ catch (error) {
6
+ const cause = error instanceof Error ? error.cause : undefined;
7
+ if (cause &&
8
+ typeof cause === "object" &&
9
+ "code" in cause &&
10
+ cause.code === "UND_ERR_HEADERS_OVERFLOW") {
11
+ throw new Error("Gemini response headers exceed Node's configured limit. Start Oracle with NODE_OPTIONS=--max-http-header-size=65536 (preserving any existing Node options) and retry.", { cause: error });
12
+ }
13
+ throw error;
14
+ }
15
+ }
@@ -6,29 +6,29 @@ const MODEL_SPECS = {
6
6
  hash: "1d44b34bcaa1c04d",
7
7
  modelCode: 6,
8
8
  thinkingCode: 1,
9
+ capacity: 2,
9
10
  },
10
11
  "gemini-3.5-flash": {
11
12
  hash: "56fdd199312815e2",
12
13
  modelCode: 1,
13
14
  thinkingCode: 1,
15
+ capacity: 2,
14
16
  },
15
17
  "gemini-3.1-pro": {
16
- hash: "797f3d0293f288ad",
18
+ hash: "e6fa609c3fa255c0",
17
19
  modelCode: 3,
18
20
  thinkingCode: 1,
21
+ capacity: 2,
19
22
  },
20
23
  "gemini-3-pro-deep-think": {
21
- hash: "797f3d0293f288ad",
22
- modelCode: 3,
23
- thinkingCode: 3,
24
+ hash: "e051ce1aa80aa576",
25
+ modelCode: 5,
26
+ thinkingCode: 2,
27
+ capacity: 2,
24
28
  },
25
29
  };
26
- let clientId;
27
- function getGeminiWebClientId() {
28
- clientId ??= randomUUID().toUpperCase();
29
- return clientId;
30
- }
31
- export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClientId()) {
30
+ const clientId = randomUUID().toUpperCase();
31
+ export function buildGeminiWebModelHeader(model, webClientId = clientId) {
32
32
  const spec = MODEL_SPECS[model];
33
33
  return JSON.stringify([
34
34
  1,
@@ -42,7 +42,7 @@ export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClien
42
42
  [4, 5, 6, 8],
43
43
  null,
44
44
  null,
45
- 3,
45
+ spec.capacity,
46
46
  null,
47
47
  null,
48
48
  spec.modelCode,
@@ -50,6 +50,10 @@ export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClien
50
50
  webClientId,
51
51
  ]);
52
52
  }
53
+ export function getGeminiWebModelSelection(model) {
54
+ const { modelCode, thinkingCode } = MODEL_SPECS[model];
55
+ return { modelCode, thinkingCode };
56
+ }
53
57
  export function resolveGeminiWebModel(desiredModel, log) {
54
58
  const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
55
59
  if (!desired)
@@ -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."),
@@ -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);