UNPKG

capsule-ai-cli

Version:

The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing

210 lines 7.64 kB
import { BaseTool } from '../base.js'; import { configManager } from '../../core/config.js'; export class GoogleSearchTool extends BaseTool { name = 'google_search'; displayName = '🔎 Google Search'; description = 'Search Google and get web results - requires API key'; category = 'web'; icon = '🔎'; parameters = [ { name: 'query', type: 'string', description: 'The search query', required: true }, { name: 'numResults', type: 'number', description: 'Number of results to return', required: false, default: 5 }, { name: 'region', type: 'string', description: 'Region code (e.g., "us", "uk", "jp")', required: false, default: 'us' }, { name: 'language', type: 'string', description: 'Language code (e.g., "en", "es", "ja")', required: false, default: 'en' }, { name: 'safeSearch', type: 'boolean', description: 'Enable safe search filtering', required: false, default: true }, { name: 'timeRange', type: 'string', description: 'Time range for results', required: false, enum: ['day', 'week', 'month', 'year'] } ]; permissions = { network: true }; ui = { showProgress: true, collapsible: true, dangerous: false }; async run(params, _context) { const { query, numResults = 5, region = 'us', language = 'en', safeSearch = true, timeRange } = params; this.reportProgress(_context, `Searching Google for "${query}"...`); try { const config = configManager.getConfig(); const apiKey = config.services?.googleSearch?.apiKey || process.env.GOOGLE_API_KEY || process.env.GOOGLE_SEARCH_API_KEY; const searchEngineId = config.services?.googleSearch?.searchEngineId || process.env.GOOGLE_SEARCH_ENGINE_ID; if (apiKey && searchEngineId) { return await this.searchWithGoogleAPI(query, numResults, apiKey, searchEngineId, { region, language, safeSearch, timeRange }, _context); } const serpApiKey = config.services?.serpApi?.apiKey || process.env.SERP_API_KEY; if (serpApiKey) { return await this.searchWithSerpAPI(query, numResults, serpApiKey, { region, language, safeSearch, timeRange }, _context); } this.reportProgress(_context, 'No API keys found, using basic web search...'); return await this.searchWithScraping(query, numResults, _context); } catch (error) { throw new Error(`Google search failed: ${error.message}`); } } async searchWithGoogleAPI(query, numResults, apiKey, searchEngineId, options, _context) { const baseUrl = 'https://www.googleapis.com/customsearch/v1'; const params = new URLSearchParams({ q: query, key: apiKey, cx: searchEngineId, num: numResults.toString(), gl: options.region, hl: options.language, safe: options.safeSearch ? 'active' : 'off' }); if (options.timeRange) { const dateRestrict = { 'day': 'd1', 'week': 'w1', 'month': 'm1', 'year': 'y1' }; params.append('dateRestrict', dateRestrict[options.timeRange]); } const response = await fetch(`${baseUrl}?${params}`); if (!response.ok) { const error = await response.text(); throw new Error(`Google API error: ${response.status} - ${error}`); } const data = await response.json(); const results = (data.items || []).map((item, index) => ({ title: item.title, url: item.link, snippet: item.snippet, position: index + 1 })); return this.formatResults(query, results, 'Google Custom Search API'); } async searchWithSerpAPI(query, numResults, apiKey, options, _context) { const baseUrl = 'https://serpapi.com/search.json'; const params = new URLSearchParams({ q: query, api_key: apiKey, num: numResults.toString(), gl: options.region, hl: options.language, safe: options.safeSearch ? 'active' : 'off', engine: 'google' }); if (options.timeRange) { const tbs = { 'day': 'qdr:d', 'week': 'qdr:w', 'month': 'qdr:m', 'year': 'qdr:y' }; params.append('tbs', tbs[options.timeRange]); } const response = await fetch(`${baseUrl}?${params}`); if (!response.ok) { throw new Error(`SerpAPI error: ${response.status}`); } const data = await response.json(); const results = (data.organic_results || []) .slice(0, numResults) .map((item, index) => ({ title: item.title, url: item.link, snippet: item.snippet, position: item.position || index + 1 })); return this.formatResults(query, results, 'SerpAPI'); } async searchWithScraping(query, numResults, _context) { const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`; const response = await fetch(searchUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; CapsuleCLI/1.0)' } }); if (!response.ok) { throw new Error(`Search failed: ${response.status}`); } const html = await response.text(); const results = []; const resultRegex = /<a rel="nofollow" class="result__a" href="([^"]+)">([^<]+)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([^<]+)</g; let match; let position = 1; while ((match = resultRegex.exec(html)) !== null && results.length < numResults) { results.push({ url: match[1], title: this.decodeHtml(match[2]), snippet: this.decodeHtml(match[3]), position: position++ }); } return this.formatResults(query, results, 'DuckDuckGo (fallback)'); } formatResults(query, results, source) { const summary = `🔎 Google Search Results for "${query}" (via ${source})\n` + `Found ${results.length} results\n\n` + results.map(r => `${r.position}. ${r.title}\n` + ` ${r.url}\n` + ` ${r.snippet}\n`).join('\n'); return { query, source, resultCount: results.length, results, summary, display: summary }; } decodeHtml(html) { return html .replace(/&amp;/g, '&') .replace(/&lt;/g, '<') .replace(/&gt;/g, '>') .replace(/&quot;/g, '"') .replace(/&#39;/g, "'") .replace(/<\/?b>/g, ''); } } //# sourceMappingURL=google-search.js.map