@duckmind/dm-windows-x64 0.63.0 → 0.63.4

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 (71) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +138 -2
  3. package/extensions/dm-web-access/SECURITY.md +5 -0
  4. package/extensions/dm-web-access/activity.js +65 -0
  5. package/extensions/dm-web-access/anysearch.js +158 -0
  6. package/extensions/dm-web-access/auth-fetch.js +131 -0
  7. package/extensions/dm-web-access/bocha.js +214 -0
  8. package/extensions/dm-web-access/brave.js +196 -0
  9. package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
  10. package/extensions/dm-web-access/brightdata.js +334 -0
  11. package/extensions/dm-web-access/chrome-cookies.js +627 -0
  12. package/extensions/dm-web-access/content-find.js +114 -0
  13. package/extensions/dm-web-access/credential-source.js +150 -0
  14. package/extensions/dm-web-access/curator-page.js +3559 -0
  15. package/extensions/dm-web-access/curator-server.js +691 -0
  16. package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
  17. package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
  18. package/extensions/dm-web-access/declared-web-links.js +167 -0
  19. package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
  20. package/extensions/dm-web-access/duckduckgo.js +118 -0
  21. package/extensions/dm-web-access/exa.js +401 -0
  22. package/extensions/dm-web-access/extract.js +1220 -0
  23. package/extensions/dm-web-access/feature-config.js +24 -0
  24. package/extensions/dm-web-access/fetch-params.js +81 -0
  25. package/extensions/dm-web-access/firecrawl.js +378 -0
  26. package/extensions/dm-web-access/gemini-adc.js +241 -0
  27. package/extensions/dm-web-access/gemini-api.js +258 -0
  28. package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
  29. package/extensions/dm-web-access/gemini-search.js +889 -0
  30. package/extensions/dm-web-access/gemini-url-context.js +97 -0
  31. package/extensions/dm-web-access/gemini-web-config.js +84 -0
  32. package/extensions/dm-web-access/gemini-web.js +351 -0
  33. package/extensions/dm-web-access/github-api.js +166 -0
  34. package/extensions/dm-web-access/github-extract.js +991 -0
  35. package/extensions/dm-web-access/github-issue-pr.js +750 -0
  36. package/extensions/dm-web-access/index.js +3117 -0
  37. package/extensions/dm-web-access/jina-search.js +242 -0
  38. package/extensions/dm-web-access/kagi.js +255 -0
  39. package/extensions/dm-web-access/kimi-search.js +214 -0
  40. package/extensions/dm-web-access/ollama.js +209 -0
  41. package/extensions/dm-web-access/openai-search.js +510 -0
  42. package/extensions/dm-web-access/package.json +34 -0
  43. package/extensions/dm-web-access/page-query.js +124 -0
  44. package/extensions/dm-web-access/parallel-mcp.js +223 -0
  45. package/extensions/dm-web-access/parallel.js +345 -0
  46. package/extensions/dm-web-access/pdf-extract.js +257 -0
  47. package/extensions/dm-web-access/perplexity.js +151 -0
  48. package/extensions/dm-web-access/querit.js +327 -0
  49. package/extensions/dm-web-access/query-rewrite.js +40 -0
  50. package/extensions/dm-web-access/render-search-error.js +80 -0
  51. package/extensions/dm-web-access/rsc-extract.js +347 -0
  52. package/extensions/dm-web-access/search1api.js +245 -0
  53. package/extensions/dm-web-access/searchinfinity.js +221 -0
  54. package/extensions/dm-web-access/searxng.js +223 -0
  55. package/extensions/dm-web-access/serpbase.js +205 -0
  56. package/extensions/dm-web-access/serpdive.js +238 -0
  57. package/extensions/dm-web-access/serper.js +200 -0
  58. package/extensions/dm-web-access/source-check.js +198 -0
  59. package/extensions/dm-web-access/ssrf-protection.js +436 -0
  60. package/extensions/dm-web-access/storage.js +451 -0
  61. package/extensions/dm-web-access/summary-model-scope.js +83 -0
  62. package/extensions/dm-web-access/summary-review.js +364 -0
  63. package/extensions/dm-web-access/tavily.js +199 -0
  64. package/extensions/dm-web-access/tinyfish.js +325 -0
  65. package/extensions/dm-web-access/utils.js +476 -0
  66. package/extensions/dm-web-access/valyu.js +189 -0
  67. package/extensions/dm-web-access/video-extract.js +336 -0
  68. package/extensions/dm-web-access/xai-search.js +285 -0
  69. package/extensions/dm-web-access/xcrawl.js +221 -0
  70. package/extensions/dm-web-access/youtube-extract.js +279 -0
  71. package/package.json +9 -1
