@andrewting19/oracle 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +300 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +1480 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/scripts/agent-send.js +147 -0
  7. package/dist/scripts/browser-tools.js +536 -0
  8. package/dist/scripts/check.js +21 -0
  9. package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
  10. package/dist/scripts/docs-list.js +110 -0
  11. package/dist/scripts/git-policy.js +127 -0
  12. package/dist/scripts/run-cli.js +14 -0
  13. package/dist/scripts/runner.js +1386 -0
  14. package/dist/scripts/test-browser.js +106 -0
  15. package/dist/scripts/test-remote-chrome.js +68 -0
  16. package/dist/src/bridge/connection.js +103 -0
  17. package/dist/src/bridge/userConfigFile.js +28 -0
  18. package/dist/src/browser/actions/assistantResponse.js +1085 -0
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
  20. package/dist/src/browser/actions/attachments.js +1939 -0
  21. package/dist/src/browser/actions/domEvents.js +19 -0
  22. package/dist/src/browser/actions/modelSelection.js +549 -0
  23. package/dist/src/browser/actions/navigation.js +451 -0
  24. package/dist/src/browser/actions/promptComposer.js +487 -0
  25. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  26. package/dist/src/browser/actions/thinkingTime.js +206 -0
  27. package/dist/src/browser/chromeLifecycle.js +346 -0
  28. package/dist/src/browser/config.js +105 -0
  29. package/dist/src/browser/constants.js +75 -0
  30. package/dist/src/browser/cookies.js +191 -0
  31. package/dist/src/browser/detect.js +164 -0
  32. package/dist/src/browser/domDebug.js +36 -0
  33. package/dist/src/browser/index.js +1826 -0
  34. package/dist/src/browser/modelStrategy.js +13 -0
  35. package/dist/src/browser/pageActions.js +5 -0
  36. package/dist/src/browser/policies.js +46 -0
  37. package/dist/src/browser/profileState.js +285 -0
  38. package/dist/src/browser/prompt.js +182 -0
  39. package/dist/src/browser/promptSummary.js +20 -0
  40. package/dist/src/browser/providerDomFlow.js +17 -0
  41. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  42. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  43. package/dist/src/browser/providers/index.js +2 -0
  44. package/dist/src/browser/reattach.js +189 -0
  45. package/dist/src/browser/reattachHelpers.js +387 -0
  46. package/dist/src/browser/sessionRunner.js +131 -0
  47. package/dist/src/browser/types.js +1 -0
  48. package/dist/src/browser/utils.js +122 -0
  49. package/dist/src/browserMode.js +1 -0
  50. package/dist/src/cli/bridge/claudeConfig.js +54 -0
  51. package/dist/src/cli/bridge/client.js +81 -0
  52. package/dist/src/cli/bridge/codexConfig.js +43 -0
  53. package/dist/src/cli/bridge/doctor.js +115 -0
  54. package/dist/src/cli/bridge/host.js +261 -0
  55. package/dist/src/cli/browserConfig.js +293 -0
  56. package/dist/src/cli/browserDefaults.js +82 -0
  57. package/dist/src/cli/bundleWarnings.js +9 -0
  58. package/dist/src/cli/clipboard.js +10 -0
  59. package/dist/src/cli/detach.js +14 -0
  60. package/dist/src/cli/dryRun.js +109 -0
  61. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  62. package/dist/src/cli/engine.js +41 -0
  63. package/dist/src/cli/errorUtils.js +9 -0
  64. package/dist/src/cli/fileSize.js +11 -0
  65. package/dist/src/cli/format.js +13 -0
  66. package/dist/src/cli/help.js +77 -0
  67. package/dist/src/cli/hiddenAliases.js +22 -0
  68. package/dist/src/cli/markdownBundle.js +21 -0
  69. package/dist/src/cli/markdownRenderer.js +97 -0
  70. package/dist/src/cli/notifier.js +316 -0
  71. package/dist/src/cli/options.js +305 -0
  72. package/dist/src/cli/oscUtils.js +2 -0
  73. package/dist/src/cli/promptRequirement.js +17 -0
  74. package/dist/src/cli/renderFlags.js +9 -0
  75. package/dist/src/cli/renderOutput.js +26 -0
  76. package/dist/src/cli/rootAlias.js +30 -0
  77. package/dist/src/cli/runOptions.js +90 -0
  78. package/dist/src/cli/sessionCommand.js +121 -0
  79. package/dist/src/cli/sessionDisplay.js +670 -0
  80. package/dist/src/cli/sessionLineage.js +60 -0
  81. package/dist/src/cli/sessionRunner.js +630 -0
  82. package/dist/src/cli/sessionTable.js +96 -0
  83. package/dist/src/cli/tagline.js +255 -0
  84. package/dist/src/cli/tui/index.js +499 -0
  85. package/dist/src/cli/writeOutputPath.js +21 -0
  86. package/dist/src/config.js +26 -0
  87. package/dist/src/gemini-web/browserSessionManager.js +81 -0
  88. package/dist/src/gemini-web/client.js +339 -0
  89. package/dist/src/gemini-web/executionClients.js +1 -0
  90. package/dist/src/gemini-web/executionMode.js +16 -0
  91. package/dist/src/gemini-web/executor.js +443 -0
  92. package/dist/src/gemini-web/index.js +1 -0
  93. package/dist/src/gemini-web/types.js +1 -0
  94. package/dist/src/heartbeat.js +43 -0
  95. package/dist/src/mcp/server.js +40 -0
  96. package/dist/src/mcp/tools/consult.js +307 -0
  97. package/dist/src/mcp/tools/sessionResources.js +75 -0
  98. package/dist/src/mcp/tools/sessions.js +114 -0
  99. package/dist/src/mcp/types.js +22 -0
  100. package/dist/src/mcp/utils.js +45 -0
  101. package/dist/src/oracle/background.js +141 -0
  102. package/dist/src/oracle/claude.js +107 -0
  103. package/dist/src/oracle/client.js +235 -0
  104. package/dist/src/oracle/config.js +192 -0
  105. package/dist/src/oracle/errors.js +132 -0
  106. package/dist/src/oracle/files.js +402 -0
  107. package/dist/src/oracle/finishLine.js +34 -0
  108. package/dist/src/oracle/format.js +30 -0
  109. package/dist/src/oracle/fsAdapter.js +10 -0
  110. package/dist/src/oracle/gemini.js +194 -0
  111. package/dist/src/oracle/logging.js +36 -0
  112. package/dist/src/oracle/markdown.js +46 -0
  113. package/dist/src/oracle/modelResolver.js +183 -0
  114. package/dist/src/oracle/multiModelRunner.js +153 -0
  115. package/dist/src/oracle/oscProgress.js +24 -0
  116. package/dist/src/oracle/promptAssembly.js +16 -0
  117. package/dist/src/oracle/request.js +58 -0
  118. package/dist/src/oracle/run.js +628 -0
  119. package/dist/src/oracle/runUtils.js +34 -0
  120. package/dist/src/oracle/tokenEstimate.js +37 -0
  121. package/dist/src/oracle/tokenStats.js +39 -0
  122. package/dist/src/oracle/tokenStringifier.js +24 -0
  123. package/dist/src/oracle/types.js +1 -0
  124. package/dist/src/oracle.js +12 -0
  125. package/dist/src/oracleHome.js +13 -0
  126. package/dist/src/remote/client.js +129 -0
  127. package/dist/src/remote/health.js +113 -0
  128. package/dist/src/remote/remoteServiceConfig.js +31 -0
  129. package/dist/src/remote/server.js +544 -0
  130. package/dist/src/remote/types.js +1 -0
  131. package/dist/src/sessionManager.js +643 -0
  132. package/dist/src/sessionStore.js +56 -0
  133. package/dist/src/version.js +39 -0
  134. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  135. package/dist/vendor/oracle-notifier/README.md +26 -0
  136. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  137. package/package.json +120 -0
  138. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  139. package/vendor/oracle-notifier/README.md +26 -0
  140. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,443 @@
