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.
- package/README.md +7 -8
- package/backend-dist/{agentPrompts.js → agent/agentPrompts.js} +18 -0
- package/backend-dist/{agentServer.js → agent/agentServer.js} +147 -54
- package/backend-dist/agent/index.js +17 -0
- package/backend-dist/agent/web-search-provider.js +135 -0
- package/backend-dist/ai-client.js +469 -0
- package/backend-dist/config.js +31 -2
- package/backend-dist/featureRoutes.js +17 -36
- package/backend-dist/index.js +1 -1
- package/dist/daemon.js +11 -3
- package/dist/index.js +7 -7
- package/dist/killDaemon.js +1 -1
- package/dist/onboard.js +97 -10
- package/dist/removeConfig.js +37 -29
- package/package.json +3 -1
- package/src/daemon.ts +19 -4
- package/src/index.ts +7 -9
- package/src/killDaemon.ts +1 -1
- package/src/onboard.ts +103 -10
- package/src/removeConfig.ts +40 -29
package/dist/killDaemon.js
CHANGED
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
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
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: '
|
|
24
|
-
message: 'Enter your
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/removeConfig.js
CHANGED
|
@@ -66,48 +66,56 @@ function killPersistenceAgent() {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Removes the ~/.omnikey config directory and the SQLite database file
|
|
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 (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
106
|
-
console.
|
|
113
|
+
else {
|
|
114
|
+
console.log(`SQLite database does not exist: ${sqlitePath}`);
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
else {
|
|
110
|
-
console.log(
|
|
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.
|
|
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 {
|
|
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({
|
|
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(
|
|
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
|
|
21
|
-
.
|
|
22
|
-
|
|
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
|
-
|
|
46
|
-
)
|
|
47
|
-
|
|
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
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
|
|
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(
|
|
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
|
-
|
|
16
|
-
|
|
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: '
|
|
20
|
-
message: 'Enter your
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|
package/src/removeConfig.ts
CHANGED
|
@@ -56,48 +56,59 @@ export function killPersistenceAgent() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Removes the ~/.omnikey config directory and the SQLite database file
|
|
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 (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.
|
|
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
|
-
|
|
97
|
-
|
|
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(
|
|
111
|
+
console.log('Skipping SQLite database removal (use --db to remove it).');
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
// Remove .omnikey directory
|