@oevortex/ddg_search 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.1.3] - 2025-11-30
6
+ ### Changed
7
+ - Replaced Felo AI tool with IAsk AI tool for advanced AI-powered search
8
+ - Added `src/utils/search_iask.js` implementing IAsk API client
9
+ - Added `src/tools/iaskTool.js` tool definition and handler
10
+ - Updated `src/index.ts` to use IAsk tool instead of Felo
11
+ - Updated `package.json` description, keywords, and dependencies (`turndown`, `ws`)
12
+ - Updated `README.md` to reference IAsk AI and document new tool parameters
13
+ - Removed old Felo tool files (`feloTool.js`, `search_felo.js`)
14
+
15
+ ## [1.1.2] - 2025-11-29
16
+ ### Added
17
+ - Initial release with DuckDuckGo and Felo AI search tools
18
+ - MCP server implementation
19
+ - Caching, rotating user agents, and web scraping features
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  <img src="https://img.shields.io/npm/v/@oevortex/ddg_search.svg" alt="npm version" />
3
3
  <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License: Apache 2.0" />
4
4
  <img src="https://img.shields.io/badge/YouTube-%40OEvortex-red.svg" alt="YouTube Channel" />
5
- <h1>DuckDuckGo & Felo AI Search MCP 🔍🧠</h1>
6
- <p>A blazing-fast, privacy-friendly Model Context Protocol (MCP) server for web search and AI-powered responses using DuckDuckGo and Felo AI.</p>
5
+ <h1>DuckDuckGo & IAsk AI Search MCP 🔍🧠</h1>
6
+ <p>A blazing-fast, privacy-friendly Model Context Protocol (MCP) server for web search and AI-powered responses using DuckDuckGo and IAsk AI.</p>
7
7
  <a href="https://glama.ai/mcp/servers/@OEvortex/ddg_search">
8
8
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@OEvortex/ddg_search/badge" alt="DuckDuckGo Search MCP server" />
9
9
  </a>
@@ -20,9 +20,7 @@
20
20
  ## ✨ Features
21
21
 
22
22
  <div style="display: flex; flex-wrap: wrap; gap: 1.5em; margin-bottom: 1.5em;"> <div><b>🌐 Web search</b> using DuckDuckGo HTML</div>
23
- <div><b>🧠 AI search</b> using Felo AI</div>
24
- <div><b>📄 URL content extraction</b> with smart filtering</div>
25
- <div><b>📊 URL metadata extraction</b> (title, description, images)</div>
23
+ <div><b>🧠 AI search</b> using IAsk AI</div>
26
24
  <div><b>⚡ Performance optimized</b> with caching</div>
27
25
  <div><b>🛡️ Security features</b> including rate limiting and rotating user agents</div>
28
26
  <div><b>🔌 MCP-compliant</b> server implementation</div>
@@ -52,7 +50,7 @@ npx -y @oevortex/ddg_search@latest
52
50
  ## 🛠️ Installation Options
53
51
 
54
52
  <details>
55
- <summary><b>Global Installation</b></summary>
53
+ <summary><b>Global Installation (npm)</b></summary>
56
54
 
57
55
  ```bash
58
56
  npm install -g @oevortex/ddg_search
@@ -66,6 +64,36 @@ ddg-search-mcp
66
64
 
67
65
  </details>
68
66
 
67
+ <details>
68
+ <summary><b>Global Installation (Yarn)</b></summary>
69
+
70
+ ```bash
71
+ yarn global add @oevortex/ddg_search
72
+ ```
73
+
74
+ Run globally:
75
+
76
+ ```bash
77
+ ddg-search-mcp
78
+ ```
79
+
80
+ </details>
81
+
82
+ <details>
83
+ <summary><b>Global Installation (pnpm)</b></summary>
84
+
85
+ ```bash
86
+ pnpm add -g @oevortex/ddg_search
87
+ ```
88
+
89
+ Run globally:
90
+
91
+ ```bash
92
+ ddg-search-mcp
93
+ ```
94
+
95
+ </details>
96
+
69
97
  <details>
70
98
  <summary><b>Local Installation (Development)</b></summary>
71
99
 
@@ -76,6 +104,20 @@ npm install
76
104
  npm start
77
105
  ```
78
106
 
107
+ Or with Yarn:
108
+
109
+ ```bash
110
+ yarn install
111
+ yarn start
112
+ ```
113
+
114
+ Or with pnpm:
115
+
116
+ ```bash
117
+ pnpm install
118
+ pnpm start
119
+ ```
120
+
79
121
  </details>
80
122
 
81
123
  ---
@@ -140,34 +182,15 @@ Or if installed globally:
140
182
  <i>Example: Search the web for "climate change solutions"</i>
141
183
  </div>
142
184
  <div style="margin-bottom: 1.5em;">
143
- <b>🧠 Felo AI Search Tool</b><br/>
144
- <code>felo-search</code><br/>
185
+ <b>🧠 IAsk AI Search Tool</b><br/>
186
+ <code>iask-search</code><br/>
145
187
  <ul>
146
- <li><b>query</b> (string, required): The search query or prompt</li>
188
+ <li><b>query</b> (string, required): The search query or question</li>
189
+ <li><b>mode</b> (string, optional, default: "question"): Search mode - "question", "academic", "forums", "wiki", or "thinking"</li>
190
+ <li><b>detailLevel</b> (string, optional): Response detail level - "concise", "detailed", or "comprehensive"</li>
147
191
  <li><b>stream</b> (boolean, optional, default: false): Whether to stream the response</li>
148
192
  </ul>
