neoagent 1.1.0 → 1.1.2

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.
@@ -4,6 +4,7 @@ const path = require('path');
4
4
  const router = express.Router();
5
5
  const db = require('../db/database');
6
6
  const { requireAuth } = require('../middleware/auth');
7
+ const { normalizeWhatsAppWhitelist } = require('../utils/whatsapp');
7
8
 
8
9
  router.use(requireAuth);
9
10
 
@@ -55,6 +56,19 @@ router.get('/', (req, res) => {
55
56
  router.put('/', (req, res) => {
56
57
  const userId = req.session.userId;
57
58
  const upsert = db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value');
59
+ const normalizedBody = { ...req.body };
60
+
61
+ if ('platform_whitelist_whatsapp' in normalizedBody) {
62
+ let whitelist = normalizedBody.platform_whitelist_whatsapp;
63
+ if (typeof whitelist === 'string') {
64
+ try {
65
+ whitelist = JSON.parse(whitelist);
66
+ } catch {
67
+ whitelist = [];
68
+ }
69
+ }
70
+ normalizedBody.platform_whitelist_whatsapp = JSON.stringify(normalizeWhatsAppWhitelist(whitelist));
71
+ }
58
72
 
59
73
  const tx = db.transaction((entries) => {
60
74
  for (const [key, value] of entries) {
@@ -63,17 +77,66 @@ router.put('/', (req, res) => {
63
77
  }
64
78
  });
65
79
 
66
- tx(Object.entries(req.body));
80
+ tx(Object.entries(normalizedBody));
67
81
 
68
82
  // Apply headless toggle immediately without restarting
69
- if ('headless_browser' in req.body) {
83
+ if ('headless_browser' in normalizedBody) {
70
84
  const bc = req.app.locals.browserController;
71
- if (bc) bc.setHeadless(req.body.headless_browser).catch(() => { });
85
+ if (bc) bc.setHeadless(normalizedBody.headless_browser).catch(() => { });
72
86
  }
73
87
 
74
88
  res.json({ success: true });
75
89
  });
76
90
 
91
+ // Token usage summary for settings UI
92
+ router.get('/token-usage/summary', (req, res) => {
93
+ const userId = req.session.userId;
94
+ const totals = db.prepare(`
95
+ SELECT
96
+ COALESCE(SUM(total_tokens), 0) AS totalTokens,
97
+ COUNT(*) AS totalRuns,
98
+ COALESCE(AVG(CASE WHEN total_tokens > 0 THEN total_tokens END), 0) AS avgTokensPerRun
99
+ FROM agent_runs
100
+ WHERE user_id = ?
101
+ `).get(userId);
102
+
103
+ const recentRows = db.prepare(`
104
+ SELECT
105
+ date(created_at) AS day,
106
+ COALESCE(SUM(total_tokens), 0) AS tokens,
107
+ COUNT(*) AS runs
108
+ FROM agent_runs
109
+ WHERE user_id = ? AND created_at >= datetime('now', '-6 days')
110
+ GROUP BY date(created_at)
111
+ ORDER BY day ASC
112
+ `).all(userId);
113
+
114
+ const byDay = new Map(recentRows.map(r => [r.day, { tokens: Number(r.tokens || 0), runs: Number(r.runs || 0) }]));
115
+ const last7Days = [];
116
+ for (let offset = 6; offset >= 0; offset--) {
117
+ const day = db.prepare(`SELECT date('now', ?) AS day`).get(`-${offset} days`).day;
118
+ const dayRow = byDay.get(day) || { tokens: 0, runs: 0 };
119
+ last7Days.push({ date: day, tokens: dayRow.tokens, runs: dayRow.runs });
120
+ }
121
+
122
+ const last7Totals = last7Days.reduce((acc, d) => {
123
+ acc.tokens += d.tokens;
124
+ acc.runs += d.runs;
125
+ return acc;
126
+ }, { tokens: 0, runs: 0 });
127
+
128
+ res.json({
129
+ totals: {
130
+ totalTokens: Number(totals?.totalTokens || 0),
131
+ totalRuns: Number(totals?.totalRuns || 0),
132
+ avgTokensPerRun: Math.round(Number(totals?.avgTokensPerRun || 0)),
133
+ last7DaysTokens: last7Totals.tokens,
134
+ last7DaysRuns: last7Totals.runs
135
+ },
136
+ last7Days
137
+ });
138
+ });
139
+
77
140
  // Get single setting
78
141
  router.get('/:key', (req, res) => {
79
142
  const row = db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?').get(req.session.userId, req.params.key);
@@ -90,7 +153,18 @@ router.get('/:key', (req, res) => {
90
153
 
91
154
  // Set single setting
92
155
  router.put('/:key', (req, res) => {
93
- const v = typeof req.body.value === 'string' ? req.body.value : JSON.stringify(req.body.value);
156
+ let value = req.body.value;
157
+ if (req.params.key === 'platform_whitelist_whatsapp') {
158
+ if (typeof value === 'string') {
159
+ try {
160
+ value = JSON.parse(value);
161
+ } catch {
162
+ value = [];
163
+ }
164
+ }
165
+ value = normalizeWhatsAppWhitelist(value);
166
+ }
167
+ const v = typeof value === 'string' ? value : JSON.stringify(value);
94
168
  db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
95
169
  .run(req.session.userId, req.params.key, v);
96
170
  res.json({ success: true });
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Sets up Telnyx voice webhook route
7
+ * @param {import('express').Application} app
8
+ */
9
+ function setupTelnyxWebhook(app) {
10
+ const tokenMiddleware = (req, res, next) => {
11
+ const expected = process.env.TELNYX_WEBHOOK_TOKEN;
12
+ if (expected) {
13
+ const provided = req.query.token || '';
14
+ const a = Buffer.from(provided.padEnd(expected.length));
15
+ const b = Buffer.from(expected);
16
+ if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
17
+ console.warn('[Telnyx webhook] Rejected request with invalid or missing token');
18
+ return res.status(403).send('Forbidden');
19
+ }
20
+ }
21
+ next();
22
+ };
23
+
24
+ app.post('/api/telnyx/webhook', tokenMiddleware, async (req, res) => {
25
+ res.status(200).send('OK');
26
+ const manager = app.locals.messagingManager;
27
+ if (manager) {
28
+ await manager.handleTelnyxWebhook(req.body).catch(err =>
29
+ console.error('[Telnyx webhook]', err.message)
30
+ );
31
+ }
32
+ });
33
+ }
34
+
35
+ module.exports = { setupTelnyxWebhook };
@@ -2,9 +2,10 @@ async function compact(messages, provider, model) {
2
2
  const systemMsg = messages.find(m => m.role === 'system');
3
3
  const nonSystem = messages.filter(m => m.role !== 'system');
4
4
 
5
- if (nonSystem.length < 6) return messages;
5
+ // Only compact once history is clearly old enough to avoid touching recent context.
6
+ if (nonSystem.length < 12) return messages;
6
7
 
7
- const keepRecent = 6;
8
+ const keepRecent = 10;
8
9
  const toCompact = nonSystem.slice(0, -keepRecent);
9
10
  const recent = nonSystem.slice(-keepRecent);
10
11