agent-search-mcp 2.1.0

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/README.md ADDED
@@ -0,0 +1,480 @@
1
+ # Agent Search MCP
2
+
3
+ > 🔍 Free multi-source search for AI agents — multi-source verification, token savings, MCP native.
4
+
5
+ [![License](https://img.shields.io/github/license/lennney/agent-search-mcp)](LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](package.json)
7
+ [![MCP](https://img.shields.io/badge/MCP-compatible-blue)](https://modelcontextprotocol.io)
8
+ [![Tests](https://img.shields.io/badge/tests-65%20passing-brightgreen)](https://github.com/lennney/agent-search-mcp)
9
+
10
+ **Works with Hermes, Claude Code, Cursor, Windsurf, OpenClaw, Codex, and any MCP-compatible client.**
11
+
12
+ ---
13
+
14
+ [English](#why-agent-search-mcp) · [中文](README_zh.md) · [安装](#quick-start) · [工具文档](#tools) · [竞品对比](#competitor-comparison)
15
+
16
+ ---
17
+
18
+ ## Why Agent Search MCP
19
+
20
+ **AI agents need to search the internet. But existing solutions have problems:**
21
+
22
+ - **Tavily** — Great quality, but $0.01/search adds up fast. Monthly cost: $20-50+.
23
+ - **Exa** — Semantic search is powerful, but $50/month minimum.
24
+ - **Brave Search** — 2000 free queries/month, then $3/1000. Not enough for heavy use.
25
+ - **DDG MCP** — Single source, no verification, no dedup, results vary wildly.
26
+ - **open-websearch** — 13 engines, but 300MB+ dependency tree, no token optimization.
27
+
28
+ **Agent Search MCP solves this differently:**
29
+
30
+ 1. **Free + high quality** — DuckDuckGo + Sogou as core engines, no API key needed
31
+ 2. **Multi-source verification** — Results cross-checked across engines, each result gets a confidence score (1-3)
32
+ 3. **Token optimization** — Title ≤100 chars, snippet ≤200 chars, dedup removes redundancy. Saves ~40-50% tokens.
33
+ 4. **MCP native** — Built for Model Context Protocol from day one. Zero config, works out of the box.
34
+ 5. **Self-hostable** — No data sent to third parties. Run it on your own VPS.
35
+ 6. **Security built-in** — Prompt injection detection, output boundary markers, phishing URL filtering.
36
+
37
+ **Who is this for?**
38
+
39
+ - AI agent developers (Hermes, OpenClaw, custom agents)
40
+ - IDE users who want AI-powered search (Claude Code, Cursor, Windsurf)
41
+ - Anyone building MCP-compatible tools
42
+ - Users who need Chinese web search (Sogou integration)
43
+
44
+ **The math:** If you search 100 times/day, Tavily costs ~$1/day. Agent Search MCP costs $0. Over a year, that's $365 saved.
45
+
46
+ ---
47
+
48
+ ## 为什么选择 Agent Search MCP
49
+
50
+ **AI Agent 需要搜索互联网。但现有方案都有问题:**
51
+
52
+ - **Tavily** — 质量好,但每次搜索 $0.01,月费 $20-50+
53
+ - **Exa** — 语义搜索强,但最低 $50/月
54
+ - **Brave Search** — 2000 次/月免费,之后 $3/1000,重度使用不够
55
+ - **DDG MCP** — 单源,无验证,无去重,结果质量不稳定
56
+ - **open-websearch** — 13 引擎,但 300MB+ 依赖,无 token 优化
57
+
58
+ **Agent Search MCP 的解决方案:**
59
+
60
+ 1. **免费 + 高质量** — DuckDuckGo + Sogou 为核心,无需 API Key
61
+ 2. **多源验证** — 跨引擎交叉验证,每个结果有置信度评分(1-3)
62
+ 3. **Token 优化** — 标题 ≤100 字符,摘要 ≤200 字符,去重去除冗余。节省 ~40-50% token
63
+ 4. **MCP 原生** — 基于 Model Context Protocol 构建,零配置开箱即用
64
+ 5. **可自托管** — 数据不经过第三方,可在自有 VPS 运行
65
+ 6. **内置安全** — Prompt 注入检测、输出边界标记、钓鱼 URL 过滤
66
+
67
+ **适用人群:**
68
+
69
+ - AI Agent 开发者(Hermes、OpenClaw、自定义 Agent)
70
+ - IDE 用户(Claude Code、Cursor、Windsurf)
71
+ - 构建 MCP 兼容工具的开发者
72
+ - 需要中文搜索的用户(搜狗集成)
73
+
74
+ **成本对比:** 如果每天搜索 100 次,Tavily 月费约 $30。Agent Search MCP 完全免费。一年省 $365。
75
+
76
+ ---
77
+
78
+ ## Competitor Comparison
79
+
80
+ | Feature | Agent Search MCP | Tavily | Exa | Brave Search | DDG MCP |
81
+ |---------|:---:|:---:|:---:|:---:|:---:|
82
+ | **Price** | Free | $0.01/search | $50/mo | $3/1000 | Free |
83
+ | **API Key** | Not required | Required | Required | Required | Required |
84
+ | **Multi-source** | ✅ 2-4 engines | ❌ Single | ❌ Single | ❌ Single | ❌ Single |
85
+ | **Confidence score** | ✅ 1-3 | ❌ | ❌ | ❌ | ❌ |
86
+ | **Deduplication** | ✅ URL + title | ❌ | ❌ | ❌ | ❌ |
87
+ | **Token optimization** | ✅ ~40-50% | ❌ | ❌ | ❌ | ❌ |
88
+ | **Chinese search** | ✅ Sogou | ❌ | ❌ | ❌ | ❌ |
89
+ | **MCP native** | ✅ | ✅ | ✅ | ✅ | ✅ |
90
+ | **Self-hostable** | ✅ | ❌ Cloud only | ❌ Cloud only | ❌ Cloud only | ✅ |
91
+ | **Progressive disclosure** | ✅ 3 tools | ❌ | ❌ | ❌ | ❌ |
92
+ | **Health monitoring** | ✅ | ❌ | ❌ | ❌ | ❌ |
93
+ | **Fallback chain** | ✅ Free→Paid | ❌ | ❌ | ❌ | ❌ |
94
+ | **Security** | ✅ Injection protection | ❌ | ❌ | ❌ | ❌ |
95
+ | **Dependencies** | 4 | 12+ | 15+ | 8 | 3 |
96
+
97
+ **Key differences:**
98
+
99
+ 1. **Free by default** — No API key, no credit card, no limits. DuckDuckGo + Sogou work out of the box.
100
+ 2. **Multi-source verification** — Results from multiple engines are cross-checked. Confidence score tells you how reliable a result is.
101
+ 3. **Token optimization** — Smart truncation and dedup reduce token consumption by ~40-50%. This is crucial for cost-sensitive applications.
102
+ 4. **Chinese support** — Sogou engine provides native Chinese web search. Not a translation layer.
103
+ 5. **Progressive disclosure** — 3 tools at different complexity levels. Agents discover capabilities on-demand (Exa model).
104
+ 6. **Security** — Built-in protection against prompt injection, phishing URLs, and output boundary markers.
105
+
106
+ ---
107
+
108
+ ## Quick Start
109
+
110
+ ### Prerequisites
111
+
112
+ - Node.js >= 18
113
+ - Python 3 with `ddgs` library:
114
+ ```bash
115
+ pip install ddgs
116
+ ```
117
+
118
+ ### Install
119
+
120
+ ```bash
121
+ # Option 1: npx (recommended)
122
+ npx agent-search-mcp
123
+
124
+ # Option 2: global install
125
+ npm install -g agent-search-mcp
126
+ ```
127
+
128
+ ### Platform Setup
129
+
130
+ <details>
131
+ <summary><b>Hermes</b></summary>
132
+
133
+ ```yaml
134
+ # ~/.hermes/config.yaml
135
+ mcp_servers:
136
+ agent-search:
137
+ command: npx
138
+ args: ["agent-search-mcp"]
139
+ ```
140
+ </details>
141
+
142
+ <details>
143
+ <summary><b>Claude Code</b></summary>
144
+
145
+ ```json
146
+ // ~/.claude/mcp.json
147
+ {
148
+ "mcpServers": {
149
+ "agent-search": {
150
+ "command": "npx",
151
+ "args": ["agent-search-mcp"]
152
+ }
153
+ }
154
+ }
155
+ ```
156
+ </details>
157
+
158
+ <details>
159
+ <summary><b>Cursor</b></summary>
160
+
161
+ ```json
162
+ // .cursor/mcp.json
163
+ {
164
+ "mcpServers": {
165
+ "agent-search": {
166
+ "command": "npx",
167
+ "args": ["agent-search-mcp"]
168
+ }
169
+ }
170
+ }
171
+ ```
172
+ </details>
173
+
174
+ <details>
175
+ <summary><b>Windsurf</b></summary>
176
+
177
+ ```json
178
+ // ~/.codeium/windsurf/mcp_config.json
179
+ {
180
+ "mcpServers": {
181
+ "agent-search": {
182
+ "command": "npx",
183
+ "args": ["agent-search-mcp"]
184
+ }
185
+ }
186
+ }
187
+ ```
188
+ </details>
189
+
190
+ <details>
191
+ <summary><b>OpenClaw</b></summary>
192
+
193
+ ```typescript
194
+ // openclaw.config.ts
195
+ {
196
+ mcpServers: {
197
+ "agent-search": {
198
+ command: "npx",
199
+ args: ["agent-search-mcp"]
200
+ }
201
+ }
202
+ }
203
+ ```
204
+ </details>
205
+
206
+ <details>
207
+ <summary><b>Codex</b></summary>
208
+
209
+ ```json
210
+ // ~/.codex/mcp.json
211
+ {
212
+ "mcpServers": {
213
+ "agent-search": {
214
+ "command": "npx",
215
+ "args": ["agent-search-mcp"]
216
+ }
217
+ }
218
+ }
219
+ ```
220
+ </details>
221
+
222
+ ---
223
+
224
+ ## Features
225
+
226
+ - **Free by default** — DuckDuckGo + Sogou as core engines, no API key required. Brave and Tavily available as optional paid fallback.
227
+ - **Multi-source verification** — Results cross-checked across engines, each result gets a confidence score (1-3) based on how many sources returned it.
228
+ - **Token optimization** — Title truncation (≤100 chars), snippet truncation (≤200 chars), URL + title dedup. Saves ~40-50% tokens.
229
+ - **Progressive disclosure** — 3 tools at different complexity levels. `free_search` for quick queries, `free_search_advanced` for filtered search, `free_extract` for page content. Agents discover capabilities on-demand.
230
+ - **Fallback chain** — Free engines first, paid engines as backup. Automatic merge, dedup, and scoring.
231
+ - **Health monitoring** — Real-time provider health tracking. Unhealthy providers filtered automatically.
232
+ - **Security** — Prompt injection detection, output boundary markers, phishing URL filtering, and security metadata on every response.
233
+ - **CLI tool** — Use as a command-line tool for terminal search, web extraction, and HTTP server.
234
+
235
+ ---
236
+
237
+ ## CLI Usage
238
+
239
+ free-agent-search-mcp also works as a CLI tool.
240
+
241
+ ### Install
242
+
243
+ ```bash
244
+ npm install -g agent-search-mcp
245
+ ```
246
+
247
+ ### Search
248
+
249
+ ```bash
250
+ # Basic search
251
+ fasm search "TypeScript MCP server"
252
+
253
+ # With options
254
+ fasm search "query" --count 5 --engines bing,baidu
255
+
256
+ # JSON output
257
+ fasm search "query" --json
258
+ ```
259
+
260
+ ### Extract Web Page
261
+
262
+ ```bash
263
+ fasm extract "https://example.com"
264
+ fasm extract "https://example.com" --json
265
+ ```
266
+
267
+ ### Start HTTP Server
268
+
269
+ ```bash
270
+ fasm serve --port 8080
271
+ ```
272
+
273
+ ### Help
274
+
275
+ ```bash
276
+ fasm --help
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Tools
282
+
283
+ ### `free_search`
284
+
285
+ Basic web search with multi-source verification.
286
+
287
+ ```json
288
+ {
289
+ "query": "TypeScript MCP server",
290
+ "count": 5
291
+ }
292
+ ```
293
+
294
+ **Returns:** Array of search results with confidence scores.
295
+
296
+ ### `free_search_advanced`
297
+
298
+ Advanced search with filters.
299
+
300
+ ```json
301
+ {
302
+ "query": "MCP server",
303
+ "count": 10,
304
+ "min_confidence": 2,
305
+ "time_range": "week",
306
+ "language": "zh",
307
+ "include_domains": ["github.com"],
308
+ "exclude_domains": ["reddit.com"]
309
+ }
310
+ ```
311
+
312
+ **Parameters:**
313
+ - `min_confidence` (1-3): Only return results verified by N+ sources
314
+ - `time_range`: day, week, month, year
315
+ - `language`: auto, en, zh
316
+ - `include_domains`: Only search these domains
317
+ - `exclude_domains`: Exclude these domains
318
+
319
+ ### `free_extract`
320
+
321
+ Extract full content from a URL as Markdown.
322
+
323
+ ```json
324
+ {
325
+ "url": "https://example.com/article",
326
+ "max_length": 5000
327
+ }
328
+ ```
329
+
330
+ **Returns:** Markdown content with metadata.
331
+
332
+ ---
333
+
334
+ ## Resources
335
+
336
+ ### `search://capabilities`
337
+
338
+ Returns a Markdown document describing all available tools and features. Agents can discover capabilities on-demand.
339
+
340
+ ### `search://health`
341
+
342
+ Returns JSON with health status of each search provider. Useful for monitoring and debugging.
343
+
344
+ ---
345
+
346
+ ## Configuration
347
+
348
+ ### Environment Variables
349
+
350
+ | Variable | Description | Required |
351
+ |----------|-------------|----------|
352
+ | `BRAVE_API_KEY` | Brave Search API key (2000 free/month) | No |
353
+ | `TAVILY_API_KEY` | Tavily API key (1000 free/month) | No |
354
+ | `LOG_LEVEL` | Log level (info, debug) | No |
355
+
356
+ **Zero config works** — no API keys needed for basic search.
357
+
358
+ ### With Paid Engines
359
+
360
+ Set environment variables to enable fallback to paid engines when free results are insufficient:
361
+
362
+ ```bash
363
+ export BRAVE_API_KEY=your_key_here
364
+ export TAVILY_API_KEY=your_key_here
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Dependencies
370
+
371
+ | Dependency | License | Purpose |
372
+ |------------|---------|---------|
373
+ | @modelcontextprotocol/sdk | MIT | MCP protocol |
374
+ | zod | MIT | Schema validation |
375
+ | pino | MIT | Logging |
376
+ | yaml | ISC | Config parsing |
377
+ | ddgs (Python) | MIT | DuckDuckGo search backend (bypasses anti-bot) |
378
+
379
+ **Note:** `ddgs` is a Python library called via subprocess. It must be installed separately:
380
+ ```bash
381
+ pip install ddgs
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Architecture
387
+
388
+ ```
389
+ Agent
390
+ ↓ MCP Protocol (stdio)
391
+ MCP Server
392
+ ├── Tools Layer (progressive disclosure)
393
+ │ ├── free_search (default)
394
+ │ ├── free_search_advanced (optional)
395
+ │ └── free_extract (optional)
396
+ ├── Aggregation Layer
397
+ │ ├── Top-1 Snippet merge
398
+ │ ├── URL + Title dedup
399
+ │ ├── Scoring + Confidence
400
+ │ └── Output truncation
401
+ ├── Security Layer
402
+ │ ├── Prompt injection detection
403
+ │ ├── Output boundary markers
404
+ │ ├── Phishing URL filtering
405
+ │ └── Security metadata
406
+ ├── Fallback Chain
407
+ │ ├── Phase 1: Free engines (DDG + Sogou)
408
+ │ └── Phase 2: Paid engines (Brave + Tavily)
409
+ └── Infrastructure
410
+ ├── Cache (LRU, 60s TTL)
411
+ ├── Rate Limiter (1s per provider)
412
+ ├── Health Tracker
413
+ └── SSRF Protection
414
+ ```
415
+
416
+ ---
417
+
418
+ ## Documentation / 文档
419
+
420
+ | Document | Description |
421
+ |----------|-------------|
422
+ | [PRD](docs/prd.md) | Product Requirements Document |
423
+ | [Architecture](docs/architecture.md) | Technical Architecture |
424
+ | [Plan](docs/plan.md) | Implementation Plan |
425
+ | [Review Results](docs/review-results.md) | 5-Team Review Results |
426
+ | [Fork Plan](docs/fork-plan.md) | Fork & Modification Plan |
427
+ | [CHANGELOG](CHANGELOG.md) | Version History |
428
+
429
+ ---
430
+
431
+ ## Development
432
+
433
+ ```bash
434
+ # Clone
435
+ git clone https://github.com/lennney/agent-search-mcp.git
436
+ cd agent-search-mcp
437
+
438
+ # Install
439
+ npm install
440
+
441
+ # Build
442
+ npm run build
443
+
444
+ # Test
445
+ npm test
446
+
447
+ # Run
448
+ npm start
449
+ ```
450
+
451
+ ---
452
+
453
+ ## Roadmap
454
+
455
+ - [ ] v0.1.0 — Initial release with DDG + Sogou
456
+ - [ ] v0.2.0 — Brave + Tavily fallback
457
+ - [ ] v0.3.0 — Health monitoring + rate limiting
458
+ - [ ] v1.0.0 — Stable release with documentation
459
+ - [ ] v1.1.0 — Plugin system for custom engines
460
+ - [ ] v2.0.0 — Browser-based extraction (Playwright)
461
+
462
+ ---
463
+
464
+ ## License
465
+
466
+ [Apache License 2.0](LICENSE)
467
+
468
+ Based on [open-websearch](https://github.com/Aas-ee/open-websearch) by Aas-ee.
469
+
470
+ ```
471
+ Copyright 2025 Open-WebSearch MCP Server Contributors
472
+ Based on open-websearch by Aas-ee (Apache 2.0).
473
+ Modified by Agent Search MCP Contributors.
474
+ Copyright 2026 Agent Search MCP Contributors
475
+ ```
476
+
477
+ ---
478
+
479
+ ## Contributing
480
+
@@ -0,0 +1,102 @@
1
+ export function normalizeUrl(url) {
2
+ try {
3
+ const u = new URL(url);
4
+ return `${u.hostname}${u.pathname.replace(/\/$/, '')}`.toLowerCase();
5
+ }
6
+ catch {
7
+ return url.toLowerCase();
8
+ }
9
+ }
10
+ /**
11
+ * Provider-aware dedup: same provider only searches once.
12
+ * From ddgs: track which providers we've already queried.
13
+ */
14
+ export function dedupByProvider(engines) {
15
+ // Map engine -> provider (e.g., 'ddg' -> 'bing', 'sogou' -> 'sogou')
16
+ const providerMap = {
17
+ duckduckgo: 'bing', // DDG uses Bing backend
18
+ sogou: 'sogou',
19
+ brave: 'brave',
20
+ tavily: 'tavily',
21
+ };
22
+ const seenProviders = new Set();
23
+ const uniqueEngines = [];
24
+ for (const engine of engines) {
25
+ const provider = providerMap[engine] || engine;
26
+ if (!seenProviders.has(provider)) {
27
+ seenProviders.add(provider);
28
+ uniqueEngines.push(engine);
29
+ }
30
+ }
31
+ return uniqueEngines;
32
+ }
33
+ /**
34
+ * URL dedup with frequency counting.
35
+ * From ddgs: track how many engines returned each URL.
36
+ * Keep the item with longer body (richer content).
37
+ */
38
+ export function dedupByUrl(results) {
39
+ const seen = new Map();
40
+ const frequencies = new Map();
41
+ for (const r of results) {
42
+ const key = normalizeUrl(r.url);
43
+ frequencies.set(key, (frequencies.get(key) || 0) + 1);
44
+ if (!seen.has(key)) {
45
+ seen.set(key, r);
46
+ }
47
+ else {
48
+ // From ddgs: keep the item with longer body (richer content)
49
+ const existing = seen.get(key);
50
+ if ((r.snippet?.length || 0) > (existing.snippet?.length || 0)) {
51
+ seen.set(key, r);
52
+ }
53
+ }
54
+ }
55
+ return { results: Array.from(seen.values()), frequencies };
56
+ }
57
+ /**
58
+ * Title dedup with Jaccard similarity.
59
+ */
60
+ export function dedupByTitle(results, threshold = 0.85) {
61
+ const kept = [];
62
+ for (const r of results) {
63
+ const isDuplicate = kept.some(k => jaccard(k.title, r.title) > threshold);
64
+ if (!isDuplicate)
65
+ kept.push(r);
66
+ }
67
+ return kept;
68
+ }
69
+ /**
70
+ * Filter low-quality results.
71
+ * From ddgs: post_extract_results filters ads and invalid results.
72
+ */
73
+ export function filterLowQuality(results) {
74
+ return results.filter(r => {
75
+ // Filter empty snippets
76
+ if (!r.snippet || r.snippet.length < 20)
77
+ return false;
78
+ // Filter DDG ads
79
+ if (r.url.includes('y.js?') || r.url.includes('/ad/'))
80
+ return false;
81
+ // Filter invalid URLs
82
+ if (!r.url.startsWith('http'))
83
+ return false;
84
+ // Filter DDG ad redirects
85
+ if (r.url.includes('duckduckgo.com/y.js'))
86
+ return false;
87
+ // Filter search engine internal links
88
+ if (r.url.includes('sogou.com/link'))
89
+ return false;
90
+ // Filter Wikipedia categories (low quality)
91
+ if (r.url.includes('wikipedia.org/wiki/Category:'))
92
+ return false;
93
+ return true;
94
+ });
95
+ }
96
+ function jaccard(a, b) {
97
+ const setA = new Set(a.split(/\s+/));
98
+ const setB = new Set(b.split(/\s+/));
99
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
100
+ const union = new Set([...setA, ...setB]);
101
+ return union.size > 0 ? intersection.size / union.size : 0;
102
+ }
@@ -0,0 +1,60 @@
1
+ import { processResultSecurity, getSecurityNote, wrapWithBoundaryMarkers } from '../infrastructure/security.js';
2
+ /**
3
+ * Format search results with security processing.
4
+ *
5
+ * Security features:
6
+ * - Snippet injection detection and marking
7
+ * - URL phishing detection
8
+ * - Boundary markers for agent clarity
9
+ * - Security metadata per result
10
+ */
11
+ export function formatResults(results) {
12
+ // Process security for each result
13
+ const secured = results.map(r => processResultSecurity(r));
14
+ return {
15
+ results: secured.map(r => ({
16
+ title: r.title.slice(0, 100),
17
+ url: r.url,
18
+ snippet: r.snippet.slice(0, 200),
19
+ confidence: r.confidence,
20
+ // Only include security details if threats detected
21
+ ...(r.security.injectionDetected || !r.security.urlSafe ? {
22
+ security: {
23
+ injection_detected: r.security.injectionDetected,
24
+ url_safe: r.security.urlSafe,
25
+ threats: r.security.threats,
26
+ warnings: r.security.warnings,
27
+ },
28
+ } : {}),
29
+ })),
30
+ meta: {
31
+ total: results.length,
32
+ high_confidence: results.filter(r => r.confidence >= 2).length,
33
+ engines: [...new Set(results.flatMap(r => r.engines || [r.source]))],
34
+ },
35
+ security_note: getSecurityNote(),
36
+ };
37
+ }
38
+ /**
39
+ * Format results as XML boundary-marked output.
40
+ * Useful for agents that need clear data/instruction separation.
41
+ */
42
+ export function formatResultsXml(results) {
43
+ const header = [
44
+ '<?xml version="1.0" encoding="UTF-8"?>',
45
+ '<search-response>',
46
+ ` <security-note>${getSecurityNote()}</security-note>`,
47
+ ' <results>',
48
+ ].join('\n');
49
+ const body = results.map(r => {
50
+ const secured = processResultSecurity(r);
51
+ return wrapWithBoundaryMarkers({
52
+ title: secured.title.slice(0, 100),
53
+ url: secured.url,
54
+ snippet: secured.snippet.slice(0, 200),
55
+ confidence: secured.confidence,
56
+ });
57
+ }).join('\n');
58
+ const footer = ' </results>\n</search-response>';
59
+ return [header, body, footer].join('\n');
60
+ }
@@ -0,0 +1,3 @@
1
+ export { dedupByProvider, dedupByUrl, dedupByTitle, filterLowQuality, normalizeUrl } from './dedup.js';
2
+ export { scoreAndRank } from './scorer.js';
3
+ export { formatResults } from './format.js';