@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.
Files changed (206) hide show
  1. package/.codebuddy/skills/bundled/brave-search/SKILL.md +490 -0
  2. package/.codebuddy/skills/bundled/exa-search/SKILL.md +1122 -0
  3. package/.codebuddy/skills/bundled/perplexity/SKILL.md +748 -0
  4. package/.codebuddy/skills/bundled/playwright/SKILL.md +520 -0
  5. package/.codebuddy/skills/bundled/puppeteer/SKILL.md +708 -0
  6. package/.codebuddy/skills/bundled/web-fetch/SKILL.md +1003 -0
  7. package/README.md +56 -0
  8. package/dist/agent/agent-state.d.ts +3 -3
  9. package/dist/agent/agent-state.js +6 -6
  10. package/dist/agent/agent-state.js.map +1 -1
  11. package/dist/agent/base-agent.d.ts +4 -4
  12. package/dist/agent/base-agent.js +22 -9
  13. package/dist/agent/base-agent.js.map +1 -1
  14. package/dist/agent/cache-trace.d.ts +56 -0
  15. package/dist/agent/cache-trace.js +98 -0
  16. package/dist/agent/cache-trace.js.map +1 -0
  17. package/dist/agent/codebuddy-agent.js +3 -2
  18. package/dist/agent/codebuddy-agent.js.map +1 -1
  19. package/dist/agent/execution/agent-executor.d.ts +4 -4
  20. package/dist/agent/execution/agent-executor.js +41 -7
  21. package/dist/agent/execution/agent-executor.js.map +1 -1
  22. package/dist/agent/facades/agent-context-facade.js +1 -3
  23. package/dist/agent/facades/agent-context-facade.js.map +1 -1
  24. package/dist/agent/facades/message-history-manager.js +14 -12
  25. package/dist/agent/facades/message-history-manager.js.map +1 -1
  26. package/dist/agent/facades/session-facade.d.ts +3 -3
  27. package/dist/agent/facades/session-facade.js +6 -6
  28. package/dist/agent/facades/session-facade.js.map +1 -1
  29. package/dist/agent/history-repair.d.ts +37 -0
  30. package/dist/agent/history-repair.js +124 -0
  31. package/dist/agent/history-repair.js.map +1 -0
  32. package/dist/agent/specialized/archive-agent.d.ts +3 -0
  33. package/dist/agent/specialized/archive-agent.js +71 -31
  34. package/dist/agent/specialized/archive-agent.js.map +1 -1
  35. package/dist/agent/specialized/security-review/agent.js +19 -8
  36. package/dist/agent/specialized/security-review/agent.js.map +1 -1
  37. package/dist/agent/tool-executor.js +5 -0
  38. package/dist/agent/tool-executor.js.map +1 -1
  39. package/dist/agent/turn-diff-tracker.d.ts +79 -0
  40. package/dist/agent/turn-diff-tracker.js +195 -0
  41. package/dist/agent/turn-diff-tracker.js.map +1 -0
  42. package/dist/checkpoints/checkpoint-versioning.js +78 -20
  43. package/dist/checkpoints/checkpoint-versioning.js.map +1 -1
  44. package/dist/cli/config-loader.js +2 -4
  45. package/dist/cli/config-loader.js.map +1 -1
  46. package/dist/commands/handlers/fcs-handlers.js +1 -1
  47. package/dist/commands/handlers/fcs-handlers.js.map +1 -1
  48. package/dist/commands/handlers/memory-handlers.js +2 -1
  49. package/dist/commands/handlers/memory-handlers.js.map +1 -1
  50. package/dist/commands/handlers/worktree-handlers.js +11 -0
  51. package/dist/commands/handlers/worktree-handlers.js.map +1 -1
  52. package/dist/commands/mcp.d.ts +1 -0
  53. package/dist/commands/mcp.js +66 -7
  54. package/dist/commands/mcp.js.map +1 -1
  55. package/dist/commands/pipeline.js +25 -13
  56. package/dist/commands/pipeline.js.map +1 -1
  57. package/dist/config/model-tools.d.ts +41 -0
  58. package/dist/config/model-tools.js +194 -0
  59. package/dist/config/model-tools.js.map +1 -0
  60. package/dist/context/context-manager-v2.d.ts +2 -1
  61. package/dist/context/context-manager-v2.js +34 -5
  62. package/dist/context/context-manager-v2.js.map +1 -1
  63. package/dist/daemon/daemon-manager.js +23 -19
  64. package/dist/daemon/daemon-manager.js.map +1 -1
  65. package/dist/database/database-manager.d.ts +4 -0
  66. package/dist/database/database-manager.js +16 -7
  67. package/dist/database/database-manager.js.map +1 -1
  68. package/dist/desktop-automation/nutjs-provider.js +89 -0
  69. package/dist/desktop-automation/nutjs-provider.js.map +1 -1
  70. package/dist/fcs/builtins.d.ts +2 -6
  71. package/dist/fcs/builtins.js +2 -568
  72. package/dist/fcs/builtins.js.map +1 -1
  73. package/dist/fcs/codebuddy-bindings.d.ts +3 -43
  74. package/dist/fcs/codebuddy-bindings.js +2 -606
  75. package/dist/fcs/codebuddy-bindings.js.map +1 -1
  76. package/dist/fcs/index.d.ts +2 -27
  77. package/dist/fcs/index.js +2 -53
  78. package/dist/fcs/index.js.map +1 -1
  79. package/dist/fcs/lexer.d.ts +2 -37
  80. package/dist/fcs/lexer.js +2 -459
  81. package/dist/fcs/lexer.js.map +1 -1
  82. package/dist/fcs/parser.d.ts +2 -68
  83. package/dist/fcs/parser.js +2 -893
  84. package/dist/fcs/parser.js.map +1 -1
  85. package/dist/fcs/runtime.d.ts +2 -59
  86. package/dist/fcs/runtime.js +2 -623
  87. package/dist/fcs/runtime.js.map +1 -1
  88. package/dist/fcs/script-registry.d.ts +3 -69
  89. package/dist/fcs/script-registry.js +2 -219
  90. package/dist/fcs/script-registry.js.map +1 -1
  91. package/dist/fcs/sync-bindings.d.ts +3 -101
  92. package/dist/fcs/sync-bindings.js +2 -410
  93. package/dist/fcs/sync-bindings.js.map +1 -1
  94. package/dist/fcs/types.d.ts +2 -285
  95. package/dist/fcs/types.js +2 -103
  96. package/dist/fcs/types.js.map +1 -1
  97. package/dist/hooks/use-input-handler.d.ts +1 -1
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/input/voice-control.js +11 -5
  101. package/dist/input/voice-control.js.map +1 -1
  102. package/dist/integrations/json-rpc/server.js +5 -5
  103. package/dist/integrations/json-rpc/server.js.map +1 -1
  104. package/dist/integrations/mcp/mcp-server.js +1 -1
  105. package/dist/integrations/mcp/mcp-server.js.map +1 -1
  106. package/dist/mcp/client.js +2 -1
  107. package/dist/mcp/client.js.map +1 -1
  108. package/dist/mcp/config.js +89 -5
  109. package/dist/mcp/config.js.map +1 -1
  110. package/dist/mcp/mcp-client.js +65 -14
  111. package/dist/mcp/mcp-client.js.map +1 -1
  112. package/dist/mcp/transports.d.ts +0 -1
  113. package/dist/mcp/transports.js +1 -5
  114. package/dist/mcp/transports.js.map +1 -1
  115. package/dist/mcp/types.d.ts +2 -0
  116. package/dist/persistence/session-lock.d.ts +42 -0
  117. package/dist/persistence/session-lock.js +165 -0
  118. package/dist/persistence/session-lock.js.map +1 -0
  119. package/dist/persistence/session-store.d.ts +18 -3
  120. package/dist/persistence/session-store.js +90 -21
  121. package/dist/persistence/session-store.js.map +1 -1
  122. package/dist/plugins/isolated-plugin-runner.d.ts +6 -0
  123. package/dist/plugins/isolated-plugin-runner.js +19 -1
  124. package/dist/plugins/isolated-plugin-runner.js.map +1 -1
  125. package/dist/providers/local-llm-provider.js +21 -4
  126. package/dist/providers/local-llm-provider.js.map +1 -1
  127. package/dist/sandbox/docker-sandbox.js +7 -4
  128. package/dist/sandbox/docker-sandbox.js.map +1 -1
  129. package/dist/scripting/builtins.d.ts +8 -3
  130. package/dist/scripting/builtins.js +506 -355
  131. package/dist/scripting/builtins.js.map +1 -1
  132. package/dist/scripting/codebuddy-bindings.d.ts +47 -0
  133. package/dist/scripting/codebuddy-bindings.js +487 -0
  134. package/dist/scripting/codebuddy-bindings.js.map +1 -0
  135. package/dist/scripting/index.d.ts +33 -30
  136. package/dist/scripting/index.js +41 -36
  137. package/dist/scripting/index.js.map +1 -1
  138. package/dist/scripting/lexer.d.ts +31 -13
  139. package/dist/scripting/lexer.js +379 -292
  140. package/dist/scripting/lexer.js.map +1 -1
  141. package/dist/scripting/parser.d.ts +63 -44
  142. package/dist/scripting/parser.js +700 -473
  143. package/dist/scripting/parser.js.map +1 -1
  144. package/dist/scripting/runtime.d.ts +55 -24
  145. package/dist/scripting/runtime.js +600 -288
  146. package/dist/scripting/runtime.js.map +1 -1
  147. package/dist/scripting/script-registry.d.ts +54 -0
  148. package/dist/scripting/script-registry.js +202 -0
  149. package/dist/scripting/script-registry.js.map +1 -0
  150. package/dist/scripting/sync-bindings.d.ts +105 -0
  151. package/dist/scripting/sync-bindings.js +353 -0
  152. package/dist/scripting/sync-bindings.js.map +1 -0
  153. package/dist/scripting/types.d.ts +297 -199
  154. package/dist/scripting/types.js +86 -60
  155. package/dist/scripting/types.js.map +1 -1
  156. package/dist/search/usearch-index.js +42 -7
  157. package/dist/search/usearch-index.js.map +1 -1
  158. package/dist/security/bash-parser.d.ts +51 -0
  159. package/dist/security/bash-parser.js +327 -0
  160. package/dist/security/bash-parser.js.map +1 -0
  161. package/dist/security/skill-scanner.d.ts +36 -0
  162. package/dist/security/skill-scanner.js +149 -0
  163. package/dist/security/skill-scanner.js.map +1 -0
  164. package/dist/security/trust-folders.d.ts +1 -0
  165. package/dist/security/trust-folders.js +19 -1
  166. package/dist/security/trust-folders.js.map +1 -1
  167. package/dist/server/websocket/handler.js +15 -5
  168. package/dist/server/websocket/handler.js.map +1 -1
  169. package/dist/skills/eligibility.js +26 -4
  170. package/dist/skills/eligibility.js.map +1 -1
  171. package/dist/tasks/background-tasks.js +5 -1
  172. package/dist/tasks/background-tasks.js.map +1 -1
  173. package/dist/tools/apply-patch.d.ts +55 -0
  174. package/dist/tools/apply-patch.js +273 -0
  175. package/dist/tools/apply-patch.js.map +1 -0
  176. package/dist/tools/registry/bash-tools.js +6 -3
  177. package/dist/tools/registry/bash-tools.js.map +1 -1
  178. package/dist/tools/registry/misc-tools.js +1 -2
  179. package/dist/tools/registry/misc-tools.js.map +1 -1
  180. package/dist/tools/registry/search-tools.js +1 -1
  181. package/dist/tools/registry/search-tools.js.map +1 -1
  182. package/dist/tools/registry/text-editor-tools.js +1 -1
  183. package/dist/tools/registry/text-editor-tools.js.map +1 -1
  184. package/dist/tools/registry/todo-tools.js +37 -5
  185. package/dist/tools/registry/todo-tools.js.map +1 -1
  186. package/dist/tools/registry/tool-registry.js +5 -4
  187. package/dist/tools/registry/tool-registry.js.map +1 -1
  188. package/dist/tools/registry/web-tools.d.ts +1 -1
  189. package/dist/tools/registry/web-tools.js +28 -8
  190. package/dist/tools/registry/web-tools.js.map +1 -1
  191. package/dist/tools/text-editor.d.ts +1 -1
  192. package/dist/tools/text-editor.js +23 -5
  193. package/dist/tools/text-editor.js.map +1 -1
  194. package/dist/tools/web-search.d.ts +52 -37
  195. package/dist/tools/web-search.js +368 -163
  196. package/dist/tools/web-search.js.map +1 -1
  197. package/dist/ui/components/ChatInterface.d.ts +1 -1
  198. package/dist/utils/head-tail-truncation.d.ts +34 -0
  199. package/dist/utils/head-tail-truncation.js +98 -0
  200. package/dist/utils/head-tail-truncation.js.map +1 -0
  201. package/dist/utils/sanitize.d.ts +5 -0
  202. package/dist/utils/sanitize.js +19 -0
  203. package/dist/utils/sanitize.js.map +1 -1
  204. package/dist/utils/settings-manager.js +4 -4
  205. package/dist/utils/settings-manager.js.map +1 -1
  206. package/package.json +3 -1
