neoagent 3.0.1-beta.2 → 3.0.1-beta.21

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.
Files changed (94) hide show
  1. package/.env.example +19 -0
  2. package/README.md +20 -3
  3. package/docs/benchmarking.md +102 -0
  4. package/docs/billing.md +224 -0
  5. package/docs/configuration.md +22 -0
  6. package/docs/getting-started.md +10 -8
  7. package/docs/index.md +1 -0
  8. package/docs/operations.md +1 -1
  9. package/flutter_app/lib/main.dart +4 -1
  10. package/flutter_app/lib/main_account_settings.dart +138 -0
  11. package/flutter_app/lib/main_app_shell.dart +316 -0
  12. package/flutter_app/lib/main_billing.dart +1465 -0
  13. package/flutter_app/lib/main_chat.dart +1594 -224
  14. package/flutter_app/lib/main_controller.dart +263 -26
  15. package/flutter_app/lib/main_devices.dart +1 -1
  16. package/flutter_app/lib/main_install.dart +1147 -0
  17. package/flutter_app/lib/main_navigation.dart +12 -0
  18. package/flutter_app/lib/main_operations.dart +134 -9
  19. package/flutter_app/lib/main_settings.dart +148 -52
  20. package/flutter_app/lib/main_shared.dart +1 -0
  21. package/flutter_app/lib/src/backend_client.dart +86 -1
  22. package/landing/index.html +2 -2
  23. package/lib/manager.js +184 -66
  24. package/lib/schema_migrations.js +84 -0
  25. package/package.json +10 -4
  26. package/server/admin/access.js +12 -7
  27. package/server/admin/admin.css +78 -0
  28. package/server/admin/admin.js +511 -23
  29. package/server/admin/billing.js +415 -0
  30. package/server/admin/index.html +120 -13
  31. package/server/admin/users.js +15 -15
  32. package/server/db/database.js +13 -21
  33. package/server/db/sessions_db.js +8 -0
  34. package/server/http/middleware.js +4 -4
  35. package/server/http/routes.js +9 -0
  36. package/server/http/static.js +4 -2
  37. package/server/middleware/requireBilling.js +12 -0
  38. package/server/public/.last_build_id +1 -1
  39. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  40. package/server/public/flutter_bootstrap.js +1 -1
  41. package/server/public/main.dart.js +89347 -85449
  42. package/server/routes/account.js +53 -0
  43. package/server/routes/admin.js +515 -63
  44. package/server/routes/agents.js +203 -21
  45. package/server/routes/billing.js +127 -0
  46. package/server/routes/billing_webhook.js +43 -0
  47. package/server/routes/memory.js +1 -4
  48. package/server/routes/settings.js +1 -2
  49. package/server/routes/skills.js +1 -1
  50. package/server/routes/triggers.js +2 -2
  51. package/server/services/account/erasure.js +263 -0
  52. package/server/services/account/sessions.js +1 -9
  53. package/server/services/ai/loop/agent_engine_core.js +42 -0
  54. package/server/services/ai/loop/blank_recovery.js +36 -0
  55. package/server/services/ai/loop/completion_judge.js +42 -0
  56. package/server/services/ai/loop/conversation_loop.js +248 -34
  57. package/server/services/ai/loop/progress_monitor.js +6 -6
  58. package/server/services/ai/loopPolicy.js +37 -44
  59. package/server/services/ai/messagingFallback.js +25 -1
  60. package/server/services/ai/models.js +18 -0
  61. package/server/services/ai/preModelCompaction.js +25 -5
  62. package/server/services/ai/rate_limits.js +28 -4
  63. package/server/services/ai/systemPrompt.js +23 -5
  64. package/server/services/ai/taskAnalysis.js +2 -0
  65. package/server/services/ai/tools.js +231 -20
  66. package/server/services/android/controller.js +6 -2
  67. package/server/services/billing/billing_email.js +106 -0
  68. package/server/services/billing/config.js +17 -0
  69. package/server/services/billing/plans.js +121 -0
  70. package/server/services/billing/stripe_client.js +21 -0
  71. package/server/services/billing/subscriptions.js +462 -0
  72. package/server/services/billing/trial_guard.js +111 -0
  73. package/server/services/browser/contentExtractor.js +629 -0
  74. package/server/services/browser/controller.js +4 -8
  75. package/server/services/desktop/gateway.js +7 -4
  76. package/server/services/integrations/google/calendar.js +30 -2
  77. package/server/services/integrations/secrets.js +7 -4
  78. package/server/services/manager.js +11 -0
  79. package/server/services/messaging/automation.js +1 -1
  80. package/server/services/messaging/telnyx.js +12 -11
  81. package/server/services/messaging/whatsapp.js +1 -1
  82. package/server/services/recordings/manager.js +13 -7
  83. package/server/services/runtime/backends/local-vm.js +40 -22
  84. package/server/services/runtime/docker-vm-manager.js +157 -266
  85. package/server/services/runtime/guest_bootstrap.js +17 -5
  86. package/server/services/runtime/guest_image.js +182 -0
  87. package/server/services/runtime/manager.js +0 -1
  88. package/server/services/runtime/validation.js +3 -8
  89. package/server/services/tasks/runtime.js +103 -15
  90. package/server/services/tasks/schedule_utils.js +5 -5
  91. package/server/services/voice/runtimeManager.js +19 -10
  92. package/server/services/wearable/gateway.js +8 -5
  93. package/server/services/websocket.js +26 -8
  94. package/server/services/workspace/manager.js +37 -3
