omnikey-cli 1.6.0 → 1.6.1
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/backend-dist/aiProviderRoutes.js +247 -0
- package/backend-dist/index.js +27 -3
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
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.aiProviderRouter = aiProviderRouter;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const zod_1 = __importDefault(require("zod"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const authMiddleware_1 = require("./authMiddleware");
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const logger_1 = require("./logger");
|
|
15
|
+
const providerEnum = zod_1.default.enum(['openai', 'anthropic', 'gemini', 'nemotron']);
|
|
16
|
+
const putSchema = zod_1.default.object({
|
|
17
|
+
apiKey: zod_1.default.string().min(1).max(4096),
|
|
18
|
+
baseUrl: zod_1.default
|
|
19
|
+
.string()
|
|
20
|
+
.max(1000)
|
|
21
|
+
.url({ message: 'baseUrl must be a valid URL.' })
|
|
22
|
+
.nullable()
|
|
23
|
+
.optional(),
|
|
24
|
+
});
|
|
25
|
+
/** Mapping from provider → env var that holds its API key. */
|
|
26
|
+
const PROVIDER_ENV_KEY = {
|
|
27
|
+
openai: 'OPENAI_API_KEY',
|
|
28
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
29
|
+
gemini: 'GEMINI_API_KEY',
|
|
30
|
+
nemotron: 'NEMOTRON_API_KEY',
|
|
31
|
+
};
|
|
32
|
+
/** Legacy aliases that should be cleared whenever the canonical env is rewritten. */
|
|
33
|
+
const PROVIDER_LEGACY_ALIASES = {
|
|
34
|
+
nemotron: ['NVIDIA_API_KEY'],
|
|
35
|
+
};
|
|
36
|
+
function getConfigPath() {
|
|
37
|
+
const home = process.env.HOME || process.env.USERPROFILE || os_1.default.homedir();
|
|
38
|
+
return path_1.default.join(home, '.omnikey', 'config.json');
|
|
39
|
+
}
|
|
40
|
+
function readConfigFile() {
|
|
41
|
+
const configPath = getConfigPath();
|
|
42
|
+
try {
|
|
43
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
44
|
+
const raw = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
logger_1.logger.warn('Could not read ~/.omnikey/config.json — treating as empty.', { error: err });
|
|
53
|
+
}
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
function writeConfigFile(data) {
|
|
57
|
+
const configPath = getConfigPath();
|
|
58
|
+
fs_1.default.mkdirSync(path_1.default.dirname(configPath), { recursive: true });
|
|
59
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
60
|
+
}
|
|
61
|
+
/** Mask an API key so a small prefix/suffix is visible (e.g. `sk-…AB12`). */
|
|
62
|
+
function maskApiKey(key) {
|
|
63
|
+
if (!key)
|
|
64
|
+
return '';
|
|
65
|
+
if (key.length <= 8)
|
|
66
|
+
return '••••••••';
|
|
67
|
+
return `${key.slice(0, 3)}••••••••${key.slice(-4)}`;
|
|
68
|
+
}
|
|
69
|
+
/** Reads any provider key (canonical or legacy alias) out of the config object. */
|
|
70
|
+
function readProviderKey(cfg, provider) {
|
|
71
|
+
const canonical = cfg[PROVIDER_ENV_KEY[provider]];
|
|
72
|
+
if (canonical && typeof canonical === 'string' && canonical.length > 0)
|
|
73
|
+
return canonical;
|
|
74
|
+
for (const alias of PROVIDER_LEGACY_ALIASES[provider] ?? []) {
|
|
75
|
+
const v = cfg[alias];
|
|
76
|
+
if (v && typeof v === 'string' && v.length > 0)
|
|
77
|
+
return v;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
function describeProvider(provider, cfg) {
|
|
82
|
+
const key = readProviderKey(cfg, provider);
|
|
83
|
+
return {
|
|
84
|
+
provider,
|
|
85
|
+
isConfigured: Boolean(key),
|
|
86
|
+
apiKeyMasked: maskApiKey(key),
|
|
87
|
+
baseUrl: provider === 'nemotron'
|
|
88
|
+
? (typeof cfg.NEMOTRON_BASE_URL === 'string' ? cfg.NEMOTRON_BASE_URL : null)
|
|
89
|
+
: null,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function readActiveProvider(cfg) {
|
|
93
|
+
const raw = cfg.AI_PROVIDER;
|
|
94
|
+
if (raw === 'openai' || raw === 'anthropic' || raw === 'gemini' || raw === 'nemotron') {
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
// Fall back to the provider currently bound to the running process (config.ts
|
|
98
|
+
// already auto-detects from the first configured key in env).
|
|
99
|
+
return config_1.config.aiProvider;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Triggers a graceful daemon restart. The process is supervised by launchd
|
|
103
|
+
* (macOS) / NSSM (Windows) with auto-restart enabled, so exiting will cause
|
|
104
|
+
* the supervisor to relaunch the API with the freshly written config.json.
|
|
105
|
+
*/
|
|
106
|
+
function scheduleDaemonRestart(reason) {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
logger_1.logger.info(`Exiting process to trigger supervised restart: ${reason}`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}, 500);
|
|
111
|
+
}
|
|
112
|
+
function aiProviderRouter() {
|
|
113
|
+
const router = express_1.default.Router();
|
|
114
|
+
/** GET /api/providers — list which providers have a key in config.json. */
|
|
115
|
+
router.get('/', authMiddleware_1.authMiddleware, async (_req, res) => {
|
|
116
|
+
const { logger: reqLogger } = res.locals;
|
|
117
|
+
try {
|
|
118
|
+
const cfg = readConfigFile();
|
|
119
|
+
const providers = ['openai', 'anthropic', 'gemini', 'nemotron'];
|
|
120
|
+
const active = readActiveProvider(cfg);
|
|
121
|
+
res.json({
|
|
122
|
+
providers: providers.map((p) => describeProvider(p, cfg)),
|
|
123
|
+
activeProvider: active,
|
|
124
|
+
// The in-process provider may differ from cfg.AI_PROVIDER if the user
|
|
125
|
+
// changed config.json out of band — surface both so the UI can warn.
|
|
126
|
+
runtimeProvider: config_1.config.aiProvider,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
reqLogger.error('Error reading provider config.', { error: err });
|
|
131
|
+
res.status(500).json({ error: 'Failed to read provider config.' });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
/** PUT /api/providers/:provider — store/update API key for that provider. */
|
|
135
|
+
router.put('/:provider', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
136
|
+
const { logger: reqLogger } = res.locals;
|
|
137
|
+
const providerParam = providerEnum.safeParse(req.params.provider);
|
|
138
|
+
if (!providerParam.success) {
|
|
139
|
+
return res.status(400).json({ error: 'Unknown provider.' });
|
|
140
|
+
}
|
|
141
|
+
const provider = providerParam.data;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = putSchema.parse(req.body);
|
|
144
|
+
const cfg = readConfigFile();
|
|
145
|
+
// Always clear legacy aliases so the canonical env wins on next boot.
|
|
146
|
+
for (const alias of PROVIDER_LEGACY_ALIASES[provider] ?? []) {
|
|
147
|
+
delete cfg[alias];
|
|
148
|
+
}
|
|
149
|
+
cfg[PROVIDER_ENV_KEY[provider]] = parsed.apiKey;
|
|
150
|
+
if (provider === 'nemotron') {
|
|
151
|
+
if (parsed.baseUrl) {
|
|
152
|
+
cfg.NEMOTRON_BASE_URL = parsed.baseUrl;
|
|
153
|
+
}
|
|
154
|
+
else if (parsed.baseUrl === null) {
|
|
155
|
+
delete cfg.NEMOTRON_BASE_URL;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
writeConfigFile(cfg);
|
|
159
|
+
// If the user updated the currently-active provider's key, the running
|
|
160
|
+
// process is now using a stale key — schedule a restart so it picks
|
|
161
|
+
// the new one up. We only restart when AI_PROVIDER is *explicitly*
|
|
162
|
+
// pinned in config.json, because the auto-detected fallback may differ
|
|
163
|
+
// from what the user is configuring and we don't want to bounce the
|
|
164
|
+
// server on first-time key save.
|
|
165
|
+
const explicitActive = typeof cfg.AI_PROVIDER === 'string' ? cfg.AI_PROVIDER : null;
|
|
166
|
+
let restartScheduled = false;
|
|
167
|
+
if (explicitActive === provider) {
|
|
168
|
+
scheduleDaemonRestart(`updated active provider key (${provider})`);
|
|
169
|
+
restartScheduled = true;
|
|
170
|
+
}
|
|
171
|
+
res.json({
|
|
172
|
+
...describeProvider(provider, cfg),
|
|
173
|
+
restartScheduled,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
reqLogger.error('Error storing provider key.', { error: err });
|
|
178
|
+
if (err instanceof zod_1.default.ZodError) {
|
|
179
|
+
return res.status(400).json({ error: 'Invalid provider data.' });
|
|
180
|
+
}
|
|
181
|
+
res.status(500).json({ error: 'Failed to store provider key.' });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
/** DELETE /api/providers/:provider — remove the saved key. */
|
|
185
|
+
router.delete('/:provider', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
186
|
+
const { logger: reqLogger } = res.locals;
|
|
187
|
+
const providerParam = providerEnum.safeParse(req.params.provider);
|
|
188
|
+
if (!providerParam.success) {
|
|
189
|
+
return res.status(400).json({ error: 'Unknown provider.' });
|
|
190
|
+
}
|
|
191
|
+
const provider = providerParam.data;
|
|
192
|
+
try {
|
|
193
|
+
const cfg = readConfigFile();
|
|
194
|
+
const active = readActiveProvider(cfg);
|
|
195
|
+
if (active === provider) {
|
|
196
|
+
return res.status(409).json({
|
|
197
|
+
error: 'Cannot remove the active provider. Activate a different provider first, then remove this one.',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
delete cfg[PROVIDER_ENV_KEY[provider]];
|
|
201
|
+
for (const alias of PROVIDER_LEGACY_ALIASES[provider] ?? []) {
|
|
202
|
+
delete cfg[alias];
|
|
203
|
+
}
|
|
204
|
+
if (provider === 'nemotron') {
|
|
205
|
+
delete cfg.NEMOTRON_BASE_URL;
|
|
206
|
+
}
|
|
207
|
+
writeConfigFile(cfg);
|
|
208
|
+
res.status(204).send();
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
reqLogger.error('Error removing provider key.', { error: err });
|
|
212
|
+
res.status(500).json({ error: 'Failed to remove provider key.' });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
/** POST /api/providers/:provider/activate — switch AI_PROVIDER + restart. */
|
|
216
|
+
router.post('/:provider/activate', authMiddleware_1.authMiddleware, async (req, res) => {
|
|
217
|
+
const { logger: reqLogger } = res.locals;
|
|
218
|
+
const providerParam = providerEnum.safeParse(req.params.provider);
|
|
219
|
+
if (!providerParam.success) {
|
|
220
|
+
return res.status(400).json({ error: 'Unknown provider.' });
|
|
221
|
+
}
|
|
222
|
+
const provider = providerParam.data;
|
|
223
|
+
try {
|
|
224
|
+
const cfg = readConfigFile();
|
|
225
|
+
const key = readProviderKey(cfg, provider);
|
|
226
|
+
if (!key) {
|
|
227
|
+
return res.status(400).json({
|
|
228
|
+
error: `No API key saved for provider "${provider}". Save a key first, then activate.`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
cfg.AI_PROVIDER = provider;
|
|
232
|
+
writeConfigFile(cfg);
|
|
233
|
+
res.json({
|
|
234
|
+
provider,
|
|
235
|
+
activeProvider: provider,
|
|
236
|
+
restartScheduled: true,
|
|
237
|
+
message: 'Provider activated. Server will restart shortly to apply the change.',
|
|
238
|
+
});
|
|
239
|
+
scheduleDaemonRestart(`activated provider ${provider}`);
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
reqLogger.error('Error activating provider.', { error: err });
|
|
243
|
+
res.status(500).json({ error: 'Failed to activate provider.' });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return router;
|
|
247
|
+
}
|
package/backend-dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const logger_1 = require("./logger");
|
|
|
15
15
|
const taskInstructionRoutes_1 = require("./taskInstructionRoutes");
|
|
16
16
|
const scheduledJobRoutes_1 = require("./scheduledJobRoutes");
|
|
17
17
|
const mcpServerRoutes_1 = require("./mcpServerRoutes");
|
|
18
|
+
const aiProviderRoutes_1 = require("./aiProviderRoutes");
|
|
18
19
|
const spawn_1 = require("./workers/spawn");
|
|
19
20
|
const scheduledJobWorkerClient_1 = require("./workers/scheduledJobWorkerClient");
|
|
20
21
|
const config_1 = require("./config");
|
|
@@ -36,6 +37,7 @@ app.use('/api/feature', (0, featureRoutes_1.createFeatureRouter)());
|
|
|
36
37
|
app.use('/api/instructions', (0, taskInstructionRoutes_1.taskInstructionRouter)());
|
|
37
38
|
app.use('/api/scheduled-jobs', (0, scheduledJobRoutes_1.scheduledJobRouter)());
|
|
38
39
|
app.use('/api/mcp-servers', (0, mcpServerRoutes_1.mcpServerRouter)());
|
|
40
|
+
app.use('/api/providers', (0, aiProviderRoutes_1.aiProviderRouter)());
|
|
39
41
|
app.use('/api/agent', (0, agentServer_1.createAgentRouter)());
|
|
40
42
|
app.get('/macos/download', (_req, res) => {
|
|
41
43
|
const dmgPath = path_1.default.join(process.cwd(), 'macOS', 'OmniKeyAI.dmg');
|
|
@@ -43,21 +45,25 @@ app.get('/macos/download', (_req, res) => {
|
|
|
43
45
|
res.status(404).send('File not found.');
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
48
|
+
let fileSize = 0;
|
|
49
|
+
try {
|
|
50
|
+
fileSize = fs_1.default.statSync(dmgPath).size;
|
|
51
|
+
}
|
|
52
|
+
catch (_) { }
|
|
46
53
|
res.set({
|
|
47
54
|
'Content-Type': 'application/octet-stream',
|
|
48
55
|
'Content-Disposition': 'attachment; filename="OmniKeyAI.dmg"',
|
|
49
|
-
'Content-
|
|
56
|
+
...(fileSize ? { 'Content-Length': String(fileSize) } : {}),
|
|
50
57
|
});
|
|
51
58
|
(0, bucket_adapter_1.incrementDownloadCount)('macos').catch(() => { });
|
|
52
59
|
const fileStream = fs_1.default.createReadStream(dmgPath);
|
|
53
|
-
const gzip = zlib_1.default.createGzip();
|
|
54
60
|
fileStream.on('error', (err) => {
|
|
55
61
|
logger_1.logger.error('Failed to send OmniKeyAI.dmg for download.', { error: err });
|
|
56
62
|
if (!res.headersSent) {
|
|
57
63
|
res.status(500).send('Unable to download file.');
|
|
58
64
|
}
|
|
59
65
|
});
|
|
60
|
-
fileStream.pipe(
|
|
66
|
+
fileStream.pipe(res);
|
|
61
67
|
});
|
|
62
68
|
// Sparkle appcast feed for macOS updates.
|
|
63
69
|
// This feed uses the existing /macos/download endpoint as the
|
|
@@ -181,6 +187,24 @@ app.get('/downloads/stats', async (_req, res) => {
|
|
|
181
187
|
app.get('/health', (_req, res) => {
|
|
182
188
|
res.json({ status: 'ok' });
|
|
183
189
|
});
|
|
190
|
+
app.get('/install.sh', (_req, res) => {
|
|
191
|
+
const scriptPath = path_1.default.join(process.cwd(), 'install.sh');
|
|
192
|
+
if (!fs_1.default.existsSync(scriptPath)) {
|
|
193
|
+
res.status(404).send('Not found.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
res.set('Content-Type', 'text/plain; charset=utf-8');
|
|
197
|
+
res.sendFile(scriptPath);
|
|
198
|
+
});
|
|
199
|
+
app.get('/install.ps1', (_req, res) => {
|
|
200
|
+
const scriptPath = path_1.default.join(process.cwd(), 'install.ps1');
|
|
201
|
+
if (!fs_1.default.existsSync(scriptPath)) {
|
|
202
|
+
res.status(404).send('Not found.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
res.set('Content-Type', 'text/plain; charset=utf-8');
|
|
206
|
+
res.sendFile(scriptPath);
|
|
207
|
+
});
|
|
184
208
|
app.get('*', (_req, res) => {
|
|
185
209
|
res.sendFile(path_1.default.join(process.cwd(), 'public', 'index.html'));
|
|
186
210
|
});
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.6.
|
|
7
|
+
"version": "1.6.1",
|
|
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",
|