omnikey-cli 1.0.24 → 1.0.26

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.
@@ -21,6 +21,10 @@ const authMiddleware_1 = require("../authMiddleware");
21
21
  * @returns The authenticated `Subscription`, or `null` if authentication fails for any reason.
22
22
  */
23
23
  async function authenticateFromAuthHeader(authHeader, log) {
24
+ if (config_1.config.blockSaas) {
25
+ log.warn('Blocking SaaS access: rejecting agent WebSocket connection due to BLOCK_SAAS=true');
26
+ return null;
27
+ }
24
28
  if (config_1.config.isSelfHosted) {
25
29
  log.info('Self-hosted mode: skipping JWT authentication for agent WebSocket connection.');
26
30
  try {
@@ -37,16 +37,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.attachAgentWebSocketServer = attachAgentWebSocketServer;
40
+ exports.createAgentRouter = createAgentRouter;
41
+ const express_1 = __importDefault(require("express"));
40
42
  const ws_1 = __importStar(require("ws"));
41
43
  const cuid_1 = __importDefault(require("cuid"));
42
44
  const config_1 = require("../config");
43
45
  const logger_1 = require("../logger");
44
46
  const subscription_1 = require("../models/subscription");
45
47
  const subscriptionUsage_1 = require("../models/subscriptionUsage");
48
+ const agentSession_1 = require("../models/agentSession");
46
49
  const agentPrompts_1 = require("./agentPrompts");
47
50
  const featureRoutes_1 = require("../featureRoutes");
48
- const web_search_provider_1 = require("../web-search-provider");
51
+ const web_search_provider_1 = require("../web-search/web-search-provider");
49
52
  const agentAuth_1 = require("./agentAuth");
53
+ const authMiddleware_1 = require("../authMiddleware");
50
54
  const utils_1 = require("./utils");
51
55
  const ai_client_1 = require("../ai-client");
52
56
  async function runToolLoop(initialResult, session, sessionId, send, log, tools, onUsage) {
@@ -137,12 +141,55 @@ async function runToolLoop(initialResult, session, sessionId, send, log, tools,
137
141
  return result;
138
142
  }
139
143
  const aiModel = (0, ai_client_1.getDefaultModel)(config_1.config.aiProvider, 'smart');
144
+ // In-memory cache: sessionId -> live SessionState. Hydrated from DB on first
145
+ // access and written back after each turn so restarts resume correctly.
140
146
  const sessionMessages = new Map();
141
147
  const MAX_TURNS = 10;
148
+ // ─── DB helpers ───────────────────────────────────────────────────────────────
149
+ async function persistSessionToDB(sessionId, state) {
150
+ try {
151
+ const historyJson = JSON.stringify(state.history);
152
+ await agentSession_1.AgentSession.update({
153
+ historyJson,
154
+ turns: state.turns,
155
+ lastActiveAt: new Date(),
156
+ }, { where: { id: sessionId } });
157
+ }
158
+ catch (err) {
159
+ logger_1.logger.error('Failed to persist agent session to DB', { sessionId, error: err });
160
+ }
161
+ }
162
+ // Maximum number of sessions stored per subscription. When this limit is
163
+ // exceeded the oldest sessions (by lastActiveAt) are pruned automatically.
164
+ const SESSION_CAP = 50;
165
+ async function enforceSessionCap(subscriptionId, logger) {
166
+ try {
167
+ const count = await agentSession_1.AgentSession.count({ where: { subscriptionId } });
168
+ if (count <= SESSION_CAP)
169
+ return;
170
+ const excess = count - SESSION_CAP;
171
+ const oldest = await agentSession_1.AgentSession.findAll({
172
+ where: { subscriptionId },
173
+ order: [['last_active_at', 'ASC']],
174
+ limit: excess,
175
+ attributes: ['id'],
176
+ });
177
+ const ids = oldest.map((s) => s.id);
178
+ await agentSession_1.AgentSession.destroy({ where: { id: ids } });
179
+ logger.info('Pruned oldest agent sessions to enforce cap', {
180
+ subscriptionId,
181
+ pruned: ids.length,
182
+ });
183
+ }
184
+ catch (err) {
185
+ logger.error('Failed to enforce agent session cap', { subscriptionId, error: err });
186
+ }
187
+ }
142
188
  async function getOrCreateSession(sessionId, subscription, platform, log) {
189
+ // 1. Return the live in-memory entry if already loaded this process lifetime.
143
190
  const existing = sessionMessages.get(sessionId);
144
191
  if (existing) {
145
- log.debug('Reusing existing agent session', {
192
+ log.debug('Reusing existing agent session (in-memory)', {
146
193
  sessionId,
147
194
  subscriptionId: existing.subscription.id,
148
195
  turns: existing.turns,
@@ -151,10 +198,42 @@ async function getOrCreateSession(sessionId, subscription, platform, log) {
151
198
  sessionState: existing,
152
199
  hasStoredPrompt: existing.history
153
200
  .filter((h) => h.role === 'user')
154
- .some((h) => h.content.includes('<stored_instructions>')),
201
+ .some((h) => typeof h.content === 'string' && h.content.includes('<stored_instructions>')),
155
202
  };
156
203
  }
157
- // use these instructions as user instructions
204
+ // 2. Try to resume from a persisted DB record.
205
+ try {
206
+ const dbSession = await agentSession_1.AgentSession.findOne({
207
+ where: { id: sessionId, subscriptionId: subscription.id },
208
+ });
209
+ if (dbSession) {
210
+ const history = JSON.parse(dbSession.historyJson);
211
+ const entry = {
212
+ subscription,
213
+ history,
214
+ turns: dbSession.turns,
215
+ };
216
+ sessionMessages.set(sessionId, entry);
217
+ log.info('Resumed agent session from DB', {
218
+ sessionId,
219
+ subscriptionId: subscription.id,
220
+ turns: entry.turns,
221
+ });
222
+ return {
223
+ sessionState: entry,
224
+ hasStoredPrompt: history
225
+ .filter((h) => h.role === 'user')
226
+ .some((h) => typeof h.content === 'string' && h.content.includes('<stored_instructions>')),
227
+ };
228
+ }
229
+ }
230
+ catch (err) {
231
+ log.error('Failed to load agent session from DB; creating a fresh one', {
232
+ sessionId,
233
+ error: err,
234
+ });
235
+ }
236
+ // 3. Create a brand-new session in-memory and persist it to the DB.
158
237
  const prompt = await (0, featureRoutes_1.getPromptForCommand)(log, 'task', subscription).catch((err) => {
159
238
  log.error('Failed to get system prompt for new agent session', { error: err });
160
239
  return '';
@@ -185,6 +264,23 @@ ${prompt}
185
264
  turns: 0,
186
265
  };
187
266
  sessionMessages.set(sessionId, entry);
267
+ // Persist immediately so that GET /sessions picks it up right away.
268
+ try {
269
+ await agentSession_1.AgentSession.create({
270
+ id: sessionId,
271
+ subscriptionId: subscription.id,
272
+ title: 'New Session',
273
+ platform: platform ?? null,
274
+ historyJson: JSON.stringify(entry.history),
275
+ turns: 0,
276
+ lastActiveAt: new Date(),
277
+ });
278
+ // Prune oldest sessions after each creation so the cap is always respected.
279
+ void enforceSessionCap(subscription.id, log);
280
+ }
281
+ catch (err) {
282
+ log.error('Failed to create agent session in DB', { sessionId, error: err });
283
+ }
188
284
  log.info('Created new agent session', {
189
285
  sessionId,
190
286
  subscriptionId: subscription.id,
@@ -243,6 +339,16 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
243
339
  ? userContent
244
340
  : `<user_input>${(0, utils_1.createUserContent)(userContent, hasStoredPrompt)}</user_input>`,
245
341
  });
342
+ // Use the first real user message (turn 1) as the session title.
343
+ if (session.turns === 1 && !isAssistance) {
344
+ const rawInput = clientMessage.content || '';
345
+ const titleSlug = rawInput.trim().slice(0, 60).replace(/\s+/g, ' ');
346
+ if (titleSlug) {
347
+ agentSession_1.AgentSession.update({ title: titleSlug }, { where: { id: sessionId } }).catch((err) => {
348
+ log.error('Failed to update agent session title', { sessionId, error: err });
349
+ });
350
+ }
351
+ }
246
352
  }
247
353
  // On the final turn we omit tools so the model is forced to emit a
248
354
  // plain text <final_answer> rather than issuing another tool call.
@@ -250,7 +356,20 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
250
356
  const tools = isFinalTurn ? undefined : (0, utils_1.buildAvailableTools)();
251
357
  const recordUsage = async (result) => {
252
358
  const usage = result.usage;
253
- if (!usage || !subscription.id || config_1.config.isSelfHosted)
359
+ if (!usage)
360
+ return;
361
+ // Always update the per-session token counters in the DB.
362
+ try {
363
+ await agentSession_1.AgentSession.increment({
364
+ promptTokensUsed: usage.prompt_tokens,
365
+ completionTokensUsed: usage.completion_tokens,
366
+ totalTokensUsed: usage.total_tokens,
367
+ }, { where: { id: sessionId } });
368
+ }
369
+ catch (err) {
370
+ log.error('Failed to update agent session token usage', { sessionId, error: err });
371
+ }
372
+ if (!subscription.id || config_1.config.isSelfHosted)
254
373
  return;
255
374
  try {
256
375
  await subscriptionUsage_1.SubscriptionUsage.create({
@@ -290,8 +409,8 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
290
409
  log.warn('Agent LLM returned empty content; sending generic error to client.');
291
410
  const errorMessage = 'The agent returned an empty response. Please try again.';
292
411
  (0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
293
- // Clear any cached session state so a subsequent attempt can
294
- // start fresh without a polluted history.
412
+ // Evict from the in-memory cache; the DB record is kept so the session
413
+ // appears in the list and can be retried or deleted by the user.
295
414
  sessionMessages.delete(sessionId);
296
415
  return;
297
416
  }
@@ -398,6 +517,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
398
517
  sender: 'agent',
399
518
  content: hasFinalAnswerTag ? content : `<final_answer>\n${content}\n</final_answer>`,
400
519
  });
520
+ await persistSessionToDB(sessionId, session);
401
521
  sessionMessages.delete(sessionId);
402
522
  }
403
523
  else if (content) {
@@ -416,6 +536,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
416
536
  sender: 'agent',
417
537
  content: `<final_answer>\n${content}\n</final_answer>`,
418
538
  });
539
+ await persistSessionToDB(sessionId, session);
419
540
  sessionMessages.delete(sessionId);
420
541
  }
421
542
  else {
@@ -423,6 +544,7 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
423
544
  sessionId,
424
545
  });
425
546
  (0, utils_1.sendFinalAnswer)(send, sessionId, 'The agent returned an empty response. Please try again.', true);
547
+ // Evict from in-memory cache; DB record is preserved.
426
548
  sessionMessages.delete(sessionId);
427
549
  }
428
550
  }
@@ -430,8 +552,8 @@ async function runAgentTurn(sessionId, subscription, clientMessage, send, log) {
430
552
  log.error('Agent LLM call failed', { error: err });
431
553
  const errorMessage = 'Agent failed to call language model. Please try again later.';
432
554
  (0, utils_1.sendFinalAnswer)(send, sessionId, errorMessage, true);
433
- // Clear any cached session state so a subsequent attempt can
434
- // start fresh without being polluted by a failed turn.
555
+ // Evict from in-memory cache; DB record is preserved so the user can
556
+ // review or delete the session from the client.
435
557
  sessionMessages.delete(sessionId);
436
558
  }
437
559
  }
@@ -504,3 +626,125 @@ function attachAgentWebSocketServer(server) {
504
626
  logger_1.logger.info('Agent WebSocket server attached at path /ws/omni-agent');
505
627
  return wss;
506
628
  }
629
+ // ─── REST router ─────────────────────────────────────────────────────────────
630
+ // Exposes agent session management endpoints that the macOS (and Windows)
631
+ // clients can call over plain HTTP before/during a session.
632
+ function createAgentRouter() {
633
+ const router = express_1.default.Router();
634
+ // Apply auth to every route in this router.
635
+ router.use(authMiddleware_1.authMiddleware);
636
+ // GET /api/agent/sessions
637
+ // Returns the most recent 50 sessions for the authenticated subscription,
638
+ // ordered by last activity descending.
639
+ router.get('/sessions', async (req, res) => {
640
+ const { subscription, logger: log } = res.locals;
641
+ try {
642
+ const sessions = await agentSession_1.AgentSession.findAll({
643
+ where: { subscriptionId: subscription.id },
644
+ order: [['last_active_at', 'DESC']],
645
+ limit: 50,
646
+ attributes: [
647
+ 'id',
648
+ 'title',
649
+ 'platform',
650
+ 'turns',
651
+ 'totalTokensUsed',
652
+ 'promptTokensUsed',
653
+ 'completionTokensUsed',
654
+ 'lastActiveAt',
655
+ 'createdAt',
656
+ 'updatedAt',
657
+ ],
658
+ });
659
+ res.json(sessions.map((s) => ({
660
+ id: s.id,
661
+ title: s.title,
662
+ platform: s.platform,
663
+ turns: s.turns,
664
+ totalTokensUsed: Number(s.totalTokensUsed),
665
+ promptTokensUsed: Number(s.promptTokensUsed),
666
+ completionTokensUsed: Number(s.completionTokensUsed),
667
+ remainingContextTokens: Math.max(0, utils_1.MAX_HISTORY_TOTAL - Number(s.totalTokensUsed)),
668
+ contextBudget: utils_1.MAX_HISTORY_TOTAL,
669
+ lastActiveAt: s.lastActiveAt,
670
+ createdAt: s.createdAt,
671
+ updatedAt: s.updatedAt,
672
+ })));
673
+ }
674
+ catch (err) {
675
+ log.error('Failed to list agent sessions', { error: err });
676
+ res.status(500).json({ error: 'Internal server error' });
677
+ }
678
+ });
679
+ // DELETE /api/agent/sessions/:sessionId
680
+ // Allows the client to explicitly delete a session and its stored history.
681
+ router.delete('/sessions/:sessionId', async (req, res) => {
682
+ const { subscription, logger: log } = res.locals;
683
+ const { sessionId } = req.params;
684
+ if (!sessionId || typeof sessionId !== 'string' || sessionId.length > 128) {
685
+ res.status(400).json({ error: 'Invalid session ID' });
686
+ return;
687
+ }
688
+ try {
689
+ const deleted = await agentSession_1.AgentSession.destroy({
690
+ where: { id: sessionId, subscriptionId: subscription.id },
691
+ });
692
+ if (deleted === 0) {
693
+ res.status(404).json({ error: 'Session not found' });
694
+ return;
695
+ }
696
+ // Also remove from the in-memory cache if it was loaded.
697
+ sessionMessages.delete(sessionId);
698
+ res.status(200).json({ deleted: true });
699
+ }
700
+ catch (err) {
701
+ log.error('Failed to delete agent session', { sessionId, error: err });
702
+ res.status(500).json({ error: 'Internal server error' });
703
+ }
704
+ });
705
+ // GET /api/agent/sessions/:sessionId/context
706
+ // Returns token usage and remaining context budget for a single session.
707
+ router.get('/sessions/:sessionId/context', async (req, res) => {
708
+ const { subscription, logger: log } = res.locals;
709
+ const { sessionId } = req.params;
710
+ // Validate that sessionId is a well-formed non-empty string (no path traversal).
711
+ if (!sessionId || typeof sessionId !== 'string' || sessionId.length > 128) {
712
+ res.status(400).json({ error: 'Invalid session ID' });
713
+ return;
714
+ }
715
+ try {
716
+ const session = await agentSession_1.AgentSession.findOne({
717
+ where: { id: sessionId, subscriptionId: subscription.id },
718
+ attributes: [
719
+ 'id',
720
+ 'title',
721
+ 'turns',
722
+ 'totalTokensUsed',
723
+ 'promptTokensUsed',
724
+ 'completionTokensUsed',
725
+ 'lastActiveAt',
726
+ ],
727
+ });
728
+ if (!session) {
729
+ res.status(404).json({ error: 'Session not found' });
730
+ return;
731
+ }
732
+ res.json({
733
+ id: session.id,
734
+ title: session.title,
735
+ turns: session.turns,
736
+ totalTokensUsed: Number(session.totalTokensUsed),
737
+ promptTokensUsed: Number(session.promptTokensUsed),
738
+ completionTokensUsed: Number(session.completionTokensUsed),
739
+ remainingContextTokens: Math.max(0, utils_1.MAX_HISTORY_TOTAL - Number(session.totalTokensUsed)),
740
+ contextBudget: utils_1.MAX_HISTORY_TOTAL,
741
+ lastActiveAt: session.lastActiveAt,
742
+ });
743
+ }
744
+ catch (err) {
745
+ log.error('Failed to fetch agent session context', { error: err });
746
+ res.status(500).json({ error: 'Internal server error' });
747
+ }
748
+ });
749
+ return router;
750
+ }
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_HISTORY_TOTAL = void 0;
3
4
  exports.buildAvailableTools = buildAvailableTools;
4
5
  exports.createUserContent = createUserContent;
5
6
  exports.sendFinalAnswer = sendFinalAnswer;
6
7
  exports.pushToSessionHistory = pushToSessionHistory;
7
- const web_search_provider_1 = require("../web-search-provider");
8
+ const web_search_provider_1 = require("../web-search/web-search-provider");
8
9
  const ai_client_1 = require("../ai-client");
9
10
  const config_1 = require("../config");
10
11
  /**
@@ -60,7 +61,7 @@ function sendFinalAnswer(send, sessionId, message, isError) {
60
61
  const MAX_MESSAGE_CONTENT = (0, ai_client_1.getMaxMessageContentLength)(config_1.config.aiProvider);
61
62
  // Total character budget across all history messages (derived from the
62
63
  // provider's context-window size minus headroom for output + system prompt).
63
- const MAX_HISTORY_TOTAL = (0, ai_client_1.getMaxHistoryLength)(config_1.config.aiProvider);
64
+ exports.MAX_HISTORY_TOTAL = (0, ai_client_1.getMaxHistoryLength)(config_1.config.aiProvider);
64
65
  const FINAL_ANSWER_REQUEST = {
65
66
  role: 'user',
66
67
  content: 'Content was truncated because a length limit was reached. ' +
@@ -91,7 +92,7 @@ function pushToSessionHistory(logger, session, message) {
91
92
  }
92
93
  // 2. Total history length limit.
93
94
  const currentTotal = session.history.reduce((acc, msg) => acc + (typeof msg.content === 'string' ? msg.content.length : 0), 0);
94
- const remaining = MAX_HISTORY_TOTAL - currentTotal;
95
+ const remaining = exports.MAX_HISTORY_TOTAL - currentTotal;
95
96
  if (content.length > remaining) {
96
97
  content = content.slice(0, Math.max(0, remaining - FINAL_ANSWER_REQUEST.content.length));
97
98
  limitHit = true;
@@ -32,6 +32,10 @@ async function selfHostedSubscription() {
32
32
  async function authMiddleware(req, res, next) {
33
33
  const authHeader = req.headers.authorization;
34
34
  logger_1.logger.defaultMeta = { traceId: (0, crypto_1.randomUUID)() };
35
+ if (config_1.config.blockSaas) {
36
+ logger_1.logger.warn('Blocking SaaS access: rejecting request due to BLOCK_SAAS=true');
37
+ return res.status(403).json({ error: 'SaaS access is blocked.' });
38
+ }
35
39
  if (config_1.config.isSelfHosted || !config_1.config.jwtSecret) {
36
40
  logger_1.logger.info('Self-hosted mode: skipping auth middleware.');
37
41
  if (config_1.config.isSelfHosted) {
@@ -92,4 +92,5 @@ exports.config = {
92
92
  tavilyApiKey: getEnv('TAVILY_API_KEY', false),
93
93
  searxngUrl: getEnv('SEARXNG_URL', false),
94
94
  terminalPlatform: getEnv('TERMINAL_PLATFORM', false),
95
+ blockSaas: getBooleanEnv('BLOCK_SAAS', false),
95
96
  };
@@ -14,6 +14,13 @@ if (config_1.config.isSelfHosted) {
14
14
  logging: config_1.config.dbLogging ? console.log : false,
15
15
  });
16
16
  }
17
+ else if (config_1.config.blockSaas) {
18
+ exports.sequelize = sequelize = new sequelize_1.Sequelize({
19
+ dialect: 'sqlite',
20
+ storage: ':memory:',
21
+ logging: false,
22
+ });
23
+ }
17
24
  else if (config_1.config.databaseUrl) {
18
25
  // Use Postgres for cloud/hosted
19
26
  exports.sequelize = sequelize = new sequelize_1.Sequelize(config_1.config.databaseUrl, {
@@ -15,7 +15,8 @@ const logger_1 = require("./logger");
15
15
  const taskInstructionRoutes_1 = require("./taskInstructionRoutes");
16
16
  const config_1 = require("./config");
17
17
  const agentServer_1 = require("./agent/agentServer");
18
- const appDownload_1 = require("./models/appDownload");
18
+ // Importing AgentSession ensures the model is registered with Sequelize before initDatabase().
19
+ require("./models/agentSession");
19
20
  const app = (0, express_1.default)();
20
21
  const PORT = Number(config_1.config.port);
21
22
  app.set('trust proxy', 1);
@@ -26,20 +27,13 @@ app.use(express_1.default.static(path_1.default.join(process.cwd(), 'public')));
26
27
  app.use('/api/subscription', (0, subscriptionRoutes_1.createSubscriptionRouter)(logger_1.logger));
27
28
  app.use('/api/feature', (0, featureRoutes_1.createFeatureRouter)());
28
29
  app.use('/api/instructions', (0, taskInstructionRoutes_1.taskInstructionRouter)());
30
+ app.use('/api/agent', (0, agentServer_1.createAgentRouter)());
29
31
  app.get('/macos/download', (_req, res) => {
30
32
  const dmgPath = path_1.default.join(process.cwd(), 'macOS', 'OmniKeyAI.dmg');
31
33
  if (!fs_1.default.existsSync(dmgPath)) {
32
34
  res.status(404).send('File not found.');
33
35
  return;
34
36
  }
35
- if (!config_1.config.isSelfHosted) {
36
- appDownload_1.AppDownload.findOrCreate({
37
- where: { platform: 'macos' },
38
- defaults: { platform: 'macos', count: 0 },
39
- })
40
- .then(([record]) => record.increment('count'))
41
- .catch((err) => logger_1.logger.error('Failed to increment macOS download count.', { error: err }));
42
- }
43
37
  res.set({
44
38
  'Content-Type': 'application/octet-stream',
45
39
  'Content-Disposition': 'attachment; filename="OmniKeyAI.dmg"',
@@ -74,8 +68,8 @@ app.get('/macos/appcast', (req, res) => {
74
68
  const appcastUrl = `${baseUrl}/macos/appcast`;
75
69
  // These should match the values embedded into the macOS app
76
70
  // Info.plist in macOS/build_release_dmg.sh.
77
- const bundleVersion = '19';
78
- const shortVersion = '1.0.18';
71
+ const bundleVersion = '20';
72
+ const shortVersion = '1.0.19';
79
73
  const xml = `<?xml version="1.0" encoding="utf-8"?>
80
74
  <rss version="2.0"
81
75
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
@@ -113,14 +107,6 @@ app.get('/windows/download', (_req, res) => {
113
107
  res.status(404).send('File not found.');
114
108
  return;
115
109
  }
116
- if (!config_1.config.isSelfHosted) {
117
- appDownload_1.AppDownload.findOrCreate({
118
- where: { platform: 'windows' },
119
- defaults: { platform: 'windows', count: 0 },
120
- })
121
- .then(([record]) => record.increment('count'))
122
- .catch((err) => logger_1.logger.error('Failed to increment Windows download count.', { error: err }));
123
- }
124
110
  res.set({
125
111
  'Content-Type': 'application/zip',
126
112
  'Content-Disposition': `attachment; filename="${WIN_ZIP_FILENAME}"`,
@@ -155,11 +141,6 @@ app.get('/windows/update', (req, res) => {
155
141
  releaseNotes: '',
156
142
  });
157
143
  });
158
- app.get('/api/downloads', async (_req, res) => {
159
- const rows = await appDownload_1.AppDownload.findAll({ where: { platform: ['macos', 'windows'] } });
160
- const find = (p) => Number(rows.find((r) => r.platform === p)?.count ?? 0);
161
- res.json({ macos: find('macos'), windows: find('windows') });
162
- });
163
144
  app.get('/health', (_req, res) => {
164
145
  res.json({ status: 'ok' });
165
146
  });
@@ -0,0 +1,80 @@
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.AgentSession = void 0;
7
+ const sequelize_1 = require("sequelize");
8
+ const cuid_1 = __importDefault(require("cuid"));
9
+ const db_1 = require("../db");
10
+ const subscription_1 = require("./subscription");
11
+ class AgentSession extends sequelize_1.Model {
12
+ }
13
+ exports.AgentSession = AgentSession;
14
+ AgentSession.init({
15
+ id: {
16
+ type: sequelize_1.DataTypes.STRING,
17
+ primaryKey: true,
18
+ allowNull: false,
19
+ defaultValue: () => (0, cuid_1.default)(),
20
+ },
21
+ subscriptionId: {
22
+ type: sequelize_1.DataTypes.STRING,
23
+ allowNull: false,
24
+ field: 'subscription_id',
25
+ references: {
26
+ model: subscription_1.Subscription,
27
+ key: 'id',
28
+ },
29
+ onUpdate: 'CASCADE',
30
+ onDelete: 'CASCADE',
31
+ },
32
+ title: {
33
+ type: sequelize_1.DataTypes.STRING,
34
+ allowNull: false,
35
+ defaultValue: 'New Session',
36
+ },
37
+ platform: {
38
+ type: sequelize_1.DataTypes.STRING,
39
+ allowNull: true,
40
+ },
41
+ historyJson: {
42
+ type: sequelize_1.DataTypes.TEXT,
43
+ allowNull: false,
44
+ defaultValue: '[]',
45
+ field: 'history_json',
46
+ },
47
+ turns: {
48
+ type: sequelize_1.DataTypes.INTEGER,
49
+ allowNull: false,
50
+ defaultValue: 0,
51
+ },
52
+ promptTokensUsed: {
53
+ type: sequelize_1.DataTypes.BIGINT,
54
+ allowNull: false,
55
+ defaultValue: 0,
56
+ field: 'prompt_tokens_used',
57
+ },
58
+ completionTokensUsed: {
59
+ type: sequelize_1.DataTypes.BIGINT,
60
+ allowNull: false,
61
+ defaultValue: 0,
62
+ field: 'completion_tokens_used',
63
+ },
64
+ totalTokensUsed: {
65
+ type: sequelize_1.DataTypes.BIGINT,
66
+ allowNull: false,
67
+ defaultValue: 0,
68
+ field: 'total_tokens_used',
69
+ },
70
+ lastActiveAt: {
71
+ type: sequelize_1.DataTypes.DATE,
72
+ allowNull: false,
73
+ defaultValue: sequelize_1.DataTypes.NOW,
74
+ field: 'last_active_at',
75
+ },
76
+ }, {
77
+ sequelize: db_1.sequelize,
78
+ tableName: 'agent_sessions',
79
+ indexes: [{ fields: ['subscription_id'] }],
80
+ });
@@ -77,6 +77,10 @@ function createSubscriptionRouter(logger) {
77
77
  router.post('/activate', async (req, res) => {
78
78
  logger.defaultMeta = { traceId: (0, crypto_1.randomUUID)() };
79
79
  logger.info('Handling subscription activation request using user key.');
80
+ if (config_1.config.blockSaas) {
81
+ logger.warn('Blocking SaaS access: rejecting subscription activation due to BLOCK_SAAS=true');
82
+ return res.status(403).json({ error: 'SaaS access is blocked.' });
83
+ }
80
84
  try {
81
85
  const body = zod_1.default.custom().parse(req.body);
82
86
  const subscription = config_1.config.isSelfHosted
@@ -39,14 +39,6 @@ function taskInstructionRouter() {
39
39
  router.post('/templates', authMiddleware_1.authMiddleware, async (req, res) => {
40
40
  const { logger, subscription } = res.locals;
41
41
  try {
42
- const existingCount = await subscriptionTaskTemplate_1.SubscriptionTaskTemplate.count({
43
- where: { subscriptionId: subscription.id },
44
- });
45
- if (existingCount >= 5) {
46
- return res
47
- .status(400)
48
- .json({ error: 'You can save up to 5 task templates per subscription.' });
49
- }
50
42
  const parseResult = taskTemplateSchema.parse(req.body);
51
43
  const template = await subscriptionTaskTemplate_1.SubscriptionTaskTemplate.create({
52
44
  subscriptionId: subscription.id,
@@ -153,5 +145,16 @@ function taskInstructionRouter() {
153
145
  res.status(500).json({ error: 'Failed to set default task template.' });
154
146
  }
155
147
  });
148
+ router.post('/templates/clear-default', authMiddleware_1.authMiddleware, async (req, res) => {
149
+ const { logger, subscription } = res.locals;
150
+ try {
151
+ await subscriptionTaskTemplate_1.SubscriptionTaskTemplate.update({ isDefault: false }, { where: { subscriptionId: subscription.id, isDefault: true } });
152
+ res.status(204).send();
153
+ }
154
+ catch (err) {
155
+ logger.error('Error clearing default task template.', { error: err });
156
+ res.status(500).json({ error: 'Failed to clear default task template.' });
157
+ }
158
+ });
156
159
  return router;
157
160
  }