@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.
Files changed (72) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +140 -4
  3. package/extensions/dm-ponytail/extensions/ponytail.mjs +1 -1
  4. package/extensions/dm-web-access/SECURITY.md +5 -0
  5. package/extensions/dm-web-access/activity.js +65 -0
  6. package/extensions/dm-web-access/anysearch.js +158 -0
  7. package/extensions/dm-web-access/auth-fetch.js +131 -0
  8. package/extensions/dm-web-access/bocha.js +214 -0
  9. package/extensions/dm-web-access/brave.js +196 -0
  10. package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
  11. package/extensions/dm-web-access/brightdata.js +334 -0
  12. package/extensions/dm-web-access/chrome-cookies.js +627 -0
  13. package/extensions/dm-web-access/content-find.js +114 -0
  14. package/extensions/dm-web-access/credential-source.js +150 -0
  15. package/extensions/dm-web-access/curator-page.js +3559 -0
  16. package/extensions/dm-web-access/curator-server.js +691 -0
  17. package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
  18. package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
  19. package/extensions/dm-web-access/declared-web-links.js +167 -0
  20. package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
  21. package/extensions/dm-web-access/duckduckgo.js +118 -0
  22. package/extensions/dm-web-access/exa.js +401 -0
  23. package/extensions/dm-web-access/extract.js +1220 -0
  24. package/extensions/dm-web-access/feature-config.js +24 -0
  25. package/extensions/dm-web-access/fetch-params.js +81 -0
  26. package/extensions/dm-web-access/firecrawl.js +378 -0
  27. package/extensions/dm-web-access/gemini-adc.js +241 -0
  28. package/extensions/dm-web-access/gemini-api.js +258 -0
  29. package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
  30. package/extensions/dm-web-access/gemini-search.js +889 -0
  31. package/extensions/dm-web-access/gemini-url-context.js +97 -0
  32. package/extensions/dm-web-access/gemini-web-config.js +84 -0
  33. package/extensions/dm-web-access/gemini-web.js +351 -0
  34. package/extensions/dm-web-access/github-api.js +166 -0
  35. package/extensions/dm-web-access/github-extract.js +991 -0
  36. package/extensions/dm-web-access/github-issue-pr.js +750 -0
  37. package/extensions/dm-web-access/index.js +3117 -0
  38. package/extensions/dm-web-access/jina-search.js +242 -0
  39. package/extensions/dm-web-access/kagi.js +255 -0
  40. package/extensions/dm-web-access/kimi-search.js +214 -0
  41. package/extensions/dm-web-access/ollama.js +209 -0
  42. package/extensions/dm-web-access/openai-search.js +510 -0
  43. package/extensions/dm-web-access/package.json +34 -0
  44. package/extensions/dm-web-access/page-query.js +124 -0
  45. package/extensions/dm-web-access/parallel-mcp.js +223 -0
  46. package/extensions/dm-web-access/parallel.js +345 -0
  47. package/extensions/dm-web-access/pdf-extract.js +257 -0
  48. package/extensions/dm-web-access/perplexity.js +151 -0
  49. package/extensions/dm-web-access/querit.js +327 -0
  50. package/extensions/dm-web-access/query-rewrite.js +40 -0
  51. package/extensions/dm-web-access/render-search-error.js +80 -0
  52. package/extensions/dm-web-access/rsc-extract.js +347 -0
  53. package/extensions/dm-web-access/search1api.js +245 -0
  54. package/extensions/dm-web-access/searchinfinity.js +221 -0
  55. package/extensions/dm-web-access/searxng.js +223 -0
  56. package/extensions/dm-web-access/serpbase.js +205 -0
  57. package/extensions/dm-web-access/serpdive.js +238 -0
  58. package/extensions/dm-web-access/serper.js +200 -0
  59. package/extensions/dm-web-access/source-check.js +198 -0
  60. package/extensions/dm-web-access/ssrf-protection.js +436 -0
  61. package/extensions/dm-web-access/storage.js +451 -0
  62. package/extensions/dm-web-access/summary-model-scope.js +83 -0
  63. package/extensions/dm-web-access/summary-review.js +364 -0
  64. package/extensions/dm-web-access/tavily.js +199 -0
  65. package/extensions/dm-web-access/tinyfish.js +325 -0
  66. package/extensions/dm-web-access/utils.js +476 -0
  67. package/extensions/dm-web-access/valyu.js +189 -0
  68. package/extensions/dm-web-access/video-extract.js +336 -0
  69. package/extensions/dm-web-access/xai-search.js +285 -0
  70. package/extensions/dm-web-access/xcrawl.js +221 -0
  71. package/extensions/dm-web-access/youtube-extract.js +279 -0
  72. package/package.json +9 -1
