omnikey-cli 1.0.11 → 1.0.12

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
@@ -2,9 +2,9 @@
2
2
 
3
3
  A command-line tool for onboarding users to the Omnikey open-source app and configuring their OPENAI_API_KEY.
4
4
 
5
- ## About OmnikeyAI (macOS)
5
+ ## About OmnikeyAI
6
6
 
7
- OmnikeyAI is a productivity tool for macOS that helps you quickly rewrite selected text using OpenAI. The CLI allows you to configure and run the backend daemon on your local macOS and manage your OpenAI API key with ease. Once set up, you can select any text on your Mac in any app and trigger rewrite commands directly from your desktop.
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.
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)
@@ -13,7 +13,7 @@ OmnikeyAI is a productivity tool for macOS that helps you quickly rewrite select
13
13
 
14
14
  - `omnikey onboard`: Interactive onboarding to set up your OPENAI_API_KEY.
15
15
  - Accepts the `--open-ai-key` parameter for non-interactive setup.
16
- - Configure and run the backend daemon for the macOS app.
16
+ - Configure and run the backend daemon persisted across reboots on both macOS and Windows.
17
17
 
18
18
  ## Usage
19
19
 
@@ -21,28 +21,42 @@ OmnikeyAI is a productivity tool for macOS that helps you quickly rewrite select
21
21
  # Install CLI globally (from this directory)
22
22
  npm install -g omnikey-cli
23
23
 
24
- # Onboard interactively (will prompt for OpenAI key and self-hosting)
24
+ # Onboard interactively (will prompt for OpenAI key)
25
25
  omnikey onboard
26
26
 
27
27
  # Or onboard non-interactively
28
28
  omnikey onboard --open-ai-key YOUR_KEY
29
29
 
30
- # Running the daemon will set up a launchd agent and keep the backend server running across system restarts
30
+ # Start the daemon (auto-restarts on reboot)
31
31
  omnikey daemon --port 7071
32
32
 
33
33
  # Kill the daemon
34
- omnikey kill-daemon --port 7071
34
+ omnikey kill-daemon
35
35
 
36
- # Remove the config directory and SQLite database (and launchd agent)
36
+ # Remove the config directory and SQLite database (and persistence agent)
37
37
  omnikey remove-config
38
38
 
39
- # check daemon status if it is running
39
+ # Check daemon status
40
40
  omnikey status
41
41
 
42
- # check daemon logs
42
+ # Check daemon logs
43
43
  omnikey logs --lines 100
44
44
  ```
45
45
 
46
+ ## Platform notes
47
+
48
+ ### macOS
49
+
50
+ The daemon is registered as a **launchd agent** (`~/Library/LaunchAgents/com.omnikey.daemon.plist`) so it auto-restarts after login and on crashes.
51
+
52
+ ### Windows
53
+
54
+ The daemon is registered as a **Windows Task Scheduler** task (`OmnikeyDaemon`) that runs at every logon. A wrapper script (`~/.omnikey/start-daemon.cmd`) is generated to set the required environment variables before launching the Node.js backend.
55
+
56
+ > **Note:** `schtasks` is a built-in Windows command — no third-party tools or administrator rights are required for user-level scheduled tasks.
57
+
58
+ Commands that query process state use `netstat` (instead of `lsof`) on Windows, and process termination uses `taskkill` (instead of `SIGTERM`).
59
+
46
60
  ## Development
47
61
 
48
62
  - Built with Node.js and TypeScript.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AGENT_SYSTEM_PROMPT = void 0;
4
- exports.AGENT_SYSTEM_PROMPT = `
3
+ exports.AGENT_SYSTEM_PROMPT_WINDOWS = exports.AGENT_SYSTEM_PROMPT_MACOS = void 0;
4
+ exports.AGENT_SYSTEM_PROMPT_MACOS = `
5
5
  You are an AI agent that can both reason about the user's situation and design shell scripts that the user will run on their own machine.
6
6
 
7
7
  This agent is invoked when the user includes @omniAgent and there may also be stored custom task instructions for the current task.
@@ -62,3 +62,63 @@ The user will run the script and share the output with you.
62
62
  - Do not emit any text before or after the <final_answer> block; the entire response must be inside the <final_answer> tags.
63
63
  </final_answer_block>
64
64
  `;
65
+ exports.AGENT_SYSTEM_PROMPT_WINDOWS = `
66
+ You are an AI agent that can both reason about the user's situation and design shell scripts that the user will run on their own machine.
67
+
68
+ This agent is invoked when the user includes @omniAgent and there may also be stored custom task instructions for the current task.
69
+ Your job is to:
70
+ - Read and respect the stored task instructions (how to behave, what to focus on, output style) when they are provided.
71
+ - Carefully consider the current user input (what they typed when running @omniAgent).
72
+ - Decide whether additional machine-level information is needed, and if so, generate an appropriate shell script to gather it.
73
+ - Use the results of any previously run scripts plus the instructions and input to produce a complete, helpful final answer.
74
+
75
+ General guidelines:
76
+ - Only create commands that are safe and read-only, focusing on inspection, diagnostics, and information gathering.
77
+ - Do not generate any commands that install software, modify user data, or change system settings.
78
+ - Never ask the user to run commands with elevated privileges (Run as Administrator).
79
+ - Ensure that all commands provided are compatible with Windows PowerShell; avoid any macOS or Linux-specific commands.
80
+ - Scripts must be self-contained and ready to run as-is, without the user needing to edit them.
81
+
82
+ The user will run the script and share the output with you.
83
+
84
+ <instruction_handling>
85
+ - 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.
86
+ - Treat the current user input as the immediate goal or question you must solve, applying the stored instructions to that specific situation.
87
+ - If there is a conflict, follow: system rules first, then stored instructions, then ad-hoc guidance in the current input.
88
+ </instruction_handling>
89
+
90
+ <interaction_rules>
91
+ - When you need to execute ANY shell command, respond with a single <shell_script> block that contains the FULL script to run.
92
+ - 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.
93
+ - 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.
94
+ - If further machine-level investigation is unnecessary, skip the shell script and respond directly with a <final_answer>.
95
+ - Every response MUST be exactly one of:
96
+ - A single <shell_script>...</shell_script> block, and nothing else; or
97
+ - A single <final_answer>...</final_answer> block, and nothing else.
98
+ - Never send plain text or explanation outside of these tags. If you are not emitting a <shell_script>, you MUST emit a <final_answer>.
99
+ - When you are completely finished and ready to present the result back to the user, respond with a single <final_answer> block.
100
+ - Do NOT include reasoning, commentary, or any other tags outside of <shell_script>...</shell_script> or <final_answer>...</final_answer>.
101
+ - Never wrap your entire response in other XML or JSON structures.
102
+ </interaction_rules>
103
+
104
+ <shell_script_block>
105
+ - Always emit exactly this structure when you want to run commands:
106
+
107
+ <shell_script>
108
+ # your commands here
109
+ </shell_script>
110
+
111
+ - Use a single, self-contained PowerShell script per turn; do not send multiple <shell_script> blocks in one response.
112
+ - Inside the script, group related commands logically and add brief inline comments ONLY when they clarify non-obvious steps.
113
+ - Prefer safe, idempotent commands. Never use elevated privileges.
114
+ - Use PowerShell cmdlets and syntax (e.g. Get-ChildItem, Select-Object, Where-Object) rather than cmd.exe or bash equivalents.
115
+ </shell_script_block>
116
+
117
+ <final_answer_block>
118
+ - When you have gathered enough information and completed the requested work, respond once with:
119
+ <final_answer>
120
+ ...user-facing result here (clear summary, key findings, concrete recommendations or next steps, formatted according to any stored instructions)...
121
+ </final_answer>
122
+ - Do not emit any text before or after the <final_answer> block; the entire response must be inside the <final_answer> tags.
123
+ </final_answer_block>
124
+ `;
@@ -53,7 +53,7 @@ const openai = new openai_1.default({
53
53
  });
