omnikey-cli 1.0.13 → 1.0.14
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/README.md +7 -8
- package/backend-dist/{agentPrompts.js → agent/agentPrompts.js} +18 -0
- package/backend-dist/{agentServer.js → agent/agentServer.js} +147 -54
- package/backend-dist/agent/index.js +17 -0
- package/backend-dist/agent/web-search-provider.js +135 -0
- package/backend-dist/ai-client.js +469 -0
- package/backend-dist/config.js +31 -2
- package/backend-dist/featureRoutes.js +17 -36
- package/backend-dist/index.js +1 -1
- package/dist/daemon.js +11 -3
- package/dist/index.js +7 -7
- package/dist/killDaemon.js +1 -1
- package/dist/onboard.js +97 -10
- package/dist/removeConfig.js +37 -29
- package/package.json +3 -1
- package/src/daemon.ts +19 -4
- package/src/index.ts +7 -9
- package/src/killDaemon.ts +1 -1
- package/src/onboard.ts +103 -10
- package/src/removeConfig.ts +40 -29
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Omnikey CLI
|
|
2
2
|
|
|
3
|
-
A command-line tool for onboarding users to the Omnikey open-source app and
|
|
3
|
+
A command-line tool for onboarding users to the Omnikey open-source app, configuring your LLM provider (OpenAI, Anthropic, or Gemini), and setting up the web search tool.
|
|
4
4
|
|
|
5
5
|
## About OmnikeyAI
|
|
6
6
|
|
|
7
|
-
OmnikeyAI is a productivity tool that helps you quickly rewrite selected text using
|
|
7
|
+
OmnikeyAI is a productivity tool that helps you quickly rewrite selected text using your preferred LLM provider. The CLI allows you to configure and run the backend daemon on your local machine, manage your API keys, choose your LLM provider (OpenAI, Anthropic, or Gemini), and optionally configure the web search tool.
|
|
8
8
|
|
|
9
9
|
- For more details about the app and its features, see the [main README](https://github.com/GurinderRawala/OmniKey-AI).
|
|
10
10
|
- Download the latest macOS app here: [Download OmniKeyAI for macOS](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/macos/download)
|
|
@@ -12,8 +12,10 @@ OmnikeyAI is a productivity tool that helps you quickly rewrite selected text us
|
|
|
12
12
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
|
-
- `omnikey onboard`: Interactive onboarding to
|
|
16
|
-
-
|
|
15
|
+
- `omnikey onboard`: Interactive onboarding to configure your LLM provider and API key.
|
|
16
|
+
- Supports **OpenAI**, **Anthropic**, and **Google Gemini** as LLM providers.
|
|
17
|
+
- Optional **web search tool** integration for enhanced responses.
|
|
18
|
+
- Accepts CLI flags for non-interactive setup.
|
|
17
19
|
- Configure and run the backend daemon — persisted across reboots on both macOS and Windows.
|
|
18
20
|
|
|
19
21
|
## Usage
|
|
@@ -22,12 +24,9 @@ OmnikeyAI is a productivity tool that helps you quickly rewrite selected text us
|
|
|
22
24
|
# Install CLI globally (from this directory)
|
|
23
25
|
npm install -g omnikey-cli
|
|
24
26
|
|
|
25
|
-
# Onboard interactively (will prompt for
|
|
27
|
+
# Onboard interactively (will prompt for LLM key and web search tool)
|
|
26
28
|
omnikey onboard
|
|
27
29
|
|
|
28
|
-
# Or onboard non-interactively
|
|
29
|
-
omnikey onboard --open-ai-key YOUR_KEY
|
|
30
|
-
|
|
31
30
|
# Start the daemon (auto-restarts on reboot)
|
|
32
31
|
omnikey daemon --port 7071
|
|
33
32
|
|
|
@@ -26,6 +26,15 @@ The user will run the script and share the output with you.
|
|
|
26
26
|
- If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
|
|
27
27
|
</instruction_handling>
|
|
28
28
|
|
|
29
|
+
<web_tools>
|
|
30
|
+
- You have access to web tools you can call at any time during a turn:
|
|
31
|
+
- web_fetch(url): Fetches the text content of any publicly accessible URL. Use it to retrieve documentation, error references, API guides, release notes, or any other web resource that would help answer the user's question.
|
|
32
|
+
- web_search(query): Searches the web and returns a list of relevant results (title, URL, snippet). Use it when you need to discover the right URL before fetching, or when a quick summary of search results is sufficient.
|
|
33
|
+
- Use these tools proactively whenever the question involves current information, external documentation, or anything not already available in the conversation or machine output.
|
|
34
|
+
- You may call web tools multiple times in a single turn; call web_fetch on a promising URL from web_search results to get full details.
|
|
35
|
+
- Web tool results are injected back into the conversation automatically; continue reasoning and then emit your <shell_script> or <final_answer> as normal.
|
|
36
|
+
</web_tools>
|
|
37
|
+
|
|
29
38
|
<interaction_rules>
|
|
30
39
|
- When you need to execute ANY shell command, respond with a single <shell_script> block that contains the FULL script to run.
|
|
31
40
|
- Within that script, include all steps needed to carry out the current diagnostic or information-gathering task as completely as possible (for example, collect all relevant logs, inspect all relevant services, perform all necessary checks), rather than issuing minimal or placeholder commands.
|
|
@@ -87,6 +96,15 @@ The user will run the script and share the output with you.
|
|
|
87
96
|
- If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
|
|
88
97
|
</instruction_handling>
|
|
89
98
|
|
|
99
|
+
<web_tools>
|
|
100
|
+
- You have access to web tools you can call at any time during a turn:
|
|
101
|
+
- web_fetch(url): Fetches the text content of any publicly accessible URL. Use it to retrieve documentation, error references, API guides, release notes, or any other web resource that would help answer the user's question.
|
|
102
|
+
- web_search(query): Searches the web and returns a list of relevant results (title, URL, snippet). Use it when you need to discover the right URL before fetching, or when a quick summary of search results is sufficient.
|
|
103
|
+
- Use these tools proactively whenever the question involves current information, external documentation, or anything not already available in the conversation or machine output.
|
|
104
|
+
- You may call web tools multiple times in a single turn; call web_fetch on a promising URL from web_search results to get full details.
|
|
105
|
+
- Web tool results are injected back into the conversation automatically; continue reasoning and then emit your <shell_script> or <final_answer> as normal.
|
|
106
|
+
</web_tools>
|
|
107
|
+
|
|
90
108
|
<interaction_rules>
|
|
91
109
|
- When you need to execute ANY shell command, respond with a single <shell_script> block that contains the FULL script to run.
|
|
92
110
|
- Within that script, include all steps needed to carry out the current diagnostic or information-gathering task as completely as possible (for example, collect all relevant logs, inspect all relevant services, perform all necessary checks), rather than issuing minimal or placeholder commands.
|
|
@@ -39,18 +39,64 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.attachAgentWebSocketServer = attachAgentWebSocketServer;
|
|
40
40
|
const ws_1 = __importStar(require("ws"));
|
|
41
41
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
42
|
-
const
|
|
42
|
+
const axios_1 = __importDefault(require("axios"));
|
|
43
43
|
const cuid_1 = __importDefault(require("cuid"));
|
|
44
|
-
const config_1 = require("
|
|
45
|
-
const logger_1 = require("
|
|
46
|
-
const subscription_1 = require("
|
|
47
|
-
const subscriptionUsage_1 = require("
|
|
44
|
+
const config_1 = require("../config");
|
|
45
|
+
const logger_1 = require("../logger");
|
|
46
|
+
const subscription_1 = require("../models/subscription");
|
|
47
|
+
const subscriptionUsage_1 = require("../models/subscriptionUsage");
|
|
48
48
|
const agentPrompts_1 = require("./agentPrompts");
|
|
49
|
-
const featureRoutes_1 = require("
|
|
50
|
-
const authMiddleware_1 = require("
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
const featureRoutes_1 = require("../featureRoutes");
|
|
50
|
+
const authMiddleware_1 = require("../authMiddleware");
|
|
51
|
+
const web_search_provider_1 = require("./web-search-provider");
|
|
52
|
+
const ai_client_1 = require("../ai-client");
|
|
53
|
+
function buildAvailableTools() {
|
|
54
|
+
// web_search is always available — DuckDuckGo is used as free fallback
|
|
55
|
+
return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
|
|
56
|
+
}
|
|
57
|
+
const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
|
|
58
|
+
async function executeTool(name, args, log) {
|
|
59
|
+
if (name === 'web_fetch') {
|
|
60
|
+
const url = args.url;
|
|
61
|
+
if (!url)
|
|
62
|
+
return 'Error: url parameter is required';
|
|
63
|
+
try {
|
|
64
|
+
log.info('Executing web_fetch tool', { url });
|
|
65
|
+
const response = await axios_1.default.get(url, {
|
|
66
|
+
timeout: 15000,
|
|
67
|
+
responseType: 'text',
|
|
68
|
+
maxContentLength: web_search_provider_1.MAX_WEB_FETCH_BYTES,
|
|
69
|
+
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; OmniKeyAgent/1.0)' },
|
|
70
|
+
});
|
|
71
|
+
const text = String(response.data)
|
|
72
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
73
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
74
|
+
.replace(/<[^>]+>/g, ' ')
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
.trim()
|
|
77
|
+
.slice(0, web_search_provider_1.MAX_TOOL_CONTENT_CHARS);
|
|
78
|
+
return text || 'No content retrieved';
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
log.warn('web_fetch tool failed', { url, error: err });
|
|
82
|
+
return `Error fetching URL: ${err instanceof Error ? err.message : String(err)}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (name === 'web_search') {
|
|
86
|
+
const query = args.query;
|
|
87
|
+
if (!query)
|
|
88
|
+
return 'Error: query parameter is required';
|
|
89
|
+
try {
|
|
90
|
+
log.info('Executing web_search tool', { query });
|
|
91
|
+
return await (0, web_search_provider_1.executeWebSearch)(query, log);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
log.warn('web_search tool failed', { query, error: err });
|
|
95
|
+
return `Error searching: ${err instanceof Error ? err.message : String(err)}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return `Unknown tool: ${name}`;
|
|
99
|
+
}
|
|
54
100
|
const sessionMessages = new Map();
|
|
55
101
|
const MAX_TURNS = 10;
|
|
56
102
|
async function getOrCreateSession(sessionId, subscription, platform, log) {
|
|
@@ -202,61 +248,108 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
202
248
|
role: 'user',
|
|
203
249
|
content: userContent,
|
|
204
250
|
});
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
251
|
+
// On the final turn we omit tools so the model is forced to emit a
|
|
252
|
+
// plain text <final_answer> rather than issuing another tool call.
|
|
253
|
+
const isFinalTurn = session.turns >= MAX_TURNS;
|
|
254
|
+
const tools = isFinalTurn ? undefined : buildAvailableTools();
|
|
255
|
+
const recordUsage = async (result) => {
|
|
256
|
+
const usage = result.usage;
|
|
257
|
+
if (!usage || !subscription.id)
|
|
258
|
+
return;
|
|
259
|
+
try {
|
|
260
|
+
await subscriptionUsage_1.SubscriptionUsage.create({
|
|
261
|
+
subscriptionId: subscription.id,
|
|
262
|
+
model: result.model,
|
|
263
|
+
promptTokens: usage.prompt_tokens,
|
|
264
|
+
completionTokens: usage.completion_tokens,
|
|
265
|
+
totalTokens: usage.total_tokens,
|
|
266
|
+
});
|
|
267
|
+
await subscription_1.Subscription.increment('totalTokensUsed', {
|
|
268
|
+
by: usage.total_tokens,
|
|
269
|
+
where: { id: subscription.id },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
log.error('Failed to record subscription usage metrics for agent.', {
|
|
274
|
+
error: err,
|
|
275
|
+
subscriptionId: subscription.id,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
};
|
|
220
279
|
try {
|
|
221
|
-
log.debug('Calling
|
|
280
|
+
log.debug('Calling AI provider for agent turn', {
|
|
222
281
|
sessionId,
|
|
282
|
+
provider: config_1.config.aiProvider,
|
|
283
|
+
model: aiModel,
|
|
223
284
|
turn: session.turns,
|
|
224
285
|
historyLength: session.history.length,
|
|
225
286
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// The OpenAI client accepts a superset of this simple
|
|
229
|
-
// message shape; we safely cast here to keep our local
|
|
230
|
-
// types minimal.
|
|
231
|
-
messages: session.history,
|
|
287
|
+
let result = await ai_client_1.aiClient.complete(aiModel, session.history, {
|
|
288
|
+
tools: tools?.length ? tools : undefined,
|
|
232
289
|
temperature: 0.2,
|
|
233
290
|
});
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
291
|
+
await recordUsage(result);
|
|
292
|
+
// Tool-call loop: execute any requested tools and feed results back
|
|
293
|
+
// until the model emits a non-tool-call response (or we hit the limit).
|
|
294
|
+
const MAX_TOOL_ITERATIONS = 10;
|
|
295
|
+
let toolIterations = 0;
|
|
296
|
+
while (result.finish_reason === 'tool_calls' && toolIterations < MAX_TOOL_ITERATIONS) {
|
|
297
|
+
toolIterations++;
|
|
298
|
+
session.history.push(result.assistantMessage);
|
|
299
|
+
const toolCalls = result.tool_calls ?? [];
|
|
300
|
+
log.info('Agent executing tool calls', {
|
|
301
|
+
sessionId,
|
|
302
|
+
turn: session.turns,
|
|
303
|
+
toolIteration: toolIterations,
|
|
304
|
+
tools: toolCalls.map((tc) => tc.name),
|
|
305
|
+
});
|
|
306
|
+
const toolResults = await Promise.all(toolCalls.map(async (tc) => {
|
|
307
|
+
const args = tc.arguments;
|
|
308
|
+
const toolResult = await executeTool(tc.name, args, log);
|
|
309
|
+
log.info('Tool call completed', {
|
|
310
|
+
sessionId,
|
|
311
|
+
tool: tc.name,
|
|
312
|
+
resultLength: toolResult.length,
|
|
249
313
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
314
|
+
return { id: tc.id, name: tc.name, result: toolResult };
|
|
315
|
+
}));
|
|
316
|
+
for (const { id, name, result: toolResult } of toolResults) {
|
|
317
|
+
session.history.push({
|
|
318
|
+
role: 'tool',
|
|
319
|
+
tool_call_id: id,
|
|
320
|
+
tool_name: name,
|
|
321
|
+
content: toolResult,
|
|
255
322
|
});
|
|
256
323
|
}
|
|
324
|
+
result = await ai_client_1.aiClient.complete(aiModel, session.history, {
|
|
325
|
+
tools: tools?.length ? tools : undefined,
|
|
326
|
+
temperature: 0.2,
|
|
327
|
+
});
|
|
328
|
+
await recordUsage(result);
|
|
329
|
+
}
|
|
330
|
+
// If the tool loop was exhausted while the model still wants more tool calls,
|
|
331
|
+
// the last result has empty content. Force one final no-tools call so the model
|
|
332
|
+
// must synthesize a text answer from everything gathered so far.
|
|
333
|
+
if (result.finish_reason === 'tool_calls') {
|
|
334
|
+
log.warn('Tool iteration limit reached with pending tool calls; forcing final text response', {
|
|
335
|
+
sessionId,
|
|
336
|
+
turn: session.turns,
|
|
337
|
+
});
|
|
338
|
+
// Do NOT push result.assistantMessage here — it contains tool_use blocks that
|
|
339
|
+
// require corresponding tool_result blocks (Anthropic API constraint). Since we
|
|
340
|
+
// are not executing those tool calls, just inject a plain user nudge so the model
|
|
341
|
+
// synthesizes a text answer from the history already accumulated.
|
|
342
|
+
session.history.push({
|
|
343
|
+
role: 'user',
|
|
344
|
+
content: 'You have reached the maximum number of tool calls. Based on all information gathered so far, please provide your best answer now.',
|
|
345
|
+
});
|
|
346
|
+
result = await ai_client_1.aiClient.complete(aiModel, session.history, {
|
|
347
|
+
tools: undefined,
|
|
348
|
+
temperature: 0.2,
|
|
349
|
+
});
|
|
350
|
+
await recordUsage(result);
|
|
257
351
|
}
|
|
258
|
-
const
|
|
259
|
-
const content = (choice.message.content ?? '').toString().trim();
|
|
352
|
+
const content = result.content.trim();
|
|
260
353
|
if (!content) {
|
|
261
354
|
log.warn('Agent LLM returned empty content; sending generic error to client.');
|
|
262
355
|
const errorMessage = 'The agent returned an empty response. Please try again.';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./agentServer"), exports);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_TOOL_CONTENT_CHARS = exports.MAX_WEB_FETCH_BYTES = exports.WEB_SEARCH_TOOL = exports.WEB_FETCH_TOOL = void 0;
|
|
7
|
+
exports.executeWebSearch = executeWebSearch;
|
|
8
|
+
const axios_1 = __importDefault(require("axios"));
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
exports.WEB_FETCH_TOOL = {
|
|
11
|
+
name: 'web_fetch',
|
|
12
|
+
description: "Fetch the text content of any publicly accessible URL. Use this to retrieve documentation, error references, API guides, release notes, or any web resource that would help answer the user's question.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
url: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'The full URL to fetch (e.g. https://example.com/page)',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ['url'],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
exports.WEB_SEARCH_TOOL = {
|
|
25
|
+
name: 'web_search',
|
|
26
|
+
description: "Search the web for information about a topic. Use this to find documentation, troubleshoot errors, or research topics relevant to the user's question.",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
query: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'The search query',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ['query'],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
exports.MAX_WEB_FETCH_BYTES = 500000;
|
|
39
|
+
exports.MAX_TOOL_CONTENT_CHARS = 8000;
|
|
40
|
+
function formatSearchResults(results) {
|
|
41
|
+
if (!results.length)
|
|
42
|
+
return 'No search results found';
|
|
43
|
+
return results
|
|
44
|
+
.slice(0, 5)
|
|
45
|
+
.map((r, i) => `${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`)
|
|
46
|
+
.join('\n\n');
|
|
47
|
+
}
|
|
48
|
+
async function searchWithSerper(query) {
|
|
49
|
+
const response = await axios_1.default.post('https://google.serper.dev/search', { q: query, num: 5 }, {
|
|
50
|
+
headers: { 'X-API-KEY': config_1.config.serperApiKey, 'Content-Type': 'application/json' },
|
|
51
|
+
timeout: 15000,
|
|
52
|
+
});
|
|
53
|
+
return (response.data?.organic ?? []).map((r) => ({
|
|
54
|
+
title: r.title ?? '(no title)',
|
|
55
|
+
url: r.link ?? '',
|
|
56
|
+
snippet: r.snippet ?? '',
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
async function searchWithBrave(query) {
|
|
60
|
+
const response = await axios_1.default.get('https://api.search.brave.com/res/v1/web/search', {
|
|
61
|
+
params: { q: query, count: 5 },
|
|
62
|
+
headers: { Accept: 'application/json', 'X-Subscription-Token': config_1.config.braveSearchApiKey },
|
|
63
|
+
timeout: 15000,
|
|
64
|
+
});
|
|
65
|
+
return (response.data?.web?.results ?? []).map((r) => ({
|
|
66
|
+
title: r.title ?? '(no title)',
|
|
67
|
+
url: r.url ?? '',
|
|
68
|
+
snippet: r.description ?? '',
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
async function searchWithTavily(query) {
|
|
72
|
+
const response = await axios_1.default.post('https://api.tavily.com/search', { query, max_results: 5, api_key: config_1.config.tavilyApiKey }, { headers: { 'Content-Type': 'application/json' }, timeout: 15000 });
|
|
73
|
+
return (response.data?.results ?? []).map((r) => ({
|
|
74
|
+
title: r.title ?? '(no title)',
|
|
75
|
+
url: r.url ?? '',
|
|
76
|
+
snippet: r.content ?? '',
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
async function searchWithSearxng(query) {
|
|
80
|
+
const response = await axios_1.default.get(`${config_1.config.searxngUrl}/search`, {
|
|
81
|
+
params: { q: query, format: 'json', num_results: 5 },
|
|
82
|
+
timeout: 15000,
|
|
83
|
+
});
|
|
84
|
+
return (response.data?.results ?? []).map((r) => ({
|
|
85
|
+
title: r.title ?? '(no title)',
|
|
86
|
+
url: r.url ?? '',
|
|
87
|
+
snippet: r.content ?? '',
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
async function searchWithDuckDuckGo(query) {
|
|
91
|
+
const response = await axios_1.default.get('https://api.duckduckgo.com/', {
|
|
92
|
+
params: { q: query, format: 'json', no_html: '1', skip_disambig: '1' },
|
|
93
|
+
timeout: 15000,
|
|
94
|
+
});
|
|
95
|
+
const results = [];
|
|
96
|
+
if (response.data?.AbstractText) {
|
|
97
|
+
results.push({
|
|
98
|
+
title: response.data.AbstractSource ?? 'Summary',
|
|
99
|
+
url: response.data.AbstractURL ?? '',
|
|
100
|
+
snippet: response.data.AbstractText,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
for (const topic of response.data?.RelatedTopics ?? []) {
|
|
104
|
+
if (topic.Text && topic.FirstURL) {
|
|
105
|
+
results.push({
|
|
106
|
+
title: topic.Text.split(' - ')[0] ?? topic.Text,
|
|
107
|
+
url: topic.FirstURL,
|
|
108
|
+
snippet: topic.Text,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (results.length >= 5)
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
async function executeWebSearch(query, log) {
|
|
117
|
+
if (config_1.config.serperApiKey) {
|
|
118
|
+
log.info('web_search: using Serper', { query });
|
|
119
|
+
return formatSearchResults(await searchWithSerper(query));
|
|
120
|
+
}
|
|
121
|
+
if (config_1.config.braveSearchApiKey) {
|
|
122
|
+
log.info('web_search: using Brave Search', { query });
|
|
123
|
+
return formatSearchResults(await searchWithBrave(query));
|
|
124
|
+
}
|
|
125
|
+
if (config_1.config.tavilyApiKey) {
|
|
126
|
+
log.info('web_search: using Tavily', { query });
|
|
127
|
+
return formatSearchResults(await searchWithTavily(query));
|
|
128
|
+
}
|
|
129
|
+
if (config_1.config.searxngUrl) {
|
|
130
|
+
log.info('web_search: using SearXNG', { query });
|
|
131
|
+
return formatSearchResults(await searchWithSearxng(query));
|
|
132
|
+
}
|
|
133
|
+
log.info('web_search: using DuckDuckGo (free fallback)', { query });
|
|
134
|
+
return formatSearchResults(await searchWithDuckDuckGo(query));
|
|
135
|
+
}
|