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 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 configuring their OPENAI_API_KEY.
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 OpenAI. The CLI allows you to configure and run the backend daemon on your local machine and manage your OpenAI API key with ease.
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 set up your OPENAI_API_KEY.
16
- - Accepts the `--open-ai-key` parameter for non-interactive setup.
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 OpenAI key)
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 openai_1 = __importDefault(require("openai"));
42
+ const axios_1 = __importDefault(require("axios"));
43
43
  const cuid_1 = __importDefault(require("cuid"));
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");
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("./featureRoutes");
50
- const authMiddleware_1 = require("./authMiddleware");
51
- const openai = new openai_1.default({
52
- apiKey: config_1.config.openaiApiKey,
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
- if (!config_1.config.openaiApiKey) {
206
- log.warn('OPENAI_API_KEY is not set; returning error to client.');
207
- const errorMessage = 'The server is missing its OpenAI API key. Please configure OPENAI_API_KEY on the backend and try again.';
208
- send({
209
- session_id: sessionId,
210
- sender: 'agent',
211
- content: `<final_answer>\n${errorMessage}\n</final_answer>`,
212
- is_terminal_output: false,
213
- is_error: true,
214
- });
215
- // Clear any cached session state so a subsequent attempt can
216
- // start fresh once the environment is correctly configured.
217
- sessionMessages.delete(sessionId);
218
- return;
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 OpenAI for agent turn', {
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
- const completion = await openai.chat.completions.create({
227
- model: 'gpt-5.1',
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
- // Record token usage for this subscription and model, if usage
235
- // data is available and we know which subscription made the call.
236
- const usage = completion.usage;
237
- if (usage && subscription.id) {
238
- try {
239
- await subscriptionUsage_1.SubscriptionUsage.create({
240
- subscriptionId: subscription.id,
241
- model: completion.model ?? 'gpt-5.1',
242
- promptTokens: usage.prompt_tokens ?? 0,
243
- completionTokens: usage.completion_tokens ?? 0,
244
- totalTokens: usage.total_tokens ?? 0,
245
- });
246
- await subscription_1.Subscription.increment('totalTokensUsed', {
247
- by: usage.total_tokens ?? 0,
248
- where: { id: subscription.id },
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
- catch (err) {
252
- log.error('Failed to record subscription usage metrics for agent.', {
253
- error: err,
254
- subscriptionId: subscription.id,
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 choice = completion.choices[0];
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
+ }