@duckmind/dm-windows-x64 0.62.9 → 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.
- package/dm.exe +0 -0
- package/extensions/.dm-extensions.json +140 -4
- package/extensions/dm-ponytail/extensions/ponytail.mjs +1 -1
- package/extensions/dm-web-access/SECURITY.md +5 -0
- package/extensions/dm-web-access/activity.js +65 -0
- package/extensions/dm-web-access/anysearch.js +158 -0
- package/extensions/dm-web-access/auth-fetch.js +131 -0
- package/extensions/dm-web-access/bocha.js +214 -0
- package/extensions/dm-web-access/brave.js +196 -0
- package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
- package/extensions/dm-web-access/brightdata.js +334 -0
- package/extensions/dm-web-access/chrome-cookies.js +627 -0
- package/extensions/dm-web-access/content-find.js +114 -0
- package/extensions/dm-web-access/credential-source.js +150 -0
- package/extensions/dm-web-access/curator-page.js +3559 -0
- package/extensions/dm-web-access/curator-server.js +691 -0
- package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
- package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
- package/extensions/dm-web-access/declared-web-links.js +167 -0
- package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
- package/extensions/dm-web-access/duckduckgo.js +118 -0
- package/extensions/dm-web-access/exa.js +401 -0
- package/extensions/dm-web-access/extract.js +1220 -0
- package/extensions/dm-web-access/feature-config.js +24 -0
- package/extensions/dm-web-access/fetch-params.js +81 -0
- package/extensions/dm-web-access/firecrawl.js +378 -0
- package/extensions/dm-web-access/gemini-adc.js +241 -0
- package/extensions/dm-web-access/gemini-api.js +258 -0
- package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
- package/extensions/dm-web-access/gemini-search.js +889 -0
- package/extensions/dm-web-access/gemini-url-context.js +97 -0
- package/extensions/dm-web-access/gemini-web-config.js +84 -0
- package/extensions/dm-web-access/gemini-web.js +351 -0
- package/extensions/dm-web-access/github-api.js +166 -0
- package/extensions/dm-web-access/github-extract.js +991 -0
- package/extensions/dm-web-access/github-issue-pr.js +750 -0
- package/extensions/dm-web-access/index.js +3117 -0
- package/extensions/dm-web-access/jina-search.js +242 -0
- package/extensions/dm-web-access/kagi.js +255 -0
- package/extensions/dm-web-access/kimi-search.js +214 -0
- package/extensions/dm-web-access/ollama.js +209 -0
- package/extensions/dm-web-access/openai-search.js +510 -0
- package/extensions/dm-web-access/package.json +34 -0
- package/extensions/dm-web-access/page-query.js +124 -0
- package/extensions/dm-web-access/parallel-mcp.js +223 -0
- package/extensions/dm-web-access/parallel.js +345 -0
- package/extensions/dm-web-access/pdf-extract.js +257 -0
- package/extensions/dm-web-access/perplexity.js +151 -0
- package/extensions/dm-web-access/querit.js +327 -0
- package/extensions/dm-web-access/query-rewrite.js +40 -0
- package/extensions/dm-web-access/render-search-error.js +80 -0
- package/extensions/dm-web-access/rsc-extract.js +347 -0
- package/extensions/dm-web-access/search1api.js +245 -0
- package/extensions/dm-web-access/searchinfinity.js +221 -0
- package/extensions/dm-web-access/searxng.js +223 -0
- package/extensions/dm-web-access/serpbase.js +205 -0
- package/extensions/dm-web-access/serpdive.js +238 -0
- package/extensions/dm-web-access/serper.js +200 -0
- package/extensions/dm-web-access/source-check.js +198 -0
- package/extensions/dm-web-access/ssrf-protection.js +436 -0
- package/extensions/dm-web-access/storage.js +451 -0
- package/extensions/dm-web-access/summary-model-scope.js +83 -0
- package/extensions/dm-web-access/summary-review.js +364 -0
- package/extensions/dm-web-access/tavily.js +199 -0
- package/extensions/dm-web-access/tinyfish.js +325 -0
- package/extensions/dm-web-access/utils.js +476 -0
- package/extensions/dm-web-access/valyu.js +189 -0
- package/extensions/dm-web-access/video-extract.js +336 -0
- package/extensions/dm-web-access/xai-search.js +285 -0
- package/extensions/dm-web-access/xcrawl.js +221 -0
- package/extensions/dm-web-access/youtube-extract.js +279 -0
- package/package.json +9 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
3
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
4
|
+
function loadFeatureConfig() {
|
|
5
|
+
if (!existsSync(CONFIG_PATH))
|
|
6
|
+
return {};
|
|
7
|
+
try {
|
|
8
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
9
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
10
|
+
} catch (err) {
|
|
11
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isImageEnabled() {
|
|
16
|
+
return loadFeatureConfig().image?.enabled !== false;
|
|
17
|
+
}
|
|
18
|
+
export function canAttachImages() {
|
|
19
|
+
try {
|
|
20
|
+
return isImageEnabled();
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { normalizeProxyUrl } from "./utils.js";
|
|
2
|
+
export function normalizeFetchContentParams(params) {
|
|
3
|
+
const normalizedUrls = uniqueUrls(normalizeUrlArray(params.urls));
|
|
4
|
+
const urlList = normalizedUrls.length > 0 ? normalizedUrls : normalizeSingleUrl(params.url);
|
|
5
|
+
const prompt = normalizeOptionalString(params.prompt);
|
|
6
|
+
const timestamp = normalizeOptionalString(params.timestamp);
|
|
7
|
+
const frames = normalizeOptionalFrameCount(params.frames);
|
|
8
|
+
const shouldIncludeFrames = frames !== undefined && (timestamp !== undefined || frames > 1);
|
|
9
|
+
const forceClone = typeof params.forceClone === "boolean" ? params.forceClone : undefined;
|
|
10
|
+
const model = normalizeOptionalString(params.model);
|
|
11
|
+
const mode = normalizeMode(params.mode);
|
|
12
|
+
const answerModel = normalizeOptionalString(params.answerModel);
|
|
13
|
+
const auth = normalizeAuth(params.auth);
|
|
14
|
+
const proxy = normalizeProxy(params.proxy);
|
|
15
|
+
return {
|
|
16
|
+
urlList,
|
|
17
|
+
options: {
|
|
18
|
+
...forceClone !== undefined ? { forceClone } : {},
|
|
19
|
+
...prompt !== undefined ? { prompt } : {},
|
|
20
|
+
...timestamp !== undefined ? { timestamp } : {},
|
|
21
|
+
...shouldIncludeFrames ? { frames } : {},
|
|
22
|
+
...model !== undefined ? { model } : {},
|
|
23
|
+
...mode !== undefined ? { mode } : {},
|
|
24
|
+
...answerModel !== undefined ? { answerModel } : {},
|
|
25
|
+
...auth !== undefined ? { auth } : {},
|
|
26
|
+
...proxy !== undefined ? { proxy } : {}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function normalizeUrlArray(value) {
|
|
31
|
+
if (!Array.isArray(value))
|
|
32
|
+
return [];
|
|
33
|
+
return value.flatMap(normalizeSingleUrl);
|
|
34
|
+
}
|
|
35
|
+
function normalizeSingleUrl(value) {
|
|
36
|
+
if (typeof value !== "string")
|
|
37
|
+
return [];
|
|
38
|
+
const trimmed = value.trim();
|
|
39
|
+
return trimmed ? [trimmed] : [];
|
|
40
|
+
}
|
|
41
|
+
function normalizeOptionalString(value) {
|
|
42
|
+
if (typeof value !== "string")
|
|
43
|
+
return;
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
return trimmed || undefined;
|
|
46
|
+
}
|
|
47
|
+
function normalizeMode(value) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return;
|
|
50
|
+
if (value === "readable" || value === "raw" || value === "answer")
|
|
51
|
+
return value;
|
|
52
|
+
throw new Error('mode must be "readable", "raw", or "answer"');
|
|
53
|
+
}
|
|
54
|
+
function normalizeAuth(value) {
|
|
55
|
+
if (value === undefined || value === false)
|
|
56
|
+
return;
|
|
57
|
+
if (value === true)
|
|
58
|
+
return true;
|
|
59
|
+
if (typeof value === "string") {
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
if (trimmed)
|
|
62
|
+
return trimmed;
|
|
63
|
+
}
|
|
64
|
+
throw new Error("auth must be a profile name, true, or false");
|
|
65
|
+
}
|
|
66
|
+
function normalizeProxy(value) {
|
|
67
|
+
if (value === undefined || value === false)
|
|
68
|
+
return;
|
|
69
|
+
if (value === null)
|
|
70
|
+
throw new Error("proxy must be an http(s) proxy URL string");
|
|
71
|
+
const normalized = normalizeProxyUrl(value, "proxy");
|
|
72
|
+
return normalized ?? "";
|
|
73
|
+
}
|
|
74
|
+
function normalizeOptionalFrameCount(value) {
|
|
75
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 12)
|
|
76
|
+
return;
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function uniqueUrls(urls) {
|
|
80
|
+
return [...new Set(urls)];
|
|
81
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import { activityMonitor } from "./activity.js";
|
|
4
|
+
import { redactCredential, resolveCredential } from "./credential-source.js";
|
|
5
|
+
import { loadSsrfConfig, validateRemoteUrl } from "./ssrf-protection.js";
|
|
6
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
7
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
8
|
+
const DEFAULT_API_VERSION = "v2";
|
|
9
|
+
const EXTRACT_TIMEOUT_MS = 60000;
|
|
10
|
+
const SEARCH_TIMEOUT_MS = 60000;
|
|
11
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
12
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
13
|
+
const SUPPORTED_API_VERSIONS = ["v1", "v2"];
|
|
14
|
+
let cachedConfig = null;
|
|
15
|
+
function loadConfig() {
|
|
16
|
+
if (cachedConfig)
|
|
17
|
+
return cachedConfig;
|
|
18
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
19
|
+
cachedConfig = {};
|
|
20
|
+
return cachedConfig;
|
|
21
|
+
}
|
|
22
|
+
const raw = readFileSync(CONFIG_PATH, "utf8");
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(raw);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
28
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
31
|
+
throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
|
|
32
|
+
}
|
|
33
|
+
cachedConfig = parsed;
|
|
34
|
+
return cachedConfig;
|
|
35
|
+
}
|
|
36
|
+
export function clearFirecrawlConfigCache() {
|
|
37
|
+
cachedConfig = null;
|
|
38
|
+
}
|
|
39
|
+
function normalizeBaseUrl(value) {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (!trimmed)
|
|
44
|
+
return null;
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = new URL(trimmed);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(`Invalid Firecrawl base URL in ${CONFIG_PATH}: expected an HTTP or HTTPS URL`);
|
|
50
|
+
}
|
|
51
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
52
|
+
throw new Error(`Invalid Firecrawl base URL in ${CONFIG_PATH}: expected an HTTP or HTTPS URL`);
|
|
53
|
+
}
|
|
54
|
+
if (parsed.username || parsed.password) {
|
|
55
|
+
throw new Error(`Invalid Firecrawl base URL in ${CONFIG_PATH}: URL credentials are not allowed`);
|
|
56
|
+
}
|
|
57
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
58
|
+
parsed.search = "";
|
|
59
|
+
parsed.hash = "";
|
|
60
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
61
|
+
}
|
|
62
|
+
function getBaseUrl() {
|
|
63
|
+
return normalizeBaseUrl(process.env.FIRECRAWL_BASE_URL) ?? normalizeBaseUrl(loadConfig().firecrawlBaseUrl);
|
|
64
|
+
}
|
|
65
|
+
function requireBaseUrl() {
|
|
66
|
+
const baseUrl = getBaseUrl();
|
|
67
|
+
if (!baseUrl) {
|
|
68
|
+
throw new Error(`Firecrawl base URL not configured. Either:
|
|
69
|
+
` + ` 1. Set firecrawlBaseUrl in ${CONFIG_PATH}
|
|
70
|
+
` + " 2. Set FIRECRAWL_BASE_URL environment variable");
|
|
71
|
+
}
|
|
72
|
+
return baseUrl;
|
|
73
|
+
}
|
|
74
|
+
function getApiVersion() {
|
|
75
|
+
const environmentValue = typeof process.env.FIRECRAWL_API_VERSION === "string" ? process.env.FIRECRAWL_API_VERSION.trim() : "";
|
|
76
|
+
const raw = environmentValue || loadConfig().firecrawlApiVersion;
|
|
77
|
+
if (raw === undefined || raw === null)
|
|
78
|
+
return DEFAULT_API_VERSION;
|
|
79
|
+
if (typeof raw !== "string") {
|
|
80
|
+
throw new Error(`firecrawlApiVersion in ${CONFIG_PATH} must be a string ("v1" or "v2")`);
|
|
81
|
+
}
|
|
82
|
+
const normalized = raw.trim().toLowerCase();
|
|
83
|
+
if (!normalized)
|
|
84
|
+
return DEFAULT_API_VERSION;
|
|
85
|
+
if (!SUPPORTED_API_VERSIONS.includes(normalized)) {
|
|
86
|
+
throw new Error(`Unsupported Firecrawl API version "${raw}". Supported versions: ${SUPPORTED_API_VERSIONS.join(", ")}`);
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
function allowFreshScrape() {
|
|
91
|
+
const environmentValue = process.env.FIRECRAWL_FRESH_SCRAPE;
|
|
92
|
+
if (environmentValue !== undefined)
|
|
93
|
+
return environmentValue === "1" || environmentValue.toLowerCase() === "true";
|
|
94
|
+
const configured = loadConfig().firecrawlFreshScrape;
|
|
95
|
+
if (configured === undefined || configured === null)
|
|
96
|
+
return false;
|
|
97
|
+
if (typeof configured !== "boolean")
|
|
98
|
+
throw new Error(`firecrawlFreshScrape in ${CONFIG_PATH} must be a boolean`);
|
|
99
|
+
return configured;
|
|
100
|
+
}
|
|
101
|
+
async function getApiKey(signal) {
|
|
102
|
+
const configKey = loadConfig().firecrawlApiKey;
|
|
103
|
+
return resolveCredential({
|
|
104
|
+
provider: "Firecrawl",
|
|
105
|
+
configuredValue: configKey,
|
|
106
|
+
environmentValue: process.env.FIRECRAWL_API_KEY,
|
|
107
|
+
signal
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
function requestSignal(timeoutMs, signal) {
|
|
111
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
112
|
+
return signal ? AbortSignal.any([timeout, signal]) : timeout;
|
|
113
|
+
}
|
|
114
|
+
function errorMessage(err) {
|
|
115
|
+
return err instanceof Error ? err.message : String(err);
|
|
116
|
+
}
|
|
117
|
+
function isAbortError(err) {
|
|
118
|
+
return errorMessage(err).toLowerCase().includes("abort");
|
|
119
|
+
}
|
|
120
|
+
function ssrfOptions(options) {
|
|
121
|
+
const config = loadSsrfConfig();
|
|
122
|
+
return {
|
|
123
|
+
allowRanges: options?.ssrf?.allowRanges ?? config.allowRanges,
|
|
124
|
+
trustEnvProxy: options?.ssrf?.trustEnvProxy ?? config.trustEnvProxy,
|
|
125
|
+
...options?.lookup ? { lookup: options.lookup } : {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function isLoopbackApiUrl(url) {
|
|
129
|
+
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
130
|
+
if (hostname === "localhost" || hostname === "::1")
|
|
131
|
+
return true;
|
|
132
|
+
if (net.isIP(hostname) !== 4)
|
|
133
|
+
return false;
|
|
134
|
+
return hostname.split(".")[0] === "127";
|
|
135
|
+
}
|
|
136
|
+
function firecrawlApiSsrfOptions(options, allowLoopback) {
|
|
137
|
+
return { ...ssrfOptions(options), allowLoopback };
|
|
138
|
+
}
|
|
139
|
+
function withoutSensitiveHeaders(headers) {
|
|
140
|
+
const next = { ...headers };
|
|
141
|
+
delete next.Authorization;
|
|
142
|
+
delete next.authorization;
|
|
143
|
+
delete next.Cookie;
|
|
144
|
+
delete next.cookie;
|
|
145
|
+
delete next["X-API-Key"];
|
|
146
|
+
delete next["x-api-key"];
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
async function fetchFirecrawlApi(url, init, options) {
|
|
150
|
+
const allowLoopback = isLoopbackApiUrl(new URL(url));
|
|
151
|
+
let current = await validateRemoteUrl(url, firecrawlApiSsrfOptions(options, allowLoopback));
|
|
152
|
+
let headers = init.headers;
|
|
153
|
+
for (let redirects = 0;redirects <= DEFAULT_MAX_REDIRECTS; redirects++) {
|
|
154
|
+
const response = await fetch(current, { ...init, headers, redirect: "manual" });
|
|
155
|
+
if (!REDIRECT_STATUSES.has(response.status))
|
|
156
|
+
return response;
|
|
157
|
+
const location = response.headers.get("location");
|
|
158
|
+
if (!location)
|
|
159
|
+
return response;
|
|
160
|
+
if (redirects === DEFAULT_MAX_REDIRECTS)
|
|
161
|
+
throw new Error(`Too many redirects fetching ${current.toString()}`);
|
|
162
|
+
const next = await validateRemoteUrl(new URL(location, current), firecrawlApiSsrfOptions(options, allowLoopback));
|
|
163
|
+
if (next.origin !== current.origin)
|
|
164
|
+
headers = withoutSensitiveHeaders(headers);
|
|
165
|
+
current = next;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Too many redirects fetching ${current.toString()}`);
|
|
168
|
+
}
|
|
169
|
+
function scrapeBody(url) {
|
|
170
|
+
return {
|
|
171
|
+
url,
|
|
172
|
+
formats: ["markdown"],
|
|
173
|
+
onlyMainContent: true,
|
|
174
|
+
...allowFreshScrape() ? {} : { lockdown: true }
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function normalizeCount(value) {
|
|
178
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
179
|
+
return 5;
|
|
180
|
+
return Math.max(1, Math.min(Math.floor(value), 20));
|
|
181
|
+
}
|
|
182
|
+
function normalizeDomain(value) {
|
|
183
|
+
let input = value.trim().toLowerCase();
|
|
184
|
+
if (!input)
|
|
185
|
+
return null;
|
|
186
|
+
if (input.startsWith("-"))
|
|
187
|
+
input = input.slice(1).trim();
|
|
188
|
+
if (!input)
|
|
189
|
+
return null;
|
|
190
|
+
try {
|
|
191
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
192
|
+
input = parsed.hostname;
|
|
193
|
+
} catch {
|
|
194
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
195
|
+
}
|
|
196
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
197
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
198
|
+
}
|
|
199
|
+
function parseDomainFilter(domainFilter) {
|
|
200
|
+
const filters = { include: [], exclude: [] };
|
|
201
|
+
for (const raw of domainFilter ?? []) {
|
|
202
|
+
const domain = normalizeDomain(raw);
|
|
203
|
+
if (!domain)
|
|
204
|
+
continue;
|
|
205
|
+
const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
|
|
206
|
+
if (!target.includes(domain))
|
|
207
|
+
target.push(domain);
|
|
208
|
+
}
|
|
209
|
+
return filters;
|
|
210
|
+
}
|
|
211
|
+
function passesDomainFilters(url, filters) {
|
|
212
|
+
if (filters.include.length === 0 && filters.exclude.length === 0)
|
|
213
|
+
return true;
|
|
214
|
+
let hostname;
|
|
215
|
+
try {
|
|
216
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
const matches = (domain) => hostname === domain || hostname.endsWith(`.${domain}`);
|
|
221
|
+
if (filters.exclude.some(matches))
|
|
222
|
+
return false;
|
|
223
|
+
return filters.include.length === 0 || filters.include.some(matches);
|
|
224
|
+
}
|
|
225
|
+
function mapRecencyFilter(value) {
|
|
226
|
+
switch (value) {
|
|
227
|
+
case "day":
|
|
228
|
+
return "qdr:d";
|
|
229
|
+
case "week":
|
|
230
|
+
return "qdr:w";
|
|
231
|
+
case "month":
|
|
232
|
+
return "qdr:m";
|
|
233
|
+
case "year":
|
|
234
|
+
return "qdr:y";
|
|
235
|
+
default:
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function searchBody(query, options, numResults, filters) {
|
|
240
|
+
return {
|
|
241
|
+
query,
|
|
242
|
+
limit: numResults,
|
|
243
|
+
sources: ["web"],
|
|
244
|
+
...filters.include.length > 0 ? { includeDomains: filters.include } : {},
|
|
245
|
+
...filters.include.length === 0 && filters.exclude.length > 0 ? { excludeDomains: filters.exclude } : {},
|
|
246
|
+
...mapRecencyFilter(options.recencyFilter) ? { tbs: mapRecencyFilter(options.recencyFilter) } : {},
|
|
247
|
+
...options.includeContent ? {
|
|
248
|
+
scrapeOptions: {
|
|
249
|
+
formats: ["markdown"],
|
|
250
|
+
onlyMainContent: true,
|
|
251
|
+
...allowFreshScrape() ? {} : { lockdown: true }
|
|
252
|
+
}
|
|
253
|
+
} : {}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function firstString(...values) {
|
|
257
|
+
for (const value of values) {
|
|
258
|
+
if (typeof value === "string" && value.trim())
|
|
259
|
+
return value.trim();
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
function buildAnswer(results) {
|
|
264
|
+
return results.map((result) => {
|
|
265
|
+
if (result.snippet)
|
|
266
|
+
return `${result.snippet}
|
|
267
|
+
Source: ${result.title} (${result.url})`;
|
|
268
|
+
return `Source: ${result.title} (${result.url})`;
|
|
269
|
+
}).join(`
|
|
270
|
+
|
|
271
|
+
`);
|
|
272
|
+
}
|
|
273
|
+
function mapSearchResults(data, numResults, filters) {
|
|
274
|
+
const web = Array.isArray(data) ? data : data && typeof data === "object" ? data.web : undefined;
|
|
275
|
+
if (!Array.isArray(web))
|
|
276
|
+
throw new Error("Firecrawl search returned an unexpected web result shape");
|
|
277
|
+
const results = [];
|
|
278
|
+
const inlineContent = [];
|
|
279
|
+
const seen = new Set;
|
|
280
|
+
for (const rawItem of web) {
|
|
281
|
+
if (!rawItem || typeof rawItem !== "object" || Array.isArray(rawItem))
|
|
282
|
+
continue;
|
|
283
|
+
const item = rawItem;
|
|
284
|
+
const url = firstString(item.url, item.metadata?.sourceURL, item.metadata?.url);
|
|
285
|
+
if (!url || seen.has(url) || !passesDomainFilters(url, filters))
|
|
286
|
+
continue;
|
|
287
|
+
seen.add(url);
|
|
288
|
+
const title = firstString(item.title, item.metadata?.title) ?? url;
|
|
289
|
+
const snippet = firstString(item.description, item.snippet, item.metadata?.description) ?? "";
|
|
290
|
+
results.push({ title, url, snippet });
|
|
291
|
+
const markdown = firstString(item.markdown);
|
|
292
|
+
if (markdown)
|
|
293
|
+
inlineContent.push({ url, title, content: markdown, error: null });
|
|
294
|
+
if (results.length >= numResults)
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
return { results, inlineContent };
|
|
298
|
+
}
|
|
299
|
+
async function firecrawlFetch(endpoint, body, signal, options, label = endpoint, activity = undefined) {
|
|
300
|
+
const baseUrl = requireBaseUrl();
|
|
301
|
+
const version = getApiVersion();
|
|
302
|
+
const apiKey = await getApiKey(signal);
|
|
303
|
+
const headers = { "Content-Type": "application/json" };
|
|
304
|
+
if (apiKey)
|
|
305
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
306
|
+
const requestUrl = `${baseUrl}/${version}/${endpoint}`;
|
|
307
|
+
const activityId = activityMonitor.logStart(activity ?? { type: "fetch", url: requestUrl });
|
|
308
|
+
try {
|
|
309
|
+
const response = await fetchFirecrawlApi(requestUrl, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers,
|
|
312
|
+
body: JSON.stringify(body),
|
|
313
|
+
signal: requestSignal(options?.timeoutMs ?? EXTRACT_TIMEOUT_MS, signal)
|
|
314
|
+
}, options);
|
|
315
|
+
if (!response.ok) {
|
|
316
|
+
const text = await response.text().catch(() => "");
|
|
317
|
+
throw new Error(`Firecrawl ${label} error ${response.status}: ${redactCredential(text.slice(0, 300), apiKey)}`);
|
|
318
|
+
}
|
|
319
|
+
let data;
|
|
320
|
+
try {
|
|
321
|
+
data = await response.json();
|
|
322
|
+
} catch (err) {
|
|
323
|
+
throw new Error(`Firecrawl ${label} returned invalid JSON: ${errorMessage(err)}`);
|
|
324
|
+
}
|
|
325
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
326
|
+
throw new Error(`Firecrawl ${label} returned an unexpected response shape`);
|
|
327
|
+
}
|
|
328
|
+
const envelope = data;
|
|
329
|
+
if (envelope.success === false) {
|
|
330
|
+
const reason = typeof envelope.error === "string" && envelope.error.trim() ? envelope.error : "unknown error";
|
|
331
|
+
throw new Error(`Firecrawl ${label} unsuccessful: ${redactCredential(reason, apiKey)}`);
|
|
332
|
+
}
|
|
333
|
+
if (envelope.success !== true) {
|
|
334
|
+
throw new Error(`Firecrawl ${label} returned an unexpected response shape`);
|
|
335
|
+
}
|
|
336
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
337
|
+
return envelope;
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (isAbortError(err))
|
|
340
|
+
activityMonitor.logComplete(activityId, 0);
|
|
341
|
+
else
|
|
342
|
+
activityMonitor.logError(activityId, errorMessage(err));
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
export function isFirecrawlAvailable() {
|
|
347
|
+
return getBaseUrl() !== null;
|
|
348
|
+
}
|
|
349
|
+
export async function searchWithFirecrawl(query, options = {}) {
|
|
350
|
+
requireBaseUrl();
|
|
351
|
+
const numResults = normalizeCount(options.numResults);
|
|
352
|
+
const filters = parseDomainFilter(options.domainFilter);
|
|
353
|
+
const envelope = await firecrawlFetch("search", searchBody(query, options, numResults, filters), options.signal, { ...options, timeoutMs: options.timeoutMs ?? SEARCH_TIMEOUT_MS }, "search", { type: "api", query });
|
|
354
|
+
const mapped = mapSearchResults(envelope.data, numResults, filters);
|
|
355
|
+
const response = { answer: buildAnswer(mapped.results), results: mapped.results };
|
|
356
|
+
if (options.includeContent && mapped.inlineContent.length > 0)
|
|
357
|
+
response.inlineContent = mapped.inlineContent;
|
|
358
|
+
return response;
|
|
359
|
+
}
|
|
360
|
+
export async function extractWithFirecrawl(url, signal, options) {
|
|
361
|
+
requireBaseUrl();
|
|
362
|
+
await validateRemoteUrl(url, ssrfOptions(options));
|
|
363
|
+
const envelope = await firecrawlFetch("scrape", scrapeBody(url), signal, options);
|
|
364
|
+
const data = envelope.data;
|
|
365
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
366
|
+
throw new Error("Firecrawl scrape returned an unexpected data shape");
|
|
367
|
+
}
|
|
368
|
+
const scrape = data;
|
|
369
|
+
if (typeof scrape.markdown !== "string") {
|
|
370
|
+
throw new Error("Firecrawl scrape returned markdown in an unexpected shape");
|
|
371
|
+
}
|
|
372
|
+
const content = scrape.markdown.trim();
|
|
373
|
+
if (!content)
|
|
374
|
+
return null;
|
|
375
|
+
const metadataTitle = scrape.metadata?.title;
|
|
376
|
+
const title = typeof metadataTitle === "string" && metadataTitle.trim() ? metadataTitle.trim() : typeof scrape.title === "string" ? scrape.title.trim() : "";
|
|
377
|
+
return { url, title, content, error: null };
|
|
378
|
+
}
|