@@ -10,6 +10,154 @@ const { buildAgentRunContext } = require('./_helpers/agentRunContext');
10
10
 
11
11
  router.use(requireAuth);
12
12
 
13
+ const CHAT_HISTORY_DEFAULT_LIMIT = 40;
14
+ const CHAT_HISTORY_MAX_LIMIT = 100;
15
+
16
+ function normalizedTimestampExpression(valueSql) {
17
+ return `julianday(CASE WHEN instr(${valueSql}, 'T') > 0 THEN ${valueSql} ELSE replace(${valueSql}, ' ', 'T') || 'Z' END)`;
18
+ }
19
+
20
+ function normalizeChatHistoryLimit(rawLimit) {
21
+ const parsed = Number.parseInt(rawLimit, 10);
22
+ if (!Number.isFinite(parsed) || parsed <= 0) {
23
+ return CHAT_HISTORY_DEFAULT_LIMIT;
24
+ }
25
+ return Math.min(parsed, CHAT_HISTORY_MAX_LIMIT);
26
+ }
27
+
28
+ function parseChatHistoryCursor(query) {
29
+ const createdAt = query.beforeCreatedAt?.toString().trim() || '';
30
+ const source = query.beforeSource?.toString().trim() || '';
31
+ const id = query.beforeId?.toString().trim() || '';
32
+ if (!createdAt || !source || !id) {
33
+ return null;
34
+ }
35
+ return { createdAt, source, id };
36
+ }
37
+
38
+ function chatHistoryTimestampMs(value) {
39
+ if (!value) return 0;
40
+ const normalized = value.includes('T') ? value : `${value.replace(' ', 'T')}Z`;
41
+ const parsed = new Date(normalized).getTime();
42
+ return Number.isFinite(parsed) ? parsed : 0;
43
+ }
44
+
45
+ function compareChatHistoryDesc(left, right) {
46
+ const timestampDiff =
47
+ chatHistoryTimestampMs(right.created_at) - chatHistoryTimestampMs(left.created_at);
48
+ if (timestampDiff !== 0) {
49
+ return timestampDiff;
50
+ }
51
+ const sourceDiff = String(right.history_source || '').localeCompare(
52
+ String(left.history_source || '')
53
+ );
54
+ if (sourceDiff !== 0) {
55
+ return sourceDiff;
56
+ }
57
+ return String(right.id || '').localeCompare(String(left.id || ''));
58
+ }
59
+
60
+ function buildRunUsageSummary(runId) {
61
+ const rows = db.prepare(`
62
+ SELECT
63
+ provider,
64
+ model,
65
+ phase,
66
+ input_tokens,
67
+ output_tokens,
68
+ reasoning_tokens,
69
+ cached_read_tokens,
70
+ cache_write_tokens,
71
+ total_tokens,
72
+ estimated_cost_usd,
73
+ latency_ms
74
+ FROM agent_model_usage
75
+ WHERE run_id = ?
76
+ ORDER BY id ASC
77
+ `).all(runId);
78
+ const totals = {
79
+ inputTokens: 0,
80
+ outputTokens: 0,
81
+ reasoningTokens: 0,
82
+ cachedReadTokens: 0,
83
+ cacheWriteTokens: 0,
84
+ totalTokens: 0,
85
+ estimatedCostUsd: 0,
86
+ pricedCallCount: 0,
87
+ latencyMs: 0,
88
+ };
89
+ const models = new Map();
90
+
91
+ for (const row of rows) {
92
+ const inputTokens = Number(row.input_tokens || 0);
93
+ const outputTokens = Number(row.output_tokens || 0);
94
+ const reasoningTokens = Number(row.reasoning_tokens || 0);
95
+ const cachedReadTokens = Number(row.cached_read_tokens || 0);
96
+ const cacheWriteTokens = Number(row.cache_write_tokens || 0);
97
+ const totalTokens = Number(row.total_tokens || 0);
98
+ const latencyMs = Number(row.latency_ms || 0);
99
+ const estimatedCostUsd = Number(row.estimated_cost_usd);
100
+ const key = `${row.provider}:${row.model}`;
101
+
102
+ totals.inputTokens += inputTokens;
103
+ totals.outputTokens += outputTokens;
104
+ totals.reasoningTokens += reasoningTokens;
105
+ totals.cachedReadTokens += cachedReadTokens;
106
+ totals.cacheWriteTokens += cacheWriteTokens;
107
+ totals.totalTokens += totalTokens;
108
+ totals.latencyMs += latencyMs;
109
+ if (Number.isFinite(estimatedCostUsd)) {
110
+ totals.estimatedCostUsd += estimatedCostUsd;
111
+ totals.pricedCallCount += 1;
112
+ }
113
+
114
+ if (!models.has(key)) {
115
+ models.set(key, {
116
+ provider: row.provider,
117
+ model: row.model,
118
+ callCount: 0,
119
+ inputTokens: 0,
120
+ outputTokens: 0,
121
+ reasoningTokens: 0,
122
+ cachedReadTokens: 0,
123
+ cacheWriteTokens: 0,
124
+ totalTokens: 0,
125
+ estimatedCostUsd: 0,
126
+ pricedCallCount: 0,
127
+ latencyMs: 0,
128
+ phases: new Set(),
129
+ });
130
+ }
131
+
132
+ const aggregate = models.get(key);
133
+ aggregate.callCount += 1;
134
+ aggregate.inputTokens += inputTokens;
135
+ aggregate.outputTokens += outputTokens;
136
+ aggregate.reasoningTokens += reasoningTokens;
137
+ aggregate.cachedReadTokens += cachedReadTokens;
138
+ aggregate.cacheWriteTokens += cacheWriteTokens;
139
+ aggregate.totalTokens += totalTokens;
140
+ aggregate.latencyMs += latencyMs;
141
+ aggregate.phases.add(String(row.phase || '').trim() || 'model_turn');
142
+ if (Number.isFinite(estimatedCostUsd)) {
143
+ aggregate.estimatedCostUsd += estimatedCostUsd;
144
+ aggregate.pricedCallCount += 1;
145
+ }
146
+ }
147
+
148
+ return {
149
+ totals: {
150
+ ...totals,
151
+ estimatedCostUsd: totals.pricedCallCount > 0 ? totals.estimatedCostUsd : null,
152
+ },
153
+ models: [...models.values()].map((entry) => ({
154
+ ...entry,
155
+ estimatedCostUsd: entry.pricedCallCount > 0 ? entry.estimatedCostUsd : null,
156
+ phases: [...entry.phases],
157
+ })),
158
+ };
159
+ }
160
+
13
161
  // List agent runs