@@ -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 using Serper API (Google Search) with DuckDuckGo fallback
50
+ * Web Search Tool OpenClaw-aligned provider chain
6
51
  *
7
- * Set SERPER_API_KEY environment variable to enable Serper
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
- cacheTTL = 15 * 60 * 1000; // 15 minutes cache
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
- if (this.serperApiKey) {
16
- logger.debug('Serper API key configured for web search');
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
- * Search the web using Serper API (Google) or DuckDuckGo fallback
21
- */
79
+ // ============================================================================
80
+ // Main search entry point
81
+ // ============================================================================
22
82
  async search(query, options = {}) {
23
- const { maxResults = 5 } = options;
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 first
26
- const cacheKey = `${query}-${maxResults}`;
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
- let results;
35
- // Use Serper API if key is available
36
- if (this.serperApiKey) {
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
- else {
40
- // Fallback to DuckDuckGo
41
- results = await this.searchDuckDuckGo(query, maxResults);
42
- }
43
- if (results.length === 0) {
44
- return {
45
- success: true,
46
- output: `No results found for: "${query}"`
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
- // Cache results
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: `Web search failed: ${getErrorMessage(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
- * Search using Serper API (Google Search)
65
- */
66
- async searchSerper(query, maxResults) {
67
- const response = await axios.post('https://google.serper.dev/search', {
68
- q: query,
69
- num: maxResults,
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: 10000,
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
- * Fetch and summarize a web page
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: 10000
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: 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: try alternative parsing if no results
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: 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
- * Detect if query is weather-related
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
- // Clean and format snippet
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
- * Extract readable text from HTML
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
- // Clean up whitespace
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
- '&amp;': '&',
350
- '&lt;': '<',
351
- '&gt;': '>',
352
- '&quot;': '"',
353
- '&#39;': "'",
354
- '&apos;': "'",
355
- '&nbsp;': ' ',
356
- '&ndash;': '–',
357
- '&mdash;': '—',
358
- '&hellip;': '…',
359
- '&copy;': '©',
360
- '&reg;': '®',
361
- '&trade;': '™',
559
+ '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"',
560
+ '&#39;': "'", '&apos;': "'", '&nbsp;': ' ', '&ndash;': '–',
561
+ '&mdash;': '', '&hellip;': '…', '&copy;': '©', '&reg;': '®', '&trade;': '™',
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