@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,339 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ 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
+ const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
5
+ const MODEL_HEADERS = {
6
+ "gemini-3-pro": '[1,null,null,null,"9d8ca3786ebdfbea",null,null,0,[4]]',
7
+ "gemini-3-pro-deep-think": '[1,null,null,null,"e6fa609c3fa255c0",null,null,0,[4],null,null,3]',
8
+ "gemini-2.5-pro": '[1,null,null,null,"4af6c7f5da75d65d",null,null,0,[4]]',
9
+ "gemini-2.5-flash": '[1,null,null,null,"9ec249fc9ad08861",null,null,0,[4]]',
10
+ };
11
+ const GEMINI_APP_URL = "https://gemini.google.com/app";
12
+ const GEMINI_STREAM_GENERATE_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate";
13
+ const GEMINI_UPLOAD_URL = "https://content-push.googleapis.com/upload";
14
+ const GEMINI_UPLOAD_PUSH_ID = "feeds/mcudyrk2a4khkz";
15
+ const GEMINI_UPLOAD_MIME_TYPES = {
16
+ ".bmp": "image/bmp",
17
+ ".gif": "image/gif",
18
+ ".jpeg": "image/jpeg",
19
+ ".jpg": "image/jpeg",
20
+ ".pdf": "application/pdf",
21
+ ".png": "image/png",
22
+ ".svg": "image/svg+xml",
23
+ ".webp": "image/webp",
24
+ };
25
+ function getNestedValue(value, pathParts, fallback) {
26
+ let current = value;
27
+ for (const part of pathParts) {
28
+ if (current == null)
29
+ return fallback;
30
+ if (typeof part === "number") {
31
+ if (!Array.isArray(current))
32
+ return fallback;
33
+ current = current[part];
34
+ }
35
+ else {
36
+ if (typeof current !== "object")
37
+ return fallback;
38
+ current = current[part];
39
+ }
40
+ }
41
+ return current ?? fallback;
42
+ }
43
+ function buildCookieHeader(cookieMap) {
44
+ return Object.entries(cookieMap)
45
+ .filter(([, value]) => typeof value === "string" && value.length > 0)
46
+ .map(([name, value]) => `${name}=${value}`)
47
+ .join("; ");
48
+ }
49
+ export async function fetchGeminiAccessToken(cookieMap, signal) {
50
+ const cookieHeader = buildCookieHeader(cookieMap);
51
+ const res = await fetch(GEMINI_APP_URL, {
52
+ redirect: "follow",
53
+ signal,
54
+ headers: {
55
+ cookie: cookieHeader,
56
+ "user-agent": USER_AGENT,
57
+ },
58
+ });
59
+ const html = await res.text();
60
+ const tokens = ["SNlM0e", "thykhd"];
61
+ for (const key of tokens) {
62
+ const match = html.match(new RegExp(`"${key}":"(.*?)"`));
63
+ if (match?.[1])
64
+ return match[1];
65
+ }
66
+ throw new Error("Unable to locate Gemini access token on gemini.google.com/app (missing SNlM0e/thykhd).");
67
+ }
68
+ function trimGeminiJsonEnvelope(text) {
69
+ const start = text.indexOf("[");
70
+ const end = text.lastIndexOf("]");
71
+ if (start === -1 || end === -1 || end <= start) {
72
+ throw new Error("Gemini response did not contain a JSON payload.");
73
+ }
74
+ return text.slice(start, end + 1);
75
+ }
76
+ function extractErrorCode(responseJson) {
77
+ const code = getNestedValue(responseJson, [0, 5, 2, 0, 1, 0], -1);
78
+ return typeof code === "number" && code >= 0 ? code : undefined;
79
+ }
80
+ function extractGgdlUrls(rawText) {
81
+ const matches = rawText.match(/https:\/\/lh3\.googleusercontent\.com\/gg-dl\/[^\s"']+/g) ?? [];
82
+ const seen = new Set();
83
+ const urls = [];
84
+ for (const match of matches) {
85
+ if (seen.has(match))
86
+ continue;
87
+ seen.add(match);
88
+ urls.push(match);
89
+ }
90
+ return urls;
91
+ }
92
+ function ensureFullSizeImageUrl(url) {
93
+ if (url.includes("=s2048"))
94
+ return url;
95
+ if (url.includes("=s"))
96
+ return url;
97
+ return `${url}=s2048`;
98
+ }
99
+ async function fetchWithCookiePreservingRedirects(url, init, signal, maxRedirects = 10) {
100
+ let current = url;
101
+ for (let i = 0; i <= maxRedirects; i += 1) {
102
+ const res = await fetch(current, { ...init, redirect: "manual", signal });
103
+ if (res.status >= 300 && res.status < 400) {
104
+ const location = res.headers.get("location");
105
+ if (!location)
106
+ return res;
107
+ current = new URL(location, current).toString();
108
+ continue;
109
+ }
110
+ return res;
111
+ }
112
+ throw new Error(`Too many redirects while downloading image (>${maxRedirects}).`);
113
+ }
114
+ async function downloadGeminiImage(url, cookieMap, outputPath, signal) {
115
+ const cookieHeader = buildCookieHeader(cookieMap);
116
+ const res = await fetchWithCookiePreservingRedirects(ensureFullSizeImageUrl(url), {
117
+ headers: {
118
+ cookie: cookieHeader,
119
+ "user-agent": USER_AGENT,
120
+ },
121
+ }, signal);
122
+ if (!res.ok) {
123
+ throw new Error(`Failed to download image: ${res.status} ${res.statusText} (${res.url})`);
124
+ }
125
+ const data = new Uint8Array(await res.arrayBuffer());
126
+ await mkdir(path.dirname(outputPath), { recursive: true });
127
+ await writeFile(outputPath, data);
128
+ }
129
+ async function uploadGeminiFile(filePath, signal) {
130
+ const absPath = path.resolve(process.cwd(), filePath);
131
+ const data = await readFile(absPath);
132
+ const fileName = path.basename(absPath);
133
+ const mimeType = GEMINI_UPLOAD_MIME_TYPES[path.extname(absPath).toLowerCase()] ?? "application/octet-stream";
134
+ const form = new FormData();
135
+ form.append("file", new Blob([data], { type: mimeType }), fileName);
136
+ const res = await fetch(GEMINI_UPLOAD_URL, {
137
+ method: "POST",
138
+ redirect: "follow",
139
+ signal,
140
+ headers: {
141
+ "push-id": GEMINI_UPLOAD_PUSH_ID,
142
+ "user-agent": USER_AGENT,
143
+ },
144
+ body: form,
145
+ });
146
+ const text = await res.text();
147
+ if (!res.ok) {
148
+ throw new Error(`File upload failed: ${res.status} ${res.statusText} (${text.slice(0, 200)})`);
149
+ }
150
+ return { id: text, name: fileName, mimeType };
151
+ }
152
+ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
153
+ const promptPayload = uploaded.length > 0
154
+ ? [
155
+ prompt,
156
+ 0,
157
+ null,
158
+ // Format: [[[fileId, 1, null, "mimeType"], "filename", ...]]
159
+ uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
160
+ ]
161
+ : [prompt];
162
+ const innerList = [promptPayload, null, chatMetadata ?? null];
163
+ return JSON.stringify([null, JSON.stringify(innerList)]);
164
+ }
165
+ export function parseGeminiStreamGenerateResponse(rawText) {
166
+ const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
167
+ const errorCode = extractErrorCode(responseJson);
168
+ const parts = Array.isArray(responseJson) ? responseJson : [];
169
+ let bodyIndex = 0;
170
+ let body = null;
171
+ for (let i = 0; i < parts.length; i += 1) {
172
+ const partBody = getNestedValue(parts[i], [2], null);
173
+ if (!partBody)
174
+ continue;
175
+ try {
176
+ const parsed = JSON.parse(partBody);
177
+ const candidateList = getNestedValue(parsed, [4], []);
178
+ if (Array.isArray(candidateList) && candidateList.length > 0) {
179
+ bodyIndex = i;
180
+ body = parsed;
181
+ break;
182
+ }
183
+ }
184
+ catch {
185
+ // ignore
186
+ }
187
+ }
188
+ const candidateList = getNestedValue(body, [4], []);
189
+ const firstCandidate = candidateList[0];
190
+ const textRaw = getNestedValue(firstCandidate, [1, 0], "");
191
+ const cardContent = /^http:\/\/googleusercontent\.com\/card_content\/\d+/.test(textRaw);
192
+ const text = cardContent
193
+ ? (getNestedValue(firstCandidate, [22, 0], null) ?? textRaw)
194
+ : textRaw;
195
+ const thoughts = getNestedValue(firstCandidate, [37, 0, 0], null);
196
+ const metadata = getNestedValue(body, [1], []);
197
+ const images = [];
198
+ const webImages = getNestedValue(firstCandidate, [12, 1], []);
199
+ for (const webImage of webImages) {
200
+ const url = getNestedValue(webImage, [0, 0, 0], null);
201
+ if (!url)
202
+ continue;
203
+ images.push({
204
+ kind: "web",
205
+ url,
206
+ title: getNestedValue(webImage, [7, 0], undefined),
207
+ alt: getNestedValue(webImage, [0, 4], undefined),
208
+ });
209
+ }
210
+ const hasGenerated = Boolean(getNestedValue(firstCandidate, [12, 7, 0], null));
211
+ if (hasGenerated) {
212
+ let imgBody = null;
213
+ for (let i = bodyIndex; i < parts.length; i += 1) {
214
+ const partBody = getNestedValue(parts[i], [2], null);
215
+ if (!partBody)
216
+ continue;
217
+ try {
218
+ const parsed = JSON.parse(partBody);
219
+ const candidateImages = getNestedValue(parsed, [4, 0, 12, 7, 0], null);
220
+ if (candidateImages != null) {
221
+ imgBody = parsed;
222
+ break;
223
+ }
224
+ }
225
+ catch {
226
+ // ignore
227
+ }
228
+ }
229
+ const imgCandidate = getNestedValue(imgBody ?? body, [4, 0], null);
230
+ const generated = getNestedValue(imgCandidate, [12, 7, 0], []);
231
+ for (const genImage of generated) {
232
+ const url = getNestedValue(genImage, [0, 3, 3], null);
233
+ if (!url)
234
+ continue;
235
+ images.push({
236
+ kind: "generated",
237
+ url,
238
+ title: "[Generated Image]",
239
+ alt: "",
240
+ });
241
+ }
242
+ }
243
+ return { metadata, text, thoughts, images, errorCode };
244
+ }
245
+ export function isGeminiModelUnavailable(errorCode) {
246
+ return errorCode === 1052;
247
+ }
248
+ export async function runGeminiWebOnce(input) {
249
+ const cookieHeader = buildCookieHeader(input.cookieMap);
250
+ const at = await fetchGeminiAccessToken(input.cookieMap, input.signal);
251
+ const uploaded = [];
252
+ for (const file of input.files ?? []) {
253
+ if (input.signal?.aborted) {
254
+ throw new Error("Gemini web run aborted before upload.");
255
+ }
256
+ uploaded.push(await uploadGeminiFile(file, input.signal));
257
+ }
258
+ const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
259
+ const params = new URLSearchParams();
260
+ params.set("at", at);
261
+ params.set("f.req", fReq);
262
+ const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
263
+ method: "POST",
264
+ redirect: "follow",
265
+ signal: input.signal,
266
+ headers: {
267
+ "content-type": "application/x-www-form-urlencoded;charset=utf-8",
268
+ origin: "https://gemini.google.com",
269
+ referer: "https://gemini.google.com/",
270
+ "x-same-domain": "1",
271
+ "user-agent": USER_AGENT,
272
+ cookie: cookieHeader,
273
+ [MODEL_HEADER_NAME]: MODEL_HEADERS[input.model],
274
+ },
275
+ body: params.toString(),
276
+ });
277
+ const rawResponseText = await res.text();
278
+ if (!res.ok) {
279
+ return {
280
+ rawResponseText,
281
+ text: "",
282
+ thoughts: null,
283
+ metadata: input.chatMetadata ?? null,
284
+ images: [],
285
+ errorMessage: `Gemini request failed: ${res.status} ${res.statusText}`,
286
+ };
287
+ }
288
+ try {
289
+ const parsed = parseGeminiStreamGenerateResponse(rawResponseText);
290
+ return {
291
+ rawResponseText,
292
+ text: parsed.text ?? "",
293
+ thoughts: parsed.thoughts,
294
+ metadata: parsed.metadata,
295
+ images: parsed.images,
296
+ errorCode: parsed.errorCode,
297
+ };
298
+ }
299
+ catch (error) {
300
+ let responseJson = null;
301
+ try {
302
+ responseJson = JSON.parse(trimGeminiJsonEnvelope(rawResponseText));
303
+ }
304
+ catch {
305
+ responseJson = null;
306
+ }
307
+ const errorCode = extractErrorCode(responseJson);
308
+ return {
309
+ rawResponseText,
310
+ text: "",
311
+ thoughts: null,
312
+ metadata: input.chatMetadata ?? null,
313
+ images: [],
314
+ errorCode: typeof errorCode === "number" ? errorCode : undefined,
315
+ errorMessage: error instanceof Error ? error.message : String(error ?? ""),
316
+ };
317
+ }
318
+ }
319
+ export async function runGeminiWebWithFallback(input) {
320
+ const attempt = await runGeminiWebOnce(input);
321
+ if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== "gemini-2.5-flash") {
322
+ const fallback = await runGeminiWebOnce({ ...input, model: "gemini-2.5-flash" });
323
+ return { ...fallback, effectiveModel: "gemini-2.5-flash" };
324
+ }
325
+ return { ...attempt, effectiveModel: input.model };
326
+ }
327
+ export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath, signal) {
328
+ const generatedOrWeb = output.images.find((img) => img.kind === "generated") ?? output.images[0];
329
+ if (generatedOrWeb?.url) {
330
+ await downloadGeminiImage(generatedOrWeb.url, cookieMap, outputPath, signal);
331
+ return { saved: true, imageCount: output.images.length };
332
+ }
333
+ const ggdl = extractGgdlUrls(output.rawResponseText);
334
+ if (ggdl[0]) {
335
+ await downloadGeminiImage(ggdl[0], cookieMap, outputPath, signal);
336
+ return { saved: true, imageCount: ggdl.length };
337
+ }
338
+ return { saved: false, imageCount: 0 };
339
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ export function selectGeminiExecutionMode(input) {
2
+ const reasons = [];
3
+ if (input.model !== "gemini-3-pro-deep-think") {
4
+ return { mode: "http", reasons: ["model"] };
5
+ }
6
+ if (input.attachmentPaths.length > 0) {
7
+ reasons.push("attachments");
8
+ }
9
+ if (input.generateImagePath) {
10
+ reasons.push("image-generation");
11
+ }
12
+ if (input.editImagePath) {
13
+ reasons.push("image-edit");
14
+ }
15
+ return reasons.length === 0 ? { mode: "dom", reasons: [] } : { mode: "http", reasons };
16
+ }