neoagent 1.0.0

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 (54) hide show
  1. package/.env.example +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +42 -0
  4. package/bin/neoagent.js +8 -0
  5. package/com.neoagent.plist +45 -0
  6. package/docs/configuration.md +45 -0
  7. package/docs/skills.md +45 -0
  8. package/lib/manager.js +459 -0
  9. package/package.json +61 -0
  10. package/server/db/database.js +239 -0
  11. package/server/index.js +442 -0
  12. package/server/middleware/auth.js +35 -0
  13. package/server/public/app.html +559 -0
  14. package/server/public/css/app.css +608 -0
  15. package/server/public/css/styles.css +472 -0
  16. package/server/public/favicon.svg +17 -0
  17. package/server/public/js/app.js +3283 -0
  18. package/server/public/login.html +313 -0
  19. package/server/routes/agents.js +125 -0
  20. package/server/routes/auth.js +105 -0
  21. package/server/routes/browser.js +116 -0
  22. package/server/routes/mcp.js +164 -0
  23. package/server/routes/memory.js +193 -0
  24. package/server/routes/messaging.js +153 -0
  25. package/server/routes/protocols.js +87 -0
  26. package/server/routes/scheduler.js +63 -0
  27. package/server/routes/settings.js +98 -0
  28. package/server/routes/skills.js +107 -0
  29. package/server/routes/store.js +1192 -0
  30. package/server/services/ai/compaction.js +82 -0
  31. package/server/services/ai/engine.js +1690 -0
  32. package/server/services/ai/models.js +46 -0
  33. package/server/services/ai/multiStep.js +112 -0
  34. package/server/services/ai/providers/anthropic.js +181 -0
  35. package/server/services/ai/providers/base.js +40 -0
  36. package/server/services/ai/providers/google.js +187 -0
  37. package/server/services/ai/providers/grok.js +121 -0
  38. package/server/services/ai/providers/ollama.js +162 -0
  39. package/server/services/ai/providers/openai.js +167 -0
  40. package/server/services/ai/toolRunner.js +218 -0
  41. package/server/services/browser/controller.js +320 -0
  42. package/server/services/cli/executor.js +204 -0
  43. package/server/services/mcp/client.js +260 -0
  44. package/server/services/memory/embeddings.js +126 -0
  45. package/server/services/memory/manager.js +431 -0
  46. package/server/services/messaging/base.js +23 -0
  47. package/server/services/messaging/discord.js +238 -0
  48. package/server/services/messaging/manager.js +328 -0
  49. package/server/services/messaging/telegram.js +243 -0
  50. package/server/services/messaging/telnyx.js +693 -0
  51. package/server/services/messaging/whatsapp.js +304 -0
  52. package/server/services/scheduler/cron.js +312 -0
  53. package/server/services/websocket.js +191 -0
  54. package/server/utils/security.js +71 -0