1
+ import path from "node:path";
2
+ import { getCookies } from "@steipete/sweet-cookie";
3
+ import { runProviderDomFlow } from "../browser/providerDomFlow.js";
4
+ import { delay } from "../browser/utils.js";
5
+ import { runGeminiWebWithFallback, saveFirstGeminiImageFromOutput } from "./client.js";
6
+ import { geminiDeepThinkDomProvider } from "../browser/providers/index.js";
7
+ import { openGeminiBrowserSession } from "./browserSessionManager.js";
8
+ import { selectGeminiExecutionMode } from "./executionMode.js";
9
+ const GEMINI_COOKIE_NAMES = [
10
+ "__Secure-1PSID",
11
+ "__Secure-1PSIDTS",
12
+ "__Secure-1PSIDCC",
13
+ "__Secure-1PAPISID",
14
+ "NID",
15
+ "AEC",
16
+ "SOCS",
17
+ "__Secure-BUCKET",
18
+ "__Secure-ENID",
19
+ "SID",
20
+ "HSID",
21
+ "SSID",
22
+ "APISID",
23
+ "SAPISID",
24
+ "__Secure-3PSID",
25
+ "__Secure-3PSIDTS",
26
+ "__Secure-3PAPISID",
27
+ "SIDCC",
28
+ ];
29
+ const GEMINI_REQUIRED_COOKIES = ["__Secure-1PSID", "__Secure-1PSIDTS"];
30
+ function estimateTokenCount(text) {
31
+ return Math.ceil(text.length / 4);
32
+ }
33
+ function resolveInvocationPath(value) {
34
+ if (!value)
35
+ return undefined;
36
+ const trimmed = value.trim();
37
+ if (!trimmed)
38
+ return undefined;
39
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
40
+ }
41
+ function resolveGeminiWebModel(desiredModel, log) {
42
+ const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
43
+ if (!desired)
44
+ return "gemini-3-pro";
45
+ const normalized = desired.toLowerCase().replace(/[_\s]+/g, "-");
46
+ switch (normalized) {
47
+ case "gemini-3-pro":
48
+ case "gemini-3.0-pro":
49
+ return "gemini-3-pro";
50
+ case "gemini-3-deep-think":
51
+ case "gemini-3-pro-deep-think":
52
+ case "gemini-3-pro-deepthink":
53
+ return "gemini-3-pro-deep-think";
54
+ case "gemini-2.5-pro":
55
+ return "gemini-2.5-pro";
56
+ case "gemini-2.5-flash":
57
+ return "gemini-2.5-flash";
58
+ default:
59
+ if (normalized.startsWith("gemini-") || normalized.includes("gemini")) {
60
+ log?.(`[gemini-web] Unsupported Gemini web model "${desired}". Falling back to gemini-3-pro.`);
61
+ }
62
+ return "gemini-3-pro";
63
+ }
64
+ }
65
+ function resolveCookieDomain(cookie) {
66
+ const rawDomain = cookie.domain?.trim();
67
+ if (rawDomain) {
68
+ return rawDomain.startsWith(".") ? rawDomain.slice(1) : rawDomain;
69
+ }
70
+ const rawUrl = cookie.url?.trim();
71
+ if (rawUrl) {
72
+ try {
73
+ return new URL(rawUrl).hostname;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ function pickCookieValue(cookies, name) {
82
+ const matches = cookies.filter((cookie) => cookie.name === name && typeof cookie.value === "string");
83
+ if (matches.length === 0)
84
+ return undefined;
85
+ const preferredDomain = matches.find((cookie) => {
86
+ const domain = resolveCookieDomain(cookie);
87
+ return domain === "google.com" && (cookie.path ?? "/") === "/";
88
+ });
89
+ const googleDomain = matches.find((cookie) => (resolveCookieDomain(cookie) ?? "").endsWith("google.com"));
90
+ return (preferredDomain ?? googleDomain ?? matches[0])?.value;
91
+ }
92
+ function buildGeminiCookieMap(cookies) {
93
+ const cookieMap = {};
94
+ for (const name of GEMINI_COOKIE_NAMES) {
95
+ const value = pickCookieValue(cookies, name);
96
+ if (value)
97
+ cookieMap[name] = value;
98
+ }
99
+ return cookieMap;
100
+ }
101
+ function hasRequiredGeminiCookies(cookieMap) {
102
+ return GEMINI_REQUIRED_COOKIES.every((name) => Boolean(cookieMap[name]));
103
+ }
104
+ const GEMINI_CDP_COOKIE_URLS = [
105
+ "https://gemini.google.com",
106
+ "https://accounts.google.com",
107
+ "https://www.google.com",
108
+ ];
109
+ async function loadGeminiCookiesFromCDP(browserConfig, log) {
110
+ const session = await openGeminiBrowserSession({
111
+ browserConfig,
112
+ keepBrowserDefault: false,
113
+ purpose: "Gemini manual-login cookie extraction (no keychain)",
114
+ log,
115
+ });
116
+ try {
117
+ const client = session.client;
118
+ const { Network, Page } = client;
119
+ await Network.enable({});
120
+ await Page.enable();
121
+ log?.("[gemini-web] Navigating to gemini.google.com for sign-in/cookie capture...");
122
+ await Page.navigate({ url: "https://gemini.google.com" });
123
+ await delay(2_000);
124
+ const pollTimeoutMs = 5 * 60_000;
125
+ const pollIntervalMs = 2_000;
126
+ const deadline = Date.now() + pollTimeoutMs;
127
+ let lastNotice = 0;
128
+ let cookieMap = {};
129
+ while (Date.now() < deadline) {
130
+ const { cookies } = await Network.getCookies({ urls: GEMINI_CDP_COOKIE_URLS });
131
+ cookieMap = buildGeminiCookieMap(cookies);
132
+ if (hasRequiredGeminiCookies(cookieMap)) {
133
+ log?.(`[gemini-web] Extracted ${Object.keys(cookieMap).length} Gemini cookie(s) via CDP.`);
134
+ return { cookieMap, warnings: [] };
135
+ }
136
+ const now = Date.now();
137
+ if (now - lastNotice > 10_000) {
138
+ log?.("[gemini-web] Waiting for Google sign-in... please sign in in the opened Chrome window.");
139
+ lastNotice = now;
140
+ }
141
+ await delay(pollIntervalMs);
142
+ }
143
+ throw new Error("Timed out waiting for Google sign-in (5 minutes). Please sign in and retry.");
144
+ }
145
+ finally {
146
+ await session.close();
147
+ }
148
+ }
149
+ async function runGeminiDeepThinkViaBrowser(prompt, browserConfig, log) {
150
+ const session = await openGeminiBrowserSession({
151
+ browserConfig,
152
+ keepBrowserDefault: true,
153
+ purpose: "Gemini Deep Think",
154
+ log,
155
+ });
156
+ try {
157
+ const client = session.client;
158
+ const { Runtime, Page } = client;
159
+ if (!Runtime ||
160
+ typeof Runtime.enable !== "function" ||
161
+ typeof Runtime.evaluate !== "function") {
162
+ throw new Error("Chrome Runtime domain unavailable for Gemini Deep Think DOM automation.");
163
+ }
164
+ if (!Page || typeof Page.enable !== "function" || typeof Page.navigate !== "function") {
165
+ throw new Error("Chrome Page domain unavailable for Gemini Deep Think DOM automation.");
166
+ }
167
+ await Runtime.enable();
168
+ await Page.enable();
169
+ const evaluate = async (expression) => {
170
+ const { result } = await Runtime.evaluate({ expression, returnByValue: true });
171
+ return result?.value;
172
+ };
173
+ log?.("[gemini-web] Navigating to gemini.google.com...");
174
+ await Page.navigate({ url: "https://gemini.google.com/app" });
175
+ await delay(3_000);
176
+ const domResult = await runProviderDomFlow(geminiDeepThinkDomProvider, {
177
+ prompt,
178
+ evaluate,
179
+ delay,
180
+ log,
181
+ state: {
182
+ inputTimeoutMs: browserConfig?.inputTimeoutMs,
183
+ timeoutMs: browserConfig?.timeoutMs,
184
+ },
185
+ });
186
+ log?.(`[gemini-web] Deep Think response received (${domResult.text.length} chars).`);
187
+ return domResult;
188
+ }
189
+ finally {
190
+ await session.close();
191
+ }
192
+ }
193
+ async function loadGeminiCookiesFromInline(browserConfig, log) {
194
+ const inline = browserConfig?.inlineCookies;
195
+ if (!inline || inline.length === 0)
196
+ return { cookieMap: {}, warnings: [] };
197
+ const cookieMap = buildGeminiCookieMap(inline.filter((cookie) => Boolean(cookie?.name && typeof cookie.value === "string")));
198
+ if (Object.keys(cookieMap).length > 0) {
199
+ const source = browserConfig?.inlineCookiesSource ?? "inline";
200
+ log?.(`[gemini-web] Loaded Gemini cookies from inline payload (${source}): ${Object.keys(cookieMap).length} cookie(s).`);
201
+ }
202
+ else {
203
+ log?.("[gemini-web] Inline cookie payload provided but no Gemini cookies matched.");
204
+ }
205
+ return { cookieMap, warnings: [] };
206
+ }
207
+ async function loadGeminiCookiesFromChrome(browserConfig, log) {
208
+ try {
209
+ // Learned: Gemini web relies on Google auth cookies in the *browser* profile, not API keys.
210
+ const profileCandidate = browserConfig?.chromeCookiePath ?? browserConfig?.chromeProfile ?? undefined;
211
+ const profile = typeof profileCandidate === "string" && profileCandidate.trim().length > 0
212
+ ? profileCandidate.trim()
213
+ : undefined;
214
+ const sources = [
215
+ "https://gemini.google.com",
216
+ "https://accounts.google.com",
217
+ "https://www.google.com",
218
+ ];
219
+ const { cookies, warnings } = await getCookies({
220
+ url: sources[0],
221
+ origins: sources,
222
+ names: [...GEMINI_COOKIE_NAMES],
223
+ browsers: ["chrome"],
224
+ mode: "merge",
225
+ chromeProfile: profile,
226
+ timeoutMs: 5_000,
227
+ });
228
+ if (warnings.length && log?.verbose) {
229
+ log(`[gemini-web] Cookie warnings:\n- ${warnings.join("\n- ")}`);
230
+ }
231
+ const cookieMap = buildGeminiCookieMap(cookies);
232
+ log?.(`[gemini-web] Loaded Gemini cookies from Chrome (node): ${Object.keys(cookieMap).length} cookie(s).`);
233
+ return { cookieMap, warnings };
234
+ }
235
+ catch (error) {
236
+ log?.(`[gemini-web] Failed to load Chrome cookies via node: ${error instanceof Error ? error.message : String(error ?? "")}`);
237
+ return { cookieMap: {}, warnings: [] };
238
+ }
239
+ }
240
+ function formatGeminiCookieError(warnings) {
241
+ const base = "Gemini browser mode requires Chrome cookies for google.com (missing __Secure-1PSID/__Secure-1PSIDTS).";
242
+ const guidance = "Try --browser-manual-login or --browser-inline-cookies-file if local cookie extraction is unavailable.";
243
+ if (warnings.length === 0) {
244
+ return `${base} ${guidance}`;
245
+ }
246
+ return `${base}\nCookie read warnings:\n- ${warnings.join("\n- ")}\n${guidance}`;
247
+ }
248
+ async function loadGeminiCookies(browserConfig, log, options) {
249
+ const inlineResult = await loadGeminiCookiesFromInline(browserConfig, log);
250
+ const hasInlineRequired = hasRequiredGeminiCookies(inlineResult.cookieMap);
251
+ if (hasInlineRequired) {
252
+ return inlineResult;
253
+ }
254
+ const manualNoKeychain = Boolean(browserConfig?.manualLogin) || Boolean(options?.preferManualNoKeychain);
255
+ if (manualNoKeychain) {
256
+ log?.("[gemini-web] Using manual-login cookie extraction path (no keychain cookie read).");
257
+ const cdpResult = await loadGeminiCookiesFromCDP(browserConfig, log);
258
+ return {
259
+ cookieMap: { ...cdpResult.cookieMap, ...inlineResult.cookieMap },
260
+ warnings: [...inlineResult.warnings, ...cdpResult.warnings],
261
+ };
262
+ }
263
+ if (browserConfig?.cookieSync === false && !hasInlineRequired) {
264
+ log?.("[gemini-web] Cookie sync disabled and inline cookies missing Gemini auth tokens.");
265
+ return inlineResult;
266
+ }
267
+ const chromeResult = await loadGeminiCookiesFromChrome(browserConfig, log);
268
+ return {
269
+ cookieMap: { ...chromeResult.cookieMap, ...inlineResult.cookieMap },
270
+ warnings: [...inlineResult.warnings, ...chromeResult.warnings],
271
+ };
272
+ }
273
+ export function createGeminiWebExecutor(geminiOptions) {
274
+ return async (runOptions) => {
275
+ const startTime = Date.now();
276
+ const log = runOptions.log;
277
+ log?.("[gemini-web] Starting Gemini web executor (TypeScript)");
278
+ const model = resolveGeminiWebModel(runOptions.config?.desiredModel, log);
279
+ const generateImagePath = resolveInvocationPath(geminiOptions.generateImage);
280
+ const editImagePath = resolveInvocationPath(geminiOptions.editImage);
281
+ const outputPath = resolveInvocationPath(geminiOptions.outputPath);
282
+ const attachmentPaths = (runOptions.attachments ?? []).map((attachment) => attachment.path);
283
+ let prompt = runOptions.prompt;
284
+ if (geminiOptions.aspectRatio && (generateImagePath || editImagePath)) {
285
+ prompt = `${prompt} (aspect ratio: ${geminiOptions.aspectRatio})`;
286
+ }
287
+ if (geminiOptions.youtube) {
288
+ prompt = `${prompt}\n\nYouTube video: ${geminiOptions.youtube}`;
289
+ }
290
+ if (generateImagePath && !editImagePath) {
291
+ prompt = `Generate an image: ${prompt}`;
292
+ }
293
+ const modeSelection = selectGeminiExecutionMode({
294
+ model,
295
+ attachmentPaths,
296
+ generateImagePath,
297
+ editImagePath,
298
+ });
299
+ const domClient = {
300
+ mode: "dom",
301
+ execute: async () => {
302
+ log?.("[gemini-web] Using browser DOM automation for Deep Think.");
303
+ const browserResult = await runGeminiDeepThinkViaBrowser(prompt, runOptions.config, log);
304
+ const tookMs = Date.now() - startTime;
305
+ let answerMarkdown = browserResult.text;
306
+ if (geminiOptions.showThoughts && browserResult.thoughts) {
307
+ answerMarkdown = `## Thinking\n\n${browserResult.thoughts}\n\n## Response\n\n${browserResult.text}`;
308
+ }
309
+ log?.(`[gemini-web] Completed in ${tookMs}ms`);
310
+ return {
311
+ answerText: browserResult.text,
312
+ answerMarkdown,
313
+ tookMs,
314
+ answerTokens: estimateTokenCount(browserResult.text),
315
+ answerChars: browserResult.text.length,
316
+ };
317
+ },
318
+ };
319
+ const httpClient = {
320
+ mode: "http",
321
+ execute: async () => {
322
+ const useNoKeychainPath = Boolean(runOptions.config?.manualLogin);
323
+ const cookieResult = await loadGeminiCookies(runOptions.config, log, {
324
+ preferManualNoKeychain: useNoKeychainPath,
325
+ });
326
+ if (!hasRequiredGeminiCookies(cookieResult.cookieMap)) {
327
+ throw new Error(formatGeminiCookieError(cookieResult.warnings));
328
+ }
329
+ const configTimeout = typeof runOptions.config?.timeoutMs === "number" &&
330
+ Number.isFinite(runOptions.config.timeoutMs)
331
+ ? Math.max(1_000, runOptions.config.timeoutMs)
332
+ : null;
333
+ const defaultTimeoutMs = geminiOptions.youtube
334
+ ? 240_000
335
+ : geminiOptions.generateImage || geminiOptions.editImage
336
+ ? 300_000
337
+ : 120_000;
338
+ const timeoutMs = Math.min(configTimeout ?? defaultTimeoutMs, 600_000);
339
+ const controller = new AbortController();
340
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
341
+ let response;
342
+ try {
343
+ if (editImagePath) {
344
+ const intro = await runGeminiWebWithFallback({
345
+ prompt: "Here is an image to edit",
346
+ files: [editImagePath],
347
+ model,
348
+ cookieMap: cookieResult.cookieMap,
349
+ chatMetadata: null,
350
+ signal: controller.signal,
351
+ });
352
+ const editPrompt = `Use image generation tool to ${prompt}`;
353
+ const out = await runGeminiWebWithFallback({
354
+ prompt: editPrompt,
355
+ files: attachmentPaths,
356
+ model,
357
+ cookieMap: cookieResult.cookieMap,
358
+ chatMetadata: intro.metadata,
359
+ signal: controller.signal,
360
+ });
361
+ response = {
362
+ text: out.text ?? null,
363
+ thoughts: geminiOptions.showThoughts ? out.thoughts : null,
364
+ has_images: false,
365
+ image_count: 0,
366
+ };
367
+ const resolvedOutputPath = outputPath ?? generateImagePath ?? "generated.png";
368
+ const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, resolvedOutputPath, controller.signal);
369
+ response.has_images = imageSave.saved;
370
+ response.image_count = imageSave.imageCount;
371
+ if (!imageSave.saved) {
372
+ throw new Error(`No images generated. Response text:\n${out.text || "(empty response)"}`);
373
+ }
374
+ }
375
+ else if (generateImagePath) {
376
+ const out = await runGeminiWebWithFallback({
377
+ prompt,
378
+ files: attachmentPaths,
379
+ model,
380
+ cookieMap: cookieResult.cookieMap,
381
+ chatMetadata: null,
382
+ signal: controller.signal,
383
+ });
384
+ response = {
385
+ text: out.text ?? null,
386
+ thoughts: geminiOptions.showThoughts ? out.thoughts : null,
387
+ has_images: false,
388
+ image_count: 0,
389
+ };
390
+ const imageSave = await saveFirstGeminiImageFromOutput(out, cookieResult.cookieMap, generateImagePath, controller.signal);
391
+ response.has_images = imageSave.saved;
392
+ response.image_count = imageSave.imageCount;
393
+ if (!imageSave.saved) {
394
+ throw new Error(`No images generated. Response text:\n${out.text || "(empty response)"}`);
395
+ }
396
+ }
397
+ else {
398
+ const out = await runGeminiWebWithFallback({
399
+ prompt,
400
+ files: attachmentPaths,
401
+ model,
402
+ cookieMap: cookieResult.cookieMap,
403
+ chatMetadata: null,
404
+ signal: controller.signal,
405
+ });
406
+ response = {
407
+ text: out.text ?? null,
408
+ thoughts: geminiOptions.showThoughts ? out.thoughts : null,
409
+ has_images: out.images.length > 0,
410
+ image_count: out.images.length,
411
+ };
412
+ }
413
+ }
414
+ finally {
415
+ clearTimeout(timeout);
416
+ }
417
+ const answerText = response.text ?? "";
418
+ let answerMarkdown = answerText;
419
+ if (geminiOptions.showThoughts && response.thoughts) {
420
+ answerMarkdown = `## Thinking\n\n${response.thoughts}\n\n## Response\n\n${answerText}`;
421
+ }
422
+ if (response.has_images && response.image_count > 0) {
423
+ const imagePath = generateImagePath || outputPath || "generated.png";
424
+ answerMarkdown += `\n\n*Generated ${response.image_count} image(s). Saved to: ${imagePath}*`;
425
+ }
426
+ const tookMs = Date.now() - startTime;
427
+ log?.(`[gemini-web] Completed in ${tookMs}ms`);
428
+ return {
429
+ answerText,
430
+ answerMarkdown,
431
+ tookMs,
432
+ answerTokens: estimateTokenCount(answerText),
433
+ answerChars: answerText.length,
434
+ };
435
+ },
436
+ };
437
+ if (model === "gemini-3-pro-deep-think" && modeSelection.mode === "http") {
438
+ log?.(`[gemini-web] Deep Think DOM path skipped (${modeSelection.reasons.join(", ")} requested); using HTTP/header fallback path.`);
439
+ }
440
+ const executionClient = modeSelection.mode === "dom" ? domClient : httpClient;
441
+ return executionClient.execute();
442
+ };
443
+ }
@@ -0,0 +1 @@
1
+ export { createGeminiWebExecutor } from "./executor.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ export function startHeartbeat(config) {
2
+ const { intervalMs, log, isActive, makeMessage } = config;
3
+ if (!intervalMs || intervalMs <= 0) {
4
+ return () => { };
5
+ }
6
+ let stopped = false;
7
+ let pending = false;
8
+ const start = Date.now();
9
+ const timer = setInterval(async () => {
10
+ // stop flag flips asynchronously
11
+ if (stopped || pending) {
12
+ return;
13
+ }
14
+ if (!isActive()) {
15
+ stop();
16
+ return;
17
+ }
18
+ pending = true;
19
+ try {
20
+ const elapsed = Date.now() - start;
21
+ const message = await makeMessage(elapsed);
22
+ if (message && !stopped) {
23
+ log(message);
24
+ }
25
+ }
26
+ catch {
27
+ // ignore heartbeat errors
28
+ }
29
+ finally {
30
+ pending = false;
31
+ }
32
+ }, intervalMs);
33
+ timer.unref?.();
34
+ const stop = () => {
35
+ // multiple callers may race to stop
36
+ if (stopped) {
37
+ return;
38
+ }
39
+ stopped = true;
40
+ clearInterval(timer);
41
+ };
42
+ return stop;
43
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import process from "node:process";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { getCliVersion } from "../version.js";
7
+ import { registerConsultTool } from "./tools/consult.js";
8
+ import { registerSessionsTool } from "./tools/sessions.js";
9
+ import { registerSessionResources } from "./tools/sessionResources.js";
10
+ export async function startMcpServer() {
11
+ const server = new McpServer({
12
+ name: "oracle-mcp",
13
+ version: getCliVersion(),
14
+ }, {
15
+ capabilities: {
16
+ logging: {},
17
+ },
18
+ });
19
+ registerConsultTool(server);
20
+ registerSessionsTool(server);
21
+ registerSessionResources(server);
22
+ const transport = new StdioServerTransport();
23
+ transport.onerror = (error) => {
24
+ console.error("MCP transport error:", error);
25
+ };
26
+ const closed = new Promise((resolve) => {
27
+ transport.onclose = () => {
28
+ resolve();
29
+ };
30
+ });
31
+ // Keep the process alive until the client closes the transport.
32
+ await server.connect(transport);
33
+ await closed;
34
+ }
35
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("oracle-mcp")) {
36
+ startMcpServer().catch((error) => {
37
+ console.error("Failed to start oracle-mcp:", error);
38
+ process.exitCode = 1;
39
+ });
40
+ }