@phuetz/code-buddy 0.1.0 → 0.1.1
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/.codebuddy/skills/bundled/brave-search/SKILL.md +490 -0
- package/.codebuddy/skills/bundled/exa-search/SKILL.md +1122 -0
- package/.codebuddy/skills/bundled/perplexity/SKILL.md +748 -0
- package/.codebuddy/skills/bundled/playwright/SKILL.md +520 -0
- package/.codebuddy/skills/bundled/puppeteer/SKILL.md +708 -0
- package/.codebuddy/skills/bundled/web-fetch/SKILL.md +1003 -0
- package/README.md +56 -0
- package/dist/agent/agent-state.d.ts +3 -3
- package/dist/agent/agent-state.js +6 -6
- package/dist/agent/agent-state.js.map +1 -1
- package/dist/agent/base-agent.d.ts +4 -4
- package/dist/agent/base-agent.js +22 -9
- package/dist/agent/base-agent.js.map +1 -1
- package/dist/agent/cache-trace.d.ts +56 -0
- package/dist/agent/cache-trace.js +98 -0
- package/dist/agent/cache-trace.js.map +1 -0
- package/dist/agent/codebuddy-agent.js +3 -2
- package/dist/agent/codebuddy-agent.js.map +1 -1
- package/dist/agent/execution/agent-executor.d.ts +4 -4
- package/dist/agent/execution/agent-executor.js +41 -7
- package/dist/agent/execution/agent-executor.js.map +1 -1
- package/dist/agent/facades/agent-context-facade.js +1 -3
- package/dist/agent/facades/agent-context-facade.js.map +1 -1
- package/dist/agent/facades/message-history-manager.js +14 -12
- package/dist/agent/facades/message-history-manager.js.map +1 -1
- package/dist/agent/facades/session-facade.d.ts +3 -3
- package/dist/agent/facades/session-facade.js +6 -6
- package/dist/agent/facades/session-facade.js.map +1 -1
- package/dist/agent/history-repair.d.ts +37 -0
- package/dist/agent/history-repair.js +124 -0
- package/dist/agent/history-repair.js.map +1 -0
- package/dist/agent/specialized/archive-agent.d.ts +3 -0
- package/dist/agent/specialized/archive-agent.js +71 -31
- package/dist/agent/specialized/archive-agent.js.map +1 -1
- package/dist/agent/specialized/security-review/agent.js +19 -8
- package/dist/agent/specialized/security-review/agent.js.map +1 -1
- package/dist/agent/tool-executor.js +5 -0
- package/dist/agent/tool-executor.js.map +1 -1
- package/dist/agent/turn-diff-tracker.d.ts +79 -0
- package/dist/agent/turn-diff-tracker.js +195 -0
- package/dist/agent/turn-diff-tracker.js.map +1 -0
- package/dist/checkpoints/checkpoint-versioning.js +78 -20
- package/dist/checkpoints/checkpoint-versioning.js.map +1 -1
- package/dist/cli/config-loader.js +2 -4
- package/dist/cli/config-loader.js.map +1 -1
- package/dist/commands/handlers/fcs-handlers.js +1 -1
- package/dist/commands/handlers/fcs-handlers.js.map +1 -1
- package/dist/commands/handlers/memory-handlers.js +2 -1
- package/dist/commands/handlers/memory-handlers.js.map +1 -1
- package/dist/commands/handlers/worktree-handlers.js +11 -0
- package/dist/commands/handlers/worktree-handlers.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -0
- package/dist/commands/mcp.js +66 -7
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/pipeline.js +25 -13
- package/dist/commands/pipeline.js.map +1 -1
- package/dist/config/model-tools.d.ts +41 -0
- package/dist/config/model-tools.js +194 -0
- package/dist/config/model-tools.js.map +1 -0
- package/dist/context/context-manager-v2.d.ts +2 -1
- package/dist/context/context-manager-v2.js +34 -5
- package/dist/context/context-manager-v2.js.map +1 -1
- package/dist/daemon/daemon-manager.js +23 -19
- package/dist/daemon/daemon-manager.js.map +1 -1
- package/dist/database/database-manager.d.ts +4 -0
- package/dist/database/database-manager.js +16 -7
- package/dist/database/database-manager.js.map +1 -1
- package/dist/desktop-automation/nutjs-provider.js +89 -0
- package/dist/desktop-automation/nutjs-provider.js.map +1 -1
- package/dist/fcs/builtins.d.ts +2 -6
- package/dist/fcs/builtins.js +2 -568
- package/dist/fcs/builtins.js.map +1 -1
- package/dist/fcs/codebuddy-bindings.d.ts +3 -43
- package/dist/fcs/codebuddy-bindings.js +2 -606
- package/dist/fcs/codebuddy-bindings.js.map +1 -1
- package/dist/fcs/index.d.ts +2 -27
- package/dist/fcs/index.js +2 -53
- package/dist/fcs/index.js.map +1 -1
- package/dist/fcs/lexer.d.ts +2 -37
- package/dist/fcs/lexer.js +2 -459
- package/dist/fcs/lexer.js.map +1 -1
- package/dist/fcs/parser.d.ts +2 -68
- package/dist/fcs/parser.js +2 -893
- package/dist/fcs/parser.js.map +1 -1
- package/dist/fcs/runtime.d.ts +2 -59
- package/dist/fcs/runtime.js +2 -623
- package/dist/fcs/runtime.js.map +1 -1
- package/dist/fcs/script-registry.d.ts +3 -69
- package/dist/fcs/script-registry.js +2 -219
- package/dist/fcs/script-registry.js.map +1 -1
- package/dist/fcs/sync-bindings.d.ts +3 -101
- package/dist/fcs/sync-bindings.js +2 -410
- package/dist/fcs/sync-bindings.js.map +1 -1
- package/dist/fcs/types.d.ts +2 -285
- package/dist/fcs/types.js +2 -103
- package/dist/fcs/types.js.map +1 -1
- package/dist/hooks/use-input-handler.d.ts +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/input/voice-control.js +11 -5
- package/dist/input/voice-control.js.map +1 -1
- package/dist/integrations/json-rpc/server.js +5 -5
- package/dist/integrations/json-rpc/server.js.map +1 -1
- package/dist/integrations/mcp/mcp-server.js +1 -1
- package/dist/integrations/mcp/mcp-server.js.map +1 -1
- package/dist/mcp/client.js +2 -1
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/config.js +89 -5
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/mcp-client.js +65 -14
- package/dist/mcp/mcp-client.js.map +1 -1
- package/dist/mcp/transports.d.ts +0 -1
- package/dist/mcp/transports.js +1 -5
- package/dist/mcp/transports.js.map +1 -1
- package/dist/mcp/types.d.ts +2 -0
- package/dist/persistence/session-lock.d.ts +42 -0
- package/dist/persistence/session-lock.js +165 -0
- package/dist/persistence/session-lock.js.map +1 -0
- package/dist/persistence/session-store.d.ts +18 -3
- package/dist/persistence/session-store.js +90 -21
- package/dist/persistence/session-store.js.map +1 -1
- package/dist/plugins/isolated-plugin-runner.d.ts +6 -0
- package/dist/plugins/isolated-plugin-runner.js +19 -1
- package/dist/plugins/isolated-plugin-runner.js.map +1 -1
- package/dist/providers/local-llm-provider.js +21 -4
- package/dist/providers/local-llm-provider.js.map +1 -1
- package/dist/sandbox/docker-sandbox.js +7 -4
- package/dist/sandbox/docker-sandbox.js.map +1 -1
- package/dist/scripting/builtins.d.ts +8 -3
- package/dist/scripting/builtins.js +506 -355
- package/dist/scripting/builtins.js.map +1 -1
- package/dist/scripting/codebuddy-bindings.d.ts +47 -0
- package/dist/scripting/codebuddy-bindings.js +487 -0
- package/dist/scripting/codebuddy-bindings.js.map +1 -0
- package/dist/scripting/index.d.ts +33 -30
- package/dist/scripting/index.js +41 -36
- package/dist/scripting/index.js.map +1 -1
- package/dist/scripting/lexer.d.ts +31 -13
- package/dist/scripting/lexer.js +379 -292
- package/dist/scripting/lexer.js.map +1 -1
- package/dist/scripting/parser.d.ts +63 -44
- package/dist/scripting/parser.js +700 -473
- package/dist/scripting/parser.js.map +1 -1
- package/dist/scripting/runtime.d.ts +55 -24
- package/dist/scripting/runtime.js +600 -288
- package/dist/scripting/runtime.js.map +1 -1
- package/dist/scripting/script-registry.d.ts +54 -0
- package/dist/scripting/script-registry.js +202 -0
- package/dist/scripting/script-registry.js.map +1 -0
- package/dist/scripting/sync-bindings.d.ts +105 -0
- package/dist/scripting/sync-bindings.js +353 -0
- package/dist/scripting/sync-bindings.js.map +1 -0
- package/dist/scripting/types.d.ts +297 -199
- package/dist/scripting/types.js +86 -60
- package/dist/scripting/types.js.map +1 -1
- package/dist/search/usearch-index.js +42 -7
- package/dist/search/usearch-index.js.map +1 -1
- package/dist/security/bash-parser.d.ts +51 -0
- package/dist/security/bash-parser.js +327 -0
- package/dist/security/bash-parser.js.map +1 -0
- package/dist/security/skill-scanner.d.ts +36 -0
- package/dist/security/skill-scanner.js +149 -0
- package/dist/security/skill-scanner.js.map +1 -0
- package/dist/security/trust-folders.d.ts +1 -0
- package/dist/security/trust-folders.js +19 -1
- package/dist/security/trust-folders.js.map +1 -1
- package/dist/server/websocket/handler.js +15 -5
- package/dist/server/websocket/handler.js.map +1 -1
- package/dist/skills/eligibility.js +26 -4
- package/dist/skills/eligibility.js.map +1 -1
- package/dist/tasks/background-tasks.js +5 -1
- package/dist/tasks/background-tasks.js.map +1 -1
- package/dist/tools/apply-patch.d.ts +55 -0
- package/dist/tools/apply-patch.js +273 -0
- package/dist/tools/apply-patch.js.map +1 -0
- package/dist/tools/registry/bash-tools.js +6 -3
- package/dist/tools/registry/bash-tools.js.map +1 -1
- package/dist/tools/registry/misc-tools.js +1 -2
- package/dist/tools/registry/misc-tools.js.map +1 -1
- package/dist/tools/registry/search-tools.js +1 -1
- package/dist/tools/registry/search-tools.js.map +1 -1
- package/dist/tools/registry/text-editor-tools.js +1 -1
- package/dist/tools/registry/text-editor-tools.js.map +1 -1
- package/dist/tools/registry/todo-tools.js +37 -5
- package/dist/tools/registry/todo-tools.js.map +1 -1
- package/dist/tools/registry/tool-registry.js +5 -4
- package/dist/tools/registry/tool-registry.js.map +1 -1
- package/dist/tools/registry/web-tools.d.ts +1 -1
- package/dist/tools/registry/web-tools.js +28 -8
- package/dist/tools/registry/web-tools.js.map +1 -1
- package/dist/tools/text-editor.d.ts +1 -1
- package/dist/tools/text-editor.js +23 -5
- package/dist/tools/text-editor.js.map +1 -1
- package/dist/tools/web-search.d.ts +52 -37
- package/dist/tools/web-search.js +368 -163
- package/dist/tools/web-search.js.map +1 -1
- package/dist/ui/components/ChatInterface.d.ts +1 -1
- package/dist/utils/head-tail-truncation.d.ts +34 -0
- package/dist/utils/head-tail-truncation.js +98 -0
- package/dist/utils/head-tail-truncation.js.map +1 -0
- package/dist/utils/sanitize.d.ts +5 -0
- package/dist/utils/sanitize.js +19 -0
- package/dist/utils/sanitize.js.map +1 -1
- package/dist/utils/settings-manager.js +4 -4
- package/dist/utils/settings-manager.js.map +1 -1
- package/package.json +3 -1
package/dist/tools/web-search.js
CHANGED
|
@@ -1,81 +1,319 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { getErrorMessage } from '../types/index.js';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Constants
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const BRAVE_SEARCH_ENDPOINT = 'https://api.search.brave.com/res/v1/web/search';
|
|
8
|
+
const DEFAULT_PERPLEXITY_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
9
|
+
const PERPLEXITY_DIRECT_BASE_URL = 'https://api.perplexity.ai';
|
|
10
|
+
const DEFAULT_PERPLEXITY_MODEL = 'perplexity/sonar-pro';
|
|
11
|
+
const DEFAULT_SEARCH_COUNT = 5;
|
|
12
|
+
const MAX_SEARCH_COUNT = 10;
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
14
|
+
const BRAVE_FRESHNESS_SHORTCUTS = new Set(['pd', 'pw', 'pm', 'py']);
|
|
15
|
+
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Helpers
|
|
18
|
+
// ============================================================================
|
|
19
|
+
function normalizeFreshness(value) {
|
|
20
|
+
if (!value)
|
|
21
|
+
return undefined;
|
|
22
|
+
const trimmed = value.trim().toLowerCase();
|
|
23
|
+
if (!trimmed)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (BRAVE_FRESHNESS_SHORTCUTS.has(trimmed))
|
|
26
|
+
return trimmed;
|
|
27
|
+
const match = value.trim().match(BRAVE_FRESHNESS_RANGE);
|
|
28
|
+
if (match)
|
|
29
|
+
return `${match[1]}to${match[2]}`;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function resolveSiteName(url) {
|
|
33
|
+
if (!url)
|
|
34
|
+
return undefined;
|
|
35
|
+
try {
|
|
36
|
+
return new URL(url).hostname;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function resolvePerplexityBaseUrl(apiKey) {
|
|
43
|
+
if (!apiKey)
|
|
44
|
+
return DEFAULT_PERPLEXITY_BASE_URL;
|
|
45
|
+
if (apiKey.startsWith('pplx-'))
|
|
46
|
+
return PERPLEXITY_DIRECT_BASE_URL;
|
|
47
|
+
return DEFAULT_PERPLEXITY_BASE_URL; // OpenRouter key or unknown
|
|
48
|
+
}
|
|
4
49
|
/**
|
|
5
|
-
* Web Search Tool
|
|
50
|
+
* Web Search Tool — OpenClaw-aligned provider chain
|
|
6
51
|
*
|
|
7
|
-
*
|
|
52
|
+
* Provider resolution (auto mode):
|
|
53
|
+
* Brave MCP → Brave API → Perplexity → Serper → DuckDuckGo
|
|
54
|
+
*
|
|
55
|
+
* Supports country, search_lang, ui_lang, freshness (Brave), Perplexity AI search.
|
|
8
56
|
*/
|
|
9
57
|
export class WebSearchTool {
|
|
10
58
|
cache = new Map();
|
|
11
|
-
|
|
59
|
+
perplexityCache = new Map();
|
|
60
|
+
cacheTTL = 15 * 60 * 1000; // 15 minutes
|
|
61
|
+
// API keys resolved once at construction
|
|
12
62
|
serperApiKey;
|
|
63
|
+
braveApiKey;
|
|
64
|
+
perplexityApiKey;
|
|
13
65
|
constructor() {
|
|
14
66
|
this.serperApiKey = process.env.SERPER_API_KEY;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
67
|
+
this.braveApiKey = process.env.BRAVE_API_KEY;
|
|
68
|
+
this.perplexityApiKey = process.env.PERPLEXITY_API_KEY || process.env.OPENROUTER_API_KEY;
|
|
69
|
+
const providers = [];
|
|
70
|
+
if (this.braveApiKey)
|
|
71
|
+
providers.push('brave');
|
|
72
|
+
if (this.perplexityApiKey)
|
|
73
|
+
providers.push('perplexity');
|
|
74
|
+
if (this.serperApiKey)
|
|
75
|
+
providers.push('serper');
|
|
76
|
+
providers.push('duckduckgo');
|
|
77
|
+
logger.debug('Web search providers available', { providers });
|
|
18
78
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Main search entry point
|
|
81
|
+
// ============================================================================
|
|
22
82
|
async search(query, options = {}) {
|
|
23
|
-
const { maxResults =
|
|
83
|
+
const { maxResults = DEFAULT_SEARCH_COUNT } = options;
|
|
84
|
+
const count = Math.max(1, Math.min(MAX_SEARCH_COUNT, maxResults));
|
|
24
85
|
try {
|
|
25
|
-
// Check cache
|
|
26
|
-
const cacheKey =
|
|
86
|
+
// Check cache
|
|
87
|
+
const cacheKey = this.buildCacheKey(query, count, options);
|
|
27
88
|
const cached = this.cache.get(cacheKey);
|
|
28
89
|
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
|
|
29
|
-
return {
|
|
30
|
-
success: true,
|
|
31
|
-
output: this.formatResults(cached.results, query)
|
|
32
|
-
};
|
|
90
|
+
return { success: true, output: this.formatResults(cached.results, query) };
|
|
33
91
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
results = await this.searchSerper(query, maxResults);
|
|
92
|
+
// If forced provider
|
|
93
|
+
if (options.provider) {
|
|
94
|
+
return await this.searchWithProvider(options.provider, query, count, options, cacheKey);
|
|
38
95
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
96
|
+
// Auto fallback chain: Brave MCP → Brave API → Perplexity → Serper → DuckDuckGo
|
|
97
|
+
const chain = this.buildProviderChain();
|
|
98
|
+
let lastError;
|
|
99
|
+
for (const provider of chain) {
|
|
100
|
+
try {
|
|
101
|
+
return await this.searchWithProvider(provider, query, count, options, cacheKey);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
lastError = getErrorMessage(error);
|
|
105
|
+
logger.debug(`Search provider ${provider} failed, trying next`, { error: lastError });
|
|
106
|
+
}
|
|
48
107
|
}
|
|
49
|
-
|
|
50
|
-
this.cache.set(cacheKey, { results, timestamp: Date.now() });
|
|
51
|
-
return {
|
|
52
|
-
success: true,
|
|
53
|
-
output: this.formatResults(results, query)
|
|
54
|
-
};
|
|
108
|
+
return { success: false, error: `All search providers failed. Last error: ${lastError}` };
|
|
55
109
|
}
|
|
56
110
|
catch (error) {
|
|
111
|
+
return { success: false, error: `Web search failed: ${getErrorMessage(error)}` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Perplexity AI search — returns synthesized answer with citations
|
|
116
|
+
*/
|
|
117
|
+
async searchPerplexity(query, options = {}) {
|
|
118
|
+
if (!this.perplexityApiKey) {
|
|
57
119
|
return {
|
|
58
120
|
success: false,
|
|
59
|
-
error:
|
|
121
|
+
error: 'Perplexity search requires PERPLEXITY_API_KEY or OPENROUTER_API_KEY.',
|
|
60
122
|
};
|
|
61
123
|
}
|
|
124
|
+
const cacheKey = `perplexity:${query}`;
|
|
125
|
+
const cached = this.perplexityCache.get(cacheKey);
|
|
126
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
|
|
127
|
+
return { success: true, output: this.formatPerplexityResult(cached.result, query) };
|
|
128
|
+
}
|
|
129
|
+
const result = await this.runPerplexitySearch(query, options);
|
|
130
|
+
this.perplexityCache.set(cacheKey, { result, timestamp: Date.now() });
|
|
131
|
+
return { success: true, output: this.formatPerplexityResult(result, query) };
|
|
62
132
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Provider chain
|
|
135
|
+
// ============================================================================
|
|
136
|
+
buildProviderChain() {
|
|
137
|
+
const chain = [];
|
|
138
|
+
// Brave MCP is checked dynamically
|
|
139
|
+
chain.push('brave-mcp');
|
|
140
|
+
if (this.braveApiKey)
|
|
141
|
+
chain.push('brave');
|
|
142
|
+
if (this.perplexityApiKey)
|
|
143
|
+
chain.push('perplexity');
|
|
144
|
+
if (this.serperApiKey)
|
|
145
|
+
chain.push('serper');
|
|
146
|
+
chain.push('duckduckgo');
|
|
147
|
+
return chain;
|
|
148
|
+
}
|
|
149
|
+
async searchWithProvider(provider, query, count, options, cacheKey) {
|
|
150
|
+
let results;
|
|
151
|
+
switch (provider) {
|
|
152
|
+
case 'brave-mcp':
|
|
153
|
+
if (!(await this.isBraveMCPAvailable())) {
|
|
154
|
+
throw new Error('Brave MCP not connected');
|
|
155
|
+
}
|
|
156
|
+
results = await this.searchViaBraveMCP(query, count);
|
|
157
|
+
break;
|
|
158
|
+
case 'brave':
|
|
159
|
+
results = await this.searchBraveAPI(query, count, options);
|
|
160
|
+
break;
|
|
161
|
+
case 'perplexity': {
|
|
162
|
+
const pResult = await this.runPerplexitySearch(query, options);
|
|
163
|
+
// Convert perplexity to SearchResult[] for caching/formatting uniformity
|
|
164
|
+
results = pResult.citations.map((url, i) => ({
|
|
165
|
+
title: `Citation ${i + 1}`,
|
|
166
|
+
url,
|
|
167
|
+
snippet: i === 0 ? pResult.content.slice(0, 300) : '',
|
|
168
|
+
siteName: resolveSiteName(url),
|
|
169
|
+
}));
|
|
170
|
+
if (results.length === 0) {
|
|
171
|
+
results = [{ title: 'Perplexity Answer', url: '', snippet: pResult.content }];
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'serper':
|
|
176
|
+
results = await this.searchSerper(query, count);
|
|
177
|
+
break;
|
|
178
|
+
case 'duckduckgo':
|
|
179
|
+
results = await this.searchDuckDuckGo(query, count);
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
183
|
+
}
|
|
184
|
+
if (results.length === 0) {
|
|
185
|
+
return { success: true, output: `No results found for: "${query}"` };
|
|
186
|
+
}
|
|
187
|
+
this.cache.set(cacheKey, { results, timestamp: Date.now() });
|
|
188
|
+
return { success: true, output: this.formatResults(results, query) };
|
|
189
|
+
}
|
|
190
|
+
buildCacheKey(query, count, options) {
|
|
191
|
+
const parts = [
|
|
192
|
+
options.provider || 'auto',
|
|
193
|
+
query,
|
|
194
|
+
count,
|
|
195
|
+
options.country || 'default',
|
|
196
|
+
options.search_lang || 'default',
|
|
197
|
+
options.freshness || 'default',
|
|
198
|
+
];
|
|
199
|
+
return parts.join(':');
|
|
200
|
+
}
|
|
201
|
+
// ============================================================================
|
|
202
|
+
// Brave MCP
|
|
203
|
+
// ============================================================================
|
|
204
|
+
async isBraveMCPAvailable() {
|
|
205
|
+
try {
|
|
206
|
+
const { getMCPManager } = await import('../codebuddy/tools.js');
|
|
207
|
+
const manager = getMCPManager();
|
|
208
|
+
return manager.getServers().includes('brave-search');
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async searchViaBraveMCP(query, maxResults) {
|
|
215
|
+
const { getMCPManager } = await import('../codebuddy/tools.js');
|
|
216
|
+
const manager = getMCPManager();
|
|
217
|
+
const result = await manager.callTool('mcp__brave-search__brave_web_search', {
|
|
218
|
+
query,
|
|
219
|
+
count: maxResults,
|
|
220
|
+
});
|
|
221
|
+
const results = [];
|
|
222
|
+
if (result.content) {
|
|
223
|
+
for (const item of result.content) {
|
|
224
|
+
if (item.type === 'text' && typeof item.text === 'string') {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(item.text);
|
|
227
|
+
const webResults = parsed.web?.results || parsed.results || [];
|
|
228
|
+
for (const r of webResults.slice(0, maxResults)) {
|
|
229
|
+
results.push({
|
|
230
|
+
title: r.title || '',
|
|
231
|
+
url: r.url || '',
|
|
232
|
+
snippet: r.description || r.snippet || '',
|
|
233
|
+
siteName: resolveSiteName(r.url),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
results.push({ title: 'Brave Search Result', url: '', snippet: item.text });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
logger.debug('Brave MCP search completed', { query, resultCount: results.length });
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Brave Direct API (OpenClaw-aligned)
|
|
248
|
+
// ============================================================================
|
|
249
|
+
async searchBraveAPI(query, count, options) {
|
|
250
|
+
const url = new URL(BRAVE_SEARCH_ENDPOINT);
|
|
251
|
+
url.searchParams.set('q', query);
|
|
252
|
+
url.searchParams.set('count', String(count));
|
|
253
|
+
if (options.country)
|
|
254
|
+
url.searchParams.set('country', options.country);
|
|
255
|
+
if (options.search_lang)
|
|
256
|
+
url.searchParams.set('search_lang', options.search_lang);
|
|
257
|
+
if (options.ui_lang)
|
|
258
|
+
url.searchParams.set('ui_lang', options.ui_lang);
|
|
259
|
+
const freshness = normalizeFreshness(options.freshness);
|
|
260
|
+
if (freshness)
|
|
261
|
+
url.searchParams.set('freshness', freshness);
|
|
262
|
+
const response = await axios.get(url.toString(), {
|
|
263
|
+
headers: {
|
|
264
|
+
'Accept': 'application/json',
|
|
265
|
+
'X-Subscription-Token': this.braveApiKey,
|
|
266
|
+
},
|
|
267
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
268
|
+
});
|
|
269
|
+
const webResults = response.data.web?.results || [];
|
|
270
|
+
const results = webResults.map((entry) => ({
|
|
271
|
+
title: entry.title || '',
|
|
272
|
+
url: entry.url || '',
|
|
273
|
+
snippet: entry.description || '',
|
|
274
|
+
siteName: resolveSiteName(entry.url),
|
|
275
|
+
published: entry.age || undefined,
|
|
276
|
+
}));
|
|
277
|
+
logger.debug('Brave API search completed', { query, resultCount: results.length });
|
|
278
|
+
return results;
|
|
279
|
+
}
|
|
280
|
+
// ============================================================================
|
|
281
|
+
// Perplexity (OpenClaw-aligned: direct or via OpenRouter)
|
|
282
|
+
// ============================================================================
|
|
283
|
+
async runPerplexitySearch(query, _options) {
|
|
284
|
+
const apiKey = this.perplexityApiKey;
|
|
285
|
+
const baseUrl = resolvePerplexityBaseUrl(apiKey);
|
|
286
|
+
const model = process.env.PERPLEXITY_MODEL || DEFAULT_PERPLEXITY_MODEL;
|
|
287
|
+
const endpoint = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
288
|
+
const response = await axios.post(endpoint, {
|
|
289
|
+
model,
|
|
290
|
+
messages: [{ role: 'user', content: query }],
|
|
70
291
|
}, {
|
|
292
|
+
headers: {
|
|
293
|
+
'Content-Type': 'application/json',
|
|
294
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
295
|
+
'HTTP-Referer': 'https://github.com/code-buddy',
|
|
296
|
+
'X-Title': 'Code Buddy Web Search',
|
|
297
|
+
},
|
|
298
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
299
|
+
});
|
|
300
|
+
const content = response.data.choices?.[0]?.message?.content ?? 'No response';
|
|
301
|
+
const citations = response.data.citations ?? [];
|
|
302
|
+
logger.debug('Perplexity search completed', { query, model, citationCount: citations.length });
|
|
303
|
+
return { content, citations, model };
|
|
304
|
+
}
|
|
305
|
+
// ============================================================================
|
|
306
|
+
// Serper (Google Search)
|
|
307
|
+
// ============================================================================
|
|
308
|
+
async searchSerper(query, maxResults) {
|
|
309
|
+
const response = await axios.post('https://google.serper.dev/search', { q: query, num: maxResults }, {
|
|
71
310
|
headers: {
|
|
72
311
|
'X-API-KEY': this.serperApiKey,
|
|
73
312
|
'Content-Type': 'application/json',
|
|
74
313
|
},
|
|
75
|
-
timeout:
|
|
314
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
76
315
|
});
|
|
77
316
|
const results = [];
|
|
78
|
-
// Add answer box if present
|
|
79
317
|
if (response.data.answerBox?.answer) {
|
|
80
318
|
results.push({
|
|
81
319
|
title: response.data.answerBox.title || 'Answer',
|
|
@@ -83,7 +321,6 @@ export class WebSearchTool {
|
|
|
83
321
|
snippet: response.data.answerBox.answer,
|
|
84
322
|
});
|
|
85
323
|
}
|
|
86
|
-
// Add knowledge graph if present
|
|
87
324
|
if (response.data.knowledgeGraph?.description) {
|
|
88
325
|
results.push({
|
|
89
326
|
title: response.data.knowledgeGraph.title || 'Knowledge',
|
|
@@ -91,58 +328,22 @@ export class WebSearchTool {
|
|
|
91
328
|
snippet: response.data.knowledgeGraph.description,
|
|
92
329
|
});
|
|
93
330
|
}
|
|
94
|
-
// Add organic results
|
|
95
331
|
if (response.data.organic) {
|
|
96
332
|
for (const result of response.data.organic.slice(0, maxResults)) {
|
|
97
333
|
results.push({
|
|
98
334
|
title: result.title,
|
|
99
335
|
url: result.link,
|
|
100
336
|
snippet: result.snippet,
|
|
337
|
+
siteName: resolveSiteName(result.link),
|
|
101
338
|
});
|
|
102
339
|
}
|
|
103
340
|
}
|
|
104
|
-
logger.debug('Serper search completed', {
|
|
105
|
-
query,
|
|
106
|
-
resultCount: results.length,
|
|
107
|
-
});
|
|
341
|
+
logger.debug('Serper search completed', { query, resultCount: results.length });
|
|
108
342
|
return results;
|
|
109
343
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
async fetchPage(url, _prompt) {
|
|
114
|
-
try {
|
|
115
|
-
const response = await axios.get(url, {
|
|
116
|
-
headers: {
|
|
117
|
-
'User-Agent': 'Mozilla/5.0 (compatible; CodeBuddyCLI/1.0; +https://github.com/code-buddy)',
|
|
118
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
119
|
-
},
|
|
120
|
-
timeout: 10000,
|
|
121
|
-
maxRedirects: 5
|
|
122
|
-
});
|
|
123
|
-
const html = response.data;
|
|
124
|
-
const text = this.extractTextFromHtml(html);
|
|
125
|
-
// Truncate if too long
|
|
126
|
-
const maxLength = 8000;
|
|
127
|
-
const truncatedText = text.length > maxLength
|
|
128
|
-
? text.substring(0, maxLength) + '\n\n[Content truncated...]'
|
|
129
|
-
: text;
|
|
130
|
-
return {
|
|
131
|
-
success: true,
|
|
132
|
-
output: `Content from ${url}:\n\n${truncatedText}`,
|
|
133
|
-
data: { url, contentLength: text.length }
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
catch (error) {
|
|
137
|
-
return {
|
|
138
|
-
success: false,
|
|
139
|
-
error: `Failed to fetch page: ${getErrorMessage(error)}`
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Search using DuckDuckGo HTML
|
|
145
|
-
*/
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// DuckDuckGo (ultimate fallback, no API key needed)
|
|
346
|
+
// ============================================================================
|
|
146
347
|
async searchDuckDuckGo(query, maxResults) {
|
|
147
348
|
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
148
349
|
const response = await axios.get(searchUrl, {
|
|
@@ -151,12 +352,10 @@ export class WebSearchTool {
|
|
|
151
352
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
152
353
|
'Accept-Language': 'en-US,en;q=0.5',
|
|
153
354
|
},
|
|
154
|
-
timeout:
|
|
355
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
155
356
|
});
|
|
156
357
|
const html = response.data;
|
|
157
358
|
const results = [];
|
|
158
|
-
// Parse DuckDuckGo HTML results
|
|
159
|
-
// Looking for result divs with class "result"
|
|
160
359
|
const resultRegex = /<div[^>]*class="[^"]*result[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi;
|
|
161
360
|
const titleRegex = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/i;
|
|
162
361
|
const snippetRegex = /<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/i;
|
|
@@ -167,53 +366,95 @@ export class WebSearchTool {
|
|
|
167
366
|
const snippetMatch = snippetRegex.exec(resultHtml);
|
|
168
367
|
if (titleMatch) {
|
|
169
368
|
let url = titleMatch[1];
|
|
170
|
-
// DuckDuckGo wraps URLs, need to extract actual URL
|
|
171
369
|
if (url.includes('uddg=')) {
|
|
172
370
|
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
173
|
-
if (uddgMatch)
|
|
371
|
+
if (uddgMatch)
|
|
174
372
|
url = decodeURIComponent(uddgMatch[1]);
|
|
175
|
-
}
|
|
176
373
|
}
|
|
177
374
|
results.push({
|
|
178
375
|
title: this.decodeHtmlEntities(titleMatch[2].trim()),
|
|
179
|
-
url
|
|
376
|
+
url,
|
|
180
377
|
snippet: snippetMatch
|
|
181
378
|
? this.decodeHtmlEntities(this.stripHtml(snippetMatch[1]).trim())
|
|
182
|
-
: ''
|
|
379
|
+
: '',
|
|
380
|
+
siteName: resolveSiteName(url),
|
|
183
381
|
});
|
|
184
382
|
}
|
|
185
383
|
}
|
|
186
|
-
// Fallback
|
|
384
|
+
// Fallback parsing
|
|
187
385
|
if (results.length === 0) {
|
|
188
386
|
const linkRegex = /<a[^>]*class="[^"]*result__url[^"]*"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi;
|
|
189
387
|
while ((match = linkRegex.exec(html)) !== null && results.length < maxResults) {
|
|
190
388
|
let url = match[1];
|
|
191
389
|
if (url.includes('uddg=')) {
|
|
192
390
|
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
193
|
-
if (uddgMatch)
|
|
391
|
+
if (uddgMatch)
|
|
194
392
|
url = decodeURIComponent(uddgMatch[1]);
|
|
195
|
-
}
|
|
196
393
|
}
|
|
197
394
|
results.push({
|
|
198
395
|
title: this.decodeHtmlEntities(match[2].trim()) || url,
|
|
199
|
-
url
|
|
200
|
-
snippet: ''
|
|
396
|
+
url,
|
|
397
|
+
snippet: '',
|
|
398
|
+
siteName: resolveSiteName(url),
|
|
201
399
|
});
|
|
202
400
|
}
|
|
203
401
|
}
|
|
204
402
|
return results;
|
|
205
403
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// Fetch page
|
|
406
|
+
// ============================================================================
|
|
407
|
+
async fetchPage(url, _prompt) {
|
|
408
|
+
try {
|
|
409
|
+
const response = await axios.get(url, {
|
|
410
|
+
headers: {
|
|
411
|
+
'User-Agent': 'Mozilla/5.0 (compatible; CodeBuddyCLI/1.0; +https://github.com/code-buddy)',
|
|
412
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
413
|
+
},
|
|
414
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
415
|
+
maxRedirects: 5,
|
|
416
|
+
});
|
|
417
|
+
const html = response.data;
|
|
418
|
+
const text = this.extractTextFromHtml(html);
|
|
419
|
+
const maxLength = 8000;
|
|
420
|
+
const truncatedText = text.length > maxLength
|
|
421
|
+
? text.substring(0, maxLength) + '\n\n[Content truncated...]'
|
|
422
|
+
: text;
|
|
423
|
+
return {
|
|
424
|
+
success: true,
|
|
425
|
+
output: `Content from ${url}:\n\n${truncatedText}`,
|
|
426
|
+
data: { url, contentLength: text.length },
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
return { success: false, error: `Failed to fetch page: ${getErrorMessage(error)}` };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// ============================================================================
|
|
434
|
+
// Formatting
|
|
435
|
+
// ============================================================================
|
|
436
|
+
formatPerplexityResult(result, query) {
|
|
437
|
+
const lines = [];
|
|
438
|
+
lines.push(`\nPerplexity Search: "${query}" (${result.model})`);
|
|
439
|
+
lines.push('='.repeat(50));
|
|
440
|
+
lines.push('');
|
|
441
|
+
lines.push(result.content);
|
|
442
|
+
if (result.citations.length > 0) {
|
|
443
|
+
lines.push('');
|
|
444
|
+
lines.push('Citations:');
|
|
445
|
+
result.citations.forEach((url, i) => {
|
|
446
|
+
lines.push(` ${i + 1}. ${url}`);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
lines.push('-'.repeat(50));
|
|
451
|
+
return lines.join('\n');
|
|
452
|
+
}
|
|
209
453
|
isWeatherQuery(query) {
|
|
210
454
|
const weatherKeywords = ['météo', 'meteo', 'weather', 'température', 'temperature', 'forecast', 'prévisions'];
|
|
211
455
|
const q = query.toLowerCase();
|
|
212
456
|
return weatherKeywords.some(kw => q.includes(kw));
|
|
213
457
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Get weather emoji based on condition
|
|
216
|
-
*/
|
|
217
458
|
getWeatherEmoji(text) {
|
|
218
459
|
const t = text.toLowerCase();
|
|
219
460
|
if (t.includes('soleil') || t.includes('sunny') || t.includes('ensoleillé'))
|
|
@@ -234,33 +475,22 @@ export class WebSearchTool {
|
|
|
234
475
|
return '⛅';
|
|
235
476
|
return '🌡️';
|
|
236
477
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Format weather results nicely
|
|
239
|
-
*/
|
|
240
478
|
formatWeatherResults(results, query) {
|
|
241
479
|
const lines = [];
|
|
242
|
-
// Header with location
|
|
243
480
|
const location = query.replace(/météo|meteo|weather/gi, '').trim();
|
|
244
481
|
lines.push(`\n🌍 Météo ${location || 'actuelle'}`);
|
|
245
482
|
lines.push('═'.repeat(40));
|
|
246
483
|
lines.push('');
|
|
247
|
-
// Extract and format weather info
|
|
248
484
|
for (const result of results.slice(0, 4)) {
|
|
249
485
|
const emoji = this.getWeatherEmoji(result.snippet);
|
|
250
486
|
if (!result.url && result.snippet) {
|
|
251
|
-
// Answer box (quick answer)
|
|
252
487
|
lines.push(`${emoji} ${result.title}: ${result.snippet}`);
|
|
253
488
|
lines.push('');
|
|
254
489
|
}
|
|
255
490
|
else if (result.url) {
|
|
256
|
-
// Regular result
|
|
257
491
|
lines.push(`${emoji} **${result.title}**`);
|
|
258
492
|
if (result.snippet) {
|
|
259
|
-
|
|
260
|
-
const cleanSnippet = result.snippet
|
|
261
|
-
.replace(/·/g, '|')
|
|
262
|
-
.replace(/\s+/g, ' ')
|
|
263
|
-
.trim();
|
|
493
|
+
const cleanSnippet = result.snippet.replace(/·/g, '|').replace(/\s+/g, ' ').trim();
|
|
264
494
|
lines.push(` ${cleanSnippet}`);
|
|
265
495
|
}
|
|
266
496
|
lines.push(` 🔗 ${result.url}`);
|
|
@@ -270,15 +500,10 @@ export class WebSearchTool {
|
|
|
270
500
|
lines.push('─'.repeat(40));
|
|
271
501
|
return lines.join('\n');
|
|
272
502
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Format search results for display
|
|
275
|
-
*/
|
|
276
503
|
formatResults(results, query) {
|
|
277
|
-
// Use special formatting for weather queries
|
|
278
504
|
if (this.isWeatherQuery(query)) {
|
|
279
505
|
return this.formatWeatherResults(results, query);
|
|
280
506
|
}
|
|
281
|
-
// Standard formatting for other queries
|
|
282
507
|
const lines = [];
|
|
283
508
|
lines.push(`\n🔍 Résultats pour: "${query}"`);
|
|
284
509
|
lines.push('═'.repeat(50));
|
|
@@ -287,36 +512,33 @@ export class WebSearchTool {
|
|
|
287
512
|
const result = results[i];
|
|
288
513
|
const num = `${i + 1}.`;
|
|
289
514
|
if (!result.url && result.snippet) {
|
|
290
|
-
// Answer box
|
|
291
515
|
lines.push(`📌 ${result.title}`);
|
|
292
516
|
lines.push(` ${result.snippet}`);
|
|
293
517
|
}
|
|
294
518
|
else {
|
|
295
519
|
lines.push(`${num} **${result.title}**`);
|
|
296
|
-
if (result.snippet)
|
|
520
|
+
if (result.snippet)
|
|
297
521
|
lines.push(` ${result.snippet}`);
|
|
298
|
-
|
|
299
|
-
if (result.url) {
|
|
522
|
+
if (result.url)
|
|
300
523
|
lines.push(` 🔗 ${result.url}`);
|
|
301
|
-
|
|
524
|
+
if (result.published)
|
|
525
|
+
lines.push(` 📅 ${result.published}`);
|
|
302
526
|
}
|
|
303
527
|
lines.push('');
|
|
304
528
|
}
|
|
305
529
|
lines.push('─'.repeat(50));
|
|
306
530
|
return lines.join('\n');
|
|
307
531
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// HTML helpers
|
|
534
|
+
// ============================================================================
|
|
311
535
|
extractTextFromHtml(html) {
|
|
312
|
-
// Remove script and style tags
|
|
313
536
|
let text = html
|
|
314
537
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
315
538
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
316
539
|
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
|
|
317
540
|
.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
|
|
318
541
|
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '');
|
|
319
|
-
// Convert common elements to newlines
|
|
320
542
|
text = text
|
|
321
543
|
.replace(/<\/p>/gi, '\n\n')
|
|
322
544
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
@@ -324,56 +546,39 @@ export class WebSearchTool {
|
|
|
324
546
|
.replace(/<\/h[1-6]>/gi, '\n\n')
|
|
325
547
|
.replace(/<\/li>/gi, '\n')
|
|
326
548
|
.replace(/<li[^>]*>/gi, '• ');
|
|
327
|
-
// Strip remaining HTML tags
|
|
328
549
|
text = this.stripHtml(text);
|
|
329
|
-
// Decode HTML entities
|
|
330
550
|
text = this.decodeHtmlEntities(text);
|
|
331
|
-
|
|
332
|
-
text = text
|
|
333
|
-
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
|
334
|
-
.replace(/[ \t]+/g, ' ')
|
|
335
|
-
.trim();
|
|
551
|
+
text = text.replace(/\n\s*\n\s*\n/g, '\n\n').replace(/[ \t]+/g, ' ').trim();
|
|
336
552
|
return text;
|
|
337
553
|
}
|
|
338
|
-
/**
|
|
339
|
-
* Strip HTML tags from text
|
|
340
|
-
*/
|
|
341
554
|
stripHtml(html) {
|
|
342
555
|
return html.replace(/<[^>]*>/g, '');
|
|
343
556
|
}
|
|
344
|
-
/**
|
|
345
|
-
* Decode HTML entities
|
|
346
|
-
*/
|
|
347
557
|
decodeHtmlEntities(text) {
|
|
348
558
|
const entities = {
|
|
349
|
-
'&': '&',
|
|
350
|
-
'&
|
|
351
|
-
'&
|
|
352
|
-
'"': '"',
|
|
353
|
-
''': "'",
|
|
354
|
-
''': "'",
|
|
355
|
-
' ': ' ',
|
|
356
|
-
'–': '–',
|
|
357
|
-
'—': '—',
|
|
358
|
-
'…': '…',
|
|
359
|
-
'©': '©',
|
|
360
|
-
'®': '®',
|
|
361
|
-
'™': '™',
|
|
559
|
+
'&': '&', '<': '<', '>': '>', '"': '"',
|
|
560
|
+
''': "'", ''': "'", ' ': ' ', '–': '–',
|
|
561
|
+
'—': '—', '…': '…', '©': '©', '®': '®', '™': '™',
|
|
362
562
|
};
|
|
363
563
|
let result = text;
|
|
364
564
|
for (const [entity, char] of Object.entries(entities)) {
|
|
365
565
|
result = result.replace(new RegExp(entity, 'gi'), char);
|
|
366
566
|
}
|
|
367
|
-
// Handle numeric entities
|
|
368
567
|
result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
|
|
369
568
|
result = result.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
370
569
|
return result;
|
|
371
570
|
}
|
|
372
|
-
/**
|
|
373
|
-
* Clear the cache
|
|
374
|
-
*/
|
|
375
571
|
clearCache() {
|
|
376
572
|
this.cache.clear();
|
|
573
|
+
this.perplexityCache.clear();
|
|
377
574
|
}
|
|
378
575
|
}
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// Exported helpers for testing
|
|
578
|
+
// ============================================================================
|
|
579
|
+
export const __testing = {
|
|
580
|
+
normalizeFreshness,
|
|
581
|
+
resolveSiteName,
|
|
582
|
+
resolvePerplexityBaseUrl,
|
|
583
|
+
};
|
|
379
584
|
//# sourceMappingURL=web-search.js.map
|