omnikey-cli 1.0.15 → 1.0.17
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 +31 -1
- package/backend-dist/agent/agentPrompts.js +10 -4
- package/backend-dist/agent/agentServer.js +23 -3
- package/backend-dist/index.js +25 -10
- package/dist/daemon.js +54 -2
- package/dist/index.js +23 -0
- package/dist/removeConfig.js +12 -4
- package/dist/setConfig.js +19 -0
- package/dist/showConfig.js +45 -0
- package/package.json +1 -1
- package/src/daemon.ts +50 -5
- package/src/index.ts +26 -0
- package/src/removeConfig.ts +12 -4
- package/src/setConfig.ts +16 -0
- package/src/showConfig.ts +47 -0
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A command-line tool for onboarding users to the Omnikey open-source app, configu
|
|
|
6
6
|
|
|
7
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
|
+
- Website: [omnikeyai.ca](https://omnikeyai.ca)
|
|
9
10
|
- For more details about the app and its features, see the [main README](https://github.com/GurinderRawala/OmniKey-AI).
|
|
10
11
|
- Download the latest macOS app here: [Download OmniKeyAI for macOS](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/macos/download)
|
|
11
12
|
- Download the latest Windows app here: [Download OmniKeyAI for Windows](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/windows/download)
|
|
@@ -33,16 +34,45 @@ omnikey daemon --port 7071
|
|
|
33
34
|
# Kill the daemon
|
|
34
35
|
omnikey kill-daemon
|
|
35
36
|
|
|
36
|
-
#
|
|
37
|
+
# Restart the daemon (kill + start in one step)
|
|
38
|
+
omnikey restart-daemon --port 7071
|
|
39
|
+
|
|
40
|
+
# Show current configuration (API keys are masked)
|
|
41
|
+
omnikey config
|
|
42
|
+
|
|
43
|
+
# Set a single configuration value
|
|
44
|
+
omnikey set OMNIKEY_PORT 8080
|
|
45
|
+
|
|
46
|
+
# Remove the config directory (keeps SQLite database)
|
|
37
47
|
omnikey remove-config
|
|
38
48
|
|
|
49
|
+
# Remove config and also the SQLite database
|
|
50
|
+
omnikey remove-config --db
|
|
51
|
+
|
|
39
52
|
# Check daemon status
|
|
40
53
|
omnikey status
|
|
41
54
|
|
|
42
55
|
# Check daemon logs
|
|
43
56
|
omnikey logs --lines 100
|
|
57
|
+
|
|
58
|
+
# Check daemon error logs only
|
|
59
|
+
omnikey logs --errors
|
|
44
60
|
```
|
|
45
61
|
|
|
62
|
+
### Command reference
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `omnikey onboard` | Interactive setup for LLM provider and web search |
|
|
67
|
+
| `omnikey daemon [--port]` | Start the backend daemon (default port: 7071) |
|
|
68
|
+
| `omnikey kill-daemon` | Stop the running daemon |
|
|
69
|
+
| `omnikey restart-daemon [--port]` | Kill and restart the daemon |
|
|
70
|
+
| `omnikey config` | Display current config with masked API keys |
|
|
71
|
+
| `omnikey set <key> <value>` | Update a single config value |
|
|
72
|
+
| `omnikey remove-config [--db]` | Remove config files; add `--db` to also delete the database |
|
|
73
|
+
| `omnikey status` | Show what process is using the daemon port |
|
|
74
|
+
| `omnikey logs [--lines N] [--errors]` | Tail daemon logs |
|
|
75
|
+
|
|
46
76
|
## Platform notes
|
|
47
77
|
|
|
48
78
|
### macOS
|
|
@@ -42,11 +42,17 @@ When you generate shell scripts, make them clear, efficient, and focused on gath
|
|
|
42
42
|
- If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
|
|
43
43
|
|
|
44
44
|
**Web tools:**
|
|
45
|
-
You have access to web tools you
|
|
46
|
-
- \`web_fetch(url)\`:
|
|
47
|
-
- \`web_search(query)\`:
|
|
45
|
+
You have access to web tools, but you must use them sparingly and only when explicitly required:
|
|
46
|
+
- \`web_fetch(url)\`: Only call this when the user has provided a specific URL in their current input or stored instructions and you need to retrieve its contents.
|
|
47
|
+
- \`web_search(query)\`: Only call this when the user has explicitly asked you to search the web or look something up online.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Do NOT use web tools proactively. Do NOT call them to look up documentation, error references, or general information you could infer from the machine output or your own knowledge. Your primary workflow is to generate shell scripts, wait for the terminal output, and reason from that output. Only reach for web tools when there is a clear, explicit instruction or a URL provided by the user.
|
|
50
|
+
|
|
51
|
+
**User message tags:**
|
|
52
|
+
User messages may be prefixed with special tags that indicate their origin:
|
|
53
|
+
- \`TERMINAL OUTPUT:\` — the content is stdout/stderr returned from a previously requested \`<shell_script>\`. Parse it as machine output and use it to continue your reasoning toward a \`<final_answer>\` or a follow-up \`<shell_script>\`.
|
|
54
|
+
- \`COMMAND ERROR:\` — the shell script failed or the terminal returned a non-zero exit code. Treat the content as error output: diagnose the failure, then either emit a corrected \`<shell_script>\` or explain the issue in a \`<final_answer>\`.
|
|
55
|
+
- No prefix — the content is a direct message from the user; treat it as the primary request or question to address.
|
|
50
56
|
|
|
51
57
|
**Interaction rules:**
|
|
52
58
|
- When you need to execute ANY shell command, respond with a single \`<shell_script>\` block that contains the FULL script to run.
|
|
@@ -255,8 +255,13 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
255
255
|
let toolIterations = 0;
|
|
256
256
|
while (result.finish_reason === 'tool_calls' && toolIterations < MAX_TOOL_ITERATIONS) {
|
|
257
257
|
toolIterations++;
|
|
258
|
-
session.history.push(result.assistantMessage);
|
|
259
258
|
const toolCalls = result.tool_calls ?? [];
|
|
259
|
+
// If the model claims tool_calls but sent none, treat it as a normal text
|
|
260
|
+
// response — pushing an assistant message with no following tool results
|
|
261
|
+
// would leave the history ending with an assistant turn, causing a 400.
|
|
262
|
+
if (!toolCalls.length)
|
|
263
|
+
break;
|
|
264
|
+
session.history.push(result.assistantMessage);
|
|
260
265
|
log.info('Agent executing tool calls', {
|
|
261
266
|
sessionId,
|
|
262
267
|
turn: session.turns,
|
|
@@ -265,6 +270,18 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
265
270
|
});
|
|
266
271
|
const toolResults = await Promise.all(toolCalls.map(async (tc) => {
|
|
267
272
|
const args = tc.arguments;
|
|
273
|
+
// Notify the frontend that a web tool call is about to execute.
|
|
274
|
+
const webCallContent = tc.name === 'web_search'
|
|
275
|
+
? `Searching the web for: "${args.query ?? ''}"`
|
|
276
|
+
: `Fetching URL: ${args.url ?? ''}`;
|
|
277
|
+
send({
|
|
278
|
+
session_id: sessionId,
|
|
279
|
+
sender: 'agent',
|
|
280
|
+
content: webCallContent,
|
|
281
|
+
is_terminal_output: false,
|
|
282
|
+
is_error: false,
|
|
283
|
+
is_web_call: true,
|
|
284
|
+
});
|
|
268
285
|
const toolResult = await (0, web_search_provider_1.executeTool)(tc.name, args, log);
|
|
269
286
|
log.info('Tool call completed', {
|
|
270
287
|
sessionId,
|
|
@@ -287,10 +304,14 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
287
304
|
});
|
|
288
305
|
await recordUsage(result);
|
|
289
306
|
}
|
|
307
|
+
log.info('Finished reasoning and tool calls: ', {
|
|
308
|
+
reason: result.finish_reason,
|
|
309
|
+
});
|
|
310
|
+
const content = result.content.trim();
|
|
290
311
|
// If the tool loop was exhausted while the model still wants more tool calls,
|
|
291
312
|
// the last result has empty content. Force one final no-tools call so the model
|
|
292
313
|
// must synthesize a text answer from everything gathered so far.
|
|
293
|
-
if (result.finish_reason === 'tool_calls') {
|
|
314
|
+
if (result.finish_reason === 'tool_calls' || !content) {
|
|
294
315
|
log.warn('Tool iteration limit reached with pending tool calls; forcing final text response', {
|
|
295
316
|
sessionId,
|
|
296
317
|
turn: session.turns,
|
|
@@ -309,7 +330,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
|
|
|
309
330
|
});
|
|
310
331
|
await recordUsage(result);
|
|
311
332
|
}
|
|
312
|
-
const content = result.content.trim();
|
|
313
333
|
if (!content) {
|
|
314
334
|
log.warn('Agent LLM returned empty content; sending generic error to client.');
|
|
315
335
|
const errorMessage = 'The agent returned an empty response. Please try again.';
|
package/backend-dist/index.js
CHANGED
|
@@ -19,19 +19,31 @@ const app = (0, express_1.default)();
|
|
|
19
19
|
const PORT = Number(config_1.config.port);
|
|
20
20
|
app.use((0, cors_1.default)());
|
|
21
21
|
app.use(express_1.default.json());
|
|
22
|
+
// Landing page
|
|
23
|
+
app.use(express_1.default.static(path_1.default.join(process.cwd(), 'public')));
|
|
22
24
|
app.use('/api/subscription', (0, subscriptionRoutes_1.createSubscriptionRouter)(logger_1.logger));
|
|
23
25
|
app.use('/api/feature', (0, featureRoutes_1.createFeatureRouter)());
|
|
24
26
|
app.use('/api/instructions', (0, taskInstructionRoutes_1.taskInstructionRouter)());
|
|
25
|
-
app.get('/macos/download', (
|
|
27
|
+
app.get('/macos/download', (_req, res) => {
|
|
26
28
|
const dmgPath = path_1.default.join(process.cwd(), 'macOS', 'OmniKeyAI.dmg');
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
if (!fs_1.default.existsSync(dmgPath)) {
|
|
30
|
+
res.status(404).send('File not found.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
res.set({
|
|
34
|
+
'Content-Type': 'application/octet-stream',
|
|
35
|
+
'Content-Disposition': 'attachment; filename="OmniKeyAI.dmg"',
|
|
36
|
+
'Content-Encoding': 'gzip',
|
|
37
|
+
});
|
|
38
|
+
const fileStream = fs_1.default.createReadStream(dmgPath);
|
|
39
|
+
const gzip = zlib_1.default.createGzip();
|
|
40
|
+
fileStream.on('error', (err) => {
|
|
41
|
+
logger_1.logger.error('Failed to send OmniKeyAI.dmg for download.', { error: err });
|
|
42
|
+
if (!res.headersSent) {
|
|
43
|
+
res.status(500).send('Unable to download file.');
|
|
33
44
|
}
|
|
34
45
|
});
|
|
46
|
+
fileStream.pipe(gzip).pipe(res);
|
|
35
47
|
});
|
|
36
48
|
// Sparkle appcast feed for macOS updates.
|
|
37
49
|
// This feed uses the existing /macos/download endpoint as the
|
|
@@ -52,8 +64,8 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
52
64
|
const appcastUrl = `${baseUrl}/macos/appcast`;
|
|
53
65
|
// These should match the values embedded into the macOS app
|
|
54
66
|
// Info.plist in macOS/build_release_dmg.sh.
|
|
55
|
-
const bundleVersion = '
|
|
56
|
-
const shortVersion = '1.0.
|
|
67
|
+
const bundleVersion = '14';
|
|
68
|
+
const shortVersion = '1.0.13';
|
|
57
69
|
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
|
58
70
|
<rss version="2.0"
|
|
59
71
|
xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
|
|
@@ -81,7 +93,7 @@ app.get('/macos/appcast', (req, res) => {
|
|
|
81
93
|
// ── Windows distribution endpoints ───────────────────────────────────────────
|
|
82
94
|
// These should match the values in windows/OmniKey.Windows.csproj
|
|
83
95
|
// <Version> and windows/build_release_zip.ps1 $APP_VERSION.
|
|
84
|
-
const WIN_VERSION = '1.
|
|
96
|
+
const WIN_VERSION = '1.2';
|
|
85
97
|
const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
|
|
86
98
|
const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
|
|
87
99
|
// Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
|
|
@@ -128,6 +140,9 @@ app.get('/windows/update', (req, res) => {
|
|
|
128
140
|
app.get('/health', (_req, res) => {
|
|
129
141
|
res.json({ status: 'ok' });
|
|
130
142
|
});
|
|
143
|
+
app.get('*', (_req, res) => {
|
|
144
|
+
res.sendFile(path_1.default.join(process.cwd(), 'public', 'index.html'));
|
|
145
|
+
});
|
|
131
146
|
let server = null;
|
|
132
147
|
async function start() {
|
|
133
148
|
try {
|
package/dist/daemon.js
CHANGED
|
@@ -68,8 +68,54 @@ function startDaemonWindows(opts) {
|
|
|
68
68
|
console.error('Failed to write start-daemon.cmd:', e);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
// Register with Windows Task Scheduler so the daemon persists across reboots
|
|
71
|
+
// Register with Windows Task Scheduler so the daemon persists across reboots.
|
|
72
|
+
// Use XML-based registration to avoid cmd.exe quoting issues with paths containing spaces.
|
|
72
73
|
const taskName = 'OmnikeyDaemon';
|
|
74
|
+
const username = process.env.USERNAME || process.env.USER || '';
|
|
75
|
+
// Escape characters that are special in XML
|
|
76
|
+
const xmlEscape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
77
|
+
const taskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
|
78
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
79
|
+
<RegistrationInfo>
|
|
80
|
+
<Description>Omnikey API Backend Daemon</Description>
|
|
81
|
+
</RegistrationInfo>
|
|
82
|
+
<Triggers>
|
|
83
|
+
<LogonTrigger>
|
|
84
|
+
<Enabled>true</Enabled>
|
|
85
|
+
<UserId>${xmlEscape(username)}</UserId>
|
|
86
|
+
</LogonTrigger>
|
|
87
|
+
</Triggers>
|
|
88
|
+
<Principals>
|
|
89
|
+
<Principal id="Author">
|
|
90
|
+
<UserId>${xmlEscape(username)}</UserId>
|
|
91
|
+
<LogonType>InteractiveToken</LogonType>
|
|
92
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
93
|
+
</Principal>
|
|
94
|
+
</Principals>
|
|
95
|
+
<Settings>
|
|
96
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
97
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
98
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
99
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
100
|
+
<Hidden>true</Hidden>
|
|
101
|
+
</Settings>
|
|
102
|
+
<Actions Context="Author">
|
|
103
|
+
<Exec>
|
|
104
|
+
<Command>cmd.exe</Command>
|
|
105
|
+
<Arguments>/c "${xmlEscape(wrapperPath)}"</Arguments>
|
|
106
|
+
<WorkingDirectory>${xmlEscape(configDir)}</WorkingDirectory>
|
|
107
|
+
</Exec>
|
|
108
|
+
</Actions>
|
|
109
|
+
</Task>`;
|
|
110
|
+
const taskXmlPath = path_1.default.join(configDir, 'task.xml');
|
|
111
|
+
try {
|
|
112
|
+
// Task Scheduler XML must be UTF-16 LE encoded
|
|
113
|
+
fs_1.default.writeFileSync(taskXmlPath, '\ufeff' + taskXml, 'utf16le');
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
console.error('Failed to write task XML:', e);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
73
119
|
try {
|
|
74
120
|
// Delete existing task silently before creating a fresh one
|
|
75
121
|
(0, child_process_2.execSync)(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
@@ -78,13 +124,19 @@ function startDaemonWindows(opts) {
|
|
|
78
124
|
// Task may not exist — that's fine
|
|
79
125
|
}
|
|
80
126
|
try {
|
|
81
|
-
(0, child_process_2.execSync)(`schtasks /create /tn "${taskName}" /
|
|
127
|
+
(0, child_process_2.execSync)(`schtasks /create /tn "${taskName}" /xml "${taskXmlPath}" /f`, { stdio: 'pipe' });
|
|
82
128
|
console.log(`Windows Task Scheduler task created: ${taskName}`);
|
|
83
129
|
console.log('Omnikey daemon will auto-start on next logon.');
|
|
84
130
|
}
|
|
85
131
|
catch (e) {
|
|
86
132
|
console.error('Failed to create Windows Task Scheduler task:', e);
|
|
87
133
|
}
|
|
134
|
+
finally {
|
|
135
|
+
try {
|
|
136
|
+
fs_1.default.rmSync(taskXmlPath);
|
|
137
|
+
}
|
|
138
|
+
catch { /* ignore */ }
|
|
139
|
+
}
|
|
88
140
|
// Also start the backend immediately for the current session
|
|
89
141
|
const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
|
|
90
142
|
const child = (0, child_process_1.spawn)(nodePath, [backendPath], {
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ const killDaemon_1 = require("./killDaemon");
|
|
|
8
8
|
const removeConfig_1 = require("./removeConfig");
|
|
9
9
|
const status_1 = require("./status");
|
|
10
10
|
const showLogs_1 = require("./showLogs");
|
|
11
|
+
const showConfig_1 = require("./showConfig");
|
|
12
|
+
const setConfig_1 = require("./setConfig");
|
|
11
13
|
const program = new commander_1.Command();
|
|
12
14
|
program
|
|
13
15
|
.name('omnikey')
|
|
@@ -56,4 +58,25 @@ program
|
|
|
56
58
|
const errorsOnly = !!options.errors;
|
|
57
59
|
(0, showLogs_1.showLogs)(lines, errorsOnly);
|
|
58
60
|
});
|
|
61
|
+
program
|
|
62
|
+
.command('config')
|
|
63
|
+
.description('Show the current Omnikey configuration (API keys are masked)')
|
|
64
|
+
.action(() => {
|
|
65
|
+
(0, showConfig_1.showConfig)();
|
|
66
|
+
});
|
|
67
|
+
program
|
|
68
|
+
.command('set <key> <value>')
|
|
69
|
+
.description('Set a single configuration key (e.g. omnikey set OMNIKEY_PORT 8080)')
|
|
70
|
+
.action((key, value) => {
|
|
71
|
+
(0, setConfig_1.setConfig)(key, value);
|
|
72
|
+
});
|
|
73
|
+
program
|
|
74
|
+
.command('restart-daemon')
|
|
75
|
+
.description('Restart the Omnikey API backend daemon')
|
|
76
|
+
.option('--port <port>', 'Port to run the backend on', '7071')
|
|
77
|
+
.action((options) => {
|
|
78
|
+
(0, killDaemon_1.killDaemon)();
|
|
79
|
+
const port = Number(options.port) || 7071;
|
|
80
|
+
(0, daemon_1.startDaemon)(port);
|
|
81
|
+
});
|
|
59
82
|
program.parseAsync(process.argv);
|
package/dist/removeConfig.js
CHANGED
|
@@ -117,14 +117,22 @@ function removeConfigAndDb(includeDb = false) {
|
|
|
117
117
|
else {
|
|
118
118
|
console.log('Skipping SQLite database removal (use --db to remove it).');
|
|
119
119
|
}
|
|
120
|
-
// Remove .omnikey
|
|
120
|
+
// Remove all files/folders inside .omnikey except the SQLite database
|
|
121
121
|
if (fs_1.default.existsSync(configDir)) {
|
|
122
122
|
try {
|
|
123
|
-
fs_1.default.
|
|
124
|
-
|
|
123
|
+
const entries = fs_1.default.readdirSync(configDir);
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (entry.endsWith('.sqlite')) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const entryPath = path_1.default.join(configDir, entry);
|
|
129
|
+
fs_1.default.rmSync(entryPath, { recursive: true, force: true });
|
|
130
|
+
console.log(`Removed: ${entryPath}`);
|
|
131
|
+
}
|
|
132
|
+
console.log(`Cleared config directory (SQLite preserved): ${configDir}`);
|
|
125
133
|
}
|
|
126
134
|
catch (e) {
|
|
127
|
-
console.error(`Failed to
|
|
135
|
+
console.error(`Failed to clear config directory: ${e}`);
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
else {
|
|
@@ -0,0 +1,19 @@
|
|
|
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.setConfig = setConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
function setConfig(key, value) {
|
|
10
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
11
|
+
const configPath = (0, utils_1.getConfigPath)();
|
|
12
|
+
if (!fs_1.default.existsSync(configDir)) {
|
|
13
|
+
fs_1.default.mkdirSync(configDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
const config = (0, utils_1.readConfig)();
|
|
16
|
+
config[key] = value;
|
|
17
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
18
|
+
console.log(`Set ${key} in ${configPath}`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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.showConfig = showConfig;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const utils_1 = require("./utils");
|
|
9
|
+
const API_KEY_FIELDS = [
|
|
10
|
+
'OPENAI_API_KEY',
|
|
11
|
+
'ANTHROPIC_API_KEY',
|
|
12
|
+
'GEMINI_API_KEY',
|
|
13
|
+
'SERPER_API_KEY',
|
|
14
|
+
'BRAVE_SEARCH_API_KEY',
|
|
15
|
+
'TAVILY_API_KEY',
|
|
16
|
+
];
|
|
17
|
+
function maskSecret(value) {
|
|
18
|
+
if (value.length <= 8)
|
|
19
|
+
return '****';
|
|
20
|
+
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
21
|
+
}
|
|
22
|
+
function showConfig() {
|
|
23
|
+
const configPath = (0, utils_1.getConfigPath)();
|
|
24
|
+
const configDir = (0, utils_1.getConfigDir)();
|
|
25
|
+
if (!fs_1.default.existsSync(configPath)) {
|
|
26
|
+
console.log('No configuration found. Run `omnikey onboard` to get started.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const config = (0, utils_1.readConfig)();
|
|
30
|
+
const keys = Object.keys(config);
|
|
31
|
+
if (keys.length === 0) {
|
|
32
|
+
console.log('Configuration file exists but is empty.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(`Config file: ${configPath}\n`);
|
|
36
|
+
console.log('Current configuration:');
|
|
37
|
+
console.log('─'.repeat(50));
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
const raw = String(config[key]);
|
|
40
|
+
const display = API_KEY_FIELDS.includes(key) ? maskSecret(raw) : raw;
|
|
41
|
+
console.log(` ${key}: ${display}`);
|
|
42
|
+
}
|
|
43
|
+
console.log('─'.repeat(50));
|
|
44
|
+
console.log(`\nConfig directory: ${configDir}`);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.17",
|
|
8
8
|
"description": "CLI for onboarding users to Omnikey AI and configuring OPENAI_API_KEY. Use Yarn for install/build.",
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=14.0.0",
|
package/src/daemon.ts
CHANGED
|
@@ -85,8 +85,54 @@ function startDaemonWindows(opts: DaemonOptions) {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Register with Windows Task Scheduler so the daemon persists across reboots
|
|
88
|
+
// Register with Windows Task Scheduler so the daemon persists across reboots.
|
|
89
|
+
// Use XML-based registration to avoid cmd.exe quoting issues with paths containing spaces.
|
|
89
90
|
const taskName = 'OmnikeyDaemon';
|
|
91
|
+
const username = process.env.USERNAME || process.env.USER || '';
|
|
92
|
+
// Escape characters that are special in XML
|
|
93
|
+
const xmlEscape = (s: string) =>
|
|
94
|
+
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
95
|
+
const taskXml = `<?xml version="1.0" encoding="UTF-16"?>
|
|
96
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
97
|
+
<RegistrationInfo>
|
|
98
|
+
<Description>Omnikey API Backend Daemon</Description>
|
|
99
|
+
</RegistrationInfo>
|
|
100
|
+
<Triggers>
|
|
101
|
+
<LogonTrigger>
|
|
102
|
+
<Enabled>true</Enabled>
|
|
103
|
+
<UserId>${xmlEscape(username)}</UserId>
|
|
104
|
+
</LogonTrigger>
|
|
105
|
+
</Triggers>
|
|
106
|
+
<Principals>
|
|
107
|
+
<Principal id="Author">
|
|
108
|
+
<UserId>${xmlEscape(username)}</UserId>
|
|
109
|
+
<LogonType>InteractiveToken</LogonType>
|
|
110
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
111
|
+
</Principal>
|
|
112
|
+
</Principals>
|
|
113
|
+
<Settings>
|
|
114
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
115
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
116
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
117
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
118
|
+
<Hidden>true</Hidden>
|
|
119
|
+
</Settings>
|
|
120
|
+
<Actions Context="Author">
|
|
121
|
+
<Exec>
|
|
122
|
+
<Command>cmd.exe</Command>
|
|
123
|
+
<Arguments>/c "${xmlEscape(wrapperPath)}"</Arguments>
|
|
124
|
+
<WorkingDirectory>${xmlEscape(configDir)}</WorkingDirectory>
|
|
125
|
+
</Exec>
|
|
126
|
+
</Actions>
|
|
127
|
+
</Task>`;
|
|
128
|
+
const taskXmlPath = path.join(configDir, 'task.xml');
|
|
129
|
+
try {
|
|
130
|
+
// Task Scheduler XML must be UTF-16 LE encoded
|
|
131
|
+
fs.writeFileSync(taskXmlPath, '\ufeff' + taskXml, 'utf16le');
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error('Failed to write task XML:', e);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
90
136
|
try {
|
|
91
137
|
// Delete existing task silently before creating a fresh one
|
|
92
138
|
execSync(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
|
|
@@ -94,14 +140,13 @@ function startDaemonWindows(opts: DaemonOptions) {
|
|
|
94
140
|
// Task may not exist — that's fine
|
|
95
141
|
}
|
|
96
142
|
try {
|
|
97
|
-
execSync(
|
|
98
|
-
`schtasks /create /tn "${taskName}" /tr "cmd /c \\"${wrapperPath}\\"" /sc ONLOGON /f`,
|
|
99
|
-
{ stdio: 'pipe' },
|
|
100
|
-
);
|
|
143
|
+
execSync(`schtasks /create /tn "${taskName}" /xml "${taskXmlPath}" /f`, { stdio: 'pipe' });
|
|
101
144
|
console.log(`Windows Task Scheduler task created: ${taskName}`);
|
|
102
145
|
console.log('Omnikey daemon will auto-start on next logon.');
|
|
103
146
|
} catch (e) {
|
|
104
147
|
console.error('Failed to create Windows Task Scheduler task:', e);
|
|
148
|
+
} finally {
|
|
149
|
+
try { fs.rmSync(taskXmlPath); } catch { /* ignore */ }
|
|
105
150
|
}
|
|
106
151
|
|
|
107
152
|
// Also start the backend immediately for the current session
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { killDaemon } from './killDaemon';
|
|
|
7
7
|
import { removeConfigAndDb } from './removeConfig';
|
|
8
8
|
import { statusCmd } from './status';
|
|
9
9
|
import { showLogs } from './showLogs';
|
|
10
|
+
import { showConfig } from './showConfig';
|
|
11
|
+
import { setConfig } from './setConfig';
|
|
10
12
|
|
|
11
13
|
const program = new Command();
|
|
12
14
|
|
|
@@ -64,4 +66,28 @@ program
|
|
|
64
66
|
showLogs(lines, errorsOnly);
|
|
65
67
|
});
|
|
66
68
|
|
|
69
|
+
program
|
|
70
|
+
.command('config')
|
|
71
|
+
.description('Show the current Omnikey configuration (API keys are masked)')
|
|
72
|
+
.action(() => {
|
|
73
|
+
showConfig();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command('set <key> <value>')
|
|
78
|
+
.description('Set a single configuration key (e.g. omnikey set OMNIKEY_PORT 8080)')
|
|
79
|
+
.action((key: string, value: string) => {
|
|
80
|
+
setConfig(key, value);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('restart-daemon')
|
|
85
|
+
.description('Restart the Omnikey API backend daemon')
|
|
86
|
+
.option('--port <port>', 'Port to run the backend on', '7071')
|
|
87
|
+
.action((options) => {
|
|
88
|
+
killDaemon();
|
|
89
|
+
const port = Number(options.port) || 7071;
|
|
90
|
+
startDaemon(port);
|
|
91
|
+
});
|
|
92
|
+
|
|
67
93
|
program.parseAsync(process.argv);
|
package/src/removeConfig.ts
CHANGED
|
@@ -111,13 +111,21 @@ export function removeConfigAndDb(includeDb = false) {
|
|
|
111
111
|
console.log('Skipping SQLite database removal (use --db to remove it).');
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// Remove .omnikey
|
|
114
|
+
// Remove all files/folders inside .omnikey except the SQLite database
|
|
115
115
|
if (fs.existsSync(configDir)) {
|
|
116
116
|
try {
|
|
117
|
-
fs.
|
|
118
|
-
|
|
117
|
+
const entries = fs.readdirSync(configDir);
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (entry.endsWith('.sqlite')) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const entryPath = path.join(configDir, entry);
|
|
123
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
124
|
+
console.log(`Removed: ${entryPath}`);
|
|
125
|
+
}
|
|
126
|
+
console.log(`Cleared config directory (SQLite preserved): ${configDir}`);
|
|
119
127
|
} catch (e) {
|
|
120
|
-
console.error(`Failed to
|
|
128
|
+
console.error(`Failed to clear config directory: ${e}`);
|
|
121
129
|
}
|
|
122
130
|
} else {
|
|
123
131
|
console.log(`Config directory does not exist: ${configDir}`);
|
package/src/setConfig.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { readConfig, getConfigPath, getConfigDir } from './utils';
|
|
3
|
+
|
|
4
|
+
export function setConfig(key: string, value: string) {
|
|
5
|
+
const configDir = getConfigDir();
|
|
6
|
+
const configPath = getConfigPath();
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(configDir)) {
|
|
9
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const config = readConfig();
|
|
13
|
+
config[key] = value;
|
|
14
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
15
|
+
console.log(`Set ${key} in ${configPath}`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { readConfig, getConfigPath, getConfigDir } from './utils';
|
|
3
|
+
|
|
4
|
+
const API_KEY_FIELDS = [
|
|
5
|
+
'OPENAI_API_KEY',
|
|
6
|
+
'ANTHROPIC_API_KEY',
|
|
7
|
+
'GEMINI_API_KEY',
|
|
8
|
+
'SERPER_API_KEY',
|
|
9
|
+
'BRAVE_SEARCH_API_KEY',
|
|
10
|
+
'TAVILY_API_KEY',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function maskSecret(value: string): string {
|
|
14
|
+
if (value.length <= 8) return '****';
|
|
15
|
+
return value.slice(0, 4) + '****' + value.slice(-4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function showConfig() {
|
|
19
|
+
const configPath = getConfigPath();
|
|
20
|
+
const configDir = getConfigDir();
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(configPath)) {
|
|
23
|
+
console.log('No configuration found. Run `omnikey onboard` to get started.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const config = readConfig();
|
|
28
|
+
const keys = Object.keys(config);
|
|
29
|
+
|
|
30
|
+
if (keys.length === 0) {
|
|
31
|
+
console.log('Configuration file exists but is empty.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log(`Config file: ${configPath}\n`);
|
|
36
|
+
console.log('Current configuration:');
|
|
37
|
+
console.log('─'.repeat(50));
|
|
38
|
+
|
|
39
|
+
for (const key of keys) {
|
|
40
|
+
const raw = String(config[key]);
|
|
41
|
+
const display = API_KEY_FIELDS.includes(key) ? maskSecret(raw) : raw;
|
|
42
|
+
console.log(` ${key}: ${display}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('─'.repeat(50));
|
|
46
|
+
console.log(`\nConfig directory: ${configDir}`);
|
|
47
|
+
}
|