@@ -0,0 +1,242 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { activityMonitor } from "./activity.js";
3
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.js";
4
+ import { getWebSearchConfigPath } from "./utils.js";
5
+ const JINA_SEARCH_BASE_URL = "https://s.jina.ai/";
6
+ const CONFIG_PATH = getWebSearchConfigPath();
7
+ const SEARCH_TIMEOUT_MS = 60000;
8
+ let cachedConfig = null;
9
+ function loadConfig() {
10
+ if (cachedConfig)
11
+ return cachedConfig;
12
+ if (!existsSync(CONFIG_PATH)) {
13
+ cachedConfig = {};
14
+ return cachedConfig;
15
+ }
16
+ const raw = readFileSync(CONFIG_PATH, "utf8");
17
+ try {
18
+ const parsed = JSON.parse(raw);
19
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
20
+ throw new Error("expected a JSON object");
21
+ }
22
+ cachedConfig = parsed;
23
+ return cachedConfig;
24
+ } catch (err) {
25
+ const message = err instanceof Error ? err.message : String(err);
26
+ throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
27
+ }
28
+ }
29
+ async function getApiKey(signal) {
30
+ return resolveCredential({
31
+ provider: "Jina Search",
32
+ configuredValue: loadConfig().jinaApiKey,
33
+ environmentValue: process.env.JINA_API_KEY,
34
+ signal
35
+ });
36
+ }
37
+ async function requireApiKey(signal) {
38
+ const apiKey = await getApiKey(signal);
39
+ if (!apiKey) {
40
+ throw new Error(`Jina Search API key not found. Either:
41
+ ` + ` 1. Create ${CONFIG_PATH} with { "jinaApiKey": "your-key" }
42
+ ` + ` 2. Set JINA_API_KEY environment variable
43
+ ` + "Get a key at https://jina.ai/api-dashboard");
44
+ }
45
+ return apiKey;
46
+ }
47
+ function normalizeCount(value) {
48
+ if (typeof value !== "number" || !Number.isFinite(value))
49
+ return 5;
50
+ return Math.max(1, Math.min(Math.floor(value), 20));
51
+ }
52
+ function normalizeDomain(value) {
53
+ let input = value.trim().toLowerCase();
54
+ if (input.startsWith("-"))
55
+ input = input.slice(1).trim();
56
+ if (!input)
57
+ return null;
58
+ try {
59
+ const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
60
+ input = parsed.hostname;
61
+ } catch {
62
+ input = input.split("/")[0]?.split(":")[0] ?? "";
63
+ }
64
+ input = input.replace(/^\.+|\.+$/g, "");
65
+ return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
66
+ }
67
+ function mapDomainFilter(domainFilter) {
68
+ const includes = [];
69
+ const excludes = [];
70
+ for (const raw of domainFilter ?? []) {
71
+ const domain = normalizeDomain(raw);
72
+ if (!domain)
73
+ continue;
74
+ const target = raw.trim().startsWith("-") ? excludes : includes;
75
+ if (!target.includes(domain))
76
+ target.push(domain);
77
+ }
78
+ return { includes, excludes };
79
+ }
80
+ function buildSearchRequest(query, options, numResults) {
81
+ const filters = mapDomainFilter(options.domainFilter);
82
+ const recency = options.recencyFilter ? ` published in the past ${options.recencyFilter}` : "";
83
+ const exclusions = filters.excludes.map((domain) => ` -site:${domain}`).join("");
84
+ const constrainedQuery = `${query.trim()}${exclusions}${recency}`.trim();
85
+ const url = new URL(encodeURIComponent(constrainedQuery), JINA_SEARCH_BASE_URL);
86
+ url.searchParams.set("count", String(numResults));
87
+ for (const domain of filters.includes)
88
+ url.searchParams.append("site", domain);
89
+ return { url: url.toString(), filters };
90
+ }
91
+ function requestSignal(signal) {
92
+ const timeout = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
93
+ return {
94
+ timeout,
95
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout
96
+ };
97
+ }
98
+ function errorMessage(err) {
99
+ return err instanceof Error ? err.message : String(err);
100
+ }
101
+ function parseItems(value) {
102
+ if (Array.isArray(value))
103
+ return value;
104
+ if (!value || typeof value !== "object") {
105
+ throw new Error("Jina Search API returned invalid response: expected an object or array");
106
+ }
107
+ const envelope = value;
108
+ if (typeof envelope.code === "number" && envelope.code !== 200) {
109
+ throw new Error(`Jina Search API error ${envelope.code}: response envelope reported failure`);
110
+ }
111
+ if (!Array.isArray(envelope.data)) {
112
+ throw new Error("Jina Search API returned invalid response: expected data array");
113
+ }
114
+ return envelope.data;
115
+ }
116
+ function passesDomainFilter(url, filters) {
117
+ let hostname;
118
+ try {
119
+ hostname = new URL(url).hostname.toLowerCase();
120
+ } catch {
121
+ return false;
122
+ }
123
+ const matches = (domain) => hostname === domain || hostname.endsWith(`.${domain}`);
124
+ if (filters.includes.length > 0 && !filters.includes.some(matches))
125
+ return false;
126
+ return !filters.excludes.some(matches);
127
+ }
128
+ function mapItems(items, numResults, filters) {
129
+ const results = [];
130
+ const content = [];
131
+ const seen = new Set;
132
+ for (const item of items) {
133
+ const url = typeof item?.url === "string" ? item.url.trim() : "";
134
+ if (!url || seen.has(url) || !passesDomainFilter(url, filters))
135
+ continue;
136
+ seen.add(url);
137
+ const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : `Source ${results.length + 1}`;
138
+ const snippet = typeof item.description === "string" ? item.description.replace(/\s+/g, " ").trim() : "";
139
+ results.push({ title, url, snippet });
140
+ if (typeof item.content === "string" && item.content.trim()) {
141
+ content.push({ url, title, content: item.content.trim(), error: null });
142
+ }
143
+ if (results.length >= numResults)
144
+ break;
145
+ }
146
+ return { results, content };
147
+ }
148
+ function buildAnswer(results) {
149
+ return results.map((result) => result.snippet ? `${result.snippet}
150
+ Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
151
+
152
+ `);
153
+ }
154
+ function rethrowRequestError(err, apiKey, activityId, request, callerSignal) {
155
+ if (request.timeout.aborted && !callerSignal?.aborted) {
156
+ activityMonitor.logComplete(activityId, 408);
157
+ throw new Error("Jina Search API error 408: request timed out");
158
+ }
159
+ const message = errorMessage(err);
160
+ const redactedMessage = redactCredential(message, apiKey);
161
+ if (redactedMessage.toLowerCase().includes("abort"))
162
+ activityMonitor.logComplete(activityId, 0);
163
+ else
164
+ activityMonitor.logError(activityId, redactedMessage);
165
+ if (redactedMessage === message)
166
+ throw err;
167
+ const redactedError = new Error(redactedMessage);
168
+ if (err instanceof Error)
169
+ redactedError.name = err.name;
170
+ throw redactedError;
171
+ }
172
+ export function isJinaSearchAvailable() {
173
+ return hasCredentialSource({
174
+ provider: "Jina Search",
175
+ configuredValue: loadConfig().jinaApiKey,
176
+ environmentValue: process.env.JINA_API_KEY
177
+ });
178
+ }
179
+ export async function searchWithJina(query, options = {}) {
180
+ const apiKey = await requireApiKey(options.signal);
181
+ const numResults = normalizeCount(options.numResults);
182
+ const { url, filters } = buildSearchRequest(query, options, numResults);
183
+ const activityId = activityMonitor.logStart({ type: "api", query });
184
+ const request = requestSignal(options.signal);
185
+ let response;
186
+ try {
187
+ response = await fetch(url, {
188
+ headers: {
189
+ Accept: "application/json",
190
+ Authorization: `Bearer ${apiKey}`,
191
+ "User-Agent": "dm-web-access",
192
+ "X-Respond-With": options.includeContent ? "content" : "no-content",
193
+ "X-Retain-Images": "none"
194
+ },
195
+ signal: request.signal
196
+ });
197
+ } catch (err) {
198
+ rethrowRequestError(err, apiKey, activityId, request, options.signal);
199
+ }
200
+ if (!response.ok) {
201
+ let body;
202
+ try {
203
+ body = redactCredential(await response.text(), apiKey);
204
+ } catch (err) {
205
+ rethrowRequestError(err, apiKey, activityId, request, options.signal);
206
+ }
207
+ activityMonitor.logComplete(activityId, response.status);
208
+ throw new Error(`Jina Search API error ${response.status}: ${body.slice(0, 300)}`);
209
+ }
210
+ let raw;
211
+ try {
212
+ raw = await response.json();
213
+ } catch (err) {
214
+ if (!(err instanceof SyntaxError)) {
215
+ rethrowRequestError(err, apiKey, activityId, request, options.signal);
216
+ }
217
+ activityMonitor.logComplete(activityId, response.status);
218
+ throw new Error("Jina Search API returned invalid JSON");
219
+ }
220
+ let mapped;
221
+ try {
222
+ mapped = mapItems(parseItems(raw), numResults, filters);
223
+ } catch (err) {
224
+ const message = errorMessage(err);
225
+ const redactedMessage = redactCredential(message, apiKey);
226
+ activityMonitor.logError(activityId, redactedMessage);
227
+ if (redactedMessage === message)
228
+ throw err;
229
+ const redactedError = new Error(redactedMessage);
230
+ if (err instanceof Error)
231
+ redactedError.name = err.name;
232
+ throw redactedError;
233
+ }
234
+ activityMonitor.logComplete(activityId, response.status);
235
+ const result = {
236
+ answer: buildAnswer(mapped.results),
237
+ results: mapped.results
238
+ };
239
+ if (options.includeContent && mapped.content.length > 0)
240
+ result.inlineContent = mapped.content;
241
+ return result;
242
+ }
@@ -0,0 +1,255 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { activityMonitor } from "./activity.js";
3
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.js";
4
+ import { fetchRemoteUrl, loadFetchContentDomainPolicy, loadSsrfConfig, validateRemoteUrl } from "./ssrf-protection.js";
5
+ import { getWebSearchConfigPath } from "./utils.js";
6
+ const KAGI_SEARCH_URL = "https://kagi.com/api/v1/search";
7
+ const KAGI_EXTRACT_URL = "https://kagi.com/api/v1/extract";
8
+ const CONFIG_PATH = getWebSearchConfigPath();
9
+ const SEARCH_TIMEOUT_MS = 60000;
10
+ let cachedConfig = null;
11
+ function loadConfig() {
12
+ if (cachedConfig)
13
+ return cachedConfig;
14
+ if (!existsSync(CONFIG_PATH)) {
15
+ cachedConfig = {};
16
+ return cachedConfig;
17
+ }
18
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(raw);
22
+ } catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
25
+ }
26
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
27
+ throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
28
+ }
29
+ cachedConfig = parsed;
30
+ return cachedConfig;
31
+ }
32
+ async function getApiKey(signal) {
33
+ return resolveCredential({
34
+ provider: "Kagi",
35
+ configuredValue: loadConfig().kagiApiKey,
36
+ environmentValue: process.env.KAGI_API_KEY,
37
+ signal
38
+ });
39
+ }
40
+ async function requireApiKey(signal) {
41
+ const apiKey = await getApiKey(signal);
42
+ if (!apiKey) {
43
+ throw new Error(`Kagi API key not found. Either:
44
+ ` + ` 1. Create ${CONFIG_PATH} with { "kagiApiKey": "your-key" }
45
+ ` + ` 2. Set KAGI_API_KEY environment variable
46
+ ` + "Create a key at https://kagi.com/settings?p=api");
47
+ }
48
+ return apiKey;
49
+ }
50
+ function normalizeCount(value) {
51
+ if (typeof value !== "number" || !Number.isFinite(value))
52
+ return 5;
53
+ return Math.max(1, Math.min(Math.floor(value), 20));
54
+ }
55
+ function errorMessage(err) {
56
+ return err instanceof Error ? err.message : String(err);
57
+ }
58
+ function invalidResponse(message) {
59
+ return new Error(`Kagi API returned invalid response: ${message}`);
60
+ }
61
+ function firstString(...values) {
62
+ for (const value of values) {
63
+ if (typeof value === "string" && value.trim())
64
+ return value.trim();
65
+ }
66
+ return null;
67
+ }
68
+ function appendSearchItems(value, results, inlineContent) {
69
+ if (Array.isArray(value)) {
70
+ for (const item of value)
71
+ appendSearchItems(item, results, inlineContent);
72
+ return;
73
+ }
74
+ if (!value || typeof value !== "object")
75
+ return;
76
+ const item = value;
77
+ const url = firstString(item.url, item.href, item.link);
78
+ if (!url)
79
+ return;
80
+ const title = firstString(item.title, item.name) ?? url;
81
+ const snippet = firstString(item.snippet, item.description, item.summary, item.content, item.markdown, item.text) ?? "";
82
+ results.push({ title, url, snippet });
83
+ const content = firstString(item.markdown, item.content, item.text);
84
+ if (content)
85
+ inlineContent.push({ url, title, content, error: null });
86
+ }
87
+ function parseErrors(value) {
88
+ if (!value || typeof value !== "object" || Array.isArray(value))
89
+ return null;
90
+ const envelope = value;
91
+ const rawErrors = envelope.errors ?? envelope.error;
92
+ if (!Array.isArray(rawErrors))
93
+ return null;
94
+ const messages = rawErrors.map((entry) => {
95
+ if (!entry || typeof entry !== "object")
96
+ return String(entry);
97
+ const raw = entry;
98
+ return firstString(raw.message, raw.msg, raw.code) ?? JSON.stringify(raw);
99
+ });
100
+ return messages.length > 0 ? messages.join("; ") : null;
101
+ }
102
+ function parseSearchResponse(value) {
103
+ if (!value || typeof value !== "object" || Array.isArray(value))
104
+ throw invalidResponse("expected an object envelope");
105
+ const message = parseErrors(value);
106
+ if (message)
107
+ throw invalidResponse(message);
108
+ const envelope = value;
109
+ const results = [];
110
+ const inlineContent = [];
111
+ const data = envelope.data;
112
+ const items = typeof data === "object" && data !== null && !Array.isArray(data) ? data.search : data;
113
+ appendSearchItems(items, results, inlineContent);
114
+ return { results, inlineContent };
115
+ }
116
+ function parseExtractResponse(value, requestedUrl) {
117
+ if (!value || typeof value !== "object" || Array.isArray(value))
118
+ throw invalidResponse("expected extract object envelope");
119
+ const message = parseErrors(value);
120
+ if (message)
121
+ throw invalidResponse(message);
122
+ const envelope = value;
123
+ const candidates = Array.isArray(envelope.data) ? envelope.data : [envelope.data ?? envelope];
124
+ for (const candidate of candidates) {
125
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
126
+ continue;
127
+ const item = candidate;
128
+ const content = firstString(item.markdown, item.content, item.text);
129
+ if (!content)
130
+ continue;
131
+ return {
132
+ url: firstString(item.url, item.href, item.link) ?? requestedUrl,
133
+ title: firstString(item.title, item.name) ?? requestedUrl,
134
+ content,
135
+ error: null
136
+ };
137
+ }
138
+ return null;
139
+ }
140
+ function buildAnswer(results) {
141
+ return results.map((result) => result.snippet ? `${result.snippet}
142
+ Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
143
+
144
+ `);
145
+ }
146
+ export function isKagiAvailable() {
147
+ return hasCredentialSource({ provider: "Kagi", configuredValue: loadConfig().kagiApiKey, environmentValue: process.env.KAGI_API_KEY });
148
+ }
149
+ export async function searchWithKagi(query, options = {}) {
150
+ const apiKey = await requireApiKey(options.signal);
151
+ const numResults = normalizeCount(options.numResults);
152
+ const activityId = activityMonitor.logStart({ type: "api", query });
153
+ let response;
154
+ try {
155
+ response = await fetch(KAGI_SEARCH_URL, {
156
+ method: "POST",
157
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "application/json" },
158
+ body: JSON.stringify({ query, limit: numResults }),
159
+ signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS)
160
+ });
161
+ } catch (err) {
162
+ const message = errorMessage(err);
163
+ const redactedMessage = redactCredential(message, apiKey);
164
+ if (redactedMessage.toLowerCase().includes("abort"))
165
+ activityMonitor.logComplete(activityId, 0);
166
+ else
167
+ activityMonitor.logError(activityId, redactedMessage);
168
+ if (redactedMessage === message)
169
+ throw err;
170
+ const redactedError = new Error(redactedMessage);
171
+ if (err instanceof Error)
172
+ redactedError.name = err.name;
173
+ throw redactedError;
174
+ }
175
+ if (!response.ok) {
176
+ activityMonitor.logComplete(activityId, response.status);
177
+ const errorText = redactCredential(await response.text(), apiKey);
178
+ throw new Error(`Kagi API error ${response.status}: ${errorText.slice(0, 300)}`);
179
+ }
180
+ let rawData;
181
+ try {
182
+ rawData = await response.json();
183
+ } catch (err) {
184
+ activityMonitor.logComplete(activityId, response.status);
185
+ throw new Error(`Kagi API returned invalid JSON: ${errorMessage(err)}`);
186
+ }
187
+ const parsed = parseSearchResponse(rawData);
188
+ activityMonitor.logComplete(activityId, response.status);
189
+ const results = parsed.results.slice(0, numResults);
190
+ const mapped = { answer: buildAnswer(results), results };
191
+ if (options.includeContent) {
192
+ const urls = new Set(results.map((result) => result.url));
193
+ const inlineContent = parsed.inlineContent.filter((content) => urls.has(content.url));
194
+ if (inlineContent.length > 0)
195
+ mapped.inlineContent = inlineContent;
196
+ }
197
+ return mapped;
198
+ }
199
+ export function isKagiExtractAvailable() {
200
+ return isKagiAvailable();
201
+ }
202
+ export async function extractWithKagi(url, signal, options = {}) {
203
+ const ssrf = options.ssrf ?? loadSsrfConfig();
204
+ const domainPolicy = loadFetchContentDomainPolicy();
205
+ await validateRemoteUrl(url, {
206
+ allowRanges: ssrf.allowRanges,
207
+ trustEnvProxy: ssrf.trustEnvProxy,
208
+ domainPolicy,
209
+ ...options.lookup ? { lookup: options.lookup } : {}
210
+ });
211
+ const apiKey = await requireApiKey(signal);
212
+ const activityId = activityMonitor.logStart({ type: "api", query: `kagi extract: ${url}` });
213
+ let response;
214
+ try {
215
+ response = await fetchRemoteUrl(KAGI_EXTRACT_URL, {
216
+ method: "POST",
217
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "application/json" },
218
+ body: JSON.stringify({ pages: [{ url }] }),
219
+ signal: signal ? AbortSignal.any([AbortSignal.timeout(options.timeoutMs ?? SEARCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(options.timeoutMs ?? SEARCH_TIMEOUT_MS)
220
+ }, {
221
+ allowRanges: ssrf.allowRanges,
222
+ trustEnvProxy: ssrf.trustEnvProxy,
223
+ onRedirect: ({ from, to, init }) => to.origin === from.origin ? init : { ...init, headers: { "Content-Type": "application/json", Accept: "application/json" } },
224
+ ...options.lookup ? { lookup: options.lookup } : {}
225
+ });
226
+ } catch (err) {
227
+ const message = errorMessage(err);
228
+ const redactedMessage = redactCredential(message, apiKey);
229
+ if (redactedMessage.toLowerCase().includes("abort"))
230
+ activityMonitor.logComplete(activityId, 0);
231
+ else
232
+ activityMonitor.logError(activityId, redactedMessage);
233
+ if (redactedMessage === message)
234
+ throw err;
235
+ const redactedError = new Error(redactedMessage);
236
+ if (err instanceof Error)
237
+ redactedError.name = err.name;
238
+ throw redactedError;
239
+ }
240
+ if (!response.ok) {
241
+ activityMonitor.logComplete(activityId, response.status);
242
+ const errorText = redactCredential(await response.text(), apiKey);
243
+ throw new Error(`Kagi Extract API error ${response.status}: ${errorText.slice(0, 300)}`);
244
+ }
245
+ let rawData;
246
+ try {
247
+ rawData = await response.json();
248
+ } catch (err) {
249
+ activityMonitor.logComplete(activityId, response.status);
250
+ throw new Error(`Kagi Extract API returned invalid JSON: ${errorMessage(err)}`);
251
+ }
252
+ const parsed = parseExtractResponse(rawData, url);
253
+ activityMonitor.logComplete(activityId, response.status);
254
+ return parsed;
255
+ }