@@ -0,0 +1,164 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const db = require('../db/database');
4
+ const { requireAuth } = require('../middleware/auth');
5
+ const { sanitizeError } = require('../utils/security');
6
+
7
+ router.use(requireAuth);
8
+
9
+ // List configured MCP servers
10
+ router.get('/', (req, res) => {
11
+ const servers = db.prepare('SELECT * FROM mcp_servers WHERE user_id = ? ORDER BY name ASC').all(req.session.userId);
12
+ const mcpClient = req.app.locals.mcpClient;
13
+ const liveStatuses = mcpClient.getStatus();
14
+
15
+ const result = servers.map(s => ({
16
+ id: s.id,
17
+ name: s.name,
18
+ command: s.command,
19
+ config: JSON.parse(s.config || '{}'),
20
+ enabled: !!s.enabled,
21
+ status: liveStatuses[s.id]?.status || 'stopped',
22
+ toolCount: liveStatuses[s.id]?.toolCount || 0
23
+ }));
24
+
25
+ res.json(result);
26
+ });
27
+
28
+ // Add a new MCP server
29
+ router.post('/', (req, res) => {
30
+ const { name, command, config, enabled } = req.body;
31
+ if (!name || !command) return res.status(400).json({ error: 'name and command are required' });
32
+
33
+ const result = db.prepare('INSERT INTO mcp_servers (user_id, name, command, config, enabled) VALUES (?, ?, ?, ?, ?)')
34
+ .run(req.session.userId, name, command, JSON.stringify(config || {}), enabled !== false ? 1 : 0);
35
+
36
+ res.status(201).json({ id: result.lastInsertRowid, name, command });
37
+ });
38
+
39
+ // Update an MCP server
40
+ router.put('/:id', (req, res) => {
41
+ const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
42
+ if (!server) return res.status(404).json({ error: 'Server not found' });
43
+
44
+ const { name, command, config, enabled } = req.body;
45
+ db.prepare('UPDATE mcp_servers SET name = ?, command = ?, config = ?, enabled = ? WHERE id = ?')
46
+ .run(name || server.name, command || server.command, JSON.stringify(config || JSON.parse(server.config)), enabled !== undefined ? (enabled ? 1 : 0) : server.enabled, server.id);
47
+
48
+ res.json({ success: true });
49
+ });
50
+
51
+ // Delete an MCP server
52
+ router.delete('/:id', async (req, res) => {
53
+ const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
54
+ if (!server) return res.status(404).json({ error: 'Server not found' });
55
+
56
+ const mcpClient = req.app.locals.mcpClient;
57
+ await mcpClient.stopServer(server.id).catch(() => { });
58
+
59
+ db.prepare('DELETE FROM mcp_servers WHERE id = ?').run(server.id);
60
+ res.json({ success: true });
61
+ });
62
+
63
+ // Start an MCP server
64
+ router.post('/:id/start', async (req, res) => {
65
+ try {
66
+ const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
67
+ if (!server) return res.status(404).json({ error: 'Server not found' });
68
+
69
+ const mcpClient = req.app.locals.mcpClient;
70
+ const result = await mcpClient.startServer(server.id, server.command, server.name);
71
+ const tools = await mcpClient.listTools(server.id);
72
+
73
+ res.json({ ...result, tools });
74
+ } catch (err) {
75
+ if (err.message && err.message.startsWith('OAUTH_REDIRECT:')) {
76
+ const url = err.message.substring(15);
77
+ return res.json({ status: 'oauth_redirect', url });
78
+ }
79
+ res.status(500).json({ error: sanitizeError(err) });
80
+ }
81
+ });
82
+
83
+ // Stop an MCP server
84
+ router.post('/:id/stop', async (req, res) => {
85
+ try {
86
+ // Verify ownership before stopping
87
+ const server = db.prepare('SELECT id FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
88
+ if (!server) return res.status(404).json({ error: 'Server not found' });
89
+ const mcpClient = req.app.locals.mcpClient;
90
+ await mcpClient.stopServer(req.params.id);
91
+ res.json({ status: 'stopped' });
92
+ } catch (err) {
93
+ res.status(500).json({ error: sanitizeError(err) });
94
+ }
95
+ });
96
+
97
+ // Get tools from a specific server
98
+ router.get('/:id/tools', async (req, res) => {
99
+ try {
100
+ // Verify ownership before listing tools
101
+ const server = db.prepare('SELECT id FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
102
+ if (!server) return res.status(404).json({ error: 'Server not found' });
103
+ const mcpClient = req.app.locals.mcpClient;
104
+ const tools = await mcpClient.listTools(req.params.id);
105
+ res.json(tools);
106
+ } catch (err) {
107
+ res.status(500).json({ error: sanitizeError(err) });
108
+ }
109
+ });
110
+
111
+ // OAuth Callback
112
+ router.get('/oauth/callback', async (req, res) => {
113
+ const { code, state, error } = req.query;
114
+ if (!state) return res.status(400).send('Missing state parameter');
115
+ if (error) return res.status(400).send(`OAuth Error: ${error}`);
116
+
117
+ const [serverIdStr] = state.split('::');
118
+ if (!serverIdStr) return res.status(400).send('Invalid state format');
119
+
120
+ const serverId = parseInt(serverIdStr, 10);
121
+ const mcpClient = req.app.locals.mcpClient;
122
+
123
+ try {
124
+ await mcpClient.finishOAuth(serverId, code);
125
+ // Render a simple script that closes the popup or redirects parent
126
+ res.send(`
127
+ <html><body>
128
+ <script>
129
+ if (window.opener) {
130
+ window.opener.postMessage({ type: 'mcp_oauth_success', serverId: ${serverId} }, '*');
131
+ window.close();
132
+ } else {
133
+ window.location.href = '/?page=mcp';
134
+ }
135
+ </script>
136
+ <p>Authentication successful. You can close this window.</p>
137
+ </body></html>
138
+ `);
139
+ } catch (err) {
140
+ res.status(500).send(`Failed to finish OAuth: ${sanitizeError(err)}`);
141
+ }
142
+ });
143
+
144
+ // Get all tools from all running servers
145
+ router.get('/tools/all', (req, res) => {
146
+ const mcpClient = req.app.locals.mcpClient;
147
+ res.json(mcpClient.getAllTools());
148
+ });
149
+
150
+ // Call a tool
151
+ router.post('/tools/call', async (req, res) => {
152
+ try {
153
+ const { serverId, toolName, args } = req.body;
154
+ if (!serverId || !toolName) return res.status(400).json({ error: 'serverId and toolName required' });
155
+
156
+ const mcpClient = req.app.locals.mcpClient;
157
+ const result = await mcpClient.callTool(serverId, toolName, args || {});
158
+ res.json(result);
159
+ } catch (err) {
160
+ res.status(500).json({ error: sanitizeError(err) });
161
+ }
162
+ });
163
+
164
+ module.exports = router;
@@ -0,0 +1,193 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const { sanitizeError } = require('../utils/security');
5
+
6
+ router.use(requireAuth);
7
+
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Overview (for initial page load)
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ router.get('/', (req, res) => {
13
+ const mm = req.app.locals.memoryManager;
14
+ const userId = req.session.userId;
15
+ res.json({
16
+ soul: mm.readSoul(),
17
+ dailyLogs: mm.listDailyLogs(7),
18
+ apiKeys: Object.keys(mm.readApiKeys()),
19
+ coreMemory: mm.getCoreMemory(userId)
20
+ });
21
+ });
22
+
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+ // Semantic Memories
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+
27
+ // List memories (with optional ?category= and ?limit= filters)
28
+ router.get('/memories', (req, res) => {
29
+ const mm = req.app.locals.memoryManager;
30
+ const userId = req.session.userId;
31
+ const { category, limit = 50, offset = 0, archived = false } = req.query;
32
+ try {
33
+ const memories = mm.listMemories(userId, {
34
+ category: category || null,
35
+ limit: parseInt(limit),
36
+ offset: parseInt(offset),
37
+ includeArchived: archived === 'true'
38
+ });
39
+ res.json(memories);
40
+ } catch (err) {
41
+ res.status(500).json({ error: sanitizeError(err) });
42
+ }
43
+ });
44
+
45
+ // Save a new memory
46
+ router.post('/memories', async (req, res) => {
47
+ const mm = req.app.locals.memoryManager;
48
+ const userId = req.session.userId;
49
+ const { content, category = 'episodic', importance = 5 } = req.body;
50
+ if (!content || !content.trim()) return res.status(400).json({ error: 'content is required' });
51
+ try {
52
+ const id = await mm.saveMemory(userId, content, category, importance);
53
+ res.json({ success: true, id });
54
+ } catch (err) {
55
+ res.status(500).json({ error: sanitizeError(err) });
56
+ }
57
+ });
58
+
59
+ // Update a memory
60
+ router.put('/memories/:id', async (req, res) => {
61
+ const mm = req.app.locals.memoryManager;
62
+ const db = require('../db/database');
63
+ // Verify ownership before updating
64
+ const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
65
+ if (!existing) return res.status(404).json({ error: 'Memory not found' });
66
+ const { content, importance, category } = req.body;
67
+ try {
68
+ const updated = await mm.updateMemory(req.params.id, { content, importance, category });
69
+ if (!updated) return res.status(404).json({ error: 'Memory not found' });
70
+ res.json(updated);
71
+ } catch (err) {
72
+ res.status(500).json({ error: sanitizeError(err) });
73
+ }
74
+ });
75
+
76
+ // Delete a memory
77
+ router.delete('/memories/:id', (req, res) => {
78
+ const mm = req.app.locals.memoryManager;
79
+ const db = require('../db/database');
80
+ // Verify ownership before deleting
81
+ const existing = db.prepare('SELECT id FROM memories WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
82
+ if (!existing) return res.status(404).json({ error: 'Memory not found' });
83
+ mm.deleteMemory(req.params.id);
84
+ res.json({ success: true });
85
+ });
86
+
87
+ // Semantic recall (search)
88
+ router.post('/memories/recall', async (req, res) => {
89
+ const mm = req.app.locals.memoryManager;
90
+ const userId = req.session.userId;
91
+ const { query, limit = 10 } = req.body;
92
+ if (!query) return res.status(400).json({ error: 'query is required' });
93
+ try {
94
+ const results = await mm.recallMemory(userId, query, parseInt(limit));
95
+ res.json(results);
96
+ } catch (err) {
97
+ res.status(500).json({ error: sanitizeError(err) });
98
+ }
99
+ });
100
+
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+ // Core Memory
103
+ // ─────────────────────────────────────────────────────────────────────────────
104
+
105
+ router.get('/core', (req, res) => {
106
+ const mm = req.app.locals.memoryManager;
107
+ const userId = req.session.userId;
108
+ res.json(mm.getCoreMemory(userId));
109
+ });
110
+
111
+ router.put('/core/:key', (req, res) => {
112
+ const mm = req.app.locals.memoryManager;
113
+ const userId = req.session.userId;
114
+ const { value } = req.body;
115
+ if (value === undefined) return res.status(400).json({ error: 'value is required' });
116
+ mm.updateCore(userId, req.params.key, value);
117
+ res.json({ success: true });
118
+ });
119
+
120
+ router.delete('/core/:key', (req, res) => {
121
+ const mm = req.app.locals.memoryManager;
122
+ const userId = req.session.userId;
123
+ mm.deleteCore(userId, req.params.key);
124
+ res.json({ success: true });
125
+ });
126
+
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+ // SOUL.md
129
+ // ─────────────────────────────────────────────────────────────────────────────
130
+
131
+ router.get('/soul', (req, res) => {
132
+ res.json({ content: req.app.locals.memoryManager.readSoul() });
133
+ });
134
+
135
+ router.put('/soul', (req, res) => {
136
+ req.app.locals.memoryManager.writeSoul(req.body.content);
137
+ res.json({ success: true });
138
+ });
139
+
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+ // Daily Logs
142
+ // ─────────────────────────────────────────────────────────────────────────────
143
+
144
+ router.get('/daily', (req, res) => {
145
+ const limit = parseInt(req.query.limit) || 7;
146
+ res.json(req.app.locals.memoryManager.listDailyLogs(limit));
147
+ });
148
+
149
+ router.get('/daily/:date', (req, res) => {
150
+ const content = req.app.locals.memoryManager.readDailyLog(new Date(req.params.date));
151
+ res.json({ date: req.params.date, content });
152
+ });
153
+
154
+ // ─────────────────────────────────────────────────────────────────────────────
155
+ // API Keys (agent-managed)
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+
158
+ router.get('/api-keys', (req, res) => {
159
+ const keys = req.app.locals.memoryManager.readApiKeys();
160
+ const masked = {};
161
+ for (const [k, v] of Object.entries(keys)) {
162
+ masked[k] = v ? `${v.slice(0, 4)}...${v.slice(-4)}` : null;
163
+ }
164
+ res.json(masked);
165
+ });
166
+
167
+ router.put('/api-keys/:service', (req, res) => {
168
+ req.app.locals.memoryManager.setApiKey(req.params.service, req.body.key);
169
+ res.json({ success: true });
170
+ });
171
+
172
+ router.delete('/api-keys/:service', (req, res) => {
173
+ req.app.locals.memoryManager.deleteApiKey(req.params.service);
174
+ res.json({ success: true });
175
+ });
176
+
177
+ // ─────────────────────────────────────────────────────────────────────────────
178
+ // Conversation History
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+
181
+ router.get('/conversations', (req, res) => {
182
+ const mm = req.app.locals.memoryManager;
183
+ const conversations = mm.getRecentConversations(req.session.userId, parseInt(req.query.limit) || 20);
184
+ res.json(conversations);
185
+ });
186
+
187
+ router.post('/conversations/search', (req, res) => {
188
+ const mm = req.app.locals.memoryManager;
189
+ const results = mm.searchConversations(req.session.userId, req.body.query);
190
+ res.json(results);
191
+ });
192
+
193
+ module.exports = router;
@@ -0,0 +1,153 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const db = require('../db/database');
4
+ const { requireAuth } = require('../middleware/auth');
5
+ const { sanitizeError } = require('../utils/security');
6
+
7
+ router.use(requireAuth);
8
+
9
+ // Get all platform statuses
10
+ router.get('/status', (req, res) => {
11
+ const manager = req.app.locals.messagingManager;
12
+ res.json(manager.getAllStatuses(req.session.userId));
13
+ });
14
+
15
+ // Connect to a platform
16
+ router.post('/connect', async (req, res) => {
17
+ try {
18
+ const { platform, config } = req.body;
19
+ if (!platform) return res.status(400).json({ error: 'Platform is required' });
20
+
21
+ const manager = req.app.locals.messagingManager;
22
+ const result = await manager.connectPlatform(req.session.userId, platform, config || {});
23
+ res.json(result);
24
+ } catch (err) {
25
+ res.status(500).json({ error: sanitizeError(err) });
26
+ }
27
+ });
28
+
29
+ // Disconnect from a platform
30
+ router.post('/disconnect', async (req, res) => {
31
+ try {
32
+ const { platform } = req.body;
33
+ const manager = req.app.locals.messagingManager;
34
+ const result = await manager.disconnectPlatform(req.session.userId, platform);
35
+ res.json(result);
36
+ } catch (err) {
37
+ res.status(500).json({ error: sanitizeError(err) });
38
+ }
39
+ });
40
+
41
+ // Logout from a platform (clear auth)
42
+ router.post('/logout', async (req, res) => {
43
+ try {
44
+ const { platform } = req.body;
45
+ const manager = req.app.locals.messagingManager;
46
+ const result = await manager.logoutPlatform(req.session.userId, platform);
47
+ res.json(result);
48
+ } catch (err) {
49
+ res.status(500).json({ error: sanitizeError(err) });
50
+ }
51
+ });
52
+
53
+ // Send a message
54
+ router.post('/send', async (req, res) => {
55
+ try {
56
+ const { platform, to, content, mediaPath } = req.body;
57
+ if (!platform || !to || !content) return res.status(400).json({ error: 'platform, to, and content required' });
58
+
59
+ const manager = req.app.locals.messagingManager;
60
+ const result = await manager.sendMessage(req.session.userId, platform, to, content, mediaPath);
61
+ res.json(result);
62
+ } catch (err) {
63
+ res.status(500).json({ error: sanitizeError(err) });
64
+ }
65
+ });
66
+
67
+ // Get message history
68
+ router.get('/messages', (req, res) => {
69
+ const { platform, chatId, limit } = req.query;
70
+ let query = 'SELECT * FROM messages WHERE user_id = ?';
71
+ const params = [req.session.userId];
72
+
73
+ if (platform) { query += ' AND platform = ?'; params.push(platform); }
74
+ if (chatId) { query += ' AND platform_chat_id = ?'; params.push(chatId); }
75
+
76
+ query += ' ORDER BY created_at DESC LIMIT ?';
77
+ params.push(Math.min(parseInt(limit) || 50, 200));
78
+
79
+ const messages = db.prepare(query).all(...params);
80
+ res.json(messages);
81
+ });
82
+
83
+ // Get platform-specific status
84
+ router.get('/status/:platform', (req, res) => {
85
+ const manager = req.app.locals.messagingManager;
86
+ res.json(manager.getPlatformStatus(req.session.userId, req.params.platform));
87
+ });
88
+
89
+ // Update Telnyx voice secret code (for non-whitelisted caller gating)
90
+ router.put('/telnyx/voice-secret', (req, res) => {
91
+ try {
92
+ const code = String(req.body.secret || '').replace(/\D/g, ''); // digits only
93
+ db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
94
+ .run(req.session.userId, 'platform_voice_secret_telnyx', JSON.stringify(code));
95
+ const manager = req.app.locals.messagingManager;
96
+ if (manager) manager.updateTelnyxVoiceSecret(req.session.userId, code);
97
+ res.json({ success: true });
98
+ } catch (err) {
99
+ res.status(500).json({ error: sanitizeError(err) });
100
+ }
101
+ });
102
+
103
+ // Update Telnyx allowed numbers (whitelist)
104
+ router.put('/telnyx/whitelist', (req, res) => {
105
+ try {
106
+ const { numbers } = req.body;
107
+ if (!Array.isArray(numbers)) return res.status(400).json({ error: 'numbers must be an array' });
108
+ const list = numbers.map(n => n.replace(/[^0-9+]/g, '')).filter(Boolean);
109
+ db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
110
+ .run(req.session.userId, 'platform_whitelist_telnyx', JSON.stringify(list));
111
+ const manager = req.app.locals.messagingManager;
112
+ if (manager) manager.updateTelnyxAllowedNumbers(req.session.userId, list);
113
+ res.json({ success: true, numbers: list });
114
+ } catch (err) {
115
+ res.status(500).json({ error: sanitizeError(err) });
116
+ }
117
+ });
118
+
119
+ // Update Discord allowed IDs (whitelist — prefixed: "user:ID", "guild:ID", "channel:ID")
120
+ router.put('/discord/whitelist', (req, res) => {
121
+ try {
122
+ const { ids } = req.body;
123
+ if (!Array.isArray(ids)) return res.status(400).json({ error: 'ids must be an array' });
124
+ // Keep prefixed format, strip only clearly unsafe characters
125
+ const list = ids.map(id => String(id).replace(/[^0-9a-z:_-]/gi, '')).filter(Boolean);
126
+ db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
127
+ .run(req.session.userId, 'platform_whitelist_discord', JSON.stringify(list));
128
+ const manager = req.app.locals.messagingManager;
129
+ if (manager) manager.updateDiscordAllowedIds(req.session.userId, list);
130
+ res.json({ success: true, ids: list });
131
+ } catch (err) {
132
+ res.status(500).json({ error: sanitizeError(err) });
133
+ }
134
+ });
135
+
136
+ // Update Telegram allowed IDs (whitelist — prefixed: "user:ID", "group:ID")
137
+ router.put('/telegram/whitelist', (req, res) => {
138
+ try {
139
+ const { ids } = req.body;
140
+ if (!Array.isArray(ids)) return res.status(400).json({ error: 'ids must be an array' });
141
+ // Keep prefixed format; group IDs are negative so allow minus sign
142
+ const list = ids.map(id => String(id).replace(/[^0-9a-z:_-]/gi, '')).filter(Boolean);
143
+ db.prepare('INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value')
144
+ .run(req.session.userId, 'platform_whitelist_telegram', JSON.stringify(list));
145
+ const manager = req.app.locals.messagingManager;
146
+ if (manager) manager.updateTelegramAllowedIds(req.session.userId, list);
147
+ res.json({ success: true, ids: list });
148
+ } catch (err) {
149
+ res.status(500).json({ error: sanitizeError(err) });
150
+ }
151
+ });
152
+
153
+ module.exports = router;
@@ -0,0 +1,87 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const db = require('../db/database');
4
+ const { requireAuth } = require('../middleware/auth');
5
+
6
+ router.use(requireAuth);
7
+
8
+ // List protocols
9
+ router.get('/', (req, res) => {
10
+ try {
11
+ const protocols = db.prepare('SELECT id, name, description, content, updated_at FROM protocols WHERE user_id = ? ORDER BY name ASC').all(req.session.userId);
12
+ res.json(protocols);
13
+ } catch (err) {
14
+ res.status(500).json({ error: err.message });
15
+ }
16
+ });
17
+
18
+ // Get single protocol
19
+ router.get('/:id', (req, res) => {
20
+ try {
21
+ const p = db.prepare('SELECT * FROM protocols WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
22
+ if (!p) return res.status(404).json({ error: 'Not found' });
23
+ res.json(p);
24
+ } catch (err) {
25
+ res.status(500).json({ error: err.message });
26
+ }
27
+ });
28
+
29
+ // Create protocol
30
+ router.post('/', (req, res) => {
31
+ try {
32
+ const { name, description, content } = req.body;
33
+ if (!name || !content) return res.status(400).json({ error: 'Name and content are required' });
34
+
35
+ const stmt = db.prepare('INSERT INTO protocols (user_id, name, description, content) VALUES (?, ?, ?, ?)');
36
+ const info = stmt.run(req.session.userId, name, description || '', content);
37
+
38
+ const p = db.prepare('SELECT * FROM protocols WHERE id = ?').get(info.lastInsertRowid);
39
+ res.status(201).json(p);
40
+ } catch (err) {
41
+ if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
42
+ return res.status(400).json({ error: 'Protocol with this name already exists' });
43
+ }
44
+ res.status(500).json({ error: err.message });
45
+ }
46
+ });
47
+
48
+ // Update protocol
49
+ router.put('/:id', (req, res) => {
50
+ try {
51
+ const { name, description, content } = req.body;
52
+
53
+ // check existence
54
+ const existing = db.prepare('SELECT id FROM protocols WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
55
+ if (!existing) return res.status(404).json({ error: 'Not found' });
56
+
57
+ const stmt = db.prepare(`
58
+ UPDATE protocols
59
+ SET name = ?, description = ?, content = ?, updated_at = datetime('now')
60
+ WHERE id = ? AND user_id = ?
61
+ `);
62
+
63
+ stmt.run(name, description || '', content, req.params.id, req.session.userId);
64
+ const p = db.prepare('SELECT * FROM protocols WHERE id = ?').get(req.params.id);
65
+ res.json(p);
66
+ } catch (err) {
67
+ if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
68
+ return res.status(400).json({ error: 'Protocol with this name already exists' });
69
+ }
70
+ res.status(500).json({ error: err.message });
71
+ }
72
+ });
73
+
74
+ // Delete protocol
75
+ router.delete('/:id', (req, res) => {
76
+ try {
77
+ const stmt = db.prepare('DELETE FROM protocols WHERE id = ? AND user_id = ?');
78
+ const info = stmt.run(req.params.id, req.session.userId);
79
+
80
+ if (info.changes === 0) return res.status(404).json({ error: 'Not found' });
81
+ res.json({ success: true });
82
+ } catch (err) {
83
+ res.status(500).json({ error: err.message });
84
+ }
85
+ });
86
+
87
+ module.exports = router;
@@ -0,0 +1,63 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { requireAuth } = require('../middleware/auth');
4
+ const { sanitizeError } = require('../utils/security');
5
+
6
+ router.use(requireAuth);
7
+
8
+ // List scheduled tasks
9
+ router.get('/', (req, res) => {
10
+ const scheduler = req.app.locals.scheduler;
11
+ res.json(scheduler.listTasks(req.session.userId));
12
+ });
13
+
14
+ // Create a new scheduled task
15
+ router.post('/', (req, res) => {
16
+ try {
17
+ const { name, cronExpression, prompt, enabled } = req.body;
18
+ if (!name || !cronExpression || !prompt) {
19
+ return res.status(400).json({ error: 'name, cronExpression, and prompt required' });
20
+ }
21
+
22
+ const scheduler = req.app.locals.scheduler;
23
+ const task = scheduler.createTask(req.session.userId, { name, cronExpression, prompt, enabled });
24
+ res.status(201).json(task);
25
+ } catch (err) {
26
+ res.status(400).json({ error: sanitizeError(err) });
27
+ }
28
+ });
29
+
30
+ // Update a scheduled task
31
+ router.put('/:id', (req, res) => {
32
+ try {
33
+ const scheduler = req.app.locals.scheduler;
34
+ const task = scheduler.updateTask(parseInt(req.params.id), req.session.userId, req.body);
35
+ res.json(task);
36
+ } catch (err) {
37
+ res.status(400).json({ error: sanitizeError(err) });
38
+ }
39
+ });
40
+
41
+ // Delete a scheduled task
42
+ router.delete('/:id', (req, res) => {
43
+ try {
44
+ const scheduler = req.app.locals.scheduler;
45
+ scheduler.deleteTask(parseInt(req.params.id), req.session.userId);
46
+ res.json({ success: true });
47
+ } catch (err) {
48
+ res.status(400).json({ error: sanitizeError(err) });
49
+ }
50
+ });
51
+
52
+ // Run a task immediately
53
+ router.post('/:id/run', (req, res) => {
54
+ try {
55
+ const scheduler = req.app.locals.scheduler;
56
+ const result = scheduler.runTaskNow(parseInt(req.params.id), req.session.userId);
57
+ res.json(result);
58
+ } catch (err) {
59
+ res.status(400).json({ error: sanitizeError(err) });
60
+ }
61
+ });
62
+
63
+ module.exports = router;