@duckmind/dm-windows-x64 0.63.0 → 0.63.6

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 (101) hide show
  1. package/dm.exe +0 -0
  2. package/extensions/.dm-extensions.json +200 -4
  3. package/extensions/dm-context/package.json +1 -1
  4. package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
  5. package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
  6. package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
  7. package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
  8. package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
  9. package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
  10. package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
  11. package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
  12. package/extensions/dm-context/src/context.js +3 -2
  13. package/extensions/dm-context/src/index.js +168 -84
  14. package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
  15. package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
  16. package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
  17. package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
  18. package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
  19. package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
  20. package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
  21. package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
  22. package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
  23. package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
  24. package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
  25. package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
  26. package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
  27. package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
  28. package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
  29. package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
  30. package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
  31. package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
  32. package/extensions/dm-skills-manager/package.json +121 -0
  33. package/extensions/dm-web-access/SECURITY.md +5 -0
  34. package/extensions/dm-web-access/activity.js +65 -0
  35. package/extensions/dm-web-access/anysearch.js +158 -0
  36. package/extensions/dm-web-access/auth-fetch.js +131 -0
  37. package/extensions/dm-web-access/bocha.js +214 -0
  38. package/extensions/dm-web-access/brave.js +196 -0
  39. package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
  40. package/extensions/dm-web-access/brightdata.js +334 -0
  41. package/extensions/dm-web-access/chrome-cookies.js +627 -0
  42. package/extensions/dm-web-access/content-find.js +114 -0
  43. package/extensions/dm-web-access/credential-source.js +150 -0
  44. package/extensions/dm-web-access/curator-page.js +3559 -0
  45. package/extensions/dm-web-access/curator-server.js +691 -0
  46. package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
  47. package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
  48. package/extensions/dm-web-access/declared-web-links.js +167 -0
  49. package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
  50. package/extensions/dm-web-access/duckduckgo.js +118 -0
  51. package/extensions/dm-web-access/exa.js +401 -0
  52. package/extensions/dm-web-access/extract.js +1220 -0
  53. package/extensions/dm-web-access/feature-config.js +24 -0
  54. package/extensions/dm-web-access/fetch-params.js +81 -0
  55. package/extensions/dm-web-access/firecrawl.js +378 -0
  56. package/extensions/dm-web-access/gemini-adc.js +241 -0
  57. package/extensions/dm-web-access/gemini-api.js +258 -0
  58. package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
  59. package/extensions/dm-web-access/gemini-search.js +889 -0
  60. package/extensions/dm-web-access/gemini-url-context.js +97 -0
  61. package/extensions/dm-web-access/gemini-web-config.js +84 -0
  62. package/extensions/dm-web-access/gemini-web.js +351 -0
  63. package/extensions/dm-web-access/github-api.js +166 -0
  64. package/extensions/dm-web-access/github-extract.js +991 -0
  65. package/extensions/dm-web-access/github-issue-pr.js +750 -0
  66. package/extensions/dm-web-access/index.js +3117 -0
  67. package/extensions/dm-web-access/jina-search.js +242 -0
  68. package/extensions/dm-web-access/kagi.js +255 -0
  69. package/extensions/dm-web-access/kimi-search.js +214 -0
  70. package/extensions/dm-web-access/ollama.js +209 -0
  71. package/extensions/dm-web-access/openai-search.js +510 -0
  72. package/extensions/dm-web-access/package.json +34 -0
  73. package/extensions/dm-web-access/page-query.js +124 -0
  74. package/extensions/dm-web-access/parallel-mcp.js +223 -0
  75. package/extensions/dm-web-access/parallel.js +345 -0
  76. package/extensions/dm-web-access/pdf-extract.js +257 -0
  77. package/extensions/dm-web-access/perplexity.js +151 -0
  78. package/extensions/dm-web-access/querit.js +327 -0
  79. package/extensions/dm-web-access/query-rewrite.js +40 -0
  80. package/extensions/dm-web-access/render-search-error.js +80 -0
  81. package/extensions/dm-web-access/rsc-extract.js +347 -0
  82. package/extensions/dm-web-access/search1api.js +245 -0
  83. package/extensions/dm-web-access/searchinfinity.js +221 -0
  84. package/extensions/dm-web-access/searxng.js +223 -0
  85. package/extensions/dm-web-access/serpbase.js +205 -0
  86. package/extensions/dm-web-access/serpdive.js +238 -0
  87. package/extensions/dm-web-access/serper.js +200 -0
  88. package/extensions/dm-web-access/source-check.js +198 -0
  89. package/extensions/dm-web-access/ssrf-protection.js +436 -0
  90. package/extensions/dm-web-access/storage.js +451 -0
  91. package/extensions/dm-web-access/summary-model-scope.js +83 -0
  92. package/extensions/dm-web-access/summary-review.js +364 -0
  93. package/extensions/dm-web-access/tavily.js +199 -0
  94. package/extensions/dm-web-access/tinyfish.js +325 -0
  95. package/extensions/dm-web-access/utils.js +476 -0
  96. package/extensions/dm-web-access/valyu.js +189 -0
  97. package/extensions/dm-web-access/video-extract.js +336 -0
  98. package/extensions/dm-web-access/xai-search.js +285 -0
  99. package/extensions/dm-web-access/xcrawl.js +221 -0
  100. package/extensions/dm-web-access/youtube-extract.js +279 -0
  101. package/package.json +9 -1
