omnikey-cli 1.0.11 → 1.0.13

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,18 +2,19 @@
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)
11
+ - Download the latest Windows app here: [Download OmniKeyAI for Windows](https://omnikeyai-saas-fmytqc3dra-uc.a.run.app/windows/download)
11
12
 
12
13
  ## Features
13
14
 
14
15
  - `omnikey onboard`: Interactive onboarding to set up your OPENAI_API_KEY.
15
16
  - Accepts the `--open-ai-key` parameter for non-interactive setup.
16
- - Configure and run the backend daemon for the macOS app.
17
+ - Configure and run the backend daemon persisted across reboots on both macOS and Windows.
17
18
 
18
19
  ## Usage
19
20
 
@@ -21,28 +22,42 @@ OmnikeyAI is a productivity tool for macOS that helps you quickly rewrite select
21
22
  # Install CLI globally (from this directory)
22
23
  npm install -g omnikey-cli
23
24
 
24
- # Onboard interactively (will prompt for OpenAI key and self-hosting)
25
+ # Onboard interactively (will prompt for OpenAI key)
25
26
  omnikey onboard
26
27
 
27
28
  # Or onboard non-interactively
28
29
  omnikey onboard --open-ai-key YOUR_KEY
29
30
 
30
- # Running the daemon will set up a launchd agent and keep the backend server running across system restarts
31
+ # Start the daemon (auto-restarts on reboot)
31
32
  omnikey daemon --port 7071
32
33
 
33
34
  # Kill the daemon
34
- omnikey kill-daemon --port 7071
35
+ omnikey kill-daemon
35
36
 
36
- # Remove the config directory and SQLite database (and launchd agent)
37
+ # Remove the config directory and SQLite database (and persistence agent)
37
38
  omnikey remove-config
38
39
 
39
- # check daemon status if it is running
40
+ # Check daemon status
40
41
  omnikey status
41
42
 
42
- # check daemon logs
43
+ # Check daemon logs
43
44
  omnikey logs --lines 100
44
45
  ```
45
46
 
47
+ ## Platform notes
48
+
49
+ ### macOS
50
+
51
+ The daemon is registered as a **launchd agent** (`~/Library/LaunchAgents/com.omnikey.daemon.plist`) so it auto-restarts after login and on crashes.
52
+
53
+ ### Windows
54
+
55
+ 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.
56
+
57
+ > **Note:** `schtasks` is a built-in Windows command — no third-party tools or administrator rights are required for user-level scheduled tasks.
58
+
59
+ Commands that query process state use `netstat` (instead of `lsof`) on Windows, and process termination uses `taskkill` (instead of `SIGTERM`).
60
+
46
61
  ## Development
47
62
 
48
63
  - 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', {
@@ -7,6 +7,7 @@ const express_1 = __importDefault(require("express"));
7
7
  const cors_1 = __importDefault(require("cors"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
+ const zlib_1 = __importDefault(require("zlib"));
10
11
  const subscriptionRoutes_1 = require("./subscriptionRoutes");
11
12
  const featureRoutes_1 = require("./featureRoutes");
12
13
  const db_1 = require("./db");
@@ -51,8 +52,8 @@ app.get('/macos/appcast', (req, res) => {
51
52
  const appcastUrl = `${baseUrl}/macos/appcast`;
52
53
  // These should match the values embedded into the macOS app
53
54
  // Info.plist in macOS/build_release_dmg.sh.
54
- const bundleVersion = '12';
55
- const shortVersion = '1.0.11';
55
+ const bundleVersion = '13';
56
+ const shortVersion = '1.0.12';
56
57
  const xml = `<?xml version="1.0" encoding="utf-8"?>
57
58
  <rss version="2.0"
58
59
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
@@ -77,6 +78,53 @@ app.get('/macos/appcast', (req, res) => {
77
78
  res.set('Content-Type', 'application/xml; charset=utf-8');
78
79
  res.send(xml);
79
80
  });
81
+ // ── Windows distribution endpoints ───────────────────────────────────────────
82
+ // These should match the values in windows/OmniKey.Windows.csproj
83
+ // <Version> and windows/build_release_zip.ps1 $APP_VERSION.
84
+ const WIN_VERSION = '1.1';
85
+ const WIN_ZIP_FILENAME = 'OmniKeyAI-windows-win-x64.zip';
86
+ const WIN_ZIP_PATH = path_1.default.join(process.cwd(), 'windows', WIN_ZIP_FILENAME);
87
+ // Serves the pre-built ZIP produced by windows/build_release_zip.ps1.
88
+ // Streams through gzip to reduce response size on Cloud Run.
89
+ app.get('/windows/download', (_req, res) => {
90
+ if (!fs_1.default.existsSync(WIN_ZIP_PATH)) {
91
+ res.status(404).send('File not found.');
92
+ return;
93
+ }
94
+ res.set({
95
+ 'Content-Type': 'application/zip',
96
+ 'Content-Disposition': `attachment; filename="${WIN_ZIP_FILENAME}"`,
97
+ 'Content-Encoding': 'gzip',
98
+ });
99
+ const fileStream = fs_1.default.createReadStream(WIN_ZIP_PATH);
100
+ const gzip = zlib_1.default.createGzip();
101
+ fileStream.on('error', (err) => {
102
+ logger_1.logger.error('Failed to send Windows ZIP for download.', { error: err });
103
+ if (!res.headersSent) {
104
+ res.status(500).send('Unable to download file.');
105
+ }
106
+ });
107
+ fileStream.pipe(gzip).pipe(res);
108
+ });
109
+ // JSON update-check endpoint consumed by UpdateChecker.cs on the Windows client.
110
+ // Returns the latest version + download URL so the client can decide whether
111
+ // to prompt the user for an update.
112
+ app.get('/windows/update', (req, res) => {
113
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
114
+ let fileSize = 0;
115
+ try {
116
+ fileSize = fs_1.default.statSync(WIN_ZIP_PATH).size;
117
+ }
118
+ catch (error) {
119
+ logger_1.logger.error('Failed to stat Windows ZIP for update endpoint.', { error });
120
+ }
121
+ res.json({
122
+ version: WIN_VERSION,
123
+ downloadUrl: `${baseUrl}/windows/download`,
124
+ fileSize,
125
+ releaseNotes: '',
126
+ });
127
+ });
80
128
  app.get('/health', (_req, res) => {
81
129
  res.json({ status: 'ok' });
82
130
  });
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,38 +28,82 @@ 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
- try {
57
- fs_1.default.rmSync(sqlitePath);
58
- console.log(`Removed SQLite database: ${sqlitePath}`);
85
+ const maxAttempts = utils_1.isWindows ? 5 : 1;
86
+ let removed = false;
87
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
88
+ try {
89
+ fs_1.default.rmSync(sqlitePath);
90
+ console.log(`Removed SQLite database: ${sqlitePath}`);
91
+ removed = true;
92
+ break;
93
+ }
94
+ catch (e) {
95
+ if (utils_1.isWindows && attempt < maxAttempts && (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'EACCES')) {
96
+ // File may still be locked by the daemon — wait ~1s and retry
97
+ (0, child_process_1.execSync)(`ping -n 2 127.0.0.1 > nul`, { stdio: 'pipe' });
98
+ }
99
+ else {
100
+ console.error(`Failed to remove SQLite database: ${e}`);
101
+ break;
102
+ }
103
+ }
59
104
  }
60
- catch (e) {
61
- console.error(`Failed to remove SQLite database: ${e}`);
105
+ if (!removed && utils_1.isWindows) {
106
+ console.error(`Failed to remove SQLite database after ${maxAttempts} attempts: ${sqlitePath}`);
62
107
  }
63
108
  }
64
109
  else {