omnikey-cli 1.0.35 → 1.0.36

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.
@@ -171,9 +171,6 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
171
171
  return result;
172
172
  }
173
173
  const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
174
- // In-memory cache: sessionId -> live SessionState. Hydrated from DB on first
175
- // access and written back after each turn so restarts resume correctly.
176
- const sessionMessages = new Map();
177
174
  const MAX_TURNS = 20;
178
175
  // ─── DB helpers ───────────────────────────────────────────────────────────────
179
176
  async function persistSessionToDB(sessionId, state) {
@@ -216,22 +213,7 @@ async function enforceSessionCap(subscriptionId, logger) {
216
213
  }
217
214
  }
218
215
  async function getOrCreateSession(sessionId, subscription, platform, log, isCronJob = false) {
219
- // 1. Return the live in-memory entry if already loaded this process lifetime.
220
- const existing = sessionMessages.get(sessionId);
221
- if (existing) {
222
- log.debug('Reusing existing agent session (in-memory)', {
223
- sessionId,
224
- subscriptionId: existing.subscription.id,
225
- turns: existing.turns,
226
- });
227
- return {
228
- sessionState: existing,
229
- hasStoredPrompt: existing.history
230
- .filter((h) => h.role === 'user')
231
- .some((h) => typeof h.content === 'string' && h.content.includes('<stored_instructions>')),
232
- };
233
- }
234
- // 2. Try to resume from a persisted DB record.
216
+ // 1. Try to resume from a persisted DB record.
235
217
  try {
236
218
  const dbSession = await agentSession_1.AgentSession.findOne({
237
219
  where: { id: sessionId, subscriptionId: subscription.id },
@@ -243,7 +225,6 @@ async function getOrCreateSession(sessionId, subscription, platform, log, isCron
243
225
  history,
244
226
  turns: dbSession.turns,
245
227
  };
246
- sessionMessages.set(sessionId, entry);
247
228
  log.info('Resumed agent session from DB', {
248
229
  sessionId,
249
230
  subscriptionId: subscription.id,
@@ -263,7 +244,7 @@ async function getOrCreateSession(sessionId, subscription, platform, log, isCron
263
244
  error: err,
264
245
  });
265
246
  }
266
- // 3. Create a brand-new session in-memory and persist it to the DB.
247
+ // 2. Create a brand-new session and persist it to the DB.
267
248
  const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
268
249
  log.error('Failed to get system prompt for new agent session', { error: err });
269
250
  return '';
@@ -293,17 +274,19 @@ ${prompt}
293
274
  ],
294
275
  turns: 0,
295
276
  };
296
- sessionMessages.set(sessionId, entry);
297
277
  // Persist immediately so that GET /sessions picks it up right away.
298
278
  try {
299
- await agentSession_1.AgentSession.create({
300
- id: sessionId,
301
- subscriptionId: subscription.id,
302
- title: 'New Session',
303
- platform: platform ?? null,
304
- historyJson: JSON.stringify(entry.history),
305
- turns: 0,
306
- lastActiveAt: new Date(),
279
+ await agentSession_1.AgentSession.findOrCreate({
280
+ where: { id: sessionId, subscriptionId: subscription.id },
281
+ defaults: {
282
+ id: sessionId,
283
+ subscriptionId: subscription.id,
284
+ title: 'New Session',
285
+ platform: platform ?? null,
286
+ historyJson: JSON.stringify(entry.history),
287
+ turns: 0,
288
+ lastActiveAt: new Date(),
289
+ },
307
290
  });
308
291
  // Prune oldest sessions after each creation so the cap is always respected.
309
292
  void enforceSessionCap(subscription.id, log);
@@ -321,7 +304,7 @@ ${prompt}
321
304
  hasStoredPrompt: !!prompt,
322
305
  };
323
306
  }
324
- async function runAgentTurn(sessionId, subscription, clientMessage, send, log, options) {
307
+ async function runAgentTurnInternal(sessionId, subscription, clientMessage, send, log, options) {
325
308
  const { sessionState: session, hasStoredPrompt } = await getOrCreateSession(sessionId, subscription, clientMessage.platform, log, options?.isCronJob);
326
309
  // Count this call as one agent iteration.
327
310
  session.turns += 1;
@@ -445,7 +428,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
445
428
  log.warn('Agent LLM returned empty content; sending generic error to client.');
446
429
  const errorMessage = 'The agent returned an empty response. Please try again.';
447
430
  await persistSessionToDB(sessionId, session);
448
- sessionMessages.delete(sessionId);
449
431
  (0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
450
432
  return;
451
433
  }
@@ -506,7 +488,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
506
488
  'No plain text. No other format.',
507
489
  ].join('\n'),
508
490
  });
509
- await runAgentTurn(sessionId, subscription, {
491
+ await runAgentTurnInternal(sessionId, subscription, {
510
492
  sender: 'agent',
511
493
  session_id: sessionId,
512
494
  content: '',
@@ -552,7 +534,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
552
534
  });
553
535
  (0, utils_1.pushToSessionHistory)(logger_1.logger, session, { role: 'assistant', content });
554
536
  await persistSessionToDB(sessionId, session);
555
- sessionMessages.delete(sessionId);
556
537
  send({
557
538
  session_id: sessionId,
558
539
  sender: 'agent',
@@ -571,7 +552,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
571
552
  });
572
553
  (0, utils_1.pushToSessionHistory)(log, session, { role: 'assistant', content });
573
554
  await persistSessionToDB(sessionId, session);
574
- sessionMessages.delete(sessionId);
575
555
  send({
576
556
  session_id: sessionId,
577
557
  sender: 'agent',
@@ -583,7 +563,6 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
583
563
  sessionId,
584
564
  });
585
565
  await persistSessionToDB(sessionId, session);
586
- sessionMessages.delete(sessionId);
587
566
  (0, utils_1.sendFinalAnswer)(send, sessionId, 'The agent returned an empty response. Please try again.', true);
588
567
  }
589
568
  }
@@ -591,10 +570,12 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, o
591
570
  log.error('Agent LLM call failed', { error: err });
592
571
  const errorMessage = 'Agent failed to call language model. Please try again later.';
593
572
  await persistSessionToDB(sessionId, session);
594
- sessionMessages.delete(sessionId);
595
573
  (0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
596
574
  }
597
575
  }
576
+ async function runAgentTurn(sessionId, subscription, clientMessage, send, log, options) {
577
+ await runAgentTurnInternal(sessionId, subscription, clientMessage, send, log, options);
578
+ }
598
579
  function attachAgentWebSocketServer(server) {
599
580
  const wss = new ws_1.WebSocketServer({ server, path: '/ws/omni-agent' });
600
581
  wss.on('connection', (ws, req) => {
@@ -731,8 +712,6 @@ function createAgentRouter() {
731
712
  res.status(404).json({ error: 'Session not found' });
732
713
  return;
733
714
  }
734
- // Also remove from the in-memory cache if it was loaded.
735
- sessionMessages.delete(sessionId);
736
715
  res.status(200).json({ deleted: true });
737
716
  }
738
717
  catch (err) {
@@ -10,18 +10,33 @@ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
10
10
  const logger_1 = require("./logger");
11
11
  const config_1 = require("./config");
12
12
  const subscription_1 = require("./models/subscription");
13
+ const SELF_HOSTED_SUBSCRIPTION_ID = 'self-hosted-local-subscription';
13
14
  async function selfHostedSubscription() {
14
15
  try {
15
- let subscription = await subscription_1.Subscription.findOne({ where: { isSelfHosted: true } });
16
- if (!subscription) {
17
- subscription = await subscription_1.Subscription.create({
16
+ // Reuse any existing self-hosted record (including legacy IDs) first.
17
+ const existing = await subscription_1.Subscription.findOne({ where: { isSelfHosted: true } });
18
+ if (existing)
19
+ return existing;
20
+ // Use a deterministic primary key so concurrent first-time requests do not
21
+ // create duplicate rows.
22
+ const [subscription, created] = await subscription_1.Subscription.findOrCreate({
23
+ where: { id: SELF_HOSTED_SUBSCRIPTION_ID },
24
+ defaults: {
25
+ id: SELF_HOSTED_SUBSCRIPTION_ID,
18
26
  email: 'local-user@omnikey.ai',
19
27
  licenseKey: 'self-hosted',
20
28
  subscriptionStatus: 'active',
21
29
  isSelfHosted: true,
22
- });
30
+ },
31
+ });
32
+ if (created) {
23
33
  logger_1.logger.info('Created self-hosted subscription record in database.');
24
34
  }
35
+ // Ensure deterministic row remains flagged for self-hosted mode.
36
+ if (!subscription.isSelfHosted) {
37
+ subscription.isSelfHosted = true;
38
+ await subscription.save();
39
+ }
25
40
  return subscription;
26
41
  }
27
42
  catch (err) {
@@ -31,37 +46,37 @@ async function selfHostedSubscription() {
31
46
  }
32
47
  async function authMiddleware(req, res, next) {
33
48
  const authHeader = req.headers.authorization;
34
- logger_1.logger.defaultMeta = { traceId: (0, crypto_1.randomUUID)() };
49
+ const requestLogger = logger_1.logger.child({ traceId: (0, crypto_1.randomUUID)() });
50
+ res.locals.logger = requestLogger;
35
51
  if (config_1.config.blockSaas) {
36
- logger_1.logger.warn('Blocking SaaS access: rejecting request due to BLOCK_SAAS=true');
52
+ requestLogger.warn('Blocking SaaS access: rejecting request due to BLOCK_SAAS=true');
37
53
  return res.status(403).json({ error: 'SaaS access is blocked.' });
38
54
  }
39
55
  if (config_1.config.isSelfHosted || !config_1.config.jwtSecret) {
40
- logger_1.logger.info('Self-hosted mode: skipping auth middleware.');
56
+ requestLogger.info('Self-hosted mode: skipping auth middleware.');
41
57
  if (config_1.config.isSelfHosted) {
42
58
  res.locals.subscription = await selfHostedSubscription();
43
- res.locals.logger = logger_1.logger;
44
59
  }
45
60
  return next();
46
61
  }
47
62
  if (!authHeader) {
48
- logger_1.logger.warn('Missing Authorization header on feature route.');
63
+ requestLogger.warn('Missing Authorization header on feature route.');
49
64
  return res.status(401).json({ error: 'Missing bearer token.' });
50
65
  }
51
66
  const [scheme, token] = authHeader.split(' ');
52
67
  if (scheme !== 'Bearer' || !token) {
53
- logger_1.logger.warn('Malformed Authorization header on feature route.');
68
+ requestLogger.warn('Malformed Authorization header on feature route.');
54
69
  return res.status(401).json({ error: 'Invalid authorization header.' });
55
70
  }
56
71
  try {
57
72
  const decoded = jsonwebtoken_1.default.verify(token, config_1.config.jwtSecret);
58
73
  const subscription = await subscription_1.Subscription.findByPk(decoded.sid);
59
74
  if (!subscription) {
60
- logger_1.logger.warn('Subscription not found for JWT.', { sid: decoded.sid });
75
+ requestLogger.warn('Subscription not found for JWT.', { sid: decoded.sid });
61
76
  return res.status(403).json({ error: 'Invalid or expired token.' });
62
77
  }
63
78
  if (subscription.subscriptionStatus == 'expired') {
64
- logger_1.logger.warn('Inactive subscription for JWT.', {
79
+ requestLogger.warn('Inactive subscription for JWT.', {
65
80
  sid: decoded.sid,
66
81
  status: subscription.subscriptionStatus,
67
82
  });
@@ -71,19 +86,18 @@ async function authMiddleware(req, res, next) {
71
86
  if (subscription.licenseKeyExpiresAt && subscription.licenseKeyExpiresAt <= now) {
72
87
  subscription.subscriptionStatus = 'expired';
73
88
  await subscription.save();
74
- logger_1.logger.info('Subscription key has expired during activation.', {
89
+ requestLogger.info('Subscription key has expired during activation.', {
75
90
  subscriptionId: subscription.id,
76
91
  });
77
92
  return res
78
93
  .status(403)
79
94
  .json({ error: 'Subscription has expired.', subscriptionStatus: 'expired' });
80
95
  }
81
- res.locals.logger = logger_1.logger;
82
96
  res.locals.subscription = subscription;
83
97
  next();
84
98
  }
85
99
  catch (err) {
86
- logger_1.logger.warn('Invalid or expired JWT on feature route.', { error: err });
100
+ requestLogger.warn('Invalid or expired JWT on feature route.', { error: err });
87
101
  return res.status(403).json({ error: 'Invalid or expired token.' });
88
102
  }
89
103
  }
@@ -3,9 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getDownloadCounts = getDownloadCounts;
4
4
  exports.incrementDownloadCount = incrementDownloadCount;
5
5
  const storage_1 = require("@google-cloud/storage");
6
+ const zod_1 = require("zod");
6
7
  const logger_1 = require("../logger");
7
8
  const config_1 = require("../config");
8
9
  const DEFAULT_COUNTS = { macos: 0, windows: 0 };
10
+ const downloadCountsSchema = zod_1.z.object({
11
+ macos: zod_1.z.number().nonnegative().optional(),
12
+ windows: zod_1.z.number().nonnegative().optional(),
13
+ });
14
+ function parseDownloadCounts(raw) {
15
+ const json = JSON.parse(raw);
16
+ const parsed = downloadCountsSchema.safeParse(json);
17
+ if (!parsed.success) {
18
+ return { ...DEFAULT_COUNTS };
19
+ }
20
+ return {
21
+ macos: parsed.data.macos ?? 0,
22
+ windows: parsed.data.windows ?? 0,
23
+ };
24
+ }
9
25
  // Initialised once at module load — uses Application Default Credentials when
10
26
  // running on Cloud Run (or any GCP environment), and falls back to ADC from
11
27
  // the local environment during development.
@@ -30,28 +46,57 @@ async function readCounts(bucketName, objectPath) {
30
46
  return { ...DEFAULT_COUNTS };
31
47
  }
32
48
  const [contents] = await file.download();
33
- const parsed = JSON.parse(contents.toString('utf8'));
49
+ return parseDownloadCounts(contents.toString('utf8'));
50
+ }
51
+ async function readCountsWithGeneration(bucketName, objectPath) {
52
+ const file = storage.bucket(bucketName).file(objectPath);
53
+ const [exists] = await file.exists();
54
+ if (!exists) {
55
+ return { counts: { ...DEFAULT_COUNTS }, generation: null, exists: false };
56
+ }
57
+ const [[metadata], [contents]] = await Promise.all([file.getMetadata(), file.download()]);
58
+ const counts = parseDownloadCounts(contents.toString('utf8'));
34
59
  return {
35
- macos: typeof parsed.macos === 'number' ? parsed.macos : 0,
36
- windows: typeof parsed.windows === 'number' ? parsed.windows : 0,
60
+ counts,
61
+ generation: metadata.generation ?? null,
62
+ exists: true,
37
63
  };
38
64
  }
39
- async function writeCounts(bucketName, objectPath, counts) {
40
- const file = storage.bucket(bucketName).file(objectPath);
41
- await file.save(JSON.stringify(counts), {
42
- contentType: 'application/json',
43
- resumable: false,
44
- });
65
+ function isGcsPreconditionError(err) {
66
+ const maybe = err;
67
+ return (maybe?.code === 412 ||
68
+ maybe?.message?.includes('conditionNotMet') === true ||
69
+ maybe?.message?.includes('Precondition Failed') === true);
45
70
  }
46
71
  async function incrementDownloadCount(platform) {
47
72
  const gcs = getGcsConfig();
48
73
  if (!gcs)
49
74
  return;
75
+ const file = storage.bucket(gcs.bucketName).file(gcs.objectPath);
76
+ const MAX_RETRIES = 6;
50
77
  try {
51
- const counts = await readCounts(gcs.bucketName, gcs.objectPath);
52
- counts[platform] += 1;
53
- await writeCounts(gcs.bucketName, gcs.objectPath, counts);
54
- logger_1.logger.info(`Download count incremented for ${platform}.`, { counts });
78
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
79
+ const { counts, generation, exists } = await readCountsWithGeneration(gcs.bucketName, gcs.objectPath);
80
+ counts[platform] += 1;
81
+ try {
82
+ await file.save(JSON.stringify(counts), {
83
+ contentType: 'application/json',
84
+ resumable: false,
85
+ preconditionOpts: exists
86
+ ? { ifGenerationMatch: Number(generation) }
87
+ : { ifGenerationMatch: 0 },
88
+ });
89
+ logger_1.logger.info(`Download count incremented for ${platform}.`, { counts, attempt });
90
+ return;
91
+ }
92
+ catch (err) {
93
+ if (isGcsPreconditionError(err) && attempt < MAX_RETRIES) {
94
+ continue;
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+ logger_1.logger.warn(`Download count increment exhausted retries for ${platform}.`);
55
100
  }
56
101
  catch (err) {
57
102
  logger_1.logger.error(`Failed to increment download count for ${platform}.`, { error: err });
@@ -8,6 +8,7 @@ const cors_1 = __importDefault(require("cors"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const zlib_1 = __importDefault(require("zlib"));
11
+ const child_process_1 = require("child_process");
11
12
  const subscriptionRoutes_1 = require("./subscriptionRoutes");
12
13
  const featureRoutes_1 = require("./featureRoutes");
13
14
  const db_1 = require("./db");
@@ -23,6 +24,7 @@ require("./models/scheduledJob");
23
24
  const bucket_adapter_1 = require("./bucket-adapter");
24
25
  const app = (0, express_1.default)();
25
26
  const PORT = Number(config_1.config.port);
27
+ const IS_CRON_CHILD = process.env.OMNIKEY_CRON_CHILD === '1';
26
28
  app.set('trust proxy', 1);
27
29
  app.use((0, cors_1.default)());
28
30
  app.use(express_1.default.json());
@@ -165,9 +167,38 @@ app.get('*', (_req, res) => {
165
167
  res.sendFile(path_1.default.join(process.cwd(), 'public', 'index.html'));
166
168
  });
167
169
  let server = null;
170
+ let cronChildProcess = null;
171
+ function startCronChildProcess() {
172
+ if (IS_CRON_CHILD || cronChildProcess)
173
+ return;
174
+ const childPort = PORT + 1;
175
+ const entry = process.argv[1] || __filename;
176
+ cronChildProcess = (0, child_process_1.fork)(entry, [], {
177
+ env: {
178
+ ...process.env,
179
+ OMNIKEY_CRON_CHILD: '1',
180
+ OMNIKEY_PORT: String(childPort),
181
+ },
182
+ execArgv: process.execArgv,
183
+ stdio: 'inherit',
184
+ });
185
+ logger_1.logger.info('Spawned cron child process.', {
186
+ pid: cronChildProcess.pid,
187
+ port: childPort,
188
+ });
189
+ cronChildProcess.on('exit', (code, signal) => {
190
+ logger_1.logger.warn('Cron child process exited.', { code, signal });
191
+ cronChildProcess = null;
192
+ });
193
+ }
168
194
  async function start() {
169
195
  try {
170
196
  await (0, db_1.initDatabase)(logger_1.logger);
197
+ if (IS_CRON_CHILD) {
198
+ logger_1.logger.info('Starting cron child process mode.', { port: PORT });
199
+ (0, scheduledJobExecutor_1.startScheduledJobExecutor)();
200
+ return;
201
+ }
171
202
  server = app.listen(PORT, () => {
172
203
  logger_1.logger.info(`Enhancer API listening on http://localhost:${PORT}`, {
173
204
  isSelfHosted: config_1.config.isSelfHosted,
@@ -181,7 +212,7 @@ async function start() {
181
212
  (0, agentServer_1.attachAgentWebSocketServer)(server);
182
213
  }
183
214
  if (config_1.config.isSelfHosted) {
184
- (0, scheduledJobExecutor_1.startScheduledJobExecutor)();
215
+ startCronChildProcess();
185
216
  }
186
217
  }
187
218
  catch (err) {
@@ -192,6 +223,15 @@ async function start() {
192
223
  start();
193
224
  function gracefulShutdown(signal) {
194
225
  logger_1.logger.info(`Received ${signal}. Starting graceful shutdown...`);
226
+ if (cronChildProcess) {
227
+ cronChildProcess.kill('SIGTERM');
228
+ cronChildProcess = null;
229
+ }
230
+ if (IS_CRON_CHILD) {
231
+ logger_1.logger.info('Cron child process exiting.');
232
+ process.exit(0);
233
+ return;
234
+ }
195
235
  if (!server) {
196
236
  logger_1.logger.info('Server was not started or already closed. Exiting process.');
197
237
  process.exit(0);
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.35",
7
+ "version": "1.0.36",
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",