@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,238 @@
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 SERPDIVE_API_URL = "https://api.serpdive.com/v1/search";
6
+ const CONFIG_PATH = getWebSearchConfigPath();
7
+ const SEARCH_TIMEOUT_MS = 60000;
8
+ const MODELS = ["krill", "mako", "moby"];
9
+ const DEFAULT_MODEL = "krill";
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
+ try {
20
+ cachedConfig = JSON.parse(raw);
21
+ return cachedConfig;
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
+ }
27
+ async function getApiKey(signal) {
28
+ return resolveCredential({
29
+ provider: "SERPdive",
30
+ configuredValue: loadConfig().serpdiveApiKey,
31
+ environmentValue: process.env.SERPDIVE_API_KEY,
32
+ signal
33
+ });
34
+ }
35
+ async function requireApiKey(signal) {
36
+ const apiKey = await getApiKey(signal);
37
+ if (!apiKey) {
38
+ throw new Error(`SERPdive API key not found. Either:
39
+ ` + ` 1. Create ${CONFIG_PATH} with { "serpdiveApiKey": "your-key" }
40
+ ` + ` 2. Set SERPDIVE_API_KEY environment variable
41
+ ` + "Get a key at https://serpdive.com/dashboard/keys");
42
+ }
43
+ return apiKey;
44
+ }
45
+ function resolveModel() {
46
+ const raw = process.env.SERPDIVE_MODEL ?? loadConfig().serpdiveModel;
47
+ if (typeof raw !== "string")
48
+ return DEFAULT_MODEL;
49
+ const value = raw.trim().toLowerCase();
50
+ return MODELS.includes(value) ? value : DEFAULT_MODEL;
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 parseDomainFilter(domainFilter) {
75
+ const filters = { include: [], exclude: [] };
76
+ if (!domainFilter?.length)
77
+ return filters;
78
+ for (const raw of domainFilter) {
79
+ const domain = normalizeDomain(raw);
80
+ if (!domain)
81
+ continue;
82
+ const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
83
+ if (!target.includes(domain))
84
+ target.push(domain);
85
+ }
86
+ return filters;
87
+ }
88
+ function domainMatches(hostname, domain) {
89
+ return hostname === domain || hostname.endsWith(`.${domain}`);
90
+ }
91
+ function passesDomainFilters(url, filters) {
92
+ if (filters.include.length === 0 && filters.exclude.length === 0)
93
+ return true;
94
+ let hostname;
95
+ try {
96
+ hostname = new URL(url).hostname.toLowerCase();
97
+ } catch {
98
+ return false;
99
+ }
100
+ if (filters.exclude.some((domain) => domainMatches(hostname, domain)))
101
+ return false;
102
+ if (filters.include.length === 0)
103
+ return true;
104
+ return filters.include.some((domain) => domainMatches(hostname, domain));
105
+ }
106
+ function applyRecencyHint(query, recencyFilter) {
107
+ if (!recencyFilter)
108
+ return query;
109
+ const hints = {
110
+ day: "past 24 hours",
111
+ week: "past week",
112
+ month: "past month",
113
+ year: "past year"
114
+ };
115
+ const hint = hints[recencyFilter];
116
+ return hint ? `${query} ${hint}` : query;
117
+ }
118
+ function requestSignal(signal) {
119
+ const timeout = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
120
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
121
+ }
122
+ function errorMessage(err) {
123
+ return err instanceof Error ? err.message : String(err);
124
+ }
125
+ function mapResults(results, numResults, filters) {
126
+ if (!Array.isArray(results))
127
+ return [];
128
+ const mapped = [];
129
+ for (const item of results) {
130
+ if (!item?.url || !passesDomainFilters(item.url, filters))
131
+ continue;
132
+ mapped.push({
133
+ title: item.title || `Source ${mapped.length + 1}`,
134
+ url: item.url,
135
+ snippet: typeof item.content === "string" ? item.content.replace(/\s+/g, " ").trim() : ""
136
+ });
137
+ if (mapped.length >= numResults)
138
+ break;
139
+ }
140
+ return mapped;
141
+ }
142
+ function mapInlineContent(results, filters) {
143
+ if (!Array.isArray(results))
144
+ return [];
145
+ return results.flatMap((item) => {
146
+ if (!item?.url || !passesDomainFilters(item.url, filters))
147
+ return [];
148
+ if (typeof item.content !== "string" || item.content.trim().length === 0)
149
+ return [];
150
+ return [{
151
+ url: item.url,
152
+ title: item.title || "",
153
+ content: item.content,
154
+ error: null
155
+ }];
156
+ });
157
+ }
158
+ function buildAnswer(apiAnswer, results) {
159
+ if (typeof apiAnswer === "string" && apiAnswer.trim().length > 0)
160
+ return apiAnswer;
161
+ return results.map((result) => {
162
+ if (result.snippet)
163
+ return `${result.snippet}
164
+ Source: ${result.title} (${result.url})`;
165
+ return `Source: ${result.title} (${result.url})`;
166
+ }).join(`
167
+
168
+ `);
169
+ }
170
+ export function isSerpdiveAvailable() {
171
+ return hasCredentialSource({
172
+ provider: "SERPdive",
173
+ configuredValue: loadConfig().serpdiveApiKey,
174
+ environmentValue: process.env.SERPDIVE_API_KEY
175
+ });
176
+ }
177
+ export async function searchWithSerpdive(query, options = {}) {
178
+ const apiKey = await requireApiKey(options.signal);
179
+ const numResults = normalizeCount(options.numResults);
180
+ const filters = parseDomainFilter(options.domainFilter);
181
+ const model = resolveModel();
182
+ const body = {
183
+ query: applyRecencyHint(query, options.recencyFilter),
184
+ model,
185
+ max_results: Math.min(numResults, 10),
186
+ ...model === "krill" ? {} : { answer: true }
187
+ };
188
+ const activityId = activityMonitor.logStart({ type: "api", query });
189
+ let response;
190
+ try {
191
+ response = await fetch(SERPDIVE_API_URL, {
192
+ method: "POST",
193
+ headers: {
194
+ Authorization: `Bearer ${apiKey}`,
195
+ "Content-Type": "application/json"
196
+ },
197
+ body: JSON.stringify(body),
198
+ signal: requestSignal(options.signal)
199
+ });
200
+ } catch (err) {
201
+ const message = errorMessage(err);
202
+ const redactedMessage = redactCredential(message, apiKey);
203
+ if (redactedMessage.toLowerCase().includes("abort"))
204
+ activityMonitor.logComplete(activityId, 0);
205
+ else
206
+ activityMonitor.logError(activityId, redactedMessage);
207
+ if (redactedMessage === message)
208
+ throw err;
209
+ const redactedError = new Error(redactedMessage);
210
+ if (err instanceof Error)
211
+ redactedError.name = err.name;
212
+ throw redactedError;
213
+ }
214
+ if (!response.ok) {
215
+ activityMonitor.logComplete(activityId, response.status);
216
+ const errorText = redactCredential(await response.text(), apiKey);
217
+ throw new Error(`SERPdive API error ${response.status}: ${errorText.slice(0, 300)}`);
218
+ }
219
+ let data;
220
+ try {
221
+ data = await response.json();
222
+ } catch (err) {
223
+ activityMonitor.logComplete(activityId, response.status);
224
+ throw new Error(`SERPdive API returned invalid JSON: ${errorMessage(err)}`);
225
+ }
226
+ activityMonitor.logComplete(activityId, response.status);
227
+ const results = mapResults(data.results, numResults, filters);
228
+ const result = {
229
+ answer: buildAnswer(data.answer, results),
230
+ results
231
+ };
232
+ if (options.includeContent) {
233
+ const inlineContent = mapInlineContent(data.results, filters);
234
+ if (inlineContent.length > 0)
235
+ result.inlineContent = inlineContent;
236
+ }
237
+ return result;
238
+ }
@@ -0,0 +1,200 @@
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 SERPER_SEARCH_URL = "https://google.serper.dev/search";
6
+ const CONFIG_PATH = getWebSearchConfigPath();
7
+ const SEARCH_TIMEOUT_MS = 60000;
8
+ const RECENCY_TBS = {
9
+ day: "qdr:d",
10
+ week: "qdr:w",
11
+ month: "qdr:m",
12
+ year: "qdr:y"
13
+ };
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, "utf-8");
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
+ async function getApiKey(signal) {
37
+ return resolveCredential({
38
+ provider: "Serper",
39
+ configuredValue: loadConfig().serperApiKey,
40
+ environmentValue: process.env.SERPER_API_KEY,
41
+ signal
42
+ });
43
+ }
44
+ async function requireApiKey(signal) {
45
+ const apiKey = await getApiKey(signal);
46
+ if (!apiKey) {
47
+ throw new Error(`Serper API key not found. Either:
48
+ ` + ` 1. Create ${CONFIG_PATH} with { "serperApiKey": "your-key" }
49
+ ` + ` 2. Set SERPER_API_KEY environment variable
50
+ ` + "Get a key at https://serper.dev");
51
+ }
52
+ return apiKey;
53
+ }
54
+ function normalizeCount(value) {
55
+ if (typeof value !== "number" || !Number.isFinite(value))
56
+ return 5;
57
+ return Math.max(1, Math.min(Math.floor(value), 20));
58
+ }
59
+ function normalizeDomain(value) {
60
+ let input = value.trim().toLowerCase();
61
+ if (!input)
62
+ return null;
63
+ if (input.startsWith("-"))
64
+ input = input.slice(1).trim();
65
+ if (!input)
66
+ return null;
67
+ try {
68
+ const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
69
+ input = parsed.hostname;
70
+ } catch {
71
+ input = input.split("/")[0]?.split(":")[0] ?? "";
72
+ }
73
+ input = input.replace(/^\.+|\.+$/g, "");
74
+ return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
75
+ }
76
+ function parseDomainFilter(domainFilter) {
77
+ const filters = { include: [], exclude: [] };
78
+ for (const raw of domainFilter ?? []) {
79
+ const domain = normalizeDomain(raw);
80
+ if (!domain)
81
+ continue;
82
+ const target = raw.trim().startsWith("-") ? filters.exclude : filters.include;
83
+ if (!target.includes(domain))
84
+ target.push(domain);
85
+ }
86
+ return filters;
87
+ }
88
+ function passesDomainFilters(url, filters) {
89
+ if (filters.include.length === 0 && filters.exclude.length === 0)
90
+ return true;
91
+ let hostname;
92
+ try {
93
+ hostname = new URL(url).hostname.toLowerCase();
94
+ } catch {
95
+ return false;
96
+ }
97
+ const matches = (domain) => hostname === domain || hostname.endsWith(`.${domain}`);
98
+ if (filters.exclude.some(matches))
99
+ return false;
100
+ return filters.include.length === 0 || filters.include.some(matches);
101
+ }
102
+ function buildQuery(query, filters) {
103
+ const parts = [query];
104
+ if (filters.include.length === 1)
105
+ parts.push(`site:${filters.include[0]}`);
106
+ if (filters.include.length > 1)
107
+ parts.push(`(${filters.include.map((domain) => `site:${domain}`).join(" OR ")})`);
108
+ for (const domain of filters.exclude)
109
+ parts.push(`-site:${domain}`);
110
+ return parts.join(" ");
111
+ }
112
+ function errorMessage(err) {
113
+ return err instanceof Error ? err.message : String(err);
114
+ }
115
+ function invalidResponse(message) {
116
+ return new Error(`Serper API returned invalid response: ${message}`);
117
+ }
118
+ function parseResponse(value) {
119
+ if (!value || typeof value !== "object" || Array.isArray(value))
120
+ throw invalidResponse("expected an object envelope");
121
+ const envelope = value;
122
+ if (!Array.isArray(envelope.organic))
123
+ throw invalidResponse("expected organic array");
124
+ return envelope.organic;
125
+ }
126
+ function buildAnswer(results) {
127
+ return results.map((result) => result.snippet ? `${result.snippet}
128
+ Source: ${result.title} (${result.url})` : `Source: ${result.title} (${result.url})`).join(`
129
+
130
+ `);
131
+ }
132
+ export function isSerperAvailable() {
133
+ return hasCredentialSource({ provider: "Serper", configuredValue: loadConfig().serperApiKey, environmentValue: process.env.SERPER_API_KEY });
134
+ }
135
+ export async function searchWithSerper(query, options = {}) {
136
+ const apiKey = await requireApiKey(options.signal);
137
+ const numResults = normalizeCount(options.numResults);
138
+ const filters = parseDomainFilter(options.domainFilter);
139
+ const requestCount = options.domainFilter?.length ? Math.min(20, numResults + 5) : numResults;
140
+ const activityId = activityMonitor.logStart({ type: "api", query });
141
+ let response;
142
+ try {
143
+ response = await fetch(SERPER_SEARCH_URL, {
144
+ method: "POST",
145
+ headers: { "X-API-KEY": apiKey, "Content-Type": "application/json", Accept: "application/json" },
146
+ body: JSON.stringify({ q: buildQuery(query, filters), num: requestCount, ...options.recencyFilter ? { tbs: RECENCY_TBS[options.recencyFilter] } : {} }),
147
+ signal: options.signal ? AbortSignal.any([AbortSignal.timeout(SEARCH_TIMEOUT_MS), options.signal]) : AbortSignal.timeout(SEARCH_TIMEOUT_MS)
148
+ });
149
+ } catch (err) {
150
+ const message = errorMessage(err);
151
+ const redactedMessage = redactCredential(message, apiKey);
152
+ if (redactedMessage.toLowerCase().includes("abort"))
153
+ activityMonitor.logComplete(activityId, 0);
154
+ else
155
+ activityMonitor.logError(activityId, redactedMessage);
156
+ if (redactedMessage === message)
157
+ throw err;
158
+ const redactedError = new Error(redactedMessage);
159
+ if (err instanceof Error)
160
+ redactedError.name = err.name;
161
+ throw redactedError;
162
+ }
163
+ if (!response.ok) {
164
+ activityMonitor.logComplete(activityId, response.status);
165
+ const errorText = redactCredential(await response.text(), apiKey);
166
+ throw new Error(`Serper API error ${response.status}: ${errorText.slice(0, 300)}`);
167
+ }
168
+ let rawData;
169
+ try {
170
+ rawData = await response.json();
171
+ } catch (err) {
172
+ activityMonitor.logComplete(activityId, response.status);
173
+ throw new Error(`Serper API returned invalid JSON: ${errorMessage(err)}`);
174
+ }
175
+ let entries;
176
+ try {
177
+ entries = parseResponse(rawData);
178
+ } catch (err) {
179
+ activityMonitor.logComplete(activityId, response.status);
180
+ throw err;
181
+ }
182
+ activityMonitor.logComplete(activityId, response.status);
183
+ const results = [];
184
+ for (const entry of entries) {
185
+ if (!entry || typeof entry !== "object")
186
+ continue;
187
+ if (typeof entry.link !== "string" || !entry.link)
188
+ continue;
189
+ if (!passesDomainFilters(entry.link, filters))
190
+ continue;
191
+ results.push({
192
+ title: typeof entry.title === "string" && entry.title.trim() ? entry.title.trim() : `Source ${results.length + 1}`,
193
+ url: entry.link,
194
+ snippet: typeof entry.snippet === "string" ? entry.snippet : ""
195
+ });
196
+ if (results.length >= numResults)
197
+ break;
198
+ }
199
+ return { answer: buildAnswer(results), results };
200
+ }
@@ -0,0 +1,198 @@
1
+ import { createHash } from "node:crypto";
2
+ import { generateId, getResult, storeResult } from "./storage.js";
3
+ const OFFICIAL_DOCS_HOSTS = /^(developers\.|docs\.|learn\.|reference\.)|\.github\.io$/i;
4
+ const OFFICIAL_DOCS_PATHS = /\/(docs?|reference)(\/|\b)/i;
5
+ const VENDOR_DOCS_PATHS = /\/(documentation|docs?)\//i;
6
+ const REPO_ISSUE_PATHS = /\/(issues|pull|pulls)\//i;
7
+ const BLOG_HOSTS = /(medium\.com|substack\.com|dev\.to|hashnode\.)/i;
8
+ const BLOG_PATHS = /\/blogs?\//i;
9
+ const FORUM_HOSTS = /(stackoverflow\.com|serverfault\.com|superuser\.com|discourse\.|community\.)/i;
10
+ const FORUM_PATHS = /\/(forum|forums|threads)\//i;
11
+ const NEWS_HOSTS = /(reuters\.com|bloomberg\.com|techcrunch\.com|theverge\.com|arstechnica\.com|wired\.com|cnet\.com|zdnet\.com)/i;
12
+ const NEWS_PATHS = /\/news(\/|$)/i;
13
+ export function classifySource(url) {
14
+ let host = "";
15
+ let path = "";
16
+ try {
17
+ const parsed = new URL(url);
18
+ host = parsed.hostname;
19
+ path = parsed.pathname;
20
+ } catch {
21
+ return "unknown";
22
+ }
23
+ if (REPO_ISSUE_PATHS.test(path))
24
+ return "repo_issue";
25
+ if (OFFICIAL_DOCS_HOSTS.test(host) || OFFICIAL_DOCS_PATHS.test(path))
26
+ return "official_docs";
27
+ if (VENDOR_DOCS_PATHS.test(path))
28
+ return "vendor_docs";
29
+ if (NEWS_HOSTS.test(host) || NEWS_PATHS.test(path))
30
+ return "news";
31
+ if (FORUM_HOSTS.test(host) || FORUM_PATHS.test(path))
32
+ return "forum";
33
+ if (BLOG_HOSTS.test(host) || BLOG_PATHS.test(path))
34
+ return "blog";
35
+ return "unknown";
36
+ }
37
+ export function hashContent(text) {
38
+ return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
39
+ }
40
+ function tokenize(value) {
41
+ return [...new Set(value.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 3))];
42
+ }
43
+ function extractRelevantSpans(content, hint) {
44
+ const sentences = [];
45
+ const sentencePattern = /[^.!?]+(?:[.!?]+(?=\s|$)|$)/g;
46
+ for (const match of content.matchAll(sentencePattern)) {
47
+ const raw = match[0];
48
+ const text = raw.trim();
49
+ if (text.length > 0 && text.length <= 400) {
50
+ const start = (match.index ?? 0) + raw.indexOf(text);
51
+ sentences.push({ text, start, end: start + text.length });
52
+ }
53
+ }
54
+ const terms = tokenize(hint);
55
+ if (terms.length === 0)
56
+ return [];
57
+ return sentences.map((sentence, index) => ({ sentence, index, score: terms.filter((term) => sentence.text.toLowerCase().includes(term)).length })).filter((item) => item.score > 0).sort((a, b) => b.score - a.score || a.index - b.index).slice(0, 3).map(({ sentence }) => sentence);
58
+ }
59
+ function passageId(sourceRank, index) {
60
+ return `p-${sourceRank}-${index}`;
61
+ }
62
+ export function buildPassages(sources, fetched = [], hint = "") {
63
+ const passages = [];
64
+ const fetchedByUrl = new Map(fetched.map((item) => [item.url, item]));
65
+ for (const source of sources) {
66
+ if (source.snippet) {
67
+ passages.push({
68
+ passage_id: passageId(source.rank, 0),
69
+ source_url: source.url,
70
+ source_rank: source.rank,
71
+ text: source.snippet,
72
+ content_hash: hashContent(source.snippet)
73
+ });
74
+ }
75
+ const page = fetchedByUrl.get(source.url);
76
+ if (page && !page.error && page.content) {
77
+ const passageHint = source.snippet?.trim() || hint;
78
+ for (const [index, span] of extractRelevantSpans(page.content, passageHint).entries()) {
79
+ passages.push({
80
+ passage_id: passageId(source.rank, index + 1),
81
+ source_url: source.url,
82
+ source_rank: source.rank,
83
+ text: span.text,
84
+ extraction_span: { start: span.start, end: span.end },
85
+ content_hash: hashContent(span.text)
86
+ });
87
+ }
88
+ }
89
+ }
90
+ return passages;
91
+ }
92
+ const CONTRADICTION_MARKERS = ["not true", "false", "incorrect", "debunked", "retracted", "no longer", "never", "denied", "contrary", "misleading"];
93
+ const SUPPORT_MARKERS = ["yes", "true", "correct", "confirmed", "according to", "shows that", "demonstrates", "reported", "verified", "established"];
94
+ function containsPhrase(value, phrase) {
95
+ const escaped = phrase.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
96
+ return new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i").test(value);
97
+ }
98
+ function markerIsNegated(value, marker) {
99
+ const escaped = marker.trim().toLowerCase().split(/\s+/).map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s+");
100
+ const markerPattern = new RegExp(`(?:^|[^a-z0-9])${escaped}(?=$|[^a-z0-9])`, "i");
101
+ const match = markerPattern.exec(value);
102
+ if (!match || match.index === undefined)
103
+ return false;
104
+ const matchedMarker = match[0].replace(/^[^a-z0-9]+/i, "");
105
+ const beforeMarker = value.slice(0, match.index + match[0].length - matchedMarker.length);
106
+ return /(?:^|[^a-z0-9])(?:not|no|never|without)\s+$/i.test(beforeMarker);
107
+ }
108
+ function hasPolarityMarker(value, markers, allowNegated = false) {
109
+ return markers.some((marker) => containsPhrase(value, marker) && (allowNegated || !markerIsNegated(value, marker)));
110
+ }
111
+ export function assessClaim(claim, passages) {
112
+ const terms = tokenize(claim);
113
+ if (terms.length === 0 || passages.length === 0) {
114
+ return { claim, status: "missing-evidence", supporting_passages: [], contradicting_passages: [], rationale: "No passages available that discuss the claim's terms.", confidence: 0.2 };
115
+ }
116
+ const supporting = [];
117
+ const contradicting = [];
118
+ for (const passage of passages) {
119
+ const lower = passage.text.toLowerCase();
120
+ const overlap = terms.filter((term) => containsPhrase(lower, term)).length;
121
+ if (overlap < Math.max(2, Math.ceil(terms.length / 4)))
122
+ continue;
123
+ const contra = hasPolarityMarker(lower, CONTRADICTION_MARKERS);
124
+ const support = hasPolarityMarker(lower, SUPPORT_MARKERS);
125
+ if (contra && !support)
126
+ contradicting.push(passage.passage_id);
127
+ else if (support && !contra)
128
+ supporting.push(passage.passage_id);
129
+ }
130
+ if (contradicting.length > 0 && supporting.length === 0) {
131
+ return { claim, status: "contradicted", supporting_passages: [], contradicting_passages: contradicting, rationale: `${contradicting.length} passage(s) contradict the claim; none support it.`, confidence: Math.min(0.85, 0.5 + contradicting.length * 0.1) };
132
+ }
133
+ if (supporting.length > 0 && contradicting.length === 0) {
134
+ return { claim, status: "supported", supporting_passages: supporting, contradicting_passages: [], rationale: `${supporting.length} passage(s) support the claim; none contradict it.`, confidence: Math.min(0.85, 0.5 + supporting.length * 0.1) };
135
+ }
136
+ if (supporting.length > 0 || contradicting.length > 0) {
137
+ return { claim, status: "unclear", supporting_passages: supporting, contradicting_passages: contradicting, rationale: `${supporting.length} supporting and ${contradicting.length} contradicting passage(s); evidence is mixed.`, confidence: 0.4 };
138
+ }
139
+ return { claim, status: "unclear", supporting_passages: [], contradicting_passages: [], rationale: "Passages mention the claim's terms but contain no clear support or contradiction markers.", confidence: 0.3 };
140
+ }
141
+ export function buildResearchArtifact(input) {
142
+ const filters = input.domainFilter ?? [];
143
+ const fetchedByUrl = new Map((input.fetched ?? []).map((page) => [page.url, page]));
144
+ const sources = [];
145
+ const seen = new Set;
146
+ for (const [index, result] of input.results.entries()) {
147
+ if (seen.has(result.url))
148
+ continue;
149
+ seen.add(result.url);
150
+ const page = fetchedByUrl.get(result.url);
151
+ const fetched = Boolean(page && !page.error);
152
+ sources.push({
153
+ rank: result.rank ?? index + 1,
154
+ url: result.url,
155
+ title: result.title,
156
+ snippet: result.snippet,
157
+ quality: classifySource(result.url),
158
+ fetched,
159
+ ...page ? { fetch_timestamp: Date.now() } : {},
160
+ ...page && !page.error ? { content_hash: hashContent(page.content) } : {},
161
+ ...page?.error ? { fetch_error: page.error } : {}
162
+ });
163
+ }
164
+ const passages = buildPassages(sources, input.fetched, input.query);
165
+ const domainInclude = filters.filter((domain) => !domain.startsWith("-"));
166
+ const domainExclude = filters.filter((domain) => domain.startsWith("-")).map((domain) => domain.slice(1));
167
+ return {
168
+ id: generateId(),
169
+ type: "research",
170
+ timestamp: Date.now(),
171
+ query: input.query,
172
+ sources,
173
+ passages,
174
+ ...input.provider !== undefined ? { provider: input.provider } : {},
175
+ ...input.summary !== undefined ? { summary: input.summary } : {},
176
+ ...passages.length > 0 ? { content_hash: hashContent(passages.map((passage) => passage.text).join(`
177
+ `)) } : {},
178
+ filters: {
179
+ ...input.recency !== undefined ? { recency: input.recency } : {},
180
+ domain_include: domainInclude,
181
+ domain_exclude: domainExclude
182
+ }
183
+ };
184
+ }
185
+ export function withClaimAssessment(artifact, claims) {
186
+ return { ...artifact, claims: claims.map((claim) => assessClaim(claim, artifact.passages)) };
187
+ }
188
+ export function storeResearchArtifact(artifact) {
189
+ if (!artifact.id)
190
+ throw new Error("Research artifact id must not be empty");
191
+ storeResult(artifact.id, { id: artifact.id, type: "research", timestamp: artifact.timestamp, artifact });
192
+ }
193
+ export function getResearchArtifact(id) {
194
+ const data = getResult(id);
195
+ if (!data || data.type !== "research" || !data.artifact || typeof data.artifact !== "object")
196
+ return null;
197
+ return data.artifact;
198
+ }