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.
- package/.env.example +28 -0
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/bin/neoagent.js +8 -0
- package/com.neoagent.plist +45 -0
- package/docs/configuration.md +45 -0
- package/docs/skills.md +45 -0
- package/lib/manager.js +459 -0
- package/package.json +61 -0
- package/server/db/database.js +239 -0
- package/server/index.js +442 -0
- package/server/middleware/auth.js +35 -0
- package/server/public/app.html +559 -0
- package/server/public/css/app.css +608 -0
- package/server/public/css/styles.css +472 -0
- package/server/public/favicon.svg +17 -0
- package/server/public/js/app.js +3283 -0
- package/server/public/login.html +313 -0
- package/server/routes/agents.js +125 -0
- package/server/routes/auth.js +105 -0
- package/server/routes/browser.js +116 -0
- package/server/routes/mcp.js +164 -0
- package/server/routes/memory.js +193 -0
- package/server/routes/messaging.js +153 -0
- package/server/routes/protocols.js +87 -0
- package/server/routes/scheduler.js +63 -0
- package/server/routes/settings.js +98 -0
- package/server/routes/skills.js +107 -0
- package/server/routes/store.js +1192 -0
- package/server/services/ai/compaction.js +82 -0
- package/server/services/ai/engine.js +1690 -0
- package/server/services/ai/models.js +46 -0
- package/server/services/ai/multiStep.js +112 -0
- package/server/services/ai/providers/anthropic.js +181 -0
- package/server/services/ai/providers/base.js +40 -0
- package/server/services/ai/providers/google.js +187 -0
- package/server/services/ai/providers/grok.js +121 -0
- package/server/services/ai/providers/ollama.js +162 -0
- package/server/services/ai/providers/openai.js +167 -0
- package/server/services/ai/toolRunner.js +218 -0
- package/server/services/browser/controller.js +320 -0
- package/server/services/cli/executor.js +204 -0
- package/server/services/mcp/client.js +260 -0
- package/server/services/memory/embeddings.js +126 -0
- package/server/services/memory/manager.js +431 -0
- package/server/services/messaging/base.js +23 -0
- package/server/services/messaging/discord.js +238 -0
- package/server/services/messaging/manager.js +328 -0
- package/server/services/messaging/telegram.js +243 -0
- package/server/services/messaging/telnyx.js +693 -0
- package/server/services/messaging/whatsapp.js +304 -0
- package/server/services/scheduler/cron.js +312 -0
- package/server/services/websocket.js +191 -0
- 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;
|