omnikey-cli 1.0.13 → 1.0.14

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.
@@ -43,7 +43,7 @@ function killDaemon() {
43
43
  pids = [];
44
44
  }
45
45
  if (pids.length === 0) {
46
- console.log(`No process found using port ${port}.`);
46
+ console.log(`No process is running on port ${port}.`);
47
47
  return;
48
48
  }
49
49
  // 3. Kill each process
package/dist/onboard.js CHANGED
@@ -8,33 +8,120 @@ const inquirer_1 = __importDefault(require("inquirer"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const utils_1 = require("./utils");
11
+ const AI_PROVIDERS = [
12
+ { name: 'OpenAI (gpt-4o-mini / gpt-5.1)', value: 'openai' },
13
+ { name: 'Anthropic — Claude (claude-haiku / claude-sonnet)', value: 'anthropic' },
14
+ { name: 'Google Gemini (gemini-2.5-flash / gemini-2.5-pro)', value: 'gemini' },
15
+ ];
16
+ const SEARCH_PROVIDERS = [
17
+ { name: 'Skip (DuckDuckGo will be used by default — no key required)', value: 'skip' },
18
+ { name: 'Serper (Google Search API — serper.dev, 2,500 free/mo)', value: 'serper' },
19
+ { name: 'Brave Search (brave.com/search/api, 2,000 free/mo)', value: 'brave' },
20
+ { name: 'Tavily (tavily.com, 1,000 free/mo, optimized for AI)', value: 'tavily' },
21
+ { name: 'SearXNG (self-hosted, no key needed — provide your instance URL)', value: 'searxng' },
22
+ ];
23
+ const AI_PROVIDER_KEY_ENV = {
24
+ openai: 'OPENAI_API_KEY',
25
+ anthropic: 'ANTHROPIC_API_KEY',
26
+ gemini: 'GEMINI_API_KEY',
27
+ };
28
+ const AI_PROVIDER_KEY_LABEL = {
29
+ openai: 'OpenAI API key (from platform.openai.com)',
30
+ anthropic: 'Anthropic API key (from console.anthropic.com)',
31
+ gemini: 'Google Gemini API key (from ai.google.dev)',
32
+ };
11
33
  /**
12
- * Onboard the user by configuring their OPENAI_API_KEY and generating a .env for self-hosted use.
13
- * @param openAiKey Optional key provided via CLI
34
+ * Onboard the user by configuring their AI provider API key and generating config for self-hosted use.
14
35
  */
15
- async function onboard(openAiKey) {
16
- let apiKey = openAiKey;
36
+ async function onboard() {
17
37
  const configDir = (0, utils_1.getConfigDir)();
18
38
  const sqlitePath = path_1.default.join(configDir, 'omnikey-selfhosted.sqlite');
19
- if (!apiKey) {
20
- const answers = await inquirer_1.default.prompt([
39
+ // Choose AI provider
40
+ const { aiProvider } = await inquirer_1.default.prompt([
41
+ {
42
+ type: 'list',
43
+ name: 'aiProvider',
44
+ message: 'Select your AI provider:',
45
+ choices: AI_PROVIDERS,
46
+ default: 'openai',
47
+ },
48
+ ]);
49
+ const { apiKey } = await inquirer_1.default.prompt([
50
+ {
51
+ type: 'input',
52
+ name: 'apiKey',
53
+ message: `Enter your ${AI_PROVIDER_KEY_LABEL[aiProvider]}:`,
54
+ validate: (input) => input.trim() !== '' || 'API key cannot be empty',
55
+ },
56
+ ]);
57
+ // Web search provider (optional)
58
+ const { provider } = await inquirer_1.default.prompt([
59
+ {
60
+ type: 'list',
61
+ name: 'provider',
62
+ message: 'Select a web search provider for the AI agent (enhances research capabilities):',
63
+ choices: SEARCH_PROVIDERS,
64
+ default: 'skip',
65
+ },
66
+ ]);
67
+ const searchConfig = {};
68
+ if (provider === 'serper') {
69
+ const { key } = await inquirer_1.default.prompt([
70
+ {
71
+ type: 'input',
72
+ name: 'key',
73
+ message: 'Enter your Serper API key (from serper.dev):',
74
+ validate: (input) => input.trim() !== '' || 'API key cannot be empty',
75
+ },
76
+ ]);
77
+ searchConfig['SERPER_API_KEY'] = key.trim();
78
+ }
79
+ else if (provider === 'brave') {
80
+ const { key } = await inquirer_1.default.prompt([
21
81
  {
22
82
  type: 'input',
23
- name: 'apiKey',
24
- message: 'Enter your OPENAI_API_KEY:',
83
+ name: 'key',
84
+ message: 'Enter your Brave Search API key (from brave.com/search/api):',
25
85
  validate: (input) => input.trim() !== '' || 'API key cannot be empty',
26
86
  },
27
87
  ]);
28
- apiKey = answers.apiKey;
88
+ searchConfig['BRAVE_SEARCH_API_KEY'] = key.trim();
89
+ }
90
+ else if (provider === 'tavily') {
91
+ const { key } = await inquirer_1.default.prompt([
92
+ {
93
+ type: 'input',
94
+ name: 'key',
95
+ message: 'Enter your Tavily API key (from tavily.com):',
96
+ validate: (input) => input.trim() !== '' || 'API key cannot be empty',
97
+ },
98
+ ]);
99
+ searchConfig['TAVILY_API_KEY'] = key.trim();
100
+ }
101
+ else if (provider === 'searxng') {
102
+ const { url } = await inquirer_1.default.prompt([
103
+ {
104
+ type: 'input',
105
+ name: 'url',
106
+ message: 'Enter your SearXNG instance URL (e.g. http://localhost:8080):',
107
+ validate: (input) => input.trim() !== '' || 'URL cannot be empty',
108
+ },
109
+ ]);
110
+ searchConfig['SEARXNG_URL'] = url.trim();
29
111
  }
112
+ // skip/duckduckgo: no config needed, DuckDuckGo is used automatically as the free fallback
30
113
  // Save all environment variables to ~/.omnikey/config.json
31
114
  const configPath = (0, utils_1.getConfigPath)();
32
115
  fs_1.default.mkdirSync(configDir, { recursive: true });
33
116
  const configVars = {
34
- OPENAI_API_KEY: apiKey,
117
+ AI_PROVIDER: aiProvider,
118
+ [AI_PROVIDER_KEY_ENV[aiProvider]]: apiKey,
35
119
  IS_SELF_HOSTED: true,
36
120
  SQLITE_PATH: sqlitePath,
121
+ ...searchConfig,
37
122
  };
38
123
  fs_1.default.writeFileSync(configPath, JSON.stringify(configVars, null, 2));
124
+ const providerLabel = SEARCH_PROVIDERS.find((p) => p.value === provider)?.name ?? provider;
125
+ console.log(`\nWeb search provider: ${providerLabel}`);
39
126
  console.log(`Environment variables saved to ${configPath}. You can edit this file to update your configuration.`);
40
127
  }
@@ -66,48 +66,56 @@ function killPersistenceAgent() {
66
66
  }
67
67
  }
68
68
  /**
69
- * Removes the ~/.omnikey config directory and the SQLite database file specified in config.json.
69
+ * Removes the ~/.omnikey config directory and optionally the SQLite database file.
70
+ * @param includeDb - When true, also removes the SQLite database file.
70
71
  */
71
- function removeConfigAndDb() {
72
+ function removeConfigAndDb(includeDb = false) {
72
73
  const homeDir = (0, utils_1.getHomeDir)();
73
74
  const configDir = (0, utils_1.getConfigDir)();
74
75
  const configData = (0, utils_1.readConfig)();
75
- let sqlitePath = path_1.default.join(homeDir, 'omnikey-selfhosted.sqlite');
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);
80
- }
81
76
  // Remove platform-appropriate persistence agent
82
77
  killPersistenceAgent();
83
- // Remove SQLite database
84
- if (fs_1.default.existsSync(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}`);
78
+ // Remove SQLite database only when --db flag is passed
79
+ if (includeDb) {
80
+ let sqlitePath = path_1.default.join(homeDir, 'omnikey-selfhosted.sqlite');
81
+ if (configData.SQLITE_PATH) {
82
+ sqlitePath = path_1.default.isAbsolute(configData.SQLITE_PATH)
83
+ ? configData.SQLITE_PATH
84
+ : path_1.default.join(homeDir, configData.SQLITE_PATH);
85
+ }
86
+ if (fs_1.default.existsSync(sqlitePath)) {
87
+ const maxAttempts = utils_1.isWindows ? 5 : 1;
88
+ let removed = false;
89
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
90
+ try {
91
+ fs_1.default.rmSync(sqlitePath);
92
+ console.log(`Removed SQLite database: ${sqlitePath}`);
93
+ removed = true;
101
94
  break;
102
95
  }
96
+ catch (e) {
97
+ if (utils_1.isWindows &&
98
+ attempt < maxAttempts &&
99
+ (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'EACCES')) {
100
+ // File may still be locked by the daemon — wait ~1s and retry
101
+ (0, child_process_1.execSync)(`ping -n 2 127.0.0.1 > nul`, { stdio: 'pipe' });
102
+ }
103
+ else {
104
+ console.error(`Failed to remove SQLite database: ${e}`);
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ if (!removed && utils_1.isWindows) {
110
+ console.error(`Failed to remove SQLite database after ${maxAttempts} attempts: ${sqlitePath}`);
103
111
  }
104
112
  }
105
- if (!removed && utils_1.isWindows) {
106
- console.error(`Failed to remove SQLite database after ${maxAttempts} attempts: ${sqlitePath}`);
113
+ else {
114
+ console.log(`SQLite database does not exist: ${sqlitePath}`);
107
115
  }
108
116
  }
109
117
  else {
110
- console.log(`SQLite database does not exist: ${sqlitePath}`);
118
+ console.log('Skipping SQLite database removal (use --db to remove it).');
111
119
  }
112
120
  // Remove .omnikey directory
113
121
  if (fs_1.default.existsSync(configDir)) {
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.13",
7
+ "version": "1.0.14",
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",
@@ -36,6 +36,8 @@
36
36
  "express": "^4.21.2",
37
37
  "jsonwebtoken": "^9.0.3",
38
38
  "openai": "^6.16.0",
39
+ "@anthropic-ai/sdk": "^0.80.0",
40
+ "@google/genai": "^1.46.0",
39
41
  "pg": "^8.18.0",
40
42
  "pg-hstore": "^2.3.4",
41
43
  "sequelize": "^6.37.7",
package/src/daemon.ts CHANGED
@@ -2,7 +2,14 @@ import { spawn } from 'child_process';
2
2
  import path from 'path';
3
3
  import fs from 'fs';
4
4
  import { execSync } from 'child_process';
5
- import { isWindows, getHomeDir, getConfigDir, getConfigPath, readConfig, initLogFiles } from './utils';
5
+ import {
6
+ isWindows,
7
+ getHomeDir,
8
+ getConfigDir,
9
+ getConfigPath,
10
+ readConfig,
11
+ initLogFiles,
12
+ } from './utils';
6
13
 
7
14
  /**
8
15
  * Start the Omnikey API backend as a daemon on the specified port.
@@ -29,7 +36,15 @@ export function startDaemon(port: number = 7071) {
29
36
  const errorLogPath = path.join(configDir, 'daemon-error.log');
30
37
 
31
38
  if (isWindows) {
32
- startDaemonWindows({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
39
+ startDaemonWindows({
40
+ port,
41
+ configDir,
42
+ configVars,
43
+ nodePath,
44
+ backendPath,
45
+ logPath,
46
+ errorLogPath,
47
+ });
33
48
  } else {
34
49
  startDaemonMacOS({ port, configDir, configVars, nodePath, backendPath, logPath, errorLogPath });
35
50
  }
@@ -148,8 +163,8 @@ function startDaemonMacOS(opts: DaemonOptions) {
148
163
  }
149
164
 
150
165
  const { out, err } = initLogFiles(logPath, errorLogPath);
151
- const child = spawn('node', [backendPath], {
152
- env: { ...configVars, OMNIKEY_PORT: String(port) },
166
+ const child = spawn(nodePath, [backendPath], {
167
+ env: { ...process.env, ...configVars, OMNIKEY_PORT: String(port) },
153
168
  detached: true,
154
169
  stdio: ['ignore', out, err],
155
170
  });
package/src/index.ts CHANGED
@@ -17,10 +17,9 @@ program
17
17
 
18
18
  program
19
19
  .command('onboard')
20
- .description('Onboard and configure your OPENAI_API_KEY')
21
- .option('--open-ai-key <key>', 'Your OpenAI API Key')
22
- .action(async (options) => {
23
- await onboard(options.openAiKey || options.openAiKey || options['open-ai-key']);
20
+ .description('Onboard and configure your AI provider')
21
+ .action(async () => {
22
+ await onboard();
24
23
  });
25
24
 
26
25
  program
@@ -41,11 +40,10 @@ program
41
40
 
42
41
  program
43
42
  .command('remove-config')
44
- .description(
45
- 'Remove the .omnikey config directory and the SQLite database from your home directory',
46
- )
47
- .action(() => {
48
- removeConfigAndDb();
43
+ .description('Remove the omnikey config. Pass --db to also remove the SQLite database.')
44
+ .option('--db', 'Also remove the SQLite database')
45
+ .action((options) => {
46
+ removeConfigAndDb(!!options.db);
49
47
  });
50
48
 
51
49
  // Add status command
package/src/killDaemon.ts CHANGED
@@ -41,7 +41,7 @@ export function killDaemon() {
41
41
  }
42
42
 
43
43
  if (pids.length === 0) {
44
- console.log(`No process found using port ${port}.`);
44
+ console.log(`No process is running on port ${port}.`);
45
45
  return;
46
46
  }
47
47
 
package/src/onboard.ts CHANGED
@@ -3,36 +3,129 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { getConfigDir, getConfigPath } from './utils';
5
5
 
6
+ const AI_PROVIDERS = [
7
+ { name: 'OpenAI (gpt-4o-mini / gpt-5.1)', value: 'openai' },
8
+ { name: 'Anthropic — Claude (claude-haiku / claude-sonnet)', value: 'anthropic' },
9
+ { name: 'Google Gemini (gemini-2.5-flash / gemini-2.5-pro)', value: 'gemini' },
10
+ ];
11
+
12
+ const SEARCH_PROVIDERS = [
13
+ { name: 'Skip (DuckDuckGo will be used by default — no key required)', value: 'skip' },
14
+ { name: 'Serper (Google Search API — serper.dev, 2,500 free/mo)', value: 'serper' },
15
+ { name: 'Brave Search (brave.com/search/api, 2,000 free/mo)', value: 'brave' },
16
+ { name: 'Tavily (tavily.com, 1,000 free/mo, optimized for AI)', value: 'tavily' },
17
+ { name: 'SearXNG (self-hosted, no key needed — provide your instance URL)', value: 'searxng' },
18
+ ];
19
+
20
+ const AI_PROVIDER_KEY_ENV: Record<string, string> = {
21
+ openai: 'OPENAI_API_KEY',
22
+ anthropic: 'ANTHROPIC_API_KEY',
23
+ gemini: 'GEMINI_API_KEY',
24
+ };
25
+
26
+ const AI_PROVIDER_KEY_LABEL: Record<string, string> = {
27
+ openai: 'OpenAI API key (from platform.openai.com)',
28
+ anthropic: 'Anthropic API key (from console.anthropic.com)',
29
+ gemini: 'Google Gemini API key (from ai.google.dev)',
30
+ };
31
+
6
32
  /**
7
- * Onboard the user by configuring their OPENAI_API_KEY and generating a .env for self-hosted use.
8
- * @param openAiKey Optional key provided via CLI
33
+ * Onboard the user by configuring their AI provider API key and generating config for self-hosted use.
9
34
  */
10
- export async function onboard(openAiKey?: string) {
11
- let apiKey = openAiKey;
35
+ export async function onboard() {
12
36
  const configDir = getConfigDir();
13
37
  const sqlitePath = path.join(configDir, 'omnikey-selfhosted.sqlite');
14
38
 
15
- if (!apiKey) {
16
- const answers = await inquirer.prompt([
39
+ // Choose AI provider
40
+ const { aiProvider } = await inquirer.prompt([
41
+ {
42
+ type: 'list',
43
+ name: 'aiProvider',
44
+ message: 'Select your AI provider:',
45
+ choices: AI_PROVIDERS,
46
+ default: 'openai',
47
+ },
48
+ ]);
49
+
50
+ const { apiKey } = await inquirer.prompt([
51
+ {
52
+ type: 'input',
53
+ name: 'apiKey',
54
+ message: `Enter your ${AI_PROVIDER_KEY_LABEL[aiProvider]}:`,
55
+ validate: (input: string) => input.trim() !== '' || 'API key cannot be empty',
56
+ },
57
+ ]);
58
+
59
+ // Web search provider (optional)
60
+ const { provider } = await inquirer.prompt([
61
+ {
62
+ type: 'list',
63
+ name: 'provider',
64
+ message: 'Select a web search provider for the AI agent (enhances research capabilities):',
65
+ choices: SEARCH_PROVIDERS,
66
+ default: 'skip',
67
+ },
68
+ ]);
69
+
70
+ const searchConfig: Record<string, string> = {};
71
+
72
+ if (provider === 'serper') {
73
+ const { key } = await inquirer.prompt([
17
74
  {
18
75
  type: 'input',
19
- name: 'apiKey',
20
- message: 'Enter your OPENAI_API_KEY:',
76
+ name: 'key',
77
+ message: 'Enter your Serper API key (from serper.dev):',
21
78
  validate: (input: string) => input.trim() !== '' || 'API key cannot be empty',
22
79
  },
23
80
  ]);
24
- apiKey = answers.apiKey;
81
+ searchConfig['SERPER_API_KEY'] = key.trim();
82
+ } else if (provider === 'brave') {
83
+ const { key } = await inquirer.prompt([
84
+ {
85
+ type: 'input',
86
+ name: 'key',
87
+ message: 'Enter your Brave Search API key (from brave.com/search/api):',
88
+ validate: (input: string) => input.trim() !== '' || 'API key cannot be empty',
89
+ },
90
+ ]);
91
+ searchConfig['BRAVE_SEARCH_API_KEY'] = key.trim();
92
+ } else if (provider === 'tavily') {
93
+ const { key } = await inquirer.prompt([
94
+ {
95
+ type: 'input',
96
+ name: 'key',
97
+ message: 'Enter your Tavily API key (from tavily.com):',
98
+ validate: (input: string) => input.trim() !== '' || 'API key cannot be empty',
99
+ },
100
+ ]);
101
+ searchConfig['TAVILY_API_KEY'] = key.trim();
102
+ } else if (provider === 'searxng') {
103
+ const { url } = await inquirer.prompt([
104
+ {
105
+ type: 'input',
106
+ name: 'url',
107
+ message: 'Enter your SearXNG instance URL (e.g. http://localhost:8080):',
108
+ validate: (input: string) => input.trim() !== '' || 'URL cannot be empty',
109
+ },
110
+ ]);
111
+ searchConfig['SEARXNG_URL'] = url.trim();
25
112
  }
113
+ // skip/duckduckgo: no config needed, DuckDuckGo is used automatically as the free fallback
26
114
 
27
115
  // Save all environment variables to ~/.omnikey/config.json
28
116
  const configPath = getConfigPath();
29
117
  fs.mkdirSync(configDir, { recursive: true });
30
118
  const configVars = {
31
- OPENAI_API_KEY: apiKey,
119
+ AI_PROVIDER: aiProvider,
120
+ [AI_PROVIDER_KEY_ENV[aiProvider]]: apiKey,
32
121
  IS_SELF_HOSTED: true,
33
122
  SQLITE_PATH: sqlitePath,
123
+ ...searchConfig,
34
124
  };
35
125
  fs.writeFileSync(configPath, JSON.stringify(configVars, null, 2));
126
+
127
+ const providerLabel = SEARCH_PROVIDERS.find((p) => p.value === provider)?.name ?? provider;
128
+ console.log(`\nWeb search provider: ${providerLabel}`);
36
129
  console.log(
37
130
  `Environment variables saved to ${configPath}. You can edit this file to update your configuration.`,
38
131
  );
@@ -56,48 +56,59 @@ export function killPersistenceAgent() {
56
56
  }
57
57
 
58
58
  /**
59
- * Removes the ~/.omnikey config directory and the SQLite database file specified in config.json.
59
+ * Removes the ~/.omnikey config directory and optionally the SQLite database file.
60
+ * @param includeDb - When true, also removes the SQLite database file.
60
61
  */
61
- export function removeConfigAndDb() {
62
+ export function removeConfigAndDb(includeDb = false) {
62
63
  const homeDir = getHomeDir();
63
64
  const configDir = getConfigDir();
64
65
  const configData = readConfig();
65
66
 
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);
71
- }
72
-
73
67
  // Remove platform-appropriate persistence agent
74
68
  killPersistenceAgent();
75
69
 
76
- // Remove SQLite database
77
- if (fs.existsSync(sqlitePath)) {
78
- const maxAttempts = isWindows ? 5 : 1;
79
- let removed = false;
80
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
81
- try {
82
- fs.rmSync(sqlitePath);
83
- console.log(`Removed SQLite database: ${sqlitePath}`);
84
- removed = true;
85
- break;
86
- } catch (e: any) {
87
- if (isWindows && attempt < maxAttempts && (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'EACCES')) {
88
- // File may still be locked by the daemon — wait ~1s and retry
89
- execSync(`ping -n 2 127.0.0.1 > nul`, { stdio: 'pipe' });
90
- } else {
91
- console.error(`Failed to remove SQLite database: ${e}`);
70
+ // Remove SQLite database only when --db flag is passed
71
+ if (includeDb) {
72
+ let sqlitePath = path.join(homeDir, 'omnikey-selfhosted.sqlite');
73
+ if (configData.SQLITE_PATH) {
74
+ sqlitePath = path.isAbsolute(configData.SQLITE_PATH)
75
+ ? configData.SQLITE_PATH
76
+ : path.join(homeDir, configData.SQLITE_PATH);
77
+ }
78
+
79
+ if (fs.existsSync(sqlitePath)) {
80
+ const maxAttempts = isWindows ? 5 : 1;
81
+ let removed = false;
82
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
83
+ try {
84
+ fs.rmSync(sqlitePath);
85
+ console.log(`Removed SQLite database: ${sqlitePath}`);
86
+ removed = true;
92
87
  break;
88
+ } catch (e: any) {
89
+ if (
90
+ isWindows &&
91
+ attempt < maxAttempts &&
92
+ (e.code === 'EBUSY' || e.code === 'EPERM' || e.code === 'EACCES')
93
+ ) {
94
+ // File may still be locked by the daemon — wait ~1s and retry
95
+ execSync(`ping -n 2 127.0.0.1 > nul`, { stdio: 'pipe' });
96
+ } else {
97
+ console.error(`Failed to remove SQLite database: ${e}`);
98
+ break;
99
+ }
93
100
  }
94
101
  }
95
- }
96
- if (!removed && isWindows) {
97
- console.error(`Failed to remove SQLite database after ${maxAttempts} attempts: ${sqlitePath}`);
102
+ if (!removed && isWindows) {
103
+ console.error(
104
+ `Failed to remove SQLite database after ${maxAttempts} attempts: ${sqlitePath}`,
105
+ );
106
+ }
107
+ } else {
108
+ console.log(`SQLite database does not exist: ${sqlitePath}`);
98
109
  }
99
110
  } else {
100
- console.log(`SQLite database does not exist: ${sqlitePath}`);
111
+ console.log('Skipping SQLite database removal (use --db to remove it).');
101
112
  }
102
113
 
103
114
  // Remove .omnikey directory