149
- <i>Example: Search Felo AI for "Explain quantum computing in simple terms"</i>
150
- </div>
151
- <div style="margin-bottom: 1.5em;">
152
- <b>📄 Fetch URL Tool</b><br/>
153
- <code>fetch-url</code><br/>
154
- <ul>
155
- <li><b>url</b> (string, required): The URL to fetch</li>
156
- <li><b>maxLength</b> (integer, optional, default: 10000): Max content length</li>
157
- <li><b>extractMainContent</b> (boolean, optional, default: true): Extract main content</li>
158
- <li><b>includeLinks</b> (boolean, optional, default: true): Include link text</li>
159
- <li><b>includeImages</b> (boolean, optional, default: true): Include image alt text</li>
160
- <li><b>excludeTags</b> (array, optional): Tags to exclude</li>
161
- </ul>
162
- <i>Example: Fetch the content from "https://example.com"</i>
163
- </div>
164
- <div style="margin-bottom: 1.5em;">
165
- <b>📊 URL Metadata Tool</b><br/>
166
- <code>url-metadata</code><br/>
167
- <ul>
168
- <li><b>url</b> (string, required): The URL to extract metadata from</li>
169
- </ul>
170
- <i>Example: Get metadata for "https://example.com"</i>
193
+ <i>Example: Search IAsk AI for "Explain quantum computing in simple terms"</i>
171
194
  </div>
172
195
  </div>
173
196
 
@@ -182,12 +205,10 @@ src/
182
205
  index.js # Main entry point
183
206
  tools/ # Tool definitions and handlers
184
207
  searchTool.js
185
- fetchUrlTool.js
186
- metadataTool.js
187
- feloTool.js
208
+ iaskTool.js
188
209
  utils/
189
210
  search.js # Search and URL utilities
190
- search_felo.js # Felo AI search utilities
211
+ search_iask.js # IAsk AI search utilities
191
212
  package.json
192
213
  README.md
