omnikey-cli 1.0.13 → 1.0.15
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/agent/agentPrompts.js +91 -0
- package/backend-dist/{agentServer.js → agent/agentServer.js} +112 -59
- package/backend-dist/agent/index.js +17 -0
- package/backend-dist/ai-client.js +469 -0
- package/backend-dist/config.js +32 -2
- package/backend-dist/featureRoutes.js +18 -37
- package/backend-dist/index.js +1 -1
- package/backend-dist/prompts.js +86 -66
- package/backend-dist/web-search-provider.js +178 -0
- package/dist/daemon.js +15 -10
- 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 +24 -12
- 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/backend-dist/agentPrompts.js +0 -124
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
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getAgentPrompt = getAgentPrompt;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
function getAgentPrompt(platform) {
|
|
6
|
+
const isWindows = config_1.config.terminalPlatform?.toLowerCase() === 'windows' || platform?.toLowerCase() === 'windows';
|
|
7
|
+
const windowsShellScriptInstructions = `
|
|
8
|
+
\`\`\`
|
|
9
|
+
<shell_script>
|
|
10
|
+
# your commands here
|
|
11
|
+
</shell_script>
|
|
12
|
+
\`\`\`
|
|
13
|
+
|
|
14
|
+
Follow these guidelines:
|
|
15
|
+
|
|
16
|
+
- Use a single, self-contained PowerShell script per response; do not send multiple \`<shell_script>\` blocks in one turn.
|
|
17
|
+
- Inside the script, group related commands logically and add brief inline comments only when they clarify non-obvious or complex steps.
|
|
18
|
+
- Prefer safe, idempotent commands that can be run multiple times without unintended side effects.
|
|
19
|
+
- Never use elevated privileges (do not use \`sudo\`, \`Run as Administrator\`, or equivalent).
|
|
20
|
+
- Use PowerShell cmdlets and syntax (for example, \`Get-ChildItem\`, \`Select-Object\`, \`Where-Object\`) rather than cmd.exe or bash equivalents.`;
|
|
21
|
+
return `
|
|
22
|
+
You are an AI assistant capable of reasoning about user situations and executing shell scripts in a terminal environment. You have full access to the terminal.
|
|
23
|
+
|
|
24
|
+
Your responsibilities are:
|
|
25
|
+
1. **Read and respect stored instructions**: When provided with \`<stored_instructions>\`, follow them carefully regarding behavior, focus areas, and output style.
|
|
26
|
+
2. **Process user input**: Analyze what the user has typed or requested.
|
|
27
|
+
3. **Gather context when needed**: Decide if additional machine-level information is required. If so, generate appropriate shell scripts to collect it.
|
|
28
|
+
4. **Produce a complete answer**: Combine results from any previously executed scripts, the stored instructions, and the user input to deliver a helpful final response.
|
|
29
|
+
|
|
30
|
+
**Guidelines for script generation:**
|
|
31
|
+
- Create only safe, read-only commands focused on inspection, diagnostics, and information gathering.
|
|
32
|
+
- Do not generate commands that install software, modify user data, or change system settings.
|
|
33
|
+
- Never ask the user to run commands with \`sudo\` or administrator/root privileges.
|
|
34
|
+
- Ensure all commands are compatible with ${!isWindows ? 'macOS and Linux; avoid Windows-specific commands.' : 'Use Windows-specific commands; avoid macOS and Linux-specific commands.'}
|
|
35
|
+
- Scripts must be self-contained and ready to run without requiring the user to edit them.
|
|
36
|
+
|
|
37
|
+
When you generate shell scripts, make them clear, efficient, and focused on gathering the information needed to answer the user's question or complete their request.
|
|
38
|
+
|
|
39
|
+
**Instruction handling:**
|
|
40
|
+
- Treat stored task instructions (if present) as authoritative for how to prioritize, what to examine, and how to format your answer, as long as they do not conflict with system rules or safety guidelines.
|
|
41
|
+
- Treat the current user input as the immediate goal or question you must solve, applying the stored instructions to that specific situation.
|
|
42
|
+
- If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
|
|
43
|
+
|
|
44
|
+
**Web tools:**
|
|
45
|
+
You have access to web tools you can call at any time during a turn:
|
|
46
|
+
- \`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.
|
|
47
|
+
- \`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.
|
|
48
|
+
|
|
49
|
+
Use these tools proactively whenever the question involves current information, external documentation, or anything not already available in the conversation or machine output. 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. Web tool results are injected back into the conversation automatically; continue reasoning and then emit your shell script or final answer as normal.
|
|
50
|
+
|
|
51
|
+
**Interaction rules:**
|
|
52
|
+
- When you need to execute ANY shell command, respond with a single \`<shell_script>\` block that contains the FULL script to run.
|
|
53
|
+
- 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.
|
|
54
|
+
- Prefer one comprehensive script over multiple small scripts; only wait for another round of output if you genuinely need the previous results to decide on the next actions.
|
|
55
|
+
- If further machine-level investigation is unnecessary, skip the shell script and respond directly with a \`<final_answer>\`.
|
|
56
|
+
- Every response MUST be exactly one of:
|
|
57
|
+
- A single \`<shell_script>...</shell_script>\` block, and nothing else; or
|
|
58
|
+
- A single \`<final_answer>...</final_answer>\` block, and nothing else.
|
|
59
|
+
- Never send plain text or explanation outside of these tags. If you are not emitting a \`<shell_script>\`, you MUST emit a \`<final_answer>\`.
|
|
60
|
+
- When you are completely finished and ready to present the result back to the user, respond with a single \`<final_answer>\` block.
|
|
61
|
+
- Do NOT include reasoning, commentary, or any other tags outside of \`<shell_script>...</shell_script>\` or \`<final_answer>...</final_answer>\`.
|
|
62
|
+
- Never wrap your entire response in other XML or JSON structures.
|
|
63
|
+
|
|
64
|
+
**Shell script block structure:**
|
|
65
|
+
Always emit exactly this structure when you want to run commands: ${!isWindows
|
|
66
|
+
? `
|
|
67
|
+
\`\`\`bash
|
|
68
|
+
<shell_script>
|
|
69
|
+
#!/usr/bin/env bash
|
|
70
|
+
set -euo pipefail
|
|
71
|
+
# your commands here
|
|
72
|
+
</shell_script>
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
- Use a single, self-contained script per turn; do not send multiple \`<shell_script>\` blocks in one response.
|
|
76
|
+
- Inside the script, group related commands logically and add brief inline comments ONLY when they clarify non-obvious steps.
|
|
77
|
+
- Prefer safe, idempotent commands. Never ask for sudo.`
|
|
78
|
+
: windowsShellScriptInstructions}
|
|
79
|
+
|
|
80
|
+
**Final answer block structure:**
|
|
81
|
+
When you have gathered enough information and completed the requested work, respond once with:
|
|
82
|
+
|
|
83
|
+
\`\`\`
|
|
84
|
+
<final_answer>
|
|
85
|
+
...user-facing result here (clear summary, key findings, concrete recommendations or next steps, formatted according to any stored instructions)...
|
|
86
|
+
</final_answer>
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
- Do not emit any text before or after the \`<final_answer>\` block; the entire response must be inside the \`<final_answer>\` tags.
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
@@ -39,18 +39,21 @@ 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"));
|
|
43
42
|
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("
|
|
43
|
+
const config_1 = require("../config");
|
|
44
|
+
const logger_1 = require("../logger");
|
|
45
|
+
const subscription_1 = require("../models/subscription");
|
|
46
|
+
const subscriptionUsage_1 = require("../models/subscriptionUsage");
|
|
48
47
|
const agentPrompts_1 = require("./agentPrompts");
|
|
49
|
-
const featureRoutes_1 = require("
|
|
50
|
-
const authMiddleware_1 = require("
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
const featureRoutes_1 = require("../featureRoutes");
|
|
49
|
+
const authMiddleware_1 = require("../authMiddleware");
|
|
50
|
+
const web_search_provider_1 = require("../web-search-provider");
|
|
51
|
+
const ai_client_1 = require("../ai-client");
|
|
52
|
+
function buildAvailableTools() {
|
|
53
|
+
// web_search is always available — DuckDuckGo is used as free fallback
|
|
54
|
+
return [web_search_provider_1.WEB_FETCH_TOOL, web_search_provider_1.WEB_SEARCH_TOOL];
|
|
55
|
+
}
|
|
56
|
+
const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
|
|
54
57
|
const sessionMessages = new Map();
|
|
55
58
|
const MAX_TURNS = 10;
|
|
56
59
|
async function getOrCreateSession(sessionId, subscription, platform, log) {
|
|
@@ -63,7 +66,7 @@ async function getOrCreateSession(sessionId, subscription, platform, log) {
|
|
|
63
66
|
});
|
|
64
67
|
return existing;
|
|
65
68
|
}
|
|
66
|
-
const systemPrompt =
|
|
69
|
+
const systemPrompt = (0, agentPrompts_1.getAgentPrompt)(platform);
|
|
67
70
|
// use these instructions as user instructions
|
|
68
71
|
const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
|
|
69
72
|
log.error('Failed to get system prompt for new agent session', { error: err });
|
|
@@ -79,11 +82,14 @@ async function getOrCreateSession(sessionId, subscription, platform, log) {
|
|
|
79
82
|
...(prompt
|
|
80
83
|
? [
|
|
81
84
|
{
|
|
82
|
-
role: '
|
|
83
|
-
content: `<
|
|
84
|
-
#
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: `<stored_instructions>
|
|
87
|
+
# Stored Instructions
|
|
88
|
+
|
|
89
|
+
"""
|
|
85
90
|
${prompt}
|
|
86
|
-
|
|
91
|
+
"""
|
|
92
|
+
</stored_instructions>`,
|
|
87
93
|
},
|
|
88
94
|
]
|
|
89
95
|
: []),
|
|
@@ -202,61 +208,108 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
202
208
|
role: 'user',
|
|
203
209
|
content: userContent,
|
|
204
210
|
});
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
// On the final turn we omit tools so the model is forced to emit a
|
|
212
|
+
// plain text <final_answer> rather than issuing another tool call.
|
|
213
|
+
const isFinalTurn = session.turns >= MAX_TURNS;
|
|
214
|
+
const tools = isFinalTurn ? undefined : buildAvailableTools();
|
|
215
|
+
const recordUsage = async (result) => {
|
|
216
|
+
const usage = result.usage;
|
|
217
|
+
if (!usage || !subscription.id)
|
|
218
|
+
return;
|
|
219
|
+
try {
|
|
220
|
+
await subscriptionUsage_1.SubscriptionUsage.create({
|
|
221
|
+
subscriptionId: subscription.id,
|
|
222
|
+
model: result.model,
|
|
223
|
+
promptTokens: usage.prompt_tokens,
|
|
224
|
+
completionTokens: usage.completion_tokens,
|
|
225
|
+
totalTokens: usage.total_tokens,
|
|
226
|
+
});
|
|
227
|
+
await subscription_1.Subscription.increment('totalTokensUsed', {
|
|
228
|
+
by: usage.total_tokens,
|
|
229
|
+
where: { id: subscription.id },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
log.error('Failed to record subscription usage metrics for agent.', {
|
|
234
|
+
error: err,
|
|
235
|
+
subscriptionId: subscription.id,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
220
239
|
try {
|
|
221
|
-
log.debug('Calling
|
|
240
|
+
log.debug('Calling AI provider for agent turn', {
|
|
222
241
|
sessionId,
|
|
242
|
+
provider: config_1.config.aiProvider,
|
|
243
|
+
model: aiModel,
|
|
223
244
|
turn: session.turns,
|
|
224
245
|
historyLength: session.history.length,
|
|
225
246
|
});
|
|
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,
|
|
247
|
+
let result = await ai_client_1.aiClient.complete(aiModel, session.history, {
|
|
248
|
+
tools: tools?.length ? tools : undefined,
|
|
232
249
|
temperature: 0.2,
|
|
233
250
|
});
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
await recordUsage(result);
|
|
252
|
+
// Tool-call loop: execute any requested tools and feed results back
|
|
253
|
+
// until the model emits a non-tool-call response (or we hit the limit).
|
|
254
|
+
const MAX_TOOL_ITERATIONS = 10;
|
|
255
|
+
let toolIterations = 0;
|
|
256
|
+
while (result.finish_reason === 'tool_calls' && toolIterations < MAX_TOOL_ITERATIONS) {
|
|
257
|
+
toolIterations++;
|
|
258
|
+
session.history.push(result.assistantMessage);
|
|
259
|
+
const toolCalls = result.tool_calls ?? [];
|
|
260
|
+
log.info('Agent executing tool calls', {
|
|
261
|
+
sessionId,
|
|
262
|
+
turn: session.turns,
|
|
263
|
+
toolIteration: toolIterations,
|
|
264
|
+
tools: toolCalls.map((tc) => tc.name),
|
|
265
|
+
});
|
|
266
|
+
const toolResults = await Promise.all(toolCalls.map(async (tc) => {
|
|
267
|
+
const args = tc.arguments;
|
|
268
|
+
const toolResult = await (0, web_search_provider_1.executeTool)(tc.name, args, log);
|
|
269
|
+
log.info('Tool call completed', {
|
|
270
|
+
sessionId,
|
|
271
|
+
tool: tc.name,
|
|
272
|
+
resultLength: toolResult.length,
|
|
245
273
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
subscriptionId: subscription.id,
|
|
274
|
+
return { id: tc.id, name: tc.name, result: toolResult };
|
|
275
|
+
}));
|
|
276
|
+
for (const { id, name, result: toolResult } of toolResults) {
|
|
277
|
+
session.history.push({
|
|
278
|
+
role: 'tool',
|
|
279
|
+
tool_call_id: id,
|
|
280
|
+
tool_name: name,
|
|
281
|
+
content: toolResult,
|
|
255
282
|
});
|
|
256
283
|
}
|
|
284
|
+
result = await ai_client_1.aiClient.complete(aiModel, session.history, {
|
|
285
|
+
tools: tools?.length ? tools : undefined,
|
|
286
|
+
temperature: 0.2,
|
|
287
|
+
});
|
|
288
|
+
await recordUsage(result);
|
|
289
|
+
}
|
|
290
|
+
// If the tool loop was exhausted while the model still wants more tool calls,
|
|
291
|
+
// the last result has empty content. Force one final no-tools call so the model
|
|
292
|
+
// must synthesize a text answer from everything gathered so far.
|
|
293
|
+
if (result.finish_reason === 'tool_calls') {
|
|
294
|
+
log.warn('Tool iteration limit reached with pending tool calls; forcing final text response', {
|
|
295
|
+
sessionId,
|
|
296
|
+
turn: session.turns,
|
|
297
|
+
});
|
|
298
|
+
// Do NOT push result.assistantMessage here — it contains tool_use blocks that
|
|
299
|
+
// require corresponding tool_result blocks (Anthropic API constraint). Since we
|
|
300
|
+
// are not executing those tool calls, just inject a plain user nudge so the model
|
|
301
|
+
// synthesizes a text answer from the history already accumulated.
|
|
302
|
+
session.history.push({
|
|
303
|
+
role: 'user',
|
|
304
|
+
content: 'You have reached the maximum number of tool calls. Based on all information gathered so far, please provide your best answer now.',
|
|
305
|
+
});
|
|
306
|
+
result = await ai_client_1.aiClient.complete(aiModel, session.history, {
|
|
307
|
+
tools: undefined,
|
|
308
|
+
temperature: 0.2,
|
|
309
|
+
});
|
|
310
|
+
await recordUsage(result);
|
|
257
311
|
}
|
|
258
|
-
const
|
|
259
|
-
const content = (choice.message.content ?? '').toString().trim();
|
|
312
|
+
const content = result.content.trim();
|
|
260
313
|
if (!content) {
|
|
261
314
|
log.warn('Agent LLM returned empty content; sending generic error to client.');
|
|
262
315
|
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);
|