omnikey-cli 1.0.16 → 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 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
- # Remove the config directory and SQLite database (and persistence agent)
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
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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 &quot;${xmlEscape(wrapperPath)}&quot;</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}" /tr "cmd /c \\"${wrapperPath}\\"" /sc ONLOGON /f`, { stdio: 'pipe' });
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);
@@ -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 directory
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.rmSync(configDir, { recursive: true, force: true });
124
- console.log(`Removed config directory: ${configDir}`);
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 remove config directory: ${e}`);
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.16",
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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 &quot;${xmlEscape(wrapperPath)}&quot;</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);
@@ -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 directory
114
+ // Remove all files/folders inside .omnikey except the SQLite database
115
115
  if (fs.existsSync(configDir)) {
116
116
  try {
117
- fs.rmSync(configDir, { recursive: true, force: true });
118
- console.log(`Removed config directory: ${configDir}`);
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 remove config directory: ${e}`);
128
+ console.error(`Failed to clear config directory: ${e}`);
121
129
  }
122
130
  } else {
123
131
  console.log(`Config directory does not exist: ${configDir}`);
@@ -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
+ }