@@ -0,0 +1,364 @@
1
+ import { clampThinkingLevel } from "@duckmind/dm-ai";
2
+ import { complete, completeSimple } from "@duckmind/dm-ai/compat";
3
+ import { findModelWithProviderRouting, loadEnabledModelPatterns, modelMatchesEnabledPatterns, splitThinkingSuffix } from "./summary-model-scope.js";
4
+ const PREFERRED_SUMMARY_MODELS = [
5
+ { provider: "anthropic", id: "claude-haiku-4-5" },
6
+ { provider: "openai-codex", id: "gpt-5.6-luna" },
7
+ { provider: "openai-codex", id: "gpt-5.6-terra" },
8
+ { provider: "google", id: "gemini-3.6-flash" },
9
+ { provider: "openai", id: "gpt-5-mini" },
10
+ { provider: "deepseek", id: "deepseek-v4-flash" }
11
+ ];
12
+ export const SUMMARY_GENERATION_DEADLINE_MS = 30000;
13
+ function estimateTokens(text) {
14
+ const trimmed = text.trim();
15
+ if (trimmed.length === 0)
16
+ return 0;
17
+ return Math.max(1, Math.ceil(trimmed.length / 4));
18
+ }
19
+ function summarizeQueryResult(result) {
20
+ if (result.error) {
21
+ return `Query: ${result.query}
22
+ Status: Error
23
+ Error: ${result.error}`;
24
+ }
25
+ const lines = [
26
+ `Query: ${result.query}`,
27
+ `Provider: ${result.provider ?? "unknown"}`,
28
+ `Answer: ${result.answer || "(no answer text returned)"}`
29
+ ];
30
+ if (result.results.length === 0) {
31
+ lines.push("Sources: none");
32
+ return lines.join(`
33
+ `);
34
+ }
35
+ lines.push("Sources:");
36
+ for (let i = 0;i < result.results.length; i++) {
37
+ const source = result.results[i];
38
+ lines.push(`${i + 1}. ${source.title} — ${source.url}`);
39
+ }
40
+ return lines.join(`
41
+ `);
42
+ }
43
+ export function buildSummaryPrompt(results, feedback) {
44
+ const sections = [
45
+ "You are writing the final web search summary for a coding assistant.",
46
+ "Write a concise, factual summary using only the provided search results.",
47
+ "Requirements:",
48
+ "- Keep it readable and skimmable.",
49
+ "- Include key findings and caveats.",
50
+ "- Do not invent sources or claims.",
51
+ "- If evidence is weak or conflicting, say so explicitly.",
52
+ '- End with a short "Sources" section listing the most relevant URLs.'
53
+ ];
54
+ if (feedback) {
55
+ sections.push("- Incorporate the user feedback provided below into the summary.");
56
+ }
57
+ sections.push("");
58
+ sections.push("<search_results>");
59
+ for (let i = 0;i < results.length; i++) {
60
+ sections.push(`
61
+ [Result ${i + 1}]`);
62
+ sections.push(summarizeQueryResult(results[i]));
63
+ }
64
+ sections.push(`
65
+ </search_results>`);
66
+ if (feedback) {
67
+ sections.push("");
68
+ sections.push("<user_feedback>");
69
+ sections.push(feedback);
70
+ sections.push("</user_feedback>");
71
+ }
72
+ return sections.join(`
73
+ `);
74
+ }
75
+ function buildDeterministicAnswerPreview(answer) {
76
+ let text = answer.replace(/\s+/g, " ").trim();
77
+ if (text.length === 0)
78
+ return "";
79
+ const sourceMarker = text.search(/\bSources?\s*:/i);
80
+ if (sourceMarker >= 0)
81
+ text = text.slice(0, sourceMarker).trim();
82
+ if (text.length === 0)
83
+ return "";
84
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
85
+ }
86
+ function buildDeterministicSummaryLines(results) {
87
+ if (results.length === 0) {
88
+ return [
89
+ "No completed search results were available when the curator session finished.",
90
+ "",
91
+ "Sources",
92
+ "- None"
93
+ ];
94
+ }
95
+ const lines = [
96
+ "Summary based on the currently selected search results.",
97
+ ""
98
+ ];
99
+ const sourceUrls = [];
100
+ let successful = 0;
101
+ let failed = 0;
102
+ for (const result of results) {
103
+ if (result.error) {
104
+ failed += 1;
105
+ lines.push(`- ${result.query}: failed (${result.error})`);
106
+ continue;
107
+ }
108
+ successful += 1;
109
+ const preview = buildDeterministicAnswerPreview(result.answer);
110
+ if (preview.length > 0) {
111
+ lines.push(`- ${result.query}: ${preview}`);
112
+ } else {
113
+ lines.push(`- ${result.query}: returned ${result.results.length} source${result.results.length === 1 ? "" : "s"} without answer text.`);
114
+ }
115
+ for (const source of result.results) {
116
+ if (!sourceUrls.includes(source.url)) {
117
+ sourceUrls.push(source.url);
118
+ }
119
+ }
120
+ }
121
+ lines.push("");
122
+ lines.push(`Completed queries: ${results.length}`);
123
+ lines.push(`Successful: ${successful}`);
124
+ lines.push(`Failed: ${failed}`);
125
+ lines.push("");
126
+ lines.push("Sources");
127
+ if (sourceUrls.length === 0) {
128
+ lines.push("- None");
129
+ } else {
130
+ for (const url of sourceUrls.slice(0, 12)) {
131
+ lines.push(`- ${url}`);
132
+ }
133
+ if (sourceUrls.length > 12) {
134
+ lines.push(`- ... and ${sourceUrls.length - 12} more`);
135
+ }
136
+ }
137
+ return lines;
138
+ }
139
+ export function buildDeterministicSummary(results) {
140
+ const summary = buildDeterministicSummaryLines(results).join(`
141
+ `).trim();
142
+ const nonEmptySummary = summary.length > 0 ? summary : `No completed search results were available when the curator session finished.
143
+
144
+ Sources
145
+ - None`;
146
+ return {
147
+ summary: nonEmptySummary,
148
+ meta: {
149
+ model: null,
150
+ durationMs: 0,
151
+ tokenEstimate: estimateTokens(nonEmptySummary),
152
+ fallbackUsed: true,
153
+ fallbackReason: "deterministic-submit-fallback",
154
+ phase: "deterministic-fallback",
155
+ edited: false
156
+ }
157
+ };
158
+ }
159
+ function parseModelSelector(value) {
160
+ const selector = splitThinkingSuffix(value);
161
+ const slashIndex = selector.value.indexOf("/");
162
+ if (slashIndex <= 0 || slashIndex >= selector.value.length - 1) {
163
+ throw new Error(`Invalid summary model: ${value}. Use provider/model-id.`);
164
+ }
165
+ return {
166
+ provider: selector.value.slice(0, slashIndex),
167
+ id: selector.value.slice(slashIndex + 1),
168
+ thinkingLevel: selector.thinkingLevel
169
+ };
170
+ }
171
+ function resolveThinkingLevel(model, requested) {
172
+ if (!requested)
173
+ return;
174
+ return clampThinkingLevel(model, requested);
175
+ }
176
+ async function resolveSummaryModelCandidates(ctx, modelOverride) {
177
+ const enabledModelPatterns = loadEnabledModelPatterns(ctx);
178
+ const specs = [];
179
+ const normalizedOverride = typeof modelOverride === "string" ? modelOverride.trim() : "";
180
+ if (normalizedOverride.length > 0)
181
+ specs.push(parseModelSelector(normalizedOverride));
182
+ specs.push(...PREFERRED_SUMMARY_MODELS);
183
+ const candidates = [];
184
+ const errors = [];
185
+ const seen = new Set;
186
+ for (const spec of specs) {
187
+ const value = `${spec.provider}/${spec.id}`;
188
+ if (seen.has(value))
189
+ continue;
190
+ seen.add(value);
191
+ const model = findModelWithProviderRouting(ctx.modelRegistry, spec.provider, spec.id);
192
+ if (!model) {
193
+ errors.push(`Summary model not found: ${value}`);
194
+ continue;
195
+ }
196
+ if (!modelMatchesEnabledPatterns(model, enabledModelPatterns)) {
197
+ errors.push(`Summary model is not enabled: ${value}`);
198
+ continue;
199
+ }
200
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
201
+ if (!auth.ok) {
202
+ errors.push(`No API key available for summary model ${value}`);
203
+ continue;
204
+ }
205
+ candidates.push({ model, apiKey: auth.apiKey, headers: auth.headers, thinkingLevel: spec.thinkingLevel });
206
+ }
207
+ return { candidates, errors };
208
+ }
209
+ function buildFallbackSummary(results, fallbackReason, durationMs = 0) {
210
+ const deterministic = buildDeterministicSummary(results);
211
+ return {
212
+ summary: deterministic.summary,
213
+ meta: {
214
+ ...deterministic.meta,
215
+ durationMs,
216
+ fallbackReason
217
+ }
218
+ };
219
+ }
220
+ function isAbortError(err) {
221
+ if (!err || typeof err !== "object")
222
+ return false;
223
+ const name = err.name;
224
+ const message = err.message;
225
+ return name === "AbortError" || typeof message === "string" && message.toLowerCase().includes("abort");
226
+ }
227
+ function getTextFromContentPart(part) {
228
+ if (!part || typeof part !== "object")
229
+ return "";
230
+ const value = part;
231
+ if (typeof value.text === "string")
232
+ return value.text;
233
+ if (typeof value.refusal === "string")
234
+ return value.refusal;
235
+ return "";
236
+ }
237
+ function getContentPartType(part) {
238
+ if (!part || typeof part !== "object")
239
+ return "unknown";
240
+ const value = part;
241
+ return typeof value.type === "string" ? value.type : "unknown";
242
+ }
243
+ export async function generateSummaryDraft(results, ctx, signal, modelOverride, feedback, completeFn, deadlineMs = SUMMARY_GENERATION_DEADLINE_MS) {
244
+ if (!ctx || !ctx.modelRegistry) {
245
+ throw new Error("Summary generation context unavailable");
246
+ }
247
+ const registry = ctx.modelRegistry;
248
+ const customCompleteFn = completeFn !== undefined;
249
+ const usesRegistryComplete = !customCompleteFn && typeof registry.complete === "function";
250
+ completeFn ??= usesRegistryComplete ? registry.complete.bind(registry) : complete;
251
+ const generationStartedAt = Date.now();
252
+ const deadlineController = new AbortController;
253
+ const deadlineMarker = Symbol("summary-generation-deadline");
254
+ let deadlineTimer;
255
+ let resolveDeadline;
256
+ const deadlinePromise = new Promise((resolve) => {
257
+ resolveDeadline = () => resolve(deadlineMarker);
258
+ deadlineTimer = setTimeout(() => {
259
+ deadlineController.abort();
260
+ resolveDeadline();
261
+ }, deadlineMs);
262
+ });
263
+ let callerAbortListener;
264
+ const callerAbortPromise = signal ? new Promise((_, reject) => {
265
+ callerAbortListener = () => reject(new Error("Aborted"));
266
+ if (signal.aborted)
267
+ callerAbortListener();
268
+ else
269
+ signal.addEventListener("abort", callerAbortListener, { once: true });
270
+ }) : undefined;
271
+ const completionSignal = signal ? AbortSignal.any([signal, deadlineController.signal]) : deadlineController.signal;
272
+ async function raceSummaryOperation(operation) {
273
+ operation.then(() => {
274
+ return;
275
+ }, () => {
276
+ return;
277
+ });
278
+ const contenders = [operation, deadlinePromise];
279
+ if (callerAbortPromise)
280
+ contenders.push(callerAbortPromise);
281
+ const result = await Promise.race(contenders);
282
+ if (result === deadlineMarker) {
283
+ if (signal?.aborted)
284
+ throw new Error("Aborted");
285
+ throw deadlineMarker;
286
+ }
287
+ return result;
288
+ }
289
+ try {
290
+ if (signal?.aborted)
291
+ throw new Error("Aborted");
292
+ const prompt = buildSummaryPrompt(results, feedback);
293
+ let resolved;
294
+ try {
295
+ resolved = await raceSummaryOperation(resolveSummaryModelCandidates(ctx, modelOverride));
296
+ } catch (err) {
297
+ if (signal?.aborted)
298
+ throw new Error("Aborted");
299
+ if (err === deadlineMarker || deadlineController.signal.aborted) {
300
+ return buildFallbackSummary(results, "summary-generation-timeout", Date.now() - generationStartedAt);
301
+ }
302
+ const message = err instanceof Error ? err.message : String(err);
303
+ return buildFallbackSummary(results, `summary-model-settings-error: ${message}`, Date.now() - generationStartedAt);
304
+ }
305
+ let lastError = resolved.errors.at(-1);
306
+ for (const { model, apiKey, headers, thinkingLevel } of resolved.candidates) {
307
+ const startedAt = Date.now();
308
+ try {
309
+ const userMessage = {
310
+ role: "user",
311
+ content: [{ type: "text", text: prompt }],
312
+ timestamp: Date.now()
313
+ };
314
+ const requestedThinkingLevel = resolveThinkingLevel(model, thinkingLevel);
315
+ const enabledThinkingLevel = requestedThinkingLevel && requestedThinkingLevel !== "off" ? requestedThinkingLevel : undefined;
316
+ const completionOptions = {
317
+ ...usesRegistryComplete ? {} : { apiKey, headers },
318
+ signal: completionSignal,
319
+ ...requestedThinkingLevel ? { reasoning: requestedThinkingLevel } : {},
320
+ ...enabledThinkingLevel ? { reasoningEffort: enabledThinkingLevel } : {}
321
+ };
322
+ const completion = thinkingLevel !== undefined && !customCompleteFn && !usesRegistryComplete ? completeSimple(model, { messages: [userMessage] }, { apiKey, headers, signal: completionSignal, ...enabledThinkingLevel ? { reasoning: enabledThinkingLevel } : {} }) : completeFn(model, { messages: [userMessage] }, completionOptions);
323
+ const response = await raceSummaryOperation(Promise.resolve(completion));
324
+ if (response.stopReason === "aborted") {
325
+ throw new Error("Aborted");
326
+ }
327
+ const contentParts = Array.isArray(response.content) ? response.content : [];
328
+ const summary = contentParts.map((part) => getTextFromContentPart(part)).filter((text) => text.trim().length > 0).join(`
329
+ `).trim();
330
+ if (summary.length === 0) {
331
+ const partTypes = contentParts.map((part) => getContentPartType(part));
332
+ const typesLabel = partTypes.length > 0 ? partTypes.join(", ") : "none";
333
+ throw new Error(`Summary model returned empty response (content parts: ${typesLabel})`);
334
+ }
335
+ return {
336
+ summary,
337
+ meta: {
338
+ model: `${model.provider}/${model.id}`,
339
+ durationMs: Math.max(0, Date.now() - startedAt),
340
+ tokenEstimate: estimateTokens(summary),
341
+ fallbackUsed: false,
342
+ phase: "summary-model",
343
+ edited: false
344
+ }
345
+ };
346
+ } catch (err) {
347
+ if (signal?.aborted)
348
+ throw new Error("Aborted");
349
+ if (err === deadlineMarker || deadlineController.signal.aborted) {
350
+ return buildFallbackSummary(results, "summary-generation-timeout", Date.now() - generationStartedAt);
351
+ }
352
+ if (isAbortError(err))
353
+ throw err;
354
+ lastError = err instanceof Error ? err.message : String(err);
355
+ }
356
+ }
357
+ return buildFallbackSummary(results, lastError ? `summary-model-unavailable: ${lastError}` : "summary-model-unavailable", Date.now() - generationStartedAt);
358
+ } finally {
359
+ if (deadlineTimer)
360
+ clearTimeout(deadlineTimer);
361
+ if (signal && callerAbortListener)
362
+ signal.removeEventListener("abort", callerAbortListener);
363
+ }
364
+ }
@@ -0,0 +1,199 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { activityMonitor } from "./activity.js";
3
+ import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.js";
4
+ import { fetchWithCredentialRedirects, getWebSearchConfigPath, resolveApiBaseUrl } from "./utils.js";
5
+ const TAVILY_API_BASE_URL = "https://api.tavily.com";
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, "utf-8");
17
+ try {
18
+ cachedConfig = JSON.parse(raw);
19
+ return cachedConfig;
20
+ } catch (err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
23
+ }
24
+ }
25
+ async function getApiKey(signal) {
26
+ return resolveCredential({
27
+ provider: "Tavily",
28
+ configuredValue: loadConfig().tavilyApiKey,
29
+ environmentValue: process.env.TAVILY_API_KEY,
30
+ signal
31
+ });
32
+ }
33
+ function getApiUrl() {
34
+ return `${resolveApiBaseUrl({
35
+ configKey: "tavilyBaseUrl",
36
+ configuredValue: loadConfig().tavilyBaseUrl,
37
+ defaultValue: TAVILY_API_BASE_URL,
38
+ environmentKey: "TAVILY_BASE_URL",
39
+ environmentValue: process.env.TAVILY_BASE_URL
40
+ })}/search`;
41
+ }
42
+ async function requireApiKey(signal) {
43
+ const apiKey = await getApiKey(signal);
44
+ if (!apiKey) {
45
+ throw new Error(`Tavily API key not found. Either:
46
+ ` + ` 1. Create ${CONFIG_PATH} with { "tavilyApiKey": "your-key" }
47
+ ` + ` 2. Set TAVILY_API_KEY environment variable
48
+ ` + "Get a key at https://app.tavily.com/");
49
+ }
50
+ return apiKey;
51
+ }
52
+ function normalizeCount(value) {
53
+ if (typeof value !== "number" || !Number.isFinite(value))
54
+ return 5;
55
+ return Math.max(1, Math.min(Math.floor(value), 20));
56
+ }
57
+ function normalizeDomain(value) {
58
+ let input = value.trim().toLowerCase();
59
+ if (!input)
60
+ return null;
61
+ if (input.startsWith("-"))
62
+ input = input.slice(1).trim();
63
+ if (!input)
64
+ return null;
65
+ try {
66
+ const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
67
+ input = parsed.hostname;
68
+ } catch {
69
+ input = input.split("/")[0]?.split(":")[0] ?? "";
70
+ }
71
+ input = input.replace(/^\.+|\.+$/g, "");
72
+ return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
73
+ }
74
+ function mapDomainFilter(domainFilter) {
75
+ if (!domainFilter?.length)
76
+ return {};
77
+ const include_domains = [];
78
+ const exclude_domains = [];
79
+ for (const raw of domainFilter) {
80
+ const domain = normalizeDomain(raw);
81
+ if (!domain)
82
+ continue;
83
+ const target = raw.trim().startsWith("-") ? exclude_domains : include_domains;
84
+ if (!target.includes(domain))
85
+ target.push(domain);
86
+ }
87
+ return {
88
+ ...include_domains.length > 0 ? { include_domains } : {},
89
+ ...exclude_domains.length > 0 ? { exclude_domains } : {}
90
+ };
91
+ }
92
+ function requestSignal(signal) {
93
+ const timeout = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
94
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
95
+ }
96
+ function errorMessage(err) {
97
+ return err instanceof Error ? err.message : String(err);
98
+ }
99
+ function mapResults(results, numResults) {
100
+ if (!Array.isArray(results))
101
+ return [];
102
+ const mapped = [];
103
+ for (const item of results) {
104
+ if (!item?.url)
105
+ continue;
106
+ mapped.push({
107
+ title: item.title || `Source ${mapped.length + 1}`,
108
+ url: item.url,
109
+ snippet: typeof item.content === "string" ? item.content.replace(/\s+/g, " ").trim() : ""
110
+ });
111
+ if (mapped.length >= numResults)
112
+ break;
113
+ }
114
+ return mapped;
115
+ }
116
+ function mapInlineContent(results) {
117
+ if (!Array.isArray(results))
118
+ return [];
119
+ return results.flatMap((item) => {
120
+ if (!item?.url || typeof item.raw_content !== "string" || item.raw_content.trim().length === 0)
121
+ return [];
122
+ return [{
123
+ url: item.url,
124
+ title: item.title || "",
125
+ content: item.raw_content,
126
+ error: null
127
+ }];
128
+ });
129
+ }
130
+ export function isTavilyAvailable() {
131
+ return hasCredentialSource({
132
+ provider: "Tavily",
133
+ configuredValue: loadConfig().tavilyApiKey,
134
+ environmentValue: process.env.TAVILY_API_KEY
135
+ });
136
+ }
137
+ export async function searchWithTavily(query, options = {}) {
138
+ const apiUrl = getApiUrl();
139
+ const apiKey = await requireApiKey(options.signal);
140
+ const numResults = normalizeCount(options.numResults);
141
+ const body = {
142
+ query,
143
+ search_depth: "basic",
144
+ max_results: numResults,
145
+ include_answer: "basic",
146
+ include_raw_content: options.includeContent ? "markdown" : false,
147
+ ...options.recencyFilter ? { time_range: options.recencyFilter } : {},
148
+ ...mapDomainFilter(options.domainFilter)
149
+ };
150
+ const activityId = activityMonitor.logStart({ type: "api", query });
151
+ let response;
152
+ try {
153
+ response = await fetchWithCredentialRedirects(apiUrl, {
154
+ method: "POST",
155
+ headers: {
156
+ Authorization: `Bearer ${apiKey}`,
157
+ "Content-Type": "application/json"
158
+ },
159
+ body: JSON.stringify(body),
160
+ signal: requestSignal(options.signal)
161
+ }, ["Authorization"]);
162
+ } catch (err) {
163
+ const message = errorMessage(err);
164
+ const redactedMessage = redactCredential(message, apiKey);
165
+ if (redactedMessage.toLowerCase().includes("abort"))
166
+ activityMonitor.logComplete(activityId, 0);
167
+ else
168
+ activityMonitor.logError(activityId, redactedMessage);
169
+ if (redactedMessage === message)
170
+ throw err;
171
+ const redactedError = new Error(redactedMessage);
172
+ if (err instanceof Error)
173
+ redactedError.name = err.name;
174
+ throw redactedError;
175
+ }
176
+ if (!response.ok) {
177
+ activityMonitor.logComplete(activityId, response.status);
178
+ const errorText = redactCredential(await response.text(), apiKey);
179
+ throw new Error(`Tavily API error ${response.status}: ${errorText.slice(0, 300)}`);
180
+ }
181
+ let data;
182
+ try {
183
+ data = await response.json();
184
+ } catch (err) {
185
+ activityMonitor.logComplete(activityId, response.status);
186
+ throw new Error(`Tavily API returned invalid JSON: ${errorMessage(err)}`);
187
+ }
188
+ activityMonitor.logComplete(activityId, response.status);
189
+ const result = {
190
+ answer: typeof data.answer === "string" ? data.answer : "",
191
+ results: mapResults(data.results, numResults)
192
+ };
193
+ if (options.includeContent) {
194
+ const inlineContent = mapInlineContent(data.results);
195
+ if (inlineContent.length > 0)
196
+ result.inlineContent = inlineContent;
197
+ }
198
+ return result;
199
+ }