@@ -0,0 +1,131 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { getWebSearchConfigPath } from "./utils.js";
3
+ const WEB_SEARCH_CONFIG_PATH = getWebSearchConfigPath();
4
+ const AUTH_PROFILE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
5
+ export function resolveAuthFetchProfile(request) {
6
+ const profiles = loadAuthFetchProfiles();
7
+ if (profiles.length === 0) {
8
+ throw new Error(`auth requires at least one authFetch profile in ${WEB_SEARCH_CONFIG_PATH}`);
9
+ }
10
+ if (request === true) {
11
+ if (profiles.length !== 1) {
12
+ throw new Error("auth: true requires exactly one authFetch profile; use a profile name instead");
13
+ }
14
+ return profiles[0];
15
+ }
16
+ const name = request.trim();
17
+ const profile = profiles.find((candidate) => candidate.name === name);
18
+ if (!profile)
19
+ throw new Error(`Unknown authFetch profile: ${name}`);
20
+ return profile;
21
+ }
22
+ export function assertAuthFetchUrl(profile, rawUrl) {
23
+ let url;
24
+ try {
25
+ url = new URL(rawUrl);
26
+ } catch (err) {
27
+ const message = err instanceof Error ? err.message : String(err);
28
+ throw new Error(`Authenticated fetch requires an HTTPS URL: ${message}`);
29
+ }
30
+ if (url.protocol !== "https:")
31
+ throw new Error("Authenticated fetch requires an HTTPS URL");
32
+ const hostname = normalizeHostname(url.hostname);
33
+ if (!profile.hosts.some((host) => hostMatches(hostname, host))) {
34
+ throw new Error(`URL host ${hostname} is not allowed by authFetch profile ${profile.name}`);
35
+ }
36
+ return url;
37
+ }
38
+ export function authFetchRedirectGuard(profile, from, to) {
39
+ if (profile.redirects === "same-origin" && to.origin !== from.origin) {
40
+ throw new Error(`Authenticated fetch refused cross-origin redirect: ${from.origin} -> ${to.origin}`);
41
+ }
42
+ }
43
+ function loadAuthFetchProfiles() {
44
+ if (!existsSync(WEB_SEARCH_CONFIG_PATH))
45
+ return [];
46
+ const raw = readFileSync(WEB_SEARCH_CONFIG_PATH, "utf-8");
47
+ let parsed;
48
+ try {
49
+ const value = JSON.parse(raw);
50
+ if (!value || typeof value !== "object" || Array.isArray(value))
51
+ throw new Error("expected a JSON object");
52
+ parsed = value;
53
+ } catch (err) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ throw new Error(`Failed to parse ${WEB_SEARCH_CONFIG_PATH}: ${message}`);
56
+ }
57
+ if (parsed.authFetch === undefined || parsed.authFetch === null)
58
+ return [];
59
+ if (typeof parsed.authFetch !== "object" || Array.isArray(parsed.authFetch)) {
60
+ throw new Error(`authFetch in ${WEB_SEARCH_CONFIG_PATH} must be an object`);
61
+ }
62
+ return Object.entries(parsed.authFetch).map(([name, value]) => parseProfile(name, value));
63
+ }
64
+ function parseProfile(name, value) {
65
+ if (!AUTH_PROFILE_NAME_PATTERN.test(name)) {
66
+ throw new Error(`authFetch profile name ${JSON.stringify(name)} must start with a letter and contain only letters, numbers, underscores, or hyphens`);
67
+ }
68
+ if (Array.isArray(value)) {
69
+ return { name, hosts: parseHosts(value, `authFetch.${name}`), redirects: "same-origin", cache: "session" };
70
+ }
71
+ if (!value || typeof value !== "object") {
72
+ throw new Error(`authFetch.${name} in ${WEB_SEARCH_CONFIG_PATH} must be an array of hosts or an object`);
73
+ }
74
+ const config = value;
75
+ if (!Array.isArray(config.hosts)) {
76
+ throw new Error(`authFetch.${name}.hosts in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);
77
+ }
78
+ const redirects = config.redirects ?? "same-origin";
79
+ if (redirects !== "same-origin") {
80
+ throw new Error(`authFetch.${name}.redirects in ${WEB_SEARCH_CONFIG_PATH} must be "same-origin"`);
81
+ }
82
+ const cache = config.cache ?? "session";
83
+ if (cache !== "session" && cache !== "off") {
84
+ throw new Error(`authFetch.${name}.cache in ${WEB_SEARCH_CONFIG_PATH} must be "session" or "off"`);
85
+ }
86
+ const chromeProfile = parseChromeProfile(config.chromeProfile, `authFetch.${name}.chromeProfile`);
87
+ return {
88
+ name,
89
+ hosts: parseHosts(config.hosts, `authFetch.${name}.hosts`),
90
+ ...chromeProfile ? { chromeProfile } : {},
91
+ redirects,
92
+ cache
93
+ };
94
+ }
95
+ function parseHosts(value, label) {
96
+ if (value.length === 0)
97
+ throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a non-empty array of hostnames`);
98
+ const hosts = value.map((entry) => {
99
+ if (typeof entry !== "string")
100
+ throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must contain only hostnames`);
101
+ return parseHost(entry, label);
102
+ });
103
+ return [...new Set(hosts)];
104
+ }
105
+ function parseHost(value, label) {
106
+ const host = normalizeHostname(value.trim());
107
+ if (!host || host.startsWith(".") || host.endsWith(".") || /\s|[\\/?:#@*]/.test(host)) {
108
+ throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);
109
+ }
110
+ if (host.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(host)) {
111
+ throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} contains an invalid hostname: ${JSON.stringify(value)}`);
112
+ }
113
+ return host;
114
+ }
115
+ function parseChromeProfile(value, label) {
116
+ if (value === undefined || value === null)
117
+ return;
118
+ if (typeof value !== "string")
119
+ throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a string`);
120
+ const normalized = value.trim();
121
+ if (!normalized || normalized === "." || normalized === ".." || normalized.includes("/") || normalized.includes("\\")) {
122
+ throw new Error(`${label} in ${WEB_SEARCH_CONFIG_PATH} must be a profile directory name, not a path`);
123
+ }
124
+ return normalized;
125
+ }
126
+ function normalizeHostname(hostname) {
127
+ return hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
128
+ }
129
+ function hostMatches(hostname, allowedHost) {
130
+ return hostname === allowedHost || hostname.endsWith(`.${allowedHost}`);
131
+ }
@@ -0,0 +1,214 @@
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 BOCHA_SEARCH_URL = "https://api.bochaai.com/v1/web-search";
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
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(raw);
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
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
25
+ throw new Error(`Invalid config in ${CONFIG_PATH}: expected a JSON object`);
26
+ }
27
+ cachedConfig = parsed;
28
+ return cachedConfig;
29
+ }
30
+ async function getApiKey(signal) {
31
+ return resolveCredential({
32
+ provider: "Bocha",
33
+ configuredValue: loadConfig().bochaApiKey,
34
+ environmentValue: process.env.BOCHA_API_KEY,
35
+ signal
36
+ });
37
+ }
38
+ async function requireApiKey(signal) {
39
+ const apiKey = await getApiKey(signal);
40
+ if (!apiKey) {
41
+ throw new Error(`Bocha API key not found. Either:
42
+ ` + ` 1. Create ${CONFIG_PATH} with { "bochaApiKey": "your-key" }
43
+ ` + ` 2. Set BOCHA_API_KEY environment variable
44
+ ` + "Create a key at https://open.bochaai.com/");
45
+ }
46
+ return apiKey;
47
+ }
48
+ function normalizeCount(value) {
49
+ if (typeof value !== "number" || !Number.isFinite(value))
50
+ return 8;
51
+ return Math.max(1, Math.min(Math.floor(value), 20));
52
+ }
53
+ function mapFreshness(value) {
54
+ switch (value) {
55
+ case "day":
56
+ return "oneDay";
57
+ case "week":
58
+ return "oneWeek";
59
+ case "month":
60
+ return "oneMonth";
61
+ case "year":
62
+ return "oneYear";
63
+ default:
64
+ return "noLimit";
65
+ }
66
+ }
67
+ function normalizeDomain(value) {
68
+ let input = value.trim().toLowerCase();
69
+ if (!input)
70
+ return null;
71
+ if (input.startsWith("-"))
72
+ input = input.slice(1).trim();
73
+ if (!input)
74
+ return null;
75
+ try {
76
+ const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
77
+ input = parsed.hostname;
78
+ } catch {
79
+ input = input.split("/")[0]?.split(":")[0] ?? "";
80
+ }
81
+ input = input.replace(/^\.+|\.+$/g, "");
82
+ return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
83
+ }
84
+ function parseDomainFilter(domainFilter) {
85
+ const filters = { include: [], exclude: [] };
86
+ for (const raw of domainFilter ?? []) {
87
+ const domain = normalizeDomain(raw);
88
+ if (!domain)
89
+ continue;
90
+ const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
91
+ if (!target.includes(domain))
92
+ target.push(domain);
93
+ }
94
+ return filters;
95
+ }
96
+ function passesDomainFilters(url, filters) {
97
+ if (filters.include.length === 0 && filters.exclude.length === 0)
98
+ return true;
99
+ let hostname;
100
+ try {
101
+ hostname = new URL(url).hostname.toLowerCase();
102
+ } catch {
103
+ return false;
104
+ }
105
+ const matches = (domain) => hostname === domain || hostname.endsWith(`.${domain}`);
106
+ if (filters.exclude.some(matches))
107
+ return false;
108
+ return filters.include.length === 0 || filters.include.some(matches);
109
+ }
110
+ function errorMessage(err) {
111
+ return err instanceof Error ? err.message : String(err);
112
+ }
113
+ function invalidResponse(message) {
114
+ return new Error(`Bocha API returned invalid response: ${message}`);
115
+ }
116
+ function firstString(...values) {
117
+ for (const value of values) {
118
+ if (typeof value === "string" && value.trim())
119
+ return value.trim();
120
+ }
121
+ return null;
122
+ }
123
+ function parseSearchResponse(value) {
124
+ if (!value || typeof value !== "object" || Array.isArray(value))
125
+ throw invalidResponse("expected an object envelope");
126
+ const envelope = value;
127
+ if (envelope.code !== undefined && Number(envelope.code) !== 200) {
128
+ throw invalidResponse(`code ${String(envelope.code)}: ${firstString(envelope.msg) ?? "unknown error"}`);
129
+ }
130
+ const data = envelope.data;
131
+ const pages = typeof data === "object" && data !== null && !Array.isArray(data) ? data.webPages : undefined;
132
+ const items = typeof pages === "object" && pages !== null && !Array.isArray(pages) ? pages.value : undefined;
133
+ if (!Array.isArray(items))
134
+ throw invalidResponse("missing data.webPages.value array");
135
+ const results = [];
136
+ for (const item of items) {
137
+ if (!item || typeof item !== "object" || Array.isArray(item))
138
+ continue;
139
+ const entry = item;
140
+ const url = firstString(entry.url, entry.link, entry.href);
141
+ if (!url)
142
+ continue;
143
+ const title = firstString(entry.title, entry.name) ?? url;
144
+ const snippet = firstString(entry.summary, entry.snippet, entry.description, entry.content) ?? "";
145
+ results.push({ title, url, snippet });
146
+ }
147
+ return { results };
148
+ }
149
+ function buildAnswer(results) {
150
+ return results.map((result) => result.snippet ? `${result.snippet}
151
+ Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
152
+
153
+ `);
154
+ }
155
+ export function isBochaAvailable() {
156
+ return hasCredentialSource({ provider: "Bocha", configuredValue: loadConfig().bochaApiKey, environmentValue: process.env.BOCHA_API_KEY });
157
+ }
158
+ export async function searchWithBocha(query, options = {}) {
159
+ const apiKey = await requireApiKey(options.signal);
160
+ const numResults = normalizeCount(options.numResults);
161
+ const filters = parseDomainFilter(options.domainFilter);
162
+ const activityId = activityMonitor.logStart({ type: "api", query });
163
+ let response;
164
+ try {
165
+ response = await fetch(BOCHA_SEARCH_URL, {
166
+ method: "POST",
167
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "application/json" },
168
+ body: JSON.stringify({ query, count: numResults, freshness: mapFreshness(options.recencyFilter), summary: true }),
169
+ signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS)
170
+ });
171
+ } catch (err) {
172
+ const message = errorMessage(err);
173
+ const redactedMessage = redactCredential(message, apiKey);
174
+ if (redactedMessage.toLowerCase().includes("abort"))
175
+ activityMonitor.logComplete(activityId, 0);
176
+ else
177
+ activityMonitor.logError(activityId, redactedMessage);
178
+ if (redactedMessage === message)
179
+ throw err;
180
+ const redactedError = new Error(redactedMessage);
181
+ if (err instanceof Error)
182
+ redactedError.name = err.name;
183
+ throw redactedError;
184
+ }
185
+ if (!response.ok) {
186
+ activityMonitor.logComplete(activityId, response.status);
187
+ const errorText = redactCredential(await response.text(), apiKey);
188
+ throw new Error(`Bocha API error ${response.status}: ${errorText.slice(0, 300)}`);
189
+ }
190
+ let rawData;
191
+ try {
192
+ rawData = await response.json();
193
+ } catch (err) {
194
+ activityMonitor.logComplete(activityId, response.status);
195
+ throw new Error(`Bocha API returned invalid JSON: ${errorMessage(err)}`);
196
+ }
197
+ let parsed;
198
+ try {
199
+ parsed = parseSearchResponse(rawData);
200
+ } catch (err) {
201
+ const message = errorMessage(err);
202
+ const redactedMessage = redactCredential(message, apiKey);
203
+ activityMonitor.logError(activityId, redactedMessage);
204
+ if (redactedMessage === message)
205
+ throw err;
206
+ const redactedError = new Error(redactedMessage);
207
+ if (err instanceof Error)
208
+ redactedError.name = err.name;
209
+ throw redactedError;
210
+ }
211
+ activityMonitor.logComplete(activityId, response.status);
212
+ const results = parsed.results.filter((result) => passesDomainFilters(result.url, filters)).slice(0, numResults);
213
+ return { answer: buildAnswer(results), results };
214
+ }
@@ -0,0 +1,196 @@
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 BRAVE_API_BASE_URL = "https://api.search.brave.com/res/v1";
6
+ const CONFIG_PATH = getWebSearchConfigPath();
7
+ const SEARCH_TIMEOUT_MS = 30000;
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: "Brave",
28
+ configuredValue: loadConfig().braveApiKey,
29
+ environmentValue: process.env.BRAVE_API_KEY,
30
+ signal
31
+ });
32
+ }
33
+ function getApiUrl() {
34
+ return `${resolveApiBaseUrl({
35
+ configKey: "braveBaseUrl",
36
+ configuredValue: loadConfig().braveBaseUrl,
37
+ defaultValue: BRAVE_API_BASE_URL,
38
+ environmentKey: "BRAVE_BASE_URL",
39
+ environmentValue: process.env.BRAVE_BASE_URL
40
+ })}/web/search`;
41
+ }
42
+ function normalizeCount(value) {
43
+ if (typeof value !== "number" || !Number.isFinite(value))
44
+ return 5;
45
+ return Math.max(1, Math.min(Math.floor(value), 20));
46
+ }
47
+ function normalizeDomain(value) {
48
+ let input = value.trim().toLowerCase();
49
+ if (!input)
50
+ return null;
51
+ if (input.startsWith("-"))
52
+ input = input.slice(1).trim();
53
+ if (!input)
54
+ return null;
55
+ try {
56
+ const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
57
+ input = parsed.hostname;
58
+ } catch {
59
+ input = input.split("/")[0]?.split(":")[0] ?? "";
60
+ }
61
+ input = input.replace(/^\.+|\.+$/g, "");
62
+ return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
63
+ }
64
+ function normalizeDomainFilters(domainFilter) {
65
+ const filters = { allowed: [], blocked: [] };
66
+ if (!domainFilter?.length)
67
+ return filters;
68
+ for (const raw of domainFilter) {
69
+ const domain = normalizeDomain(raw);
70
+ if (!domain)
71
+ continue;
72
+ const target = raw.trim().startsWith("-") ? filters.blocked : filters.allowed;
73
+ if (!target.includes(domain))
74
+ target.push(domain);
75
+ }
76
+ return filters;
77
+ }
78
+ function buildBraveQuery(query, domainFilter) {
79
+ const filters = normalizeDomainFilters(domainFilter);
80
+ const parts = [query];
81
+ if (filters.allowed.length === 1) {
82
+ parts.push(`site:${filters.allowed[0]}`);
83
+ } else if (filters.allowed.length > 1) {
84
+ parts.push(filters.allowed.map((domain) => `site:${domain}`).join(" OR "));
85
+ }
86
+ for (const domain of filters.blocked) {
87
+ parts.push(`NOT site:${domain}`);
88
+ }
89
+ return parts.join(" ");
90
+ }
91
+ function hostMatchesDomain(hostname, domain) {
92
+ return hostname === domain || hostname.endsWith(`.${domain}`);
93
+ }
94
+ function matchesDomainFilters(url, filters) {
95
+ if (filters.allowed.length === 0 && filters.blocked.length === 0)
96
+ return true;
97
+ let hostname = "";
98
+ try {
99
+ hostname = new URL(url).hostname.toLowerCase();
100
+ } catch {
101
+ return false;
102
+ }
103
+ if (filters.allowed.length > 0 && !filters.allowed.some((domain) => hostMatchesDomain(hostname, domain))) {
104
+ return false;
105
+ }
106
+ return !filters.blocked.some((domain) => hostMatchesDomain(hostname, domain));
107
+ }
108
+ export function isBraveAvailable() {
109
+ return hasCredentialSource({
110
+ provider: "Brave",
111
+ configuredValue: loadConfig().braveApiKey,
112
+ environmentValue: process.env.BRAVE_API_KEY
113
+ });
114
+ }
115
+ export async function searchWithBrave(query, options = {}) {
116
+ const apiUrl = getApiUrl();
117
+ const apiKey = await getApiKey(options.signal);
118
+ if (!apiKey) {
119
+ throw new Error(`Brave Search API key not found. Either:
120
+ ` + ` 1. Create ${CONFIG_PATH} with { "braveApiKey": "your-key" }
121
+ ` + ` 2. Set BRAVE_API_KEY environment variable
122
+ ` + "Get a key at https://brave.com/search/api/");
123
+ }
124
+ const numResults = normalizeCount(options.numResults);
125
+ const domainFilters = normalizeDomainFilters(options.domainFilter);
126
+ const searchQuery = buildBraveQuery(query, options.domainFilter);
127
+ const activityId = activityMonitor.logStart({ type: "api", query: searchQuery });
128
+ const params = new URLSearchParams({
129
+ q: searchQuery,
130
+ count: String(options.domainFilter?.length ? 20 : numResults)
131
+ });
132
+ if (options.recencyFilter) {
133
+ const freshnessMap = {
134
+ day: "pd",
135
+ week: "pw",
136
+ month: "pm",
137
+ year: "py"
138
+ };
139
+ const freshness = freshnessMap[options.recencyFilter];
140
+ if (freshness)
141
+ params.set("freshness", freshness);
142
+ }
143
+ try {
144
+ const response = await fetchWithCredentialRedirects(`${apiUrl}?${params.toString()}`, {
145
+ method: "GET",
146
+ headers: {
147
+ "X-Subscription-Token": apiKey,
148
+ Accept: "application/json",
149
+ "Accept-Encoding": "gzip"
150
+ },
151
+ signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS)
152
+ }, ["X-Subscription-Token"]);
153
+ if (!response.ok) {
154
+ activityMonitor.logError(activityId, `HTTP ${response.status}`);
155
+ const errorText = redactCredential(await response.text(), apiKey);
156
+ throw new Error(`Brave Search API error ${response.status}: ${errorText.slice(0, 300)}`);
157
+ }
158
+ const data = await response.json();
159
+ activityMonitor.logComplete(activityId, response.status);
160
+ const results = [];
161
+ for (const item of data.web?.results ?? []) {
162
+ if (!item.url || !matchesDomainFilters(item.url, domainFilters))
163
+ continue;
164
+ results.push({
165
+ title: item.title || item.url,
166
+ url: item.url,
167
+ snippet: item.description || ""
168
+ });
169
+ if (results.length >= numResults)
170
+ break;
171
+ }
172
+ const answer = results.map((result) => {
173
+ if (result.snippet)
174
+ return `${result.snippet}
175
+ Source: ${result.title} (${result.url})`;
176
+ return `Source: ${result.title} (${result.url})`;
177
+ }).join(`
178
+
179
+ `);
180
+ return { answer, results };
181
+ } catch (err) {
182
+ const message = err instanceof Error ? err.message : String(err);
183
+ const redactedMessage = redactCredential(message, apiKey);
184
+ if (redactedMessage.toLowerCase().includes("abort")) {
185
+ activityMonitor.logComplete(activityId, 0);
186
+ } else {
187
+ activityMonitor.logError(activityId, redactedMessage);
188
+ }
189
+ if (redactedMessage === message)
190
+ throw err;
191
+ const redactedError = new Error(redactedMessage);
192
+ if (err instanceof Error)
193
+ redactedError.name = err.name;
194
+ throw redactedError;
195
+ }
196
+ }