193
214
  ```
package/bin/cli.js CHANGED
@@ -12,9 +12,9 @@ async function startServer() {
12
12
  try {
13
13
  // Dynamically import the modules
14
14
  const { searchToolDefinition, searchToolHandler } = await import(`${modulePath}/tools/searchTool.js`);
15
- const { fetchUrlToolDefinition, fetchUrlToolHandler } = await import(`${modulePath}/tools/fetchUrlTool.js`);
16
- const { metadataToolDefinition, metadataToolHandler } = await import(`${modulePath}/tools/metadataTool.js`);
17
- const { feloToolDefinition, feloToolHandler } = await import(`${modulePath}/tools/feloTool.js`); // Create the MCP server
15
+ const { feloToolDefinition, feloToolHandler } = await import(`${modulePath}/tools/feloTool.js`);
16
+
17
+ // Create the MCP server
18
18
  const server = new Server({
19
19
  id: 'ddg-search-mcp',
20
20
  name: 'DuckDuckGo & Felo AI Search MCP',
@@ -31,8 +31,6 @@ async function startServer() {
31
31
  // Global variable to track available tools
32
32
  let availableTools = [
33
33
  searchToolDefinition,
34
- fetchUrlToolDefinition,
35
- metadataToolDefinition,
36
34
  feloToolDefinition
37
35
  ];
38
36
 
@@ -56,7 +54,7 @@ async function startServer() {
56
54
  const { name, arguments: args } = request.params;
57
55
 
58
56
  // Validate tool name
59
- const validTools = ['web-search', 'fetch-url', 'url-metadata', 'felo-search'];
57
+ const validTools = ['web-search', 'felo-search'];
60
58
  if (!validTools.includes(name)) {
61
59
  throw new Error(`Unknown tool: ${name}`);
62
60
  }
@@ -66,12 +64,6 @@ async function startServer() {
66
64
  case 'web-search':
67
65
  return await searchToolHandler(args);
68
66
 
69
- case 'fetch-url':
70
- return await fetchUrlToolHandler(args);
71
-
72
- case 'url-metadata':
73
- return await metadataToolHandler(args);
74
-
75
67
  case 'felo-search':
76
68
  return await feloToolHandler(args);
77
69
 
@@ -129,8 +121,6 @@ Options:
129
121
 
130
122
  This MCP server provides the following tools:
131
123
  - web-search: Search the web using DuckDuckGo
132
- - fetch-url: Fetch and extract content from a URL
133
- - url-metadata: Extract metadata from a URL
134
124
  - felo-search: Search using Felo AI for AI-generated responses
135
125
 
136
126
  Created by @OEvortex
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@oevortex/ddg_search","version":"1.1.3","description":"A Model Context Protocol server for web search using DuckDuckGo and Felo AI","main":"src/index.js","module":"src/index.ts","exports":{".":{"import":"./src/index.js","default":"./src/index.js"}},"bin":{"ddg-search-mcp":"bin/cli.js","oevortex-ddg-search":"bin/cli.js"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1","start":"node bin/cli.js","prepublishOnly":"npm run lint","lint":"echo \"No linting configured\"","build":"npx @smithery/cli build","dev":"npx @smithery/cli dev"},"publishConfig":{"access":"public"},"keywords":["mcp","model-context-protocol","duckduckgo","felo","search","web-search","ai-search","claude","ai","llm"],"author":"OEvortex","license":"Apache-2.0","type":"module","dependencies":{"@modelcontextprotocol/sdk":"^1.17.4","axios":"^1.8.4","cheerio":"^1.0.0","jsdom":"^26.1.0","smithery":"^0.5.2","uuid":"^9.0.1"},"devDependencies":{"@types/node":"^24.3.0","tsx":"^4.20.4","typescript":"^5.9.2"}}
1
+ {"name":"@oevortex/ddg_search","version":"1.1.4","description":"A Model Context Protocol server for web search using DuckDuckGo and IAsk AI","main":"src/index.js","module":"src/index.ts","exports":{".":{"import":"./src/index.js","default":"./src/index.js"}},"bin":{"ddg-search-mcp":"bin/cli.js","oevortex-ddg-search":"bin/cli.js"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1","start":"node bin/cli.js","prepublishOnly":"npm run lint","lint":"echo \"No linting configured\"","build":"npx @smithery/cli build","dev":"npx @smithery/cli dev"},"publishConfig":{"access":"public"},"keywords":["mcp","model-context-protocol","duckduckgo","iask","search","web-search","ai-search","claude","ai","llm"],"author":"OEvortex","license":"Apache-2.0","type":"module","dependencies":{"@modelcontextprotocol/sdk":"^1.17.4","axios":"^1.8.4","cheerio":"^1.0.0","jsdom":"^26.1.0","smithery":"^0.5.2","turndown":"^7.2.2","uuid":"^9.0.1","ws":"^8.18.3"},"devDependencies":{"@types/node":"^24.3.0","tsx":"^4.20.4","typescript":"^5.9.2"}}
package/src/index.js CHANGED
@@ -3,19 +3,15 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
3
3
 
4
4
  // Import tool definitions and handlers
5
5
  import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
6
- import { fetchUrlToolDefinition, fetchUrlToolHandler } from './tools/fetchUrlTool.js';
7
- import { metadataToolDefinition, metadataToolHandler } from './tools/metadataTool.js';
8
6
  import { feloToolDefinition, feloToolHandler } from './tools/feloTool.js';
9
7
 
10
8
  // Required: Export default createServer function for Smithery
11
9
  export default function createServer({ config } = {}) {
12
10
  console.log('Creating MCP server with latest SDK...');
13
-
11
+
14
12
  // Global variable to track available tools
15
13
  const availableTools = [
16
14
  searchToolDefinition,
17
- fetchUrlToolDefinition,
18
- metadataToolDefinition,
19
15
  feloToolDefinition
20
16
  ];
21
17
 
@@ -51,16 +47,10 @@ export default function createServer({ config } = {}) {
51
47
  switch (name) {
52
48
  case 'web-search':
53
49
  return await searchToolHandler(args);
54
-
55
- case 'fetch-url':
56
- return await fetchUrlToolHandler(args);
57
-
58
- case 'url-metadata':
59
- return await metadataToolHandler(args);
60
-
50
+
61
51
  case 'felo-search':
62
52
  return await feloToolHandler(args);
63
-
53
+
64
54
  default:
65
55
  throw new Error(`Tool not found: ${name}`);
66
56
  }
package/src/index.ts CHANGED
@@ -3,20 +3,16 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
3
3
 
4
4
  // Import tool definitions and handlers
5
5
  import { searchToolDefinition, searchToolHandler } from './tools/searchTool.js';
6
- import { fetchUrlToolDefinition, fetchUrlToolHandler } from './tools/fetchUrlTool.js';
7
- import { metadataToolDefinition, metadataToolHandler } from './tools/metadataTool.js';
8
- import { feloToolDefinition, feloToolHandler } from './tools/feloTool.js';
6
+ import { iaskToolDefinition, iaskToolHandler } from './tools/iaskTool.js';
9
7
 
10
8
  // Required: Export default createServer function for Smithery
11
9
  export default function createServer({ config }: { config?: any } = {}) {
12
10
  console.log('Creating MCP server with latest SDK...');
13
-
11
+
14
12
  // Global variable to track available tools
15
13
  const availableTools = [
16
14
  searchToolDefinition,
17
- fetchUrlToolDefinition,
18
- metadataToolDefinition,
19
- feloToolDefinition
15
+ iaskToolDefinition
20
16
  ];
21
17
 
22
18
  console.log('Available tools:', availableTools.map(t => t.name));
@@ -51,16 +47,10 @@ export default function createServer({ config }: { config?: any } = {}) {
51
47
  switch (name) {
52
48
  case 'web-search':
53
49
  return await searchToolHandler(args);
54
-
55
- case 'fetch-url':
56
- return await fetchUrlToolHandler(args);
57
-
58
- case 'url-metadata':
59
- return await metadataToolHandler(args);
60
-
61
- case 'felo-search':
62
- return await feloToolHandler(args);
63
-
50
+
51
+ case 'iask-search':
52
+ return await iaskToolHandler(args);
53
+
64
54
  default:
65
55
  throw new Error(`Tool not found: ${name}`);
66
56
  }
@@ -0,0 +1,101 @@
1
+ import { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS } from '../utils/search_iask.js';
2
+
3
+ /**
4
+ * IAsk AI search tool definition
5
+ */
6
+ export const iaskToolDefinition = {
7
+ name: 'iask-search',
8
+ title: 'IAsk AI Search',
9
+ description: 'AI-powered search using IAsk.ai. Retrieves comprehensive, AI-generated responses based on web content. Supports different search modes (question, academic, forums, wiki, thinking) and detail levels (concise, detailed, comprehensive). Ideal for getting well-researched answers to complex questions.',
10
+ inputSchema: {
11
+ type: 'object',
12
+ properties: {
13
+ query: {
14
+ type: 'string',
15
+ description: 'The search query or question to ask. Supports natural language questions for comprehensive AI-generated responses.'
16
+ },
17
+ mode: {
18
+ type: 'string',
19
+ description: 'Search mode to use. Options: "question" (general questions), "academic" (scholarly/research), "forums" (community discussions), "wiki" (encyclopedia-style), "thinking" (deep analysis). Default is "question".',
20
+ enum: VALID_MODES,
21
+ default: 'question'
22
+ },
23
+ detailLevel: {
24
+ type: 'string',
25
+ description: 'Level of detail in the response. Options: "concise" (brief), "detailed" (moderate), "comprehensive" (extensive). Default is null (standard response).',
26
+ enum: VALID_DETAIL_LEVELS
27
+ },
28
+ stream: {
29
+ type: 'boolean',
30
+ description: 'Enable streaming mode to receive incremental results. Default is false.',
31
+ default: false
32
+ }
33
+ },
34
+ required: ['query']
35
+ },
36
+ annotations: {
37
+ readOnlyHint: true,
38
+ openWorldHint: false
39
+ }
40
+ };
41
+
42
+ /**
43
+ * IAsk AI search tool handler
44
+ * @param {Object} params - The tool parameters
45
+ * @returns {Promise<Object>} - The tool result
46
+ */
47
+ export async function iaskToolHandler(params) {
48
+ const {
49
+ query,
50
+ mode = 'question',
51
+ detailLevel = null,
52
+ stream = false
53
+ } = params;
54
+
55
+ console.log(`Searching IAsk AI for: "${query}" (mode: ${mode}, detailLevel: ${detailLevel || 'default'}, stream: ${stream})`);
56
+
57
+ try {
58
+ if (stream) {
59
+ // For streaming responses, collect them and return
60
+ let fullResponse = '';
61
+ const chunks = [];
62
+
63
+ for await (const chunk of await searchIAsk(query, true, false, mode, detailLevel)) {
64
+ chunks.push(chunk);
65
+ fullResponse += chunk;
66
+ }
67
+
68
+ return {
69
+ content: [
70
+ {
71
+ type: 'text',
72
+ text: fullResponse || 'No results found.'
73
+ }
74
+ ]
75
+ };
76
+ } else {
77
+ // For non-streaming responses
78
+ const response = await searchIAsk(query, false, false, mode, detailLevel);
79
+
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: response || 'No results found.'
85
+ }
86
+ ]
87
+ };
88
+ }
89
+ } catch (error) {
90
+ console.error(`Error in IAsk search: ${error.message}`);
91
+ return {
92
+ isError: true,
93
+ content: [
94
+ {
95
+ type: 'text',
96
+ text: `Error searching IAsk: ${error.message}`
97
+ }
98
+ ]
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,381 @@
1
+ import axios from 'axios';
2
+ import * as cheerio from 'cheerio';
3
+ import TurndownService from 'turndown';
4
+
5
+ // Rotating User Agents
6
+ const USER_AGENTS = [
7
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
8
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
9
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
10
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
11
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
12
+ ];
13
+
14
+ // Cache results to avoid repeated requests
15
+ const resultsCache = new Map();
16
+ const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
17
+
18
+ // Valid modes and detail levels
19
+ const VALID_MODES = ['question', 'academic', 'forums', 'wiki', 'thinking'];
20
+ const VALID_DETAIL_LEVELS = ['concise', 'detailed', 'comprehensive'];
21
+
22
+ /**
23
+ * Response class for IAsk API responses
24
+ */
25
+ class Response {
26
+ /**
27
+ * Create a new Response
28
+ * @param {string} text - The text content of the response
29
+ */
30
+ constructor(text) {
31
+ this.text = text;
32
+ }
33
+
34
+ /**
35
+ * String representation of the response
36
+ * @returns {string} The text content
37
+ */
38
+ toString() {
39
+ return this.text;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Get a random user agent from the list
45
+ * @returns {string} A random user agent string
46
+ */
47
+ function getRandomUserAgent() {
48
+ return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
49
+ }
50
+
51
+ /**
52
+ * Generate a cache key for a search query
53
+ * @param {string} query - The search query
54
+ * @param {string} mode - The search mode
55
+ * @param {string|null} detailLevel - The detail level
56
+ * @returns {string} The cache key
57
+ */
58
+ function getCacheKey(query, mode, detailLevel) {
59
+ return `iask-${mode}-${detailLevel || 'default'}-${query}`;
60
+ }
61
+
62
+ /**
63
+ * Clear old entries from the cache
64
+ */
65
+ function clearOldCache() {
66
+ const now = Date.now();
67
+ for (const [key, value] of resultsCache.entries()) {
68
+ if (now - value.timestamp > CACHE_DURATION) {
69
+ resultsCache.delete(key);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Find HTML content in a nested structure
76
+ * @param {Object|Array} diff - The nested structure to search
77
+ * @returns {string|null} The found HTML content or null
78
+ */
79
+ function cacheFind(diff) {
80
+ const values = Array.isArray(diff) ? diff : Object.values(diff);
81
+ const turndown = new TurndownService();
82
+
83
+ for (const value of values) {
84
+ if (typeof value === 'object' && value !== null) {
85
+ const cache = cacheFind(value);
86
+ if (cache) return cache;
87
+ }
88
+ if (typeof value === 'string' && /<p>.+?<\/p>/.test(value)) {
89
+ return turndown.turndown(value).trim();
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /**
96
+ * Format HTML content into readable markdown text
97
+ * @param {string} htmlContent - The HTML content to format
98
+ * @returns {string} Formatted text
99
+ */
100
+ function formatHtml(htmlContent) {
101
+ const $ = cheerio.load(htmlContent);
102
+ const outputLines = [];
103
+
104
+ $('h1, h2, h3, p, ol, ul, div').each((_, element) => {
105
+ const tagName = element.tagName.toLowerCase();
106
+ const $el = $(element);
107
+
108
+ if (['h1', 'h2', 'h3'].includes(tagName)) {
109
+ outputLines.push(`\n**${$el.text().trim()}**\n`);
110
+ } else if (tagName === 'p') {
111
+ let text = $el.text().trim();
112
+ // Remove IAsk attribution
113
+ text = text.replace(/^According to Ask AI & Question AI www\.iAsk\.ai:\s*/i, '').trim();
114
+ // Remove footnote markers
115
+ text = text.replace(/\[\d+\]\(#fn:\d+ 'see footnote'\)/g, '');
116
+ if (text) outputLines.push(text + '\n');
117
+ } else if (['ol', 'ul'].includes(tagName)) {
118
+ $el.find('li').each((_, li) => {
119
+ outputLines.push('- ' + $(li).text().trim() + '\n');
120
+ });
121
+ } else if (tagName === 'div' && $el.hasClass('footnotes')) {
122
+ outputLines.push('\n**Authoritative Sources**\n');
123
+ $el.find('li').each((_, li) => {
124
+ const link = $(li).find('a');
125
+ if (link.length) {
126
+ outputLines.push(`- ${link.text().trim()} (${link.attr('href')})\n`);
127
+ }
128
+ });
129
+ }
130
+ });
131
+
132
+ return outputLines.join('');
133
+ }
134
+
135
+ /**
136
+ * Create an axios session with proper headers
137
+ * @returns {Object} Axios instance
138
+ */
139
+ function createSession() {
140
+ return axios.create({
141
+ timeout: 30000,
142
+ headers: {
143
+ 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
144
+ 'accept-encoding': 'gzip, deflate, br',
145
+ 'accept-language': 'en-US,en;q=0.9',
146
+ 'cache-control': 'no-cache',
147
+ 'dnt': '1',
148
+ 'pragma': 'no-cache',
149
+ 'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
150
+ 'sec-ch-ua-mobile': '?0',
151
+ 'sec-ch-ua-platform': '"Windows"',
152
+ 'sec-fetch-dest': 'document',
153
+ 'sec-fetch-mode': 'navigate',
154
+ 'sec-fetch-site': 'none',
155
+ 'sec-fetch-user': '?1',
156
+ 'upgrade-insecure-requests': '1',
157
+ 'user-agent': getRandomUserAgent()
158
+ }
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Search using the IAsk AI API
164
+ * @param {string} prompt - The search query or prompt
165
+ * @param {boolean} stream - If true, yields response chunks as they arrive
166
+ * @param {boolean} raw - If true, returns raw response dictionaries
167
+ * @param {string} mode - Search mode: 'question', 'academic', 'forums', 'wiki', 'thinking'
168
+ * @param {string|null} detailLevel - Detail level: 'concise', 'detailed', 'comprehensive'
169
+ * @returns {Promise<string|AsyncGenerator<string>>} The search results
170
+ */
171
+ async function searchIAsk(prompt, stream = false, raw = false, mode = 'question', detailLevel = null) {
172
+ // Validate mode
173
+ if (!VALID_MODES.includes(mode)) {
174
+ throw new Error(`Invalid mode: ${mode}. Valid modes are: ${VALID_MODES.join(', ')}`);
175
+ }
176
+
177
+ // Validate detail level
178
+ if (detailLevel && !VALID_DETAIL_LEVELS.includes(detailLevel)) {
179
+ throw new Error(`Invalid detail level: ${detailLevel}. Valid levels are: ${VALID_DETAIL_LEVELS.join(', ')}`);
180
+ }
181
+
182
+ // Clear old cache entries
183
+ clearOldCache();
184
+
185
+ // Check cache first if not streaming
186
+ if (!stream) {
187
+ const cacheKey = getCacheKey(prompt, mode, detailLevel);
188
+ const cachedResults = resultsCache.get(cacheKey);
189
+
190
+ if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
191
+ return cachedResults.results;
192
+ }
193
+ }
194
+
195
+ const session = createSession();
196
+ const apiEndpoint = 'https://iask.ai/';
197
+
198
+ // Build URL with parameters
199
+ const params = new URLSearchParams({
200
+ mode: mode,
201
+ q: prompt
202
+ });
203
+ if (detailLevel) {
204
+ params.append('options[detail_level]', detailLevel);
205
+ }
206
+
207
+ // Define the streaming function
208
+ async function* streamFunction() {
209
+ try {
210
+ // First, get the initial page to extract tokens
211
+ const initialUrl = `${apiEndpoint}?${params.toString()}`;
212
+ const initialResponse = await session.get(initialUrl);
213
+
214
+ if (initialResponse.status !== 200) {
215
+ throw new Error(`Failed to get initial page - (${initialResponse.status}, ${initialResponse.statusText})`);
216
+ }
217
+
218
+ const $ = cheerio.load(initialResponse.data);
219
+
220
+ // Extract Phoenix LiveView tokens
221
+ const phxNode = $('[id^="phx-"]').last();
222
+ const phxId = phxNode.attr('id');
223
+ const phxSession = phxNode.attr('data-phx-session');
224
+ const csrfToken = $('meta[name="csrf-token"]').attr('content');
225
+
226
+ if (!phxId || !csrfToken) {
227
+ throw new Error('Failed to extract required tokens from IAsk page');
228
+ }
229
+
230
+ // Connect to WebSocket
231
+ const WebSocket = (await import('ws')).default;
232
+ const wsUrl = `wss://iask.ai/live/websocket?_csrf_token=${encodeURIComponent(csrfToken)}&vsn=2.0.0`;
233
+
234
+ const ws = new WebSocket(wsUrl, {
235
+ headers: {
236
+ 'Origin': 'https://iask.ai',
237
+ 'User-Agent': getRandomUserAgent()
238
+ }
239
+ });
240
+
241
+ let streamingText = '';
242
+ let resolveWs;
243
+ let rejectWs;
244
+ const wsPromise = new Promise((resolve, reject) => {
245
+ resolveWs = resolve;
246
+ rejectWs = reject;
247
+ });
248
+
249
+ const chunks = [];
250
+ let isComplete = false;
251
+
252
+ ws.on('open', () => {
253
+ // Send join message
254
+ const joinMessage = [
255
+ null,
256
+ null,
257
+ `lv:${phxId}`,
258
+ 'phx_join',
259
+ {
260
+ params: { _csrf_token: csrfToken },
261
+ url: initialUrl,
262
+ session: phxSession
263
+ }
264
+ ];
265
+ ws.send(JSON.stringify(joinMessage));
266
+ });
267
+
268
+ ws.on('message', (data) => {
269
+ try {
270
+ const jsonData = JSON.parse(data.toString());
271
+ if (!jsonData || !jsonData[4]) return;
272
+
273
+ const diff = jsonData[4];
274
+
275
+ try {
276
+ // Try to extract streaming data
277
+ if (diff.e && diff.e[0] && diff.e[0][1] && diff.e[0][1].data) {
278
+ let chunk = diff.e[0][1].data;
279
+ // Check if chunk contains HTML
280
+ if (/<[^>]+>/.test(chunk)) {
281
+ chunk = formatHtml(chunk);
282
+ } else {
283
+ chunk = chunk.replace(/<br\/>/g, '\n');
284
+ }
285
+ chunks.push(chunk);
286
+ }
287
+ } catch {
288
+ // Try cache find for final response
289
+ const cache = cacheFind(diff);
290
+ if (cache) {
291
+ if (diff.response !== undefined) {
292
+ // Format if it contains HTML
293
+ let formattedCache = cache;
294
+ if (/<[^>]+>/.test(cache)) {
295
+ formattedCache = formatHtml(cache);
296
+ }
297
+ chunks.push(formattedCache);
298
+ isComplete = true;
299
+ ws.close();
300
+ }
301
+ }
302
+ }
303
+ } catch (error) {
304
+ console.debug('WebSocket message parse error:', error.message);
305
+ }
306
+ });
307
+
308
+ ws.on('close', () => {
309
+ resolveWs();
310
+ });
311
+
312
+ ws.on('error', (error) => {
313
+ rejectWs(error);
314
+ });
315
+
316
+ // Set timeout
317
+ const timeout = setTimeout(() => {
318
+ ws.close();
319
+ rejectWs(new Error('WebSocket connection timed out'));
320
+ }, 30000);
321
+
322
+ // Wait for WebSocket to complete
323
+ await wsPromise;
324
+ clearTimeout(timeout);
325
+
326
+ // Yield all collected chunks
327
+ for (const chunk of chunks) {
328
+ streamingText += chunk;
329
+ if (raw) {
330
+ yield { text: chunk };
331
+ } else {
332
+ yield new Response(chunk).toString();
333
+ }
334
+ }
335
+
336
+ // Cache the complete response
337
+ if (streamingText) {
338
+ resultsCache.set(getCacheKey(prompt, mode, detailLevel), {
339
+ results: streamingText,
340
+ timestamp: Date.now()
341
+ });
342
+ }
343
+
344
+ } catch (error) {
345
+ console.error('Error searching IAsk:', error.message);
346
+
347
+ if (error.response) {
348
+ const status = error.response.status;
349
+ const statusText = error.response.statusText;
350
+ throw new Error(`IAsk API error: ${status} ${statusText}`);
351
+ }
352
+
353
+ throw new Error(`Failed to search IAsk: ${error.message}`);
354
+ }
355
+ }
356
+
357
+ // If streaming is requested, return the generator
358
+ if (stream) {
359
+ return streamFunction();
360
+ }
361
+
362
+ // For non-streaming, collect all chunks and return as a single string
363
+ let fullResponse = '';
364
+
365
+ try {
366
+ for await (const chunk of streamFunction()) {
367
+ if (raw) {
368
+ fullResponse += chunk.text;
369
+ } else {
370
+ fullResponse += chunk;
371
+ }
372
+ }
373
+
374
+ return fullResponse;
375
+ } catch (error) {
376
+ console.error('Error in non-streaming IAsk search:', error.message);
377
+ throw error;
378
+ }
379
+ }
380
+
381
+ export { searchIAsk, VALID_MODES, VALID_DETAIL_LEVELS };
@@ -1,85 +0,0 @@
1
- import { searchFelo } from '../utils/search_felo.js';
2
-
3
- /**
4
- * Felo AI search tool definition
5
- */
6
- export const feloToolDefinition = {
7
- name: 'felo-search',
8
- title: 'Felo AI Advanced Search',
9
- description: 'Advanced AI-powered web search for technical intelligence. Retrieves up-to-date information including software releases, security advisories, migration guides, benchmarks, developer documentation, and community insights. Supports both standard and streaming responses.',
10
- inputSchema: {
11
- type: 'object',
12
- properties: {
13
- query: {
14
- type: 'string',
15
- description: 'A detailed search query or prompt describing the technical information needed. Supports natural language and keyword-based queries for precise results.'
16
- },
17
- stream: {
18
- type: 'boolean',
19
- description: 'Enable streaming mode to receive incremental, real-time search results as they are discovered. Useful for monitoring live updates or large result sets. Default is false (returns full result at once).',
20
- default: false
21
- }
22
- },
23
- required: ['query']
24
- },
25
- annotations: {
26
- readOnlyHint: true,
27
- openWorldHint: false
28
- }
29
- };
30
-
31
- /**
32
- * Felo AI search tool handler
33
- * @param {Object} params - The tool parameters
34
- * @returns {Promise<Object>} - The tool result
35
- */
36
- export async function feloToolHandler(params) {
37
- const { query, stream = false } = params;
38
- console.log(`Searching Felo AI for: "${query}" (stream: ${stream})`);
39
-
40
- try {
41
- if (stream) {
42
- // For streaming responses, we need to collect them and then return
43
- let fullResponse = '';
44
- const chunks = [];
45
-
46
- for await (const chunk of await searchFelo(query, true)) {
47
- chunks.push(chunk);
48
- fullResponse += chunk;
49
- }
50
-
51
- // Format the response
52
- return {
53
- content: [
54
- {
55
- type: 'text',
56
- text: fullResponse || 'No results found.'
57
- }
58
- ]
59
- };
60
- } else {
61
- // For non-streaming responses
62
- const response = await searchFelo(query, false);
63
-
64
- return {
65
- content: [
66
- {
67
- type: 'text',
68
- text: response || 'No results found.'
69
- }
70
- ]
71
- };
72
- }
73
- } catch (error) {
74
- console.error(`Error in Felo search: ${error.message}`);
75
- return {
76
- isError: true,
77
- content: [
78
- {
79
- type: 'text',
80
- text: `Error searching Felo: ${error.message}`
81
- }
82
- ]
83
- };
84
- }
85
- }
@@ -1,114 +0,0 @@
1
- import { fetchUrlContent } from '../utils/search.js';
2
-
3
- /**
4
- * Fetch URL tool definition
5
- */
6
- export const fetchUrlToolDefinition = {
7
- name: 'fetch-url',
8
- title: 'Fetch URL Content',
9
- description: 'Fetch and extract the main content from any URL, with customizable extraction options for text, links, and images',
10
- inputSchema: {
11
- type: 'object',
12
- properties: {
13
- url: {
14
- type: 'string',
15
- description: 'The URL to fetch content from (must be a valid HTTP/HTTPS URL)'
16
- },
17
- maxLength: {
18
- type: 'integer',
19
- description: 'Maximum length of content to return in characters (default: 10000)',
20
- default: 10000,
21
- minimum: 1000,
22
- maximum: 50000
23
- },
24
- extractMainContent: {
25
- type: 'boolean',
26
- description: 'Whether to attempt to extract main content only, filtering out navigation and ads (default: true)',
27
- default: true
28
- },
29
- includeLinks: {
30
- type: 'boolean',
31
- description: 'Whether to include link text in the extracted content (default: true)',
32
- default: true
33
- },
34
- includeImages: {
35
- type: 'boolean',
36
- description: 'Whether to include image alt text in the extracted content (default: true)',
37
- default: true
38
- },
39
- excludeTags: {
40
- type: 'array',
41
- description: 'HTML tags to exclude from extraction (default: script, style, etc.)',
42
- items: {
43
- type: 'string'
44
- }
45
- }
46
- },
47
- required: ['url']
48
- }
49
- };
50
-
51
- /**
52
- * Fetch URL tool handler
53
- * @param {Object} params - The tool parameters
54
- * @returns {Promise<Object>} - The tool result
55
- */
56
- export async function fetchUrlToolHandler(params) {
57
- const {
58
- url,
59
- maxLength = 10000,
60
- extractMainContent = true,
61
- includeLinks = true,
62
- includeImages = true,
63
- excludeTags = ['script', 'style', 'noscript', 'iframe', 'svg', 'nav', 'footer', 'header', 'aside']
64
- } = params;
65
-
66
- console.log(`Fetching content from URL: ${url} (maxLength: ${maxLength})`);
67
-
68
- try {
69
- // Fetch content with specified options
70
- const content = await fetchUrlContent(url, {
71
- extractMainContent,
72
- includeLinks,
73
- includeImages,
74
- excludeTags
75
- });
76
-
77
- // Truncate content if it's too long
78
- const truncatedContent = content.length > maxLength
79
- ? content.substring(0, maxLength) + '... [Content truncated due to length]'
80
- : content;
81
-
82
- // Add metadata about the extraction
83
- const metadata = `
84
- ---
85
- Extraction settings:
86
- - URL: ${url}
87
- - Main content extraction: ${extractMainContent ? 'Enabled' : 'Disabled'}
88
- - Links included: ${includeLinks ? 'Yes' : 'No'}
89
- - Images included: ${includeImages ? 'Yes (as alt text)' : 'No'}
90
- - Content length: ${content.length} characters${content.length > maxLength ? ` (truncated to ${maxLength})` : ''}
91
- ---
92
- `;
93
-
94
- return {
95
- content: [
96
- {
97
- type: 'text',
98
- text: truncatedContent + metadata
99
- }
100
- ]
101
- };
102
- } catch (error) {
103
- console.error(`Error fetching URL ${url}:`, error);
104
- return {
105
- isError: true,
106
- content: [
107
- {
108
- type: 'text',
109
- text: `Error fetching URL: ${error.message}`
110
- }
111
- ]
112
- };
113
- }
114
- }
@@ -1,54 +0,0 @@
1
- import { extractUrlMetadata } from '../utils/search.js';
2
-
3
- /**
4
- * URL metadata tool definition
5
- */
6
- export const metadataToolDefinition = {
7
- name: 'url-metadata',
8
- title: 'URL Metadata Extractor',
9
- description: 'Extract metadata from a URL including title, description, Open Graph data, and favicon information',
10
- inputSchema: {
11
- type: 'object',
12
- properties: {
13
- url: {
14
- type: 'string',
15
- description: 'The URL to extract metadata from (must be a valid HTTP/HTTPS URL)'
16
- }
17
- },
18
- required: ['url']
19
- }
20
- };
21
-
22
- /**
23
- * URL metadata tool handler
24
- * @param {Object} params - The tool parameters
25
- * @returns {Promise<Object>} - The tool result
26
- */
27
- export async function metadataToolHandler(params) {
28
- const { url } = params;
29
- console.log(`Extracting metadata from URL: ${url}`);
30
-
31
- const metadata = await extractUrlMetadata(url);
32
-
33
- // Format the metadata for display
34
- const formattedMetadata = `
35
- ## URL Metadata for ${url}
36
-
37
- **Title:** ${metadata.title}
38
-
39
- **Description:** ${metadata.description}
40
-
41
- **Image:** ${metadata.ogImage || 'None'}
42
-
43
- **Favicon:** ${metadata.favicon || 'None'}
44
- `.trim();
45
-
46
- return {
47
- content: [
48
- {
49
- type: 'text',
50
- text: formattedMetadata
51
- }
52
- ]
53
- };
54
- }
@@ -1,237 +0,0 @@
1
- import axios from 'axios';
2
- import { v4 as uuidv4 } from 'uuid';
3
- import https from 'https';
4
-
5
- // Rotating User Agents
6
- const USER_AGENTS = [
7
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
8
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/120.0.0.0',
9
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15',
10
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
11
- 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
12
- ];
13
-
14
- // Cache results to avoid repeated requests
15
- const resultsCache = new Map();
16
- const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
17
-
18
- // HTTPS agent configuration to handle certificate chain issues
19
- const httpsAgent = new https.Agent({
20
- rejectUnauthorized: true, // Keep security enabled
21
- keepAlive: true,
22
- timeout: 30000,
23
- // Provide fallback for certificate issues while maintaining security
24
- secureProtocol: 'TLSv1_2_method'
25
- });
26
-
27
- // Create a persistent axios instance to maintain session state
28
- const feloSession = axios.create({
29
- timeout: 30000,
30
- httpsAgent: httpsAgent,
31
- headers: {
32
- 'accept': '*/*',
33
- 'accept-encoding': 'gzip, deflate, br, zstd',
34
- 'accept-language': 'en-US,en;q=0.9,en-IN;q=0.8',
35
- 'content-type': 'application/json',
36
- 'dnt': '1',
37
- 'origin': 'https://felo.ai',
38
- 'referer': 'https://felo.ai/',
39
- 'sec-ch-ua': '"Not)A;Brand";v="99", "Microsoft Edge";v="127", "Chromium";v="127"',
40
- 'sec-ch-ua-mobile': '?0',
41
- 'sec-ch-ua-platform': '"Windows"',
42
- 'sec-fetch-dest': 'empty',
43
- 'sec-fetch-mode': 'cors',
44
- 'sec-fetch-site': 'same-site',
45
- 'user-agent': getRandomUserAgent()
46
- }
47
- });
48
-
49
- /**
50
- * Response class for Felo API responses
51
- */
52
- class Response {
53
- /**
54
- * Create a new Response
55
- * @param {string} text - The text content of the response
56
- */
57
- constructor(text) {
58
- this.text = text;
59
- }
60
-
61
- /**
62
- * String representation of the response
63
- * @returns {string} The text content
64
- */
65
- toString() {
66
- return this.text;
67
- }
68
- }
69
-
70
- /**
71
- * Get a random user agent from the list
72
- * @returns {string} A random user agent string
73
- */
74
- function getRandomUserAgent() {
75
- return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
76
- }
77
-
78
- /**
79
- * Generate a cache key for a search query
80
- * @param {string} query - The search query
81
- * @returns {string} The cache key
82
- */
83
- function getCacheKey(query) {
84
- return `felo-${query}`;
85
- }
86
-
87
- /**
88
- * Clear old entries from the cache
89
- */
90
- function clearOldCache() {
91
- const now = Date.now();
92
- for (const [key, value] of resultsCache.entries()) {
93
- if (now - value.timestamp > CACHE_DURATION) {
94
- resultsCache.delete(key);
95
- }
96
- }
97
- }
98
-
99
- /**
100
- * Search using the Felo AI API
101
- * @param {string} prompt - The search query or prompt
102
- * @param {boolean} stream - If true, yields response chunks as they arrive
103
- * @param {boolean} raw - If true, returns raw response dictionaries
104
- * @returns {Promise<string|AsyncGenerator<string>>} The search results
105
- */
106
- async function searchFelo(prompt, stream = false, raw = false) {
107
- // Clear old cache entries
108
- clearOldCache();
109
-
110
- // Check cache first if not streaming
111
- if (!stream) {
112
- const cacheKey = getCacheKey(prompt);
113
- const cachedResults = resultsCache.get(cacheKey);
114
-
115
- if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) {
116
- return cachedResults.results;
117
- }
118
- }
119
-
120
- // Create payload for Felo API with proper structure from reference
121
- const payload = {
122
- query: prompt,
123
- search_uuid: uuidv4().replace(/-/g, ''), // Remove dashes like in reference
124
- lang: "",
125
- agent_lang: "en",
126
- search_options: {
127
- langcode: "en-US",
128
- search_image: true,
129
- search_video: true
130
- },
131
- search_video: true,
132
- model: "",
133
- contexts_from: "google",
134
- auto_routing: true
135
- };
136
-
137
- // Update user agent for this request
138
- feloSession.defaults.headers['user-agent'] = getRandomUserAgent();
139
-
140
- // Define the streaming function
141
- async function* streamFunction() {
142
- try {
143
- const response = await feloSession.post('https://api.felo.ai/search/threads', payload, {
144
- responseType: 'stream'
145
- });
146
-
147
- // Check for HTTP errors
148
- if (response.status !== 200) {
149
- throw new Error(`Failed to generate response - (${response.status}, ${response.statusText}) - ${response.data}`);
150
- }
151
-
152
- let streamingText = '';
153
- let buffer = '';
154
-
155
- // Process the stream as it comes in
156
- for await (const chunk of response.data) {
157
- buffer += chunk.toString();
158
-
159
- const lines = buffer.split('\n');
160
- buffer = lines.pop() || ''; // Keep the last (potentially incomplete) line in the buffer
161
-
162
- for (const line of lines) {
163
- if (line.startsWith('data:')) {
164
- try {
165
- const dataStr = line.substring(5).trim();
166
- if (dataStr) {
167
- const data = JSON.parse(dataStr);
168
- if (data.type === 'answer' && 'text' in data.data) {
169
- const newText = data.data.text;
170
- if (newText.length > streamingText.length) {
171
- const delta = newText.substring(streamingText.length);
172
- streamingText = newText;
173
-
174
- if (raw) {
175
- yield { text: delta };
176
- } else {
177
- yield new Response(delta).toString();
178
- }
179
- }
180
- }
181
- }
182
- } catch (error) {
183
- // Ignore JSON parse errors and continue
184
- console.debug('JSON parse error:', error.message);
185
- }
186
- }
187
- }
188
- }
189
-
190
- // Cache the complete response
191
- if (streamingText) {
192
- resultsCache.set(getCacheKey(prompt), {
193
- results: streamingText,
194
- timestamp: Date.now()
195
- });
196
- }
197
-
198
- } catch (error) {
199
- console.error('Error searching Felo:', error.message);
200
-
201
- // Handle specific API errors
202
- if (error.response) {
203
- const status = error.response.status;
204
- const statusText = error.response.statusText;
205
- const data = error.response.data;
206
- throw new Error(`Felo API error: ${status} ${statusText} - ${data}`);
207
- }
208
-
209
- throw new Error(`Failed to search Felo: ${error.message}`);
210
- }
211
- }
212
-
213
- // If streaming is requested, return the generator
214
- if (stream) {
215
- return streamFunction();
216
- }
217
-
218
- // For non-streaming, collect all chunks and return as a single string
219
- let fullResponse = '';
220
-
221
- try {
222
- for await (const chunk of streamFunction()) {
223
- if (raw) {
224
- fullResponse += chunk.text;
225
- } else {
226
- fullResponse += chunk;
227
- }
228
- }
229
-
230
- return fullResponse;
231
- } catch (error) {
232
- console.error('Error in non-streaming Felo search:', error.message);
233
- throw error;
234
- }
235
- }
236
-
237
- export { searchFelo };