omnikey-cli 1.5.8 → 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/__tests__/ai-client.nemotron.test.js +127 -0
- package/backend-dist/agent/agentPrompts.js +4 -3
- package/backend-dist/agent/utils.js +6 -5
- package/backend-dist/ai-client.js +151 -16
- package/backend-dist/aiProviderRoutes.js +247 -0
- package/backend-dist/config.js +16 -1
- package/backend-dist/db.js +5 -1
- package/backend-dist/index.js +27 -3
- package/backend-dist/mcpServerRoutes.js +16 -4
- package/backend-dist/scheduledJobRoutes.js +5 -2
- package/dist/index.js +1 -1
- package/dist/onboard.js +38 -0
- package/dist/telegramClient.js +1 -1
- package/dist/telegramDaemon.js +6 -4
- package/package.json +8 -6
- package/src/index.ts +1 -1
- package/src/onboard.ts +38 -0
- package/src/telegramClient.ts +1 -1
- package/src/telegramDaemon.ts +6 -8
- package/telegram-client-dist/{dist/agentClient.js → agentClient.js} +69 -75
- package/telegram-client-dist/{dist/config.js → config.js} +10 -12
- package/telegram-client-dist/{dist/index.js → index.js} +23 -23
- package/telegram-client-dist/{dist/notifyTelegram.js → notifyTelegram.js} +177 -191
- package/telegram-client-dist/{dist/omnikeyAuth.js → omnikeyAuth.js} +8 -13
|
@@ -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/config.js
CHANGED
|
@@ -45,13 +45,16 @@ function getSqlitePath() {
|
|
|
45
45
|
}
|
|
46
46
|
function getAIProvider() {
|
|
47
47
|
const value = getEnv('AI_PROVIDER', false);
|
|
48
|
-
if (value === 'gemini' || value === 'anthropic' || value === 'openai')
|
|
48
|
+
if (value === 'gemini' || value === 'anthropic' || value === 'openai' || value === 'nemotron') {
|
|
49
49
|
return value;
|
|
50
|
+
}
|
|
50
51
|
// Auto-detect from available keys
|
|
51
52
|
if (getEnv('ANTHROPIC_API_KEY', false))
|
|
52
53
|
return 'anthropic';
|
|
53
54
|
if (getEnv('GEMINI_API_KEY', false))
|
|
54
55
|
return 'gemini';
|
|
56
|
+
if (getEnv('NVIDIA_API_KEY', false) || getEnv('NEMOTRON_API_KEY', false))
|
|
57
|
+
return 'nemotron';
|
|
55
58
|
return 'openai';
|
|
56
59
|
}
|
|
57
60
|
function getActiveApiKey(provider) {
|
|
@@ -61,6 +64,14 @@ function getActiveApiKey(provider) {
|
|
|
61
64
|
return getEnv('ANTHROPIC_API_KEY', true);
|
|
62
65
|
if (provider === 'gemini')
|
|
63
66
|
return getEnv('GEMINI_API_KEY', true);
|
|
67
|
+
if (provider === 'nemotron') {
|
|
68
|
+
// Accept either NVIDIA_API_KEY (default name on build.nvidia.com) or
|
|
69
|
+
// NEMOTRON_API_KEY (more explicit). The latter wins if both are set.
|
|
70
|
+
const explicit = getEnv('NEMOTRON_API_KEY', false);
|
|
71
|
+
if (explicit)
|
|
72
|
+
return explicit;
|
|
73
|
+
return getEnv('NVIDIA_API_KEY', true);
|
|
74
|
+
}
|
|
64
75
|
throw new Error(`Unknown AI provider: ${provider}`);
|
|
65
76
|
}
|
|
66
77
|
const _provider = getAIProvider();
|
|
@@ -73,6 +84,10 @@ exports.config = {
|
|
|
73
84
|
aiApiKey: getActiveApiKey(_provider),
|
|
74
85
|
// Legacy — kept for backwards compatibility; may be undefined when using another provider
|
|
75
86
|
openaiApiKey: getEnv('OPENAI_API_KEY', false),
|
|
87
|
+
// Optional override for the NVIDIA NIM endpoint. Defaults to the public
|
|
88
|
+
// `https://integrate.api.nvidia.com/v1` gateway when unset. Point this at a
|
|
89
|
+
// self-hosted NIM (e.g. `http://my-nim-host:8000/v1`) to use private weights.
|
|
90
|
+
nemotronBaseUrl: getEnv('NEMOTRON_BASE_URL', false),
|
|
76
91
|
// Database
|
|
77
92
|
databaseUrl: getEnv('DATABASE_URL', getBooleanEnv('IS_SELF_HOSTED', false) ? false : true),
|
|
78
93
|
dbLogging: getBooleanEnv('DB_LOGGING', false),
|
package/backend-dist/db.js
CHANGED
|
@@ -30,7 +30,11 @@ else if (config_1.config.databaseUrl) {
|
|
|
30
30
|
}
|
|
31
31
|
const COLUMN_MIGRATIONS = [
|
|
32
32
|
// Added: context-window tracking (prompt token count of last API call)
|
|
33
|
-
{
|
|
33
|
+
{
|
|
34
|
+
table: 'agent_sessions',
|
|
35
|
+
column: 'last_prompt_tokens',
|
|
36
|
+
definition: 'INTEGER NOT NULL DEFAULT 0',
|
|
37
|
+
},
|
|
34
38
|
// Added: project grouping
|
|
35
39
|
{ table: 'agent_sessions', column: 'group_name', definition: 'VARCHAR(255)' },
|
|
36
40
|
{ table: 'agent_sessions', column: 'group_description', definition: 'TEXT' },
|
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
|
});
|
|
@@ -165,11 +165,23 @@ function mcpServerRouter() {
|
|
|
165
165
|
// When transport changes, clear fields incompatible with the new transport so
|
|
166
166
|
// stale credentials/config never persist across a transport switch.
|
|
167
167
|
const command = transportChanged
|
|
168
|
-
?
|
|
169
|
-
|
|
168
|
+
? transport === 'stdio'
|
|
169
|
+
? parsed.command !== undefined
|
|
170
|
+
? parsed.command
|
|
171
|
+
: server.command
|
|
172
|
+
: null
|
|
173
|
+
: parsed.command !== undefined
|
|
174
|
+
? parsed.command
|
|
175
|
+
: server.command;
|
|
170
176
|
const url = transportChanged
|
|
171
|
-
?
|
|
172
|
-
|
|
177
|
+
? transport !== 'stdio'
|
|
178
|
+
? parsed.url !== undefined
|
|
179
|
+
? parsed.url
|
|
180
|
+
: server.url
|
|
181
|
+
: null
|
|
182
|
+
: parsed.url !== undefined
|
|
183
|
+
? parsed.url
|
|
184
|
+
: server.url;
|
|
173
185
|
const args = transportChanged && transport !== 'stdio' ? [] : (parsed.args ?? server.args);
|
|
174
186
|
const env = transportChanged && transport !== 'stdio' ? {} : (parsed.env ?? server.env);
|
|
175
187
|
const headers = transportChanged && transport === 'stdio' ? {} : (parsed.headers ?? server.headers);
|
|
@@ -14,7 +14,10 @@ const CRON_REGEX = /^(\S+\s){4}\S+$/;
|
|
|
14
14
|
const jobSchema = zod_1.default.object({
|
|
15
15
|
label: zod_1.default.string().min(1).max(200),
|
|
16
16
|
prompt: zod_1.default.string().min(1),
|
|
17
|
-
cronExpression: zod_1.default
|
|
17
|
+
cronExpression: zod_1.default
|
|
18
|
+
.string()
|
|
19
|
+
.regex(CRON_REGEX, 'Invalid cron expression (must be 5 fields)')
|
|
20
|
+
.optional(),
|
|
18
21
|
runAt: zod_1.default.string().optional(),
|
|
19
22
|
isActive: zod_1.default.boolean().optional(),
|
|
20
23
|
sessionId: zod_1.default.string().nullable().optional(),
|
|
@@ -108,7 +111,7 @@ function scheduledJobRouter() {
|
|
|
108
111
|
if (!job) {
|
|
109
112
|
return res.status(404).json({ error: 'Scheduled job not found.' });
|
|
110
113
|
}
|
|
111
|
-
const cronExpression = parsed.cronExpression !== undefined ? parsed.cronExpression ?? null : job.cronExpression;
|
|
114
|
+
const cronExpression = parsed.cronExpression !== undefined ? (parsed.cronExpression ?? null) : job.cronExpression;
|
|
112
115
|
let runAt = job.runAt;
|
|
113
116
|
if (parsed.runAt !== undefined) {
|
|
114
117
|
if (parsed.runAt) {
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const program = new commander_1.Command();
|
|
|
18
18
|
program
|
|
19
19
|
.name('omnikey')
|
|
20
20
|
.description('Omnikey CLI for onboarding and configuration')
|
|
21
|
-
.version('1.
|
|
21
|
+
.version('1.6.0');
|
|
22
22
|
program
|
|
23
23
|
.command('onboard')
|
|
24
24
|
.description('Onboard and configure your AI provider')
|
package/dist/onboard.js
CHANGED
|
@@ -12,6 +12,10 @@ const AI_PROVIDERS = [
|
|
|
12
12
|
{ name: 'OpenAI (gpt-4o-mini / gpt-5.5)', value: 'openai' },
|
|
13
13
|
{ name: 'Anthropic — Claude (claude-haiku / claude-opus)', value: 'anthropic' },
|
|
14
14
|
{ name: 'Google Gemini (gemini-2.5-flash / gemini-2.5-pro)', value: 'gemini' },
|
|
15
|
+
{
|
|
16
|
+
name: 'NVIDIA Nemotron (nemotron-3-nano / nemotron-3-ultra) — open weights',
|
|
17
|
+
value: 'nemotron',
|
|
18
|
+
},
|
|
15
19
|
];
|
|
16
20
|
const SEARCH_PROVIDERS = [
|
|
17
21
|
{ name: 'Skip', value: 'skip' },
|
|
@@ -25,11 +29,13 @@ const AI_PROVIDER_KEY_ENV = {
|
|
|
25
29
|
openai: 'OPENAI_API_KEY',
|
|
26
30
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
27
31
|
gemini: 'GEMINI_API_KEY',
|
|
32
|
+
nemotron: 'NVIDIA_API_KEY',
|
|
28
33
|
};
|
|
29
34
|
const AI_PROVIDER_KEY_LABEL = {
|
|
30
35
|
openai: 'OpenAI API key (from platform.openai.com)',
|
|
31
36
|
anthropic: 'Anthropic API key (from console.anthropic.com)',
|
|
32
37
|
gemini: 'Google Gemini API key (from ai.google.dev)',
|
|
38
|
+
nemotron: 'NVIDIA API key (from build.nvidia.com — used by NIM/Nemotron)',
|
|
33
39
|
};
|
|
34
40
|
/**
|
|
35
41
|
* Onboard the user by configuring their AI provider API key and generating config for self-hosted use.
|
|
@@ -55,6 +61,37 @@ async function onboard() {
|
|
|
55
61
|
validate: (input) => input.trim() !== '' || 'API key cannot be empty',
|
|
56
62
|
},
|
|
57
63
|
]);
|
|
64
|
+
// Provider-specific extras
|
|
65
|
+
const providerExtras = {};
|
|
66
|
+
if (aiProvider === 'nemotron') {
|
|
67
|
+
// Nemotron is served either via the public NVIDIA NIM gateway or a
|
|
68
|
+
// self-hosted NIM microservice. Always ask the user for the base URL so
|
|
69
|
+
// they can point at either. The default value matches the public gateway
|
|
70
|
+
// so pressing Enter "just works" for build.nvidia.com keys.
|
|
71
|
+
const DEFAULT_NEMOTRON_URL = 'https://integrate.api.nvidia.com/v1';
|
|
72
|
+
const { nemotronBaseUrl } = await inquirer_1.default.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'nemotronBaseUrl',
|
|
76
|
+
message: 'Enter the Nemotron / NVIDIA NIM base URL (press Enter for the public gateway):',
|
|
77
|
+
default: DEFAULT_NEMOTRON_URL,
|
|
78
|
+
validate: (input) => {
|
|
79
|
+
const trimmed = input.trim();
|
|
80
|
+
if (trimmed === '')
|
|
81
|
+
return 'URL cannot be empty';
|
|
82
|
+
try {
|
|
83
|
+
// eslint-disable-next-line no-new
|
|
84
|
+
new URL(trimmed);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return 'Please enter a valid URL (including the scheme, e.g. https://...)';
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
providerExtras['NEMOTRON_BASE_URL'] = nemotronBaseUrl.trim();
|
|
94
|
+
}
|
|
58
95
|
// Web search provider (optional)
|
|
59
96
|
const { provider } = await inquirer_1.default.prompt([
|
|
60
97
|
{
|
|
@@ -119,6 +156,7 @@ async function onboard() {
|
|
|
119
156
|
[AI_PROVIDER_KEY_ENV[aiProvider]]: apiKey,
|
|
120
157
|
IS_SELF_HOSTED: true,
|
|
121
158
|
SQLITE_PATH: sqlitePath,
|
|
159
|
+
...providerExtras,
|
|
122
160
|
...searchConfig,
|
|
123
161
|
};
|
|
124
162
|
fs_1.default.writeFileSync(configPath, JSON.stringify(configVars, null, 2));
|
package/dist/telegramClient.js
CHANGED
|
@@ -23,7 +23,7 @@ function resolveBundleRoot() {
|
|
|
23
23
|
return path_1.default.resolve(__dirname, '..', 'telegram-client-dist');
|
|
24
24
|
}
|
|
25
25
|
function resolveBundledEntry() {
|
|
26
|
-
return path_1.default.join(resolveBundleRoot(), '
|
|
26
|
+
return path_1.default.join(resolveBundleRoot(), 'index.js');
|
|
27
27
|
}
|
|
28
28
|
function persistConfig(values) {
|
|
29
29
|
const configDir = (0, utils_1.getConfigDir)();
|
package/dist/telegramDaemon.js
CHANGED
|
@@ -20,12 +20,12 @@ const LABEL = `com.${os_1.default.userInfo().username}.telegram`;
|
|
|
20
20
|
const PLIST_NAME = `${LABEL}.plist`;
|
|
21
21
|
const WINDOWS_SERVICE_NAME = 'OmnikeyTelegram';
|
|
22
22
|
// At runtime __dirname is cli/dist/. The bundled telegram app is copied into
|
|
23
|
-
// cli/telegram-client-dist/ by the build:telegram-client script
|
|
24
|
-
// up from dist/ lands at the package
|
|
23
|
+
// cli/telegram-client-dist/ by the build:telegram-client script (flat layout,
|
|
24
|
+
// matching backend-dist/), so one level up from dist/ lands at the package
|
|
25
25
|
// This matches resolveBundleRoot() in telegramClient.ts and works correctly
|
|
26
26
|
// both in the monorepo and after `npm install -g omnikey-cli`.
|
|
27
27
|
const TELEGRAM_BOT_ROOT = path_1.default.resolve(__dirname, '..', 'telegram-client-dist');
|
|
28
|
-
const ENTRY_POINT = path_1.default.join(TELEGRAM_BOT_ROOT, '
|
|
28
|
+
const ENTRY_POINT = path_1.default.join(TELEGRAM_BOT_ROOT, 'index.js');
|
|
29
29
|
const HOME = (0, utils_1.getHomeDir)();
|
|
30
30
|
// macOS — launchd LaunchAgent paths
|
|
31
31
|
const LAUNCH_AGENTS_DIR = path_1.default.join(HOME, 'Library', 'LaunchAgents');
|
|
@@ -289,7 +289,9 @@ async function startWindows() {
|
|
|
289
289
|
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'Start', 'SERVICE_AUTO_START'], {
|
|
290
290
|
stdio: 'pipe',
|
|
291
291
|
});
|
|
292
|
-
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'DisplayName', 'Omnikey Telegram'], {
|
|
292
|
+
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'DisplayName', 'Omnikey Telegram'], {
|
|
293
|
+
stdio: 'pipe',
|
|
294
|
+
});
|
|
293
295
|
(0, child_process_1.execFileSync)(nssmPath, ['set', WINDOWS_SERVICE_NAME, 'Description', 'Omnikey Telegram Daemon'], { stdio: 'pipe' });
|
|
294
296
|
(0, child_process_1.execFileSync)(nssmPath, ['start', WINDOWS_SERVICE_NAME], { stdio: 'pipe' });
|
|
295
297
|
console.log(`NSSM service installed and started: ${WINDOWS_SERVICE_NAME}`);
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"access": "public",
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
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",
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
"omnikey": "dist/index.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc &&
|
|
17
|
+
"build": "tsc && yarn run copy-backend && yarn run build:telegram-client",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
|
-
"copy-backend": "rm -rf backend-dist && mkdir -p backend-dist && cp -R ../dist/* backend-dist/",
|
|
20
|
-
"build:telegram-client": "rm -rf telegram-client-dist && mkdir -p telegram-client-dist
|
|
19
|
+
"copy-backend": "rm -rf backend-dist && mkdir -p backend-dist && cp -R ../api/dist/* backend-dist/",
|
|
20
|
+
"build:telegram-client": "rm -rf telegram-client-dist && mkdir -p telegram-client-dist && cp -R ../telegram/dist/* telegram-client-dist/",
|
|
21
|
+
"clean": "rm -rf dist backend-dist telegram-client-dist",
|
|
22
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
23
|
+
"test": "echo \"(no tests in this workspace)\" && exit 0",
|
|
24
|
+
"lint": "echo \"(no lint in this workspace)\" && exit 0"
|
|
21
25
|
},
|
|
22
26
|
"keywords": [
|
|
23
27
|
"cli",
|
|
@@ -33,7 +37,6 @@
|
|
|
33
37
|
"@google/genai": "^1.46.0",
|
|
34
38
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
39
|
"axios": "^1.13.5",
|
|
36
|
-
"better-sqlite3": "^12.10.0",
|
|
37
40
|
"commander": "^11.0.0",
|
|
38
41
|
"cors": "^2.8.5",
|
|
39
42
|
"cron-parser": "^4.9.0",
|
|
@@ -54,7 +57,6 @@
|
|
|
54
57
|
"zod": "^4.3.6"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
58
60
|
"@types/inquirer": "^9.0.9",
|
|
59
61
|
"@types/node-telegram-bot-api": "^0.64.0",
|
|
60
62
|
"typescript": "^5.0.0"
|
package/src/index.ts
CHANGED
package/src/onboard.ts
CHANGED
|
@@ -7,6 +7,10 @@ const AI_PROVIDERS = [
|
|
|
7
7
|
{ name: 'OpenAI (gpt-4o-mini / gpt-5.5)', value: 'openai' },
|
|
8
8
|
{ name: 'Anthropic — Claude (claude-haiku / claude-opus)', value: 'anthropic' },
|
|
9
9
|
{ name: 'Google Gemini (gemini-2.5-flash / gemini-2.5-pro)', value: 'gemini' },
|
|
10
|
+
{
|
|
11
|
+
name: 'NVIDIA Nemotron (nemotron-3-nano / nemotron-3-ultra) — open weights',
|
|
12
|
+
value: 'nemotron',
|
|
13
|
+
},
|
|
10
14
|
];
|
|
11
15
|
|
|
12
16
|
const SEARCH_PROVIDERS = [
|
|
@@ -22,12 +26,14 @@ const AI_PROVIDER_KEY_ENV: Record<string, string> = {
|
|
|
22
26
|
openai: 'OPENAI_API_KEY',
|
|
23
27
|
anthropic: 'ANTHROPIC_API_KEY',
|
|
24
28
|
gemini: 'GEMINI_API_KEY',
|
|
29
|
+
nemotron: 'NVIDIA_API_KEY',
|
|
25
30
|
};
|
|
26
31
|
|
|
27
32
|
const AI_PROVIDER_KEY_LABEL: Record<string, string> = {
|
|
28
33
|
openai: 'OpenAI API key (from platform.openai.com)',
|
|
29
34
|
anthropic: 'Anthropic API key (from console.anthropic.com)',
|
|
30
35
|
gemini: 'Google Gemini API key (from ai.google.dev)',
|
|
36
|
+
nemotron: 'NVIDIA API key (from build.nvidia.com — used by NIM/Nemotron)',
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -57,6 +63,37 @@ export async function onboard() {
|
|
|
57
63
|
},
|
|
58
64
|
]);
|
|
59
65
|
|
|
66
|
+
// Provider-specific extras
|
|
67
|
+
const providerExtras: Record<string, string> = {};
|
|
68
|
+
|
|
69
|
+
if (aiProvider === 'nemotron') {
|
|
70
|
+
// Nemotron is served either via the public NVIDIA NIM gateway or a
|
|
71
|
+
// self-hosted NIM microservice. Always ask the user for the base URL so
|
|
72
|
+
// they can point at either. The default value matches the public gateway
|
|
73
|
+
// so pressing Enter "just works" for build.nvidia.com keys.
|
|
74
|
+
const DEFAULT_NEMOTRON_URL = 'https://integrate.api.nvidia.com/v1';
|
|
75
|
+
const { nemotronBaseUrl } = await inquirer.prompt([
|
|
76
|
+
{
|
|
77
|
+
type: 'input',
|
|
78
|
+
name: 'nemotronBaseUrl',
|
|
79
|
+
message: 'Enter the Nemotron / NVIDIA NIM base URL (press Enter for the public gateway):',
|
|
80
|
+
default: DEFAULT_NEMOTRON_URL,
|
|
81
|
+
validate: (input: string) => {
|
|
82
|
+
const trimmed = input.trim();
|
|
83
|
+
if (trimmed === '') return 'URL cannot be empty';
|
|
84
|
+
try {
|
|
85
|
+
// eslint-disable-next-line no-new
|
|
86
|
+
new URL(trimmed);
|
|
87
|
+
return true;
|
|
88
|
+
} catch {
|
|
89
|
+
return 'Please enter a valid URL (including the scheme, e.g. https://...)';
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
providerExtras['NEMOTRON_BASE_URL'] = nemotronBaseUrl.trim();
|
|
95
|
+
}
|
|
96
|
+
|
|
60
97
|
// Web search provider (optional)
|
|
61
98
|
const { provider } = await inquirer.prompt([
|
|
62
99
|
{
|
|
@@ -122,6 +159,7 @@ export async function onboard() {
|
|
|
122
159
|
[AI_PROVIDER_KEY_ENV[aiProvider]]: apiKey,
|
|
123
160
|
IS_SELF_HOSTED: true,
|
|
124
161
|
SQLITE_PATH: sqlitePath,
|
|
162
|
+
...providerExtras,
|
|
125
163
|
...searchConfig,
|
|
126
164
|
};
|
|
127
165
|
fs.writeFileSync(configPath, JSON.stringify(configVars, null, 2));
|