capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
210 lines • 7.64 kB
JavaScript
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(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/<\/?b>/g, '');
}
}
//# sourceMappingURL=google-search.js.map