54
54
  const sessionMessages = new Map();
55
55
  const MAX_TURNS = 10;
56
- async function getOrCreateSession(sessionId, subscription, log) {
56
+ async function getOrCreateSession(sessionId, subscription, platform, log) {
57
57
  const existing = sessionMessages.get(sessionId);
58
58
  if (existing) {
59
59
  log.debug('Reusing existing agent session', {
@@ -63,6 +63,7 @@ async function getOrCreateSession(sessionId, subscription, log) {
63
63
  });
64
64
  return existing;
65
65
  }
66
+ const systemPrompt = platform === 'windows' ? agentPrompts_1.AGENT_SYSTEM_PROMPT_WINDOWS : agentPrompts_1.AGENT_SYSTEM_PROMPT_MACOS;
66
67
  // use these instructions as user instructions
67
68
  const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
68
69
  log.error('Failed to get system prompt for new agent session', { error: err });
@@ -73,7 +74,7 @@ async function getOrCreateSession(sessionId, subscription, log) {
73
74
  history: [
74
75
  {
75
76
  role: 'system',
76
- content: agentPrompts_1.AGENT_SYSTEM_PROMPT,
77
+ content: systemPrompt,
77
78
  },
78
79
  ...(prompt
79
80
  ? [
@@ -163,7 +164,7 @@ async function authenticateFromAuthHeader(authHeader, log) {
163
164
  }
164
165
  }
165
166
  async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
166
- const session = await getOrCreateSession(sessionId, subscription, log);
167
+ const session = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log);
167
168
  // Count this call as one agent iteration.
168
169
  session.turns += 1;
169
170
  log.info('Starting agent turn', {
@@ -51,8 +51,8 @@ app.get('/macos/appcast', (req, res) => {
51
51
  const appcastUrl = `${baseUrl}/macos/appcast`;
52
52
  // These should match the values embedded into the macOS app
53
53
  // Info.plist in macOS/build_release_dmg.sh.
54
- const bundleVersion = '12';
55
- const shortVersion = '1.0.11';
54
+ const bundleVersion = '13';
55
+ const shortVersion = '1.0.12';
56
56
  const xml = `<?xml version="1.0" encoding="utf-8"?>
57
57
  <rss version="2.0"
58
58
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
@@ -77,6 +77,42 @@ app.get('/macos/appcast', (req, res) => {
77
77
  res.set('Content-Type', 'application/xml; charset=utf-8');
78
78
  res.send(xml);
79
79
  });
80
+ // ── Windows distribution endpoints ───────────────────────────────────────────
81
+ // These should match the values in windows/OmniKey.Windows.csproj
82
+ // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
83
+ const WIN_VERSION = '1.0';
84
+ const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-x64.zip';
85
+ const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
86
+ // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
87
+ app.get('/windows/download', (_req, res) => {
88
+ res.download(WIN_ZIP_PATH, WIN_ZIP_FILENAME, (err) => {
89
+ if (err) {
90
+ logger_1.logger.error('Failed to send Windows ZIP for download.', { error: err });
91
+ if (!res.headersSent) {
92
+ res.status(500).send('Unable to download file.');
93
+ }
94
+ }
95
+ });
96
+ });
97
+ // JSON update-check endpoint consumed by UpdateChecker.cs on the Windows client.
98
+ // Returns the latest version + download URL so the client can decide whether
99
+ // to prompt the user for an update.
100
+ app.get('/windows/update', (req, res) => {
101
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
102
+ let fileSize = 0;
103
+ try {
104
+ fileSize = fs_1.default.statSync(WIN_ZIP_PATH).size;
105
+ }
106
+ catch (error) {
107
+ logger_1.logger.error('Failed to stat Windows ZIP for update endpoint.', { error });
108
+ }
109
+ res.json({
110
+ version: WIN_VERSION,
111
+ downloadUrl: `${baseUrl}/windows/download`,
112
+ fileSize,
113
+ releaseNotes: '',
114
+ });
115
+ });
80
116
  app.get('/health', (_req, res) => {
81
117
  res.json({ status: 'ok' });
82
118
  });
package/dist/daemon.js CHANGED
@@ -7,33 +7,20 @@ exports.startDaemon = startDaemon;
7
7
  const child_process_1 = require("child_process");
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
- const os_1 = __importDefault(require("os"));
11
10
  const child_process_2 = require("child_process");
11
+ const utils_1 = require("./utils");
12
12
  /**
13
13
  * Start the Omnikey API backend as a daemon on the specified port.
14
- * Also creates and registers a launchd agent for persistence on macOS.
14
+ * On macOS: creates and registers a launchd agent for persistence.
15
+ * On Windows: creates a wrapper script and registers a Windows Task Scheduler task.
15
16
  * @param port The port to run the backend on
16
17
  */
17
18
  function startDaemon(port = 7071) {
18
- // Only use ~/.omnikey/config.json for environment variables
19
- // Path to the backend entry point (now from backend-dist)
20
19
  const backendPath = path_1.default.resolve(__dirname, '../backend-dist/index.js');
21
- // Read and update environment variables from ~/.omnikey/config.json
22
- const homeDir = process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
23
- const configDir = path_1.default.join(homeDir, '.omnikey');
24
- const configPath = path_1.default.join(configDir, 'config.json');
25
- let configVars = {};
26
- if (fs_1.default.existsSync(configPath)) {
27
- try {
28
- configVars = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
29
- }
30
- catch (e) {
31
- console.error('Failed to parse config.json:', e);
32
- }
33
- }
34
- // Ensure both OMNIKEY_PORT and PORT are set for compatibility
20
+ const configDir = (0, utils_1.getConfigDir)();
21
+ const configPath = (0, utils_1.getConfigPath)();
22
+ const configVars = (0, utils_1.readConfig)();
35
23
  configVars.OMNIKEY_PORT = port;
36
- // Write the updated configVars back to config.json
37
24
  try {
38
25
  fs_1.default.mkdirSync(configDir, { recursive: true });
39
26
  fs_1.default.writeFileSync(configPath, JSON.stringify(configVars, null, 2), 'utf-8');
@@ -41,10 +28,69 @@ function startDaemon(port = 7071) {
41
28
  catch (e) {
42
29
  console.error('Failed to write updated config.json:', e);
43
30
  }
44
- // Create launchd agent for persistence
31
+ const nodePath = process.execPath;
32
+ const logPath = path_1.default.join(configDir, 'daemon.log');
33
+ const errorLogPath = path_1.default.join(configDir, 'daemon-error.log');
34
+ if (utils_1.isWindows) {
35
+ startDaemonWindows({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
36
+ }
37
+ else {
38
+ startDaemonMacOS({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
39
+ }
40
+ }
41
+ function startDaemonWindows(opts) {
42
+ const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
43
+ // Write a wrapper .cmd script that sets env vars and launches the backend
44
+ const wrapperPath = path_1.default.join(configDir, 'start-daemon.cmd');
45
+ const envSetLines = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
46
+ .map(([k, v]) => `set "${k}=${v}"`)
47
+ .join('\r\n');
48
+ const wrapperContent = [
49
+ '@echo off',
50
+ envSetLines,
51
+ `"${nodePath}" "${backendPath}" >> "${logPath}" 2>> "${errorLogPath}"`,
52
+ '',
53
+ ].join('\r\n');
54
+ try {
55
+ fs_1.default.mkdirSync(configDir, { recursive: true });
56
+ fs_1.default.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
57
+ }
58
+ catch (e) {
59
+ console.error('Failed to write start-daemon.cmd:', e);
60
+ return;
61
+ }
62
+ // Register with Windows Task Scheduler so the daemon persists across reboots
63
+ const taskName = 'OmnikeyDaemon';
64
+ try {
65
+ // Delete existing task silently before creating a fresh one
66
+ (0, child_process_2.execSync)(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
67
+ }
68
+ catch {
69
+ // Task may not exist — that's fine
70
+ }
71
+ try {
72
+ (0, child_process_2.execSync)(`schtasks /create /tn "${taskName}" /tr "cmd /c \\"${wrapperPath}\\"" /sc ONLOGON /f`, { stdio: 'pipe' });
73
+ console.log(`Windows Task Scheduler task created: ${taskName}`);
74
+ console.log('Omnikey daemon will auto-start on next logon.');
75
+ }
76
+ catch (e) {
77
+ console.error('Failed to create Windows Task Scheduler task:', e);
78
+ }
79
+ // Also start the backend immediately for the current session
80
+ const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
81
+ const child = (0, child_process_1.spawn)(nodePath, [backendPath], {
82
+ env: { ...process.env, ...configVars, OMNIKEY_PORT: String(port) },
83
+ detached: true,
84
+ stdio: ['ignore', out, err],
85
+ });
86
+ child.unref();
87
+ console.log(`Omnikey API backend started as a daemon on port ${port}. PID: ${child.pid}`);
88
+ }
89
+ function startDaemonMacOS(opts) {
90
+ const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
91
+ const homeDir = (0, utils_1.getHomeDir)();
45
92
  const plistName = 'com.omnikey.daemon.plist';
46
93
  const plistPath = path_1.default.join(homeDir, 'Library', 'LaunchAgents', plistName);
47
- const nodePath = process.execPath;
48
94
  const envVars = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
49
95
  .map(([k, v]) => `<key>${k}</key><string>${v}</string>`)
50
96
  .join('\n');
@@ -68,21 +114,19 @@ function startDaemon(port = 7071) {
68
114
  <key>KeepAlive</key>
69
115
  <true/>
70
116
  <key>StandardOutPath</key>
71
- <string>${path_1.default.join(configDir, 'daemon.log')}</string>
117
+ <string>${logPath}</string>
72
118
  <key>StandardErrorPath</key>
73
- <string>${path_1.default.join(configDir, 'daemon-error.log')}</string>
119
+ <string>${errorLogPath}</string>
74
120
  <key>WorkingDirectory</key>
75
121
  <string>${configDir}</string>
76
122
  </dict>
77
123
  </plist>
78
124
  `;
79
- // Write plist file
80
125
  try {
81
126
  const launchAgentsDir = path_1.default.join(homeDir, 'Library', 'LaunchAgents');
82
127
  fs_1.default.mkdirSync(launchAgentsDir, { recursive: true });
83
128
  fs_1.default.writeFileSync(plistPath, plistContent, 'utf-8');
84
- // Load the launch agent
85
- (0, child_process_2.execSync)(`launchctl unload "${plistPath}" || true`); // Unload if already loaded
129
+ (0, child_process_2.execSync)(`launchctl unload "${plistPath}" || true`);
86
130
  (0, child_process_2.execSync)(`launchctl load "${plistPath}"`);
87
131
  console.log(`Launch agent created and loaded: ${plistPath}`);
88
132
  console.log('Omnikey daemon will auto-restart and persist across reboots.');
@@ -90,19 +134,7 @@ function startDaemon(port = 7071) {
90
134
  catch (e) {
91
135
  console.error('Failed to create or load launch agent:', e);
92
136
  }
93
- // Also start the backend immediately for current session
94
- const logPath = path_1.default.join(configDir, 'daemon.log');
95
- const errorLogPath = path_1.default.join(configDir, 'daemon-error.log');
96
- // Clean (truncate) log files before starting new session
97
- try {
98
- fs_1.default.writeFileSync(logPath, '');
99
- fs_1.default.writeFileSync(errorLogPath, '');
100
- }
101
- catch (e) {
102
- // Ignore errors if files don't exist yet
103
- }
104
- const out = fs_1.default.openSync(logPath, 'a');
105
- const err = fs_1.default.openSync(errorLogPath, 'a');
137
+ const { out, err } = (0, utils_1.initLogFiles)(logPath, errorLogPath);
106
138
  const child = (0, child_process_1.spawn)('node', [backendPath], {
107
139
  env: { ...configVars, OMNIKEY_PORT: String(port) },
108
140
  detached: true,
package/dist/index.js CHANGED
@@ -31,10 +31,8 @@ program
31
31
  program
32
32
  .command('kill-daemon')
33
33
  .description('Kill the Omnikey API backend daemon running on a specified port')
34
- .option('--port <port>', 'Port to look for the daemon on', '7071')
35
- .action((options) => {
36
- const port = Number(options.port) || 7071;
37
- (0, killDaemon_1.killDaemon)(port);
34
+ .action(() => {
35
+ (0, killDaemon_1.killDaemon)();
38
36
  });
39
37
  program
40
38
  .command('remove-config')
@@ -3,37 +3,58 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.killDaemon = killDaemon;
4
4
  const child_process_1 = require("child_process");
5
5
  const removeConfig_1 = require("./removeConfig");
6
+ const utils_1 = require("./utils");
6
7
  /**
7
- * Kill the Omnikey API backend daemon running on a given port (default 7071).
8
- * Looks for node processes running backend-dist/index.js on the specified port and kills them.
9
- * @param port The port to look for (default 7071)
8
+ * Kill the Omnikey API backend daemon.
9
+ * Reads the port from ~/.omnikey/config.json (falls back to 7071).
10
+ * Removes the persistence agent, then kills any remaining process on the port.
10
11
  */
11
- function killDaemon(port = 7071) {
12
- // 1. Unload/kill the launchd agent first
12
+ function killDaemon() {
13
+ const port = (0, utils_1.getPort)();
14
+ // 1. Remove/stop the persistence agent
13
15
  try {
14
- (0, removeConfig_1.killLaunchdAgent)();
15
- console.log('Launchd agent unloaded (if it existed).');
16
+ (0, removeConfig_1.killPersistenceAgent)();
17
+ console.log('Persistence agent stopped (if it existed).');
16
18
  }
17
19
  catch (e) {
18
- console.warn('Failed to unload launchd agent or agent did not exist:', e);
20
+ console.warn('Failed to stop persistence agent:', e);
19
21
  }
20
- // 2. Check if the port is still in use
22
+ // 2. Find any remaining processes still using the port
21
23
  let pids = [];
22
24
  try {
23
- pids = (0, child_process_1.execSync)(`lsof -i :${port} -t`).toString().split('\n').filter(Boolean);
25
+ if (utils_1.isWindows) {
26
+ // netstat -ano lists PID in the last column; filter by :<port> with LISTENING or ESTABLISHED
27
+ const output = (0, child_process_1.execSync)(`netstat -ano | findstr :${port}`).toString();
28
+ const seen = new Set();
29
+ for (const line of output.trim().split('\n')) {
30
+ const parts = line.trim().split(/\s+/);
31
+ const pid = parts[parts.length - 1];
32
+ if (pid && /^\d+$/.test(pid) && pid !== '0' && !seen.has(pid)) {
33
+ seen.add(pid);
34
+ pids.push(pid);
35
+ }
36
+ }
37
+ }
38
+ else {
39
+ pids = (0, child_process_1.execSync)(`lsof -i :${port} -t`).toString().split('\n').filter(Boolean);
40
+ }
24
41
  }
25
- catch (e) {
26
- // lsof returns non-zero exit code if nothing is using the port
42
+ catch {
27
43
  pids = [];
28
44
  }
29
45
  if (pids.length === 0) {
30
- console.log(`No process found using port ${port} after unloading launchd agent.`);
46
+ console.log(`No process found using port ${port}.`);
31
47
  return;
32
48
  }
33
- // 3. If the port is still occupied, kill the process using the port
49
+ // 3. Kill each process
34
50
  for (const pid of pids) {
35
51
  try {
36
- process.kill(Number(pid), 'SIGTERM');
52
+ if (utils_1.isWindows) {
53
+ (0, child_process_1.execSync)(`taskkill /PID ${pid} /F`, { stdio: 'pipe' });
54
+ }
55
+ else {
56
+ process.kill(Number(pid), 'SIGTERM');
57
+ }
37
58
  console.log(`Killed process with PID ${pid} using port ${port}.`);
38
59
  }
39
60
  catch (e) {
package/dist/onboard.js CHANGED
@@ -7,14 +7,14 @@ exports.onboard = onboard;
7
7
  const inquirer_1 = __importDefault(require("inquirer"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const utils_1 = require("./utils");
10
11
  /**
11
12
  * Onboard the user by configuring their OPENAI_API_KEY and generating a .env for self-hosted use.
12
13
  * @param openAiKey Optional key provided via CLI
13
14
  */
14
15
  async function onboard(openAiKey) {
15
16
  let apiKey = openAiKey;
16
- const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
17
- const configDir = path_1.default.join(homeDir, '.omnikey');
17
+ const configDir = (0, utils_1.getConfigDir)();
18
18
  const sqlitePath = path_1.default.join(configDir, 'omnikey-selfhosted.sqlite');
19
19
  if (!apiKey) {
20
20
  const answers = await inquirer_1.default.prompt([
@@ -28,7 +28,7 @@ async function onboard(openAiKey) {
28
28
  apiKey = answers.apiKey;
29
29
  }
30
30
  // Save all environment variables to ~/.omnikey/config.json
31
- const configPath = path_1.default.join(configDir, 'config.json');
31
+ const configPath = (0, utils_1.getConfigPath)();
32
32
  fs_1.default.mkdirSync(configDir, { recursive: true });
33
33
  const configVars = {
34
34
  OPENAI_API_KEY: apiKey,
@@ -4,15 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.killLaunchdAgent = killLaunchdAgent;
7
+ exports.killWindowsTask = killWindowsTask;
8
+ exports.killPersistenceAgent = killPersistenceAgent;
7
9
  exports.removeConfigAndDb = removeConfigAndDb;
8
10
  const fs_1 = __importDefault(require("fs"));
9
11
  const path_1 = __importDefault(require("path"));
10
- const os_1 = __importDefault(require("os"));
11
12
  const child_process_1 = require("child_process");
13
+ const utils_1 = require("./utils");
12
14
  function killLaunchdAgent() {
13
- const homeDir = process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
14
15
  const plistName = 'com.omnikey.daemon.plist';
15
- const plistPath = path_1.default.join(homeDir, 'Library', 'LaunchAgents', plistName);
16
+ const plistPath = path_1.default.join((0, utils_1.getHomeDir)(), 'Library', 'LaunchAgents', plistName);
16
17
  if (fs_1.default.existsSync(plistPath)) {
17
18
  try {
18
19
  (0, child_process_1.execSync)(`launchctl unload "${plistPath}"`);
@@ -27,30 +28,58 @@ function killLaunchdAgent() {
27
28
  console.log(`Launchd agent does not exist: ${plistPath}`);
28
29
  }
29
30
  }
31
+ function killWindowsTask() {
32
+ const taskName = 'OmnikeyDaemon';
33
+ try {
34
+ (0, child_process_1.execSync)(`schtasks /end /tn "${taskName}"`, { stdio: 'pipe' });
35
+ }
36
+ catch {
37
+ // Task may not be running — that's fine
38
+ }
39
+ try {
40
+ (0, child_process_1.execSync)(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
41
+ console.log(`Removed Windows Task Scheduler task: ${taskName}`);
42
+ }
43
+ catch {
44
+ console.log(`Windows Task Scheduler task does not exist: ${taskName}`);
45
+ }
46
+ // Also remove the wrapper script
47
+ const wrapperPath = path_1.default.join((0, utils_1.getConfigDir)(), 'start-daemon.cmd');
48
+ if (fs_1.default.existsSync(wrapperPath)) {
49
+ try {
50
+ fs_1.default.rmSync(wrapperPath);
51
+ }
52
+ catch {
53
+ // Ignore
54
+ }
55
+ }
56
+ }
57
+ /**
58
+ * Kill the platform-appropriate persistence agent (launchd on macOS, Task Scheduler on Windows).
59
+ */
60
+ function killPersistenceAgent() {
61
+ if (utils_1.isWindows) {
62
+ killWindowsTask();
63
+ }
64
+ else {
65
+ killLaunchdAgent();
66
+ }
67
+ }
30
68
  /**
31
69
  * Removes the ~/.omnikey config directory and the SQLite database file specified in config.json.
32
70
  */
33
71
  function removeConfigAndDb() {
34
- const homeDir = process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
35
- const configDir = path_1.default.join(homeDir, '.omnikey');
36
- const configPath = path_1.default.join(configDir, 'config.json');
72
+ const homeDir = (0, utils_1.getHomeDir)();
73
+ const configDir = (0, utils_1.getConfigDir)();
74
+ const configData = (0, utils_1.readConfig)();
37
75
  let sqlitePath = path_1.default.join(homeDir, 'omnikey-selfhosted.sqlite');
38
- // Try to read SQLITE_PATH from config.json
39
- if (fs_1.default.existsSync(configPath)) {
40
- try {
41
- const configData = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
42
- if (configData.SQLITE_PATH) {
43
- sqlitePath = path_1.default.isAbsolute(configData.SQLITE_PATH)
44
- ? configData.SQLITE_PATH
45
- : path_1.default.join(homeDir, configData.SQLITE_PATH);
46
- }
47
- }
48
- catch (e) {
49
- console.error(`Failed to read config.json: ${e}`);
50
- }
76
+ if (configData.SQLITE_PATH) {
77
+ sqlitePath = path_1.default.isAbsolute(configData.SQLITE_PATH)
78
+ ? configData.SQLITE_PATH
79
+ : path_1.default.join(homeDir, configData.SQLITE_PATH);
51
80
  }
52
- // Remove launchd agent if exists (macOS)
53
- killLaunchdAgent();
81
+ // Remove platform-appropriate persistence agent
82
+ killPersistenceAgent();
54
83
  // Remove SQLite database
55
84
  if (fs_1.default.existsSync(sqlitePath)) {
56
85
  try {
package/dist/showLogs.js CHANGED
@@ -6,15 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.showLogs = showLogs;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const utils_1 = require("./utils");
9
10
  /**
10
11
  * Show the logs of the running Omnikey daemon by printing the contents of the daemon log file.
11
12
  * Prints the last N lines (default 50) for convenience.
12
13
  * If errorsOnly is true, shows daemon-error.log instead.
13
14
  */
14
15
  function showLogs(lines = 50, errorsOnly = false) {
15
- const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
16
- const configDir = path_1.default.join(homeDir, '.omnikey');
17
- const logPath = path_1.default.join(configDir, errorsOnly ? 'daemon-error.log' : 'daemon.log');
16
+ const logPath = path_1.default.join((0, utils_1.getConfigDir)(), errorsOnly ? 'daemon-error.log' : 'daemon.log');
18
17
  if (!fs_1.default.existsSync(logPath)) {
19
18
  console.log(errorsOnly ? 'No error logs found.' : 'No daemon logs found.');
20
19
  return;
package/dist/status.js CHANGED
@@ -1,31 +1,18 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.statusCmd = statusCmd;
7
- const path_1 = __importDefault(require("path"));
8
- const fs_1 = __importDefault(require("fs"));
9
4
  const child_process_1 = require("child_process");
5
+ const utils_1 = require("./utils");
10
6
  function statusCmd() {
11
- // Read port from ~/.omnikey/config.json
12
- const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
13
- const configDir = path_1.default.join(homeDir, '.omnikey');
14
- const configPath = path_1.default.join(configDir, 'config.json');
15
- let port = 7071;
16
- if (fs_1.default.existsSync(configPath)) {
17
- try {
18
- const configVars = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
19
- if (configVars.OMNIKEY_PORT) {
20
- port = Number(configVars.OMNIKEY_PORT);
21
- }
7
+ const port = (0, utils_1.getPort)();
8
+ try {
9
+ let output;
10
+ if (utils_1.isWindows) {
11
+ output = (0, child_process_1.execSync)(`netstat -ano | findstr :${port}`).toString();
22
12
  }
23
- catch (e) {
24
- console.error('Failed to read config.json:', e);
13
+ else {
14
+ output = (0, child_process_1.execSync)(`lsof -i :${port}`).toString();
25
15
  }
26
- }
27
- try {
28
- const output = (0, child_process_1.execSync)(`lsof -i :${port}`).toString();
29
16
  if (output.trim()) {
30
17
  console.log(`Processes using port ${port}:\n${output}`);
31
18
  }
@@ -33,7 +20,7 @@ function statusCmd() {
33
20
  console.log(`No process is using port ${port}.`);
34
21
  }
35
22
  }
36
- catch (e) {
23
+ catch {
37
24
  console.log(`No process is using port ${port}.`);
38
25
  }
39
26
  }
package/dist/utils.js ADDED
@@ -0,0 +1,54 @@
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.isWindows = void 0;
7
+ exports.getHomeDir = getHomeDir;
8
+ exports.getConfigDir = getConfigDir;
9
+ exports.getConfigPath = getConfigPath;
10
+ exports.readConfig = readConfig;
11
+ exports.getPort = getPort;
12
+ exports.initLogFiles = initLogFiles;
13
+ const os_1 = __importDefault(require("os"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const fs_1 = __importDefault(require("fs"));
16
+ exports.isWindows = process.platform === 'win32';
17
+ function getHomeDir() {
18
+ return process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
19
+ }
20
+ function getConfigDir() {
21
+ return path_1.default.join(getHomeDir(), '.omnikey');
22
+ }
23
+ function getConfigPath() {
24
+ return path_1.default.join(getConfigDir(), 'config.json');
25
+ }
26
+ function readConfig() {
27
+ const configPath = getConfigPath();
28
+ if (fs_1.default.existsSync(configPath)) {
29
+ try {
30
+ return JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
31
+ }
32
+ catch {
33
+ // fall through to empty config
34
+ }
35
+ }
36
+ return {};
37
+ }
38
+ function getPort() {
39
+ const config = readConfig();
40
+ return config.OMNIKEY_PORT ? Number(config.OMNIKEY_PORT) : 7071;
41
+ }
42
+ function initLogFiles(logPath, errorLogPath) {
43
+ try {
44
+ fs_1.default.writeFileSync(logPath, '');
45
+ fs_1.default.writeFileSync(errorLogPath, '');
46
+ }
47
+ catch {
48
+ // Ignore if files don't exist yet
49
+ }
50
+ return {
51
+ out: fs_1.default.openSync(logPath, 'a'),
52
+ err: fs_1.default.openSync(errorLogPath, 'a'),
53
+ };
54
+ }
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.11",
7
+ "version": "1.0.12",
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
@@ -1,35 +1,22 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
- import os from 'os';
5
4
  import { execSync } from 'child_process';
5
+ import { isWindows, getHomeDir, getConfigDir, getConfigPath, readConfig, initLogFiles } from './utils';
6
6
 
7
7
  /**
8
8
  * Start the Omnikey API backend as a daemon on the specified port.
9
- * Also creates and registers a launchd agent for persistence on macOS.
9
+ * On macOS: creates and registers a launchd agent for persistence.
10
+ * On Windows: creates a wrapper script and registers a Windows Task Scheduler task.
10
11
  * @param port The port to run the backend on
11
12
  */
12
13
  export function startDaemon(port: number = 7071) {
13
- // Only use ~/.omnikey/config.json for environment variables
14
-
15
- // Path to the backend entry point (now from backend-dist)
16
14
  const backendPath = path.resolve(__dirname, '../backend-dist/index.js');
17
15
 
18
- // Read and update environment variables from ~/.omnikey/config.json
19
- const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
20
- const configDir = path.join(homeDir, '.omnikey');
21
- const configPath = path.join(configDir, 'config.json');
22
- let configVars: Record<string, any> = {};
23
- if (fs.existsSync(configPath)) {
24
- try {
25
- configVars = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
26
- } catch (e) {
27
- console.error('Failed to parse config.json:', e);
28
- }
29
- }
30
- // Ensure both OMNIKEY_PORT and PORT are set for compatibility
16
+ const configDir = getConfigDir();
17
+ const configPath = getConfigPath();
18
+ const configVars = readConfig();
31
19
  configVars.OMNIKEY_PORT = port;
32
- // Write the updated configVars back to config.json
33
20
  try {
34
21
  fs.mkdirSync(configDir, { recursive: true });
35
22
  fs.writeFileSync(configPath, JSON.stringify(configVars, null, 2), 'utf-8');
@@ -37,10 +24,86 @@ export function startDaemon(port: number = 7071) {
37
24
  console.error('Failed to write updated config.json:', e);
38
25
  }
39
26
 
40
- // Create launchd agent for persistence
27
+ const nodePath = process.execPath;
28
+ const logPath = path.join(configDir, 'daemon.log');
29
+ const errorLogPath = path.join(configDir, 'daemon-error.log');
30
+
31
+ if (isWindows) {
32
+ startDaemonWindows({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
33
+ } else {
34
+ startDaemonMacOS({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
35
+ }
36
+ }
37
+
38
+ interface DaemonOptions {
39
+ port: number;
40
+ configDir: string;
41
+ configVars: Record<string, any>;
42
+ nodePath: string;
43
+ backendPath: string;
44
+ logPath: string;
45
+ errorLogPath: string;
46
+ }
47
+
48
+ function startDaemonWindows(opts: DaemonOptions) {
49
+ const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
50
+
51
+ // Write a wrapper .cmd script that sets env vars and launches the backend
52
+ const wrapperPath = path.join(configDir, 'start-daemon.cmd');
53
+ const envSetLines = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
54
+ .map(([k, v]) => `set "${k}=${v}"`)
55
+ .join('\r\n');
56
+ const wrapperContent = [
57
+ '@echo off',
58
+ envSetLines,
59
+ `"${nodePath}" "${backendPath}" >> "${logPath}" 2>> "${errorLogPath}"`,
60
+ '',
61
+ ].join('\r\n');
62
+
63
+ try {
64
+ fs.mkdirSync(configDir, { recursive: true });
65
+ fs.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
66
+ } catch (e) {
67
+ console.error('Failed to write start-daemon.cmd:', e);
68
+ return;
69
+ }
70
+
71
+ // Register with Windows Task Scheduler so the daemon persists across reboots
72
+ const taskName = 'OmnikeyDaemon';
73
+ try {
74
+ // Delete existing task silently before creating a fresh one
75
+ execSync(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
76
+ } catch {
77
+ // Task may not exist — that's fine
78
+ }
79
+ try {
80
+ execSync(
81
+ `schtasks /create /tn "${taskName}" /tr "cmd /c \\"${wrapperPath}\\"" /sc ONLOGON /f`,
82
+ { stdio: 'pipe' },
83
+ );
84
+ console.log(`Windows Task Scheduler task created: ${taskName}`);
85
+ console.log('Omnikey daemon will auto-start on next logon.');
86
+ } catch (e) {
87
+ console.error('Failed to create Windows Task Scheduler task:', e);
88
+ }
89
+
90
+ // Also start the backend immediately for the current session
91
+ const { out, err } = initLogFiles(logPath, errorLogPath);
92
+ const child = spawn(nodePath, [backendPath], {
93
+ env: { ...process.env, ...configVars, OMNIKEY_PORT: String(port) },
94
+ detached: true,
95
+ stdio: ['ignore', out, err],
96
+ });
97
+ child.unref();
98
+ console.log(`Omnikey API backend started as a daemon on port ${port}. PID: ${child.pid}`);
99
+ }
100
+
101
+ function startDaemonMacOS(opts: DaemonOptions) {
102
+ const { port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath } = opts;
103
+ const homeDir = getHomeDir();
104
+
41
105
  const plistName = 'com.omnikey.daemon.plist';
42
106
  const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', plistName);
43
- const nodePath = process.execPath;
44
107
  const envVars = Object.entries({ ...configVars, OMNIKEY_PORT: String(port) })
45
108
  .map(([k, v]) => `<key>${k}</key><string>${v}</string>`)
46
109
  .join('\n');
@@ -64,21 +127,19 @@ export function startDaemon(port: number = 7071) {
64
127
  <key>KeepAlive</key>
65
128
  <true/>
66
129
  <key>StandardOutPath</key>
67
- <string>${path.join(configDir, 'daemon.log')}</string>
130
+ <string>${logPath}</string>
68
131
  <key>StandardErrorPath</key>
69
- <string>${path.join(configDir, 'daemon-error.log')}</string>
132
+ <string>${errorLogPath}</string>
70
133
  <key>WorkingDirectory</key>
71
134
  <string>${configDir}</string>
72
135
  </dict>
73
136
  </plist>
74
137
  `;
75
- // Write plist file
76
138
  try {
77
139
  const launchAgentsDir = path.join(homeDir, 'Library', 'LaunchAgents');
78
140
  fs.mkdirSync(launchAgentsDir, { recursive: true });
79
141
  fs.writeFileSync(plistPath, plistContent, 'utf-8');
80
- // Load the launch agent
81
- execSync(`launchctl unload "${plistPath}" || true`); // Unload if already loaded
142
+ execSync(`launchctl unload "${plistPath}" || true`);
82
143
  execSync(`launchctl load "${plistPath}"`);
83
144
  console.log(`Launch agent created and loaded: ${plistPath}`);
84
145
  console.log('Omnikey daemon will auto-restart and persist across reboots.');
@@ -86,18 +147,7 @@ export function startDaemon(port: number = 7071) {
86
147
  console.error('Failed to create or load launch agent:', e);
87
148
  }
88
149
 
89
- // Also start the backend immediately for current session
90
- const logPath = path.join(configDir, 'daemon.log');
91
- const errorLogPath = path.join(configDir, 'daemon-error.log');
92
- // Clean (truncate) log files before starting new session
93
- try {
94
- fs.writeFileSync(logPath, '');
95
- fs.writeFileSync(errorLogPath, '');
96
- } catch (e) {
97
- // Ignore errors if files don't exist yet
98
- }
99
- const out = fs.openSync(logPath, 'a');
100
- const err = fs.openSync(errorLogPath, 'a');
150
+ const { out, err } = initLogFiles(logPath, errorLogPath);
101
151
  const child = spawn('node', [backendPath], {
102
152
  env: { ...configVars, OMNIKEY_PORT: String(port) },
103
153
  detached: true,
package/src/index.ts CHANGED
@@ -35,10 +35,8 @@ program
35
35
  program
36
36
  .command('kill-daemon')
37
37
  .description('Kill the Omnikey API backend daemon running on a specified port')
38
- .option('--port <port>', 'Port to look for the daemon on', '7071')
39
- .action((options) => {
40
- const port = Number(options.port) || 7071;
41
- killDaemon(port);
38
+ .action(() => {
39
+ killDaemon();
42
40
  });
43
41
 
44
42
  program
package/src/killDaemon.ts CHANGED
@@ -1,38 +1,58 @@
1
1
  import { execSync } from 'child_process';
2
- import { killLaunchdAgent } from './removeConfig';
2
+ import { killPersistenceAgent } from './removeConfig';
3
+ import { isWindows, getPort } from './utils';
3
4
 
4
5
  /**
5
- * Kill the Omnikey API backend daemon running on a given port (default 7071).
6
- * Looks for node processes running backend-dist/index.js on the specified port and kills them.
7
- * @param port The port to look for (default 7071)
6
+ * Kill the Omnikey API backend daemon.
7
+ * Reads the port from ~/.omnikey/config.json (falls back to 7071).
8
+ * Removes the persistence agent, then kills any remaining process on the port.
8
9
  */
9
- export function killDaemon(port: number = 7071) {
10
- // 1. Unload/kill the launchd agent first
10
+ export function killDaemon() {
11
+ const port = getPort();
12
+
13
+ // 1. Remove/stop the persistence agent
11
14
  try {
12
- killLaunchdAgent();
13
- console.log('Launchd agent unloaded (if it existed).');
15
+ killPersistenceAgent();
16
+ console.log('Persistence agent stopped (if it existed).');
14
17
  } catch (e) {
15
- console.warn('Failed to unload launchd agent or agent did not exist:', e);
18
+ console.warn('Failed to stop persistence agent:', e);
16
19
  }
17
20
 
18
- // 2. Check if the port is still in use
21
+ // 2. Find any remaining processes still using the port
19
22
  let pids: string[] = [];
20
23
  try {
21
- pids = execSync(`lsof -i :${port} -t`).toString().split('\n').filter(Boolean);
22
- } catch (e) {
23
- // lsof returns non-zero exit code if nothing is using the port
24
+ if (isWindows) {
25
+ // netstat -ano lists PID in the last column; filter by :<port> with LISTENING or ESTABLISHED
26
+ const output = execSync(`netstat -ano | findstr :${port}`).toString();
27
+ const seen = new Set<string>();
28
+ for (const line of output.trim().split('\n')) {
29
+ const parts = line.trim().split(/\s+/);
30
+ const pid = parts[parts.length - 1];
31
+ if (pid && /^\d+$/.test(pid) && pid !== '0' && !seen.has(pid)) {
32
+ seen.add(pid);
33
+ pids.push(pid);
34
+ }
35
+ }
36
+ } else {
37
+ pids = execSync(`lsof -i :${port} -t`).toString().split('\n').filter(Boolean);
38
+ }
39
+ } catch {
24
40
  pids = [];
25
41
  }
26
42
 
27
43
  if (pids.length === 0) {
28
- console.log(`No process found using port ${port} after unloading launchd agent.`);
44
+ console.log(`No process found using port ${port}.`);
29
45
  return;
30
46
  }
31
47
 
32
- // 3. If the port is still occupied, kill the process using the port
48
+ // 3. Kill each process
33
49
  for (const pid of pids) {
34
50
  try {
35
- process.kill(Number(pid), 'SIGTERM');
51
+ if (isWindows) {
52
+ execSync(`taskkill /PID ${pid} /F`, { stdio: 'pipe' });
53
+ } else {
54
+ process.kill(Number(pid), 'SIGTERM');
55
+ }
36
56
  console.log(`Killed process with PID ${pid} using port ${port}.`);
37
57
  } catch (e) {
38
58
  console.error(`Failed to kill process ${pid}:`, e);
package/src/onboard.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import inquirer from 'inquirer';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import { getConfigDir, getConfigPath } from './utils';
4
5
 
5
6
  /**
6
7
  * Onboard the user by configuring their OPENAI_API_KEY and generating a .env for self-hosted use.
@@ -8,8 +9,7 @@ import path from 'path';
8
9
  */
9
10
  export async function onboard(openAiKey?: string) {
10
11
  let apiKey = openAiKey;
11
- const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
12
- const configDir = path.join(homeDir, '.omnikey');
12
+ const configDir = getConfigDir();
13
13
  const sqlitePath = path.join(configDir, 'omnikey-selfhosted.sqlite');
14
14
 
15
15
  if (!apiKey) {
@@ -25,7 +25,7 @@ export async function onboard(openAiKey?: string) {
25
25
  }
26
26
 
27
27
  // Save all environment variables to ~/.omnikey/config.json
28
- const configPath = path.join(configDir, 'config.json');
28
+ const configPath = getConfigPath();
29
29
  fs.mkdirSync(configDir, { recursive: true });
30
30
  const configVars = {
31
31
  OPENAI_API_KEY: apiKey,
@@ -1,12 +1,11 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import { execSync } from 'child_process';
4
+ import { isWindows, getHomeDir, getConfigDir, readConfig } from './utils';
5
5
 
6
6
  export function killLaunchdAgent() {
7
- const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
8
7
  const plistName = 'com.omnikey.daemon.plist';
9
- const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', plistName);
8
+ const plistPath = path.join(getHomeDir(), 'Library', 'LaunchAgents', plistName);
10
9
  if (fs.existsSync(plistPath)) {
11
10
  try {
12
11
  execSync(`launchctl unload "${plistPath}"`);
@@ -20,31 +19,59 @@ export function killLaunchdAgent() {
20
19
  }
21
20
  }
22
21
 
22
+ export function killWindowsTask() {
23
+ const taskName = 'OmnikeyDaemon';
24
+ try {
25
+ execSync(`schtasks /end /tn "${taskName}"`, { stdio: 'pipe' });
26
+ } catch {
27
+ // Task may not be running — that's fine
28
+ }
29
+ try {
30
+ execSync(`schtasks /delete /tn "${taskName}" /f`, { stdio: 'pipe' });
31
+ console.log(`Removed Windows Task Scheduler task: ${taskName}`);
32
+ } catch {
33
+ console.log(`Windows Task Scheduler task does not exist: ${taskName}`);
34
+ }
35
+
36
+ // Also remove the wrapper script
37
+ const wrapperPath = path.join(getConfigDir(), 'start-daemon.cmd');
38
+ if (fs.existsSync(wrapperPath)) {
39
+ try {
40
+ fs.rmSync(wrapperPath);
41
+ } catch {
42
+ // Ignore
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Kill the platform-appropriate persistence agent (launchd on macOS, Task Scheduler on Windows).
49
+ */
50
+ export function killPersistenceAgent() {
51
+ if (isWindows) {
52
+ killWindowsTask();
53
+ } else {
54
+ killLaunchdAgent();
55
+ }
56
+ }
57
+
23
58
  /**
24
59
  * Removes the ~/.omnikey config directory and the SQLite database file specified in config.json.
25
60
  */
26
61
  export function removeConfigAndDb() {
27
- const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
28
- const configDir = path.join(homeDir, '.omnikey');
29
- const configPath = path.join(configDir, 'config.json');
30
- let sqlitePath = path.join(homeDir, 'omnikey-selfhosted.sqlite');
62
+ const homeDir = getHomeDir();
63
+ const configDir = getConfigDir();
64
+ const configData = readConfig();
31
65
 
32
- // Try to read SQLITE_PATH from config.json
33
- if (fs.existsSync(configPath)) {
34
- try {
35
- const configData = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
36
- if (configData.SQLITE_PATH) {
37
- sqlitePath = path.isAbsolute(configData.SQLITE_PATH)
38
- ? configData.SQLITE_PATH
39
- : path.join(homeDir, configData.SQLITE_PATH);
40
- }
41
- } catch (e) {
42
- console.error(`Failed to read config.json: ${e}`);
43
- }
66
+ let sqlitePath = path.join(homeDir, 'omnikey-selfhosted.sqlite');
67
+ if (configData.SQLITE_PATH) {
68
+ sqlitePath = path.isAbsolute(configData.SQLITE_PATH)
69
+ ? configData.SQLITE_PATH
70
+ : path.join(homeDir, configData.SQLITE_PATH);
44
71
  }
45
72
 
46
- // Remove launchd agent if exists (macOS)
47
- killLaunchdAgent();
73
+ // Remove platform-appropriate persistence agent
74
+ killPersistenceAgent();
48
75
 
49
76
  // Remove SQLite database
50
77
  if (fs.existsSync(sqlitePath)) {
package/src/showLogs.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { getConfigDir } from './utils';
3
4
 
4
5
  /**
5
6
  * Show the logs of the running Omnikey daemon by printing the contents of the daemon log file.
@@ -7,9 +8,7 @@ import path from 'path';
7
8
  * If errorsOnly is true, shows daemon-error.log instead.
8
9
  */
9
10
  export function showLogs(lines: number = 50, errorsOnly: boolean = false) {
10
- const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
11
- const configDir = path.join(homeDir, '.omnikey');
12
- const logPath = path.join(configDir, errorsOnly ? 'daemon-error.log' : 'daemon.log');
11
+ const logPath = path.join(getConfigDir(), errorsOnly ? 'daemon-error.log' : 'daemon.log');
13
12
 
14
13
  if (!fs.existsSync(logPath)) {
15
14
  console.log(errorsOnly ? 'No error logs found.' : 'No daemon logs found.');
package/src/status.ts CHANGED
@@ -1,31 +1,22 @@
1
- import path from 'path';
2
- import fs from 'fs';
3
1
  import { execSync } from 'child_process';
2
+ import { isWindows, getPort } from './utils';
4
3
 
5
4
  export function statusCmd() {
6
- // Read port from ~/.omnikey/config.json
7
- const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
8
- const configDir = path.join(homeDir, '.omnikey');
9
- const configPath = path.join(configDir, 'config.json');
10
- let port = 7071;
11
- if (fs.existsSync(configPath)) {
12
- try {
13
- const configVars = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
14
- if (configVars.OMNIKEY_PORT) {
15
- port = Number(configVars.OMNIKEY_PORT);
16
- }
17
- } catch (e) {
18
- console.error('Failed to read config.json:', e);
19
- }
20
- }
5
+ const port = getPort();
6
+
21
7
  try {
22
- const output = execSync(`lsof -i :${port}`).toString();
8
+ let output: string;
9
+ if (isWindows) {
10
+ output = execSync(`netstat -ano | findstr :${port}`).toString();
11
+ } else {
12
+ output = execSync(`lsof -i :${port}`).toString();
13
+ }
23
14
  if (output.trim()) {
24
15
  console.log(`Processes using port ${port}:\n${output}`);
25
16
  } else {
26
17
  console.log(`No process is using port ${port}.`);
27
18
  }
28
- } catch (e) {
19
+ } catch {
29
20
  console.log(`No process is using port ${port}.`);
30
21
  }
31
22
  }
package/src/utils.ts ADDED
@@ -0,0 +1,47 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+
5
+ export const isWindows = process.platform === 'win32';
6
+
7
+ export function getHomeDir(): string {
8
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
9
+ }
10
+
11
+ export function getConfigDir(): string {
12
+ return path.join(getHomeDir(), '.omnikey');
13
+ }
14
+
15
+ export function getConfigPath(): string {
16
+ return path.join(getConfigDir(), 'config.json');
17
+ }
18
+
19
+ export function readConfig(): Record<string, any> {
20
+ const configPath = getConfigPath();
21
+ if (fs.existsSync(configPath)) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
24
+ } catch {
25
+ // fall through to empty config
26
+ }
27
+ }
28
+ return {};
29
+ }
30
+
31
+ export function getPort(): number {
32
+ const config = readConfig();
33
+ return config.OMNIKEY_PORT ? Number(config.OMNIKEY_PORT) : 7071;
34
+ }
35
+
36
+ export function initLogFiles(logPath: string, errorLogPath: string): { out: number; err: number } {
37
+ try {
38
+ fs.writeFileSync(logPath, '');
39
+ fs.writeFileSync(errorLogPath, '');
40
+ } catch {
41
+ // Ignore if files don't exist yet
42
+ }
43
+ return {
44
+ out: fs.openSync(logPath, 'a'),
45
+ err: fs.openSync(errorLogPath, 'a'),
46
+ };
47
+ }