14
162
  router.get('/', (req, res) => {
15
163
  const limit = Math.min(parseInt(req.query.limit) || 50, 200);
@@ -23,9 +171,31 @@ router.get('/', (req, res) => {
23
171
 
24
172
  // Chat history (web + social messages merged)
25
173
  router.get('/chat-history', (req, res) => {
26
- const limit = Math.min(parseInt(req.query.limit) || 100, 500);
174
+ const limit = normalizeChatHistoryLimit(req.query.limit);
175
+ const queryLimit = limit + 1;
27
176
  const userId = req.session.userId;
28
177
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
178
+ const cursor = parseChatHistoryCursor(req.query);
179
+ const cursorTimestampSql = normalizedTimestampExpression('@beforeCreatedAt');
180
+ const historyTimestampSql = normalizedTimestampExpression('created_at');
181
+ const cursorClause = (sourceName) =>
182
+ cursor
183
+ ? `AND (
184
+ ${historyTimestampSql} < ${cursorTimestampSql}
185
+ OR (${historyTimestampSql} = ${cursorTimestampSql} AND '${sourceName}' < @beforeSource)
186
+ OR (${historyTimestampSql} = ${cursorTimestampSql} AND '${sourceName}' = @beforeSource AND CAST(id AS TEXT) < @beforeId)
187
+ )`
188
+ : '';
189
+ const queryParams = cursor
190
+ ? {
191
+ userId,
192
+ agentId,
193
+ limit: queryLimit,
194
+ beforeCreatedAt: cursor.createdAt,
195
+ beforeSource: cursor.source,
196
+ beforeId: cursor.id,
197
+ }
198
+ : { userId, agentId, limit: queryLimit };
29
199
 
30
200
  const webMsgs = db.prepare(`
31
201
  SELECT
@@ -37,9 +207,14 @@ router.get('/chat-history', (req, res) => {
37
207
  created_at,
38
208
  agent_run_id AS run_id,
39
209
  metadata,
40
- NULL AS tool_calls
41
- FROM conversation_history WHERE user_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT ?
42
- `).all(userId, agentId, limit);
210
+ NULL AS tool_calls,
211
+ 'conversation' AS history_source
212
+ FROM conversation_history
213
+ WHERE user_id = @userId AND agent_id = @agentId
214
+ ${cursorClause('conversation')}
215
+ ORDER BY ${historyTimestampSql} DESC, CAST(id AS TEXT) DESC
216
+ LIMIT @limit
217
+ `).all(queryParams);
43
218
 
44
219
  const socialMsgs = db.prepare(`
45
220
  SELECT
@@ -51,23 +226,27 @@ router.get('/chat-history', (req, res) => {
51
226
  created_at,
52
227
  run_id,
53
228
  metadata,
54
- tool_calls
55
- FROM messages WHERE user_id = ? AND agent_id = ? AND platform != 'web'
56
- ORDER BY created_at DESC LIMIT ?
57
- `).all(userId, agentId, limit);
58
-
59
- // Normalize SQL datetime ('YYYY-MM-DD HH:MM:SS', treated as local) and ISO-Z strings to ms
60
- const toMs = (s) => {
61
- if (!s) return 0;
62
- const normalized = s.includes('T') ? s : s.replace(' ', 'T') + 'Z';
63
- return new Date(normalized).getTime();
64
- };
229
+ tool_calls,
230
+ 'message' AS history_source
231
+ FROM messages
232
+ WHERE user_id = @userId AND agent_id = @agentId AND platform != 'web'
233
+ ${cursorClause('message')}
234
+ ORDER BY ${historyTimestampSql} DESC, CAST(id AS TEXT) DESC
235
+ LIMIT @limit
236
+ `).all(queryParams);
65
237
 
66
- const all = [...webMsgs, ...socialMsgs]
67
- .sort((a, b) => toMs(a.created_at) - toMs(b.created_at))
68
- .slice(-limit);
238
+ const merged = [...webMsgs, ...socialMsgs].sort(compareChatHistoryDesc);
239
+ const page = merged.slice(0, limit);
240
+ const oldest = page.length > 0 ? page[page.length - 1] : null;
69
241
 
70
- res.json({ messages: all, agentId });
242
+ res.json({
243
+ messages: [...page].reverse(),
244
+ agentId,
245
+ hasMore: merged.length > limit,
246
+ nextBeforeCreatedAt: oldest?.created_at || null,
247
+ nextBeforeSource: oldest?.history_source || null,
248
+ nextBeforeId: oldest?.id?.toString() || null,
249
+ });
71
250
  });
72
251
 
73
252
  // Create new agent run
@@ -130,6 +309,7 @@ router.post('/', async (req, res) => {
130
309
 
131
310
  res.json(result);
132
311
  } catch (err) {
312
+ console.error('[Agents] Run failed:', err?.stack || err);
133
313
  res.status(err?.statusCode || err?.status || 500).json({
134
314
  error: sanitizeError(err),
135
315
  code: err?.code,
@@ -145,8 +325,9 @@ router.get('/:id', (req, res) => {
145
325
 
146
326
  const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(run.id);
147
327
  const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(run.id);
328
+ const usage = buildRunUsageSummary(run.id);
148
329
 
149
- res.json({ run, steps, history });
330
+ res.json({ run, steps, history, usage });
150
331
  });
151
332
 
152
333
  // Get detailed steps for a run (for activity history replay)
@@ -180,8 +361,9 @@ router.get('/:id/steps', (req, res) => {
180
361
  || latestHistoryAssistant?.content
181
362
  || run.final_response
182
363
  || null;
364
+ const usage = buildRunUsageSummary(run.id);
183
365
 
184
- res.json({ run, steps, events: listRunEvents(run.id), response });
366
+ res.json({ run, steps, events: listRunEvents(run.id), response, usage });
185
367
  });
186
368
 
187
369
  // Abort a run
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const router = express.Router();
5
+ const db = require('../db/database');
6
+ const { requireAuth } = require('../middleware/auth');
7
+ const { requireBilling } = require('../middleware/requireBilling');
8
+ const { getStripeConfig } = require('../services/billing/config');
9
+ const { listPlans } = require('../services/billing/plans');
10
+ const subs = require('../services/billing/subscriptions');
11
+ const { getStripeClient } = require('../services/billing/stripe_client');
12
+
13
+ function isValidHttpsUrl(value) {
14
+ try {
15
+ const u = new URL(value);
16
+ return u.protocol === 'https:' || u.protocol === 'http:';
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ router.use(requireBilling);
23
+
24
+ // Public — pricing page needs this without login.
25
+ router.get('/plans', (req, res) => {
26
+ try {
27
+ res.json({ plans: listPlans() });
28
+ } catch (err) {
29
+ res.status(500).json({ error: err.message });
30
+ }
31
+ });
32
+
33
+ // All remaining routes require authentication.
34
+ router.use(requireAuth);
35
+
36
+ router.get('/', (req, res) => {
37
+ try {
38
+ const userId = req.session.userId;
39
+ const subscription = subs.getActiveSubscription(userId);
40
+ const { publicKey } = getStripeConfig();
41
+ res.json({ subscription, stripePublishableKey: publicKey || null });
42
+ } catch (err) {
43
+ res.status(500).json({ error: err.message });
44
+ }
45
+ });
46
+
47
+ router.post('/checkout', async (req, res) => {
48
+ try {
49
+ const { planId, successUrl, cancelUrl } = req.body;
50
+ if (!planId || !successUrl || !cancelUrl) {
51
+ return res.status(400).json({ error: 'planId, successUrl and cancelUrl are required.' });
52
+ }
53
+ if (!isValidHttpsUrl(successUrl) || !isValidHttpsUrl(cancelUrl)) {
54
+ return res.status(400).json({ error: 'successUrl and cancelUrl must be valid URLs.' });
55
+ }
56
+ const url = await subs.createCheckoutSession(req.session.userId, planId, successUrl, cancelUrl);
57
+ res.json({ url });
58
+ } catch (err) {
59
+ res.status(err.statusCode || 500).json({ error: err.message });
60
+ }
61
+ });
62
+
63
+ router.post('/portal', async (req, res) => {
64
+ try {
65
+ const { returnUrl } = req.body;
66
+ if (!returnUrl) return res.status(400).json({ error: 'returnUrl is required.' });
67
+ if (!isValidHttpsUrl(returnUrl)) {
68
+ return res.status(400).json({ error: 'returnUrl must be a valid URL.' });
69
+ }
70
+ const url = await subs.createCustomerPortalSession(req.session.userId, returnUrl);
71
+ res.json({ url });
72
+ } catch (err) {
73
+ res.status(err.statusCode || 500).json({ error: err.message });
74
+ }
75
+ });
76
+
77
+ router.post('/trial', async (req, res) => {
78
+ try {
79
+ const { planId, deviceFingerprint } = req.body;
80
+ if (!planId) return res.status(400).json({ error: 'planId is required.' });
81
+ if (deviceFingerprint !== undefined) {
82
+ if (typeof deviceFingerprint !== 'string' || deviceFingerprint.length > 256 || !/^[a-zA-Z0-9_+/=.-]{1,256}$/.test(deviceFingerprint)) {
83
+ return res.status(400).json({ error: 'Invalid deviceFingerprint.' });
84
+ }
85
+ }
86
+ const ip = req.ip || req.socket?.remoteAddress;
87
+ const result = await subs.startTrial(req.session.userId, planId, { ip, deviceFp: deviceFingerprint });
88
+ res.json({ subscription: result.subscription });
89
+ } catch (err) {
90
+ res.status(err.statusCode || 500).json({ error: err.message });
91
+ }
92
+ });
93
+
94
+ router.post('/cancel', async (req, res) => {
95
+ try {
96
+ await subs.cancelSubscription(req.session.userId);
97
+ res.json({ ok: true });
98
+ } catch (err) {
99
+ res.status(err.statusCode || 500).json({ error: err.message });
100
+ }
101
+ });
102
+
103
+ router.get('/invoices', async (req, res) => {
104
+ try {
105
+ const stripe = getStripeClient();
106
+ const userId = req.session.userId;
107
+ const customer = db
108
+ .prepare('SELECT stripe_customer_id FROM billing_customers WHERE user_id = ?')
109
+ .get(userId);
110
+ if (!customer?.stripe_customer_id) return res.json({ invoices: [] });
111
+ const list = await stripe.invoices.list({ customer: customer.stripe_customer_id, limit: 12 });
112
+ const invoices = list.data.map((inv) => ({
113
+ id: inv.id,
114
+ amount_paid: inv.amount_paid,
115
+ currency: inv.currency,
116
+ status: inv.status,
117
+ created: inv.created,
118
+ hosted_invoice_url: inv.hosted_invoice_url,
119
+ invoice_pdf: inv.invoice_pdf,
120
+ }));
121
+ res.json({ invoices });
122
+ } catch (err) {
123
+ res.status(err.statusCode || 500).json({ error: err.message });
124
+ }
125
+ });
126
+
127
+ module.exports = router;
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const router = express.Router();
5
+ const { isBillingEnabled, getStripeConfig } = require('../services/billing/config');
6
+ const { getStripeClient } = require('../services/billing/stripe_client');
7
+ const { handleWebhookEvent } = require('../services/billing/subscriptions');
8
+
9
+ // Raw body required for Stripe signature verification — applied inline.
10
+ router.post('/', express.raw({ type: 'application/json' }), async (req, res) => {
11
+ if (!isBillingEnabled()) return res.status(404).json({ error: 'Billing is not enabled.' });
12
+
13
+ const sig = req.headers['stripe-signature'];
14
+ if (!sig) {
15
+ return res.status(400).json({ error: 'Missing stripe-signature header.' });
16
+ }
17
+
18
+ const { webhookSecret } = getStripeConfig();
19
+ if (!webhookSecret) {
20
+ console.warn('[billing] STRIPE_WEBHOOK_SECRET is not set; rejecting webhook.');
21
+ return res.status(500).json({ error: 'Webhook secret not configured.' });
22
+ }
23
+
24
+ let event;
25
+ try {
26
+ const stripe = getStripeClient();
27
+ event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
28
+ } catch (err) {
29
+ console.warn('[billing] Webhook signature verification failed:', err.message);
30
+ return res.status(400).json({ error: `Webhook error: ${err.message}` });
31
+ }
32
+
33
+ try {
34
+ await handleWebhookEvent(event);
35
+ } catch (err) {
36
+ console.error('[billing] Webhook handler error:', err);
37
+ // Still return 200 so Stripe does not retry immediately for handler bugs.
38
+ }
39
+
40
+ res.json({ received: true });
41
+ });
42
+
43
+ module.exports = router;
@@ -1,6 +1,7 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const rateLimit = require('express-rate-limit');
4
+ const db = require('../db/database');
4
5
  const { requireAuth } = require('../middleware/auth');
5
6
  const { sanitizeError } = require('../utils/security');
6
7
  const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
@@ -317,7 +318,6 @@ router.post('/memories', async (req, res) => {
317
318
  // Update a memory
318
319
  router.put('/memories/:id', async (req, res) => {
319
320
  const mm = req.app.locals.memoryManager;
320
- const db = require('../db/database');
321
321
  // Verify ownership before updating
322
322
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
323
323
  const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ? AND agent_id = ?').get(req.params.id, req.session.userId, agentId);
@@ -335,7 +335,6 @@ router.put('/memories/:id', async (req, res) => {
335
335
  // Delete a memory
336
336
  router.delete('/memories/:id', (req, res) => {
337
337
  const mm = req.app.locals.memoryManager;
338
- const db = require('../db/database');
339
338
  // Verify ownership before deleting
340
339
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
341
340
  const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ? AND agent_id = ?').get(req.params.id, req.session.userId, agentId);
@@ -346,7 +345,6 @@ router.delete('/memories/:id', (req, res) => {
346
345
 
347
346
  router.post('/memories/bulk-delete', (req, res) => {
348
347
  const mm = req.app.locals.memoryManager;
349
- const db = require('../db/database');
350
348
  const ids = normalizeMemoryIds(req.body?.ids);
351
349
  if (!ids.length) {
352
350
  return res.status(400).json({ error: 'ids is required' });
@@ -366,7 +364,6 @@ router.post('/memories/bulk-delete', (req, res) => {
366
364
 
367
365
  router.post('/memories/bulk-archive', (req, res) => {
368
366
  const mm = req.app.locals.memoryManager;
369
- const db = require('../db/database');
370
367
  const ids = normalizeMemoryIds(req.body?.ids);
371
368
  const archived = req.body?.archived !== false;
372
369
  if (!ids.length) {
@@ -34,6 +34,7 @@ const {
34
34
  } = require('../services/runtime/settings');
35
35
  const { isManagedDeployment } = require('../utils/deployment');
36
36
  const { getAgentIdFromRequest, isMainAgent, resolveAgentId } = require('../services/agents/manager');
37
+ const { getProviderHealthCatalog, getSupportedModels } = require('../services/ai/models');
37
38
 
38
39
  const AGENT_SETTING_KEYS = new Set([
39
40
  'cost_mode',
@@ -193,14 +194,12 @@ async function resetEnvBackedSettingValue(req, key) {
193
194
 
194
195
  // Get supported models metadata
195
196
  router.get('/meta/models', async (req, res) => {
196
- const { getSupportedModels } = require('../services/ai/models');
197
197
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
198
198
  const models = await getSupportedModels(req.session.userId, agentId);
199
199
  res.json({ models });
200
200
  });
201
201
 
202
202
  router.get('/meta/ai-providers', async (req, res) => {
203
- const { getProviderHealthCatalog, getSupportedModels } = require('../services/ai/models');
204
203
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
205
204
  const [providers, models] = await Promise.all([
206
205
  getProviderHealthCatalog(req.session.userId, agentId),
@@ -1,3 +1,4 @@
1
+ const fs = require('fs');
1
2
  const express = require('express');
2
3
  const router = express.Router();
3
4
  const { requireAuth } = require('../middleware/auth');
@@ -109,7 +110,6 @@ router.get('/:name', async (req, res) => {
109
110
  const runner = await getSkillRunner(req.app);
110
111
  const skill = runner.getSkill(req.params.name, req.session.userId);
111
112
  if (!skill) return res.status(404).json({ error: 'Skill not found' });
112
- const fs = require('fs');
113
113
  const content = fs.readFileSync(skill.filePath, 'utf-8');
114
114
  res.json({
115
115
  name: skill.name,
@@ -16,7 +16,7 @@ router.post('/geofence', async (req, res) => {
16
16
  const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
17
17
  if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
18
18
 
19
- console.log(`[Triggers] Geofence entered: ${label} by user ${req.session.userId}`);
19
+ console.log(`[Triggers] Geofence event for user ${req.session.userId}`);
20
20
 
21
21
  res.json({ success: true, message: 'Geofence trigger processed' });
22
22
  } catch (err) {
@@ -51,7 +51,7 @@ router.post('/notification', async (req, res) => {
51
51
  const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
52
52
  if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
53
53
 
54
- console.log(`[Triggers] Notification received: ${app_package} - ${title} for user ${req.session.userId}`);
54
+ console.log(`[Triggers] Notification event for user ${req.session.userId}`);
55
55
 
56
56
  db.prepare(`
57
57
  INSERT INTO notification_history (user_id, app_package, title, body, action_taken)