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,191 @@
|
|
|
1
|
+
const db = require('../db/database');
|
|
2
|
+
const { sanitizeError } = require('../utils/security');
|
|
3
|
+
|
|
4
|
+
function setupWebSocket(io, services) {
|
|
5
|
+
const { agentEngine, messagingManager, mcpClient, scheduler, memoryManager } = services;
|
|
6
|
+
io.on('connection', (socket) => {
|
|
7
|
+
const session = socket.request.session;
|
|
8
|
+
if (!session?.userId) {
|
|
9
|
+
socket.disconnect(true);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const userId = session.userId;
|
|
14
|
+
socket.join(`user:${userId}`);
|
|
15
|
+
|
|
16
|
+
console.log(`[WS] User ${userId} connected (${socket.id})`);
|
|
17
|
+
|
|
18
|
+
// ── Agent Events ──
|
|
19
|
+
|
|
20
|
+
socket.on('agent:run', async (data) => {
|
|
21
|
+
try {
|
|
22
|
+
const { task, options } = data;
|
|
23
|
+
if (!task || typeof task !== 'string') return socket.emit('error', { message: 'Task must be a non-empty string' });
|
|
24
|
+
if (task.length > 50000) return socket.emit('error', { message: 'Message too long (max 50,000 characters)' });
|
|
25
|
+
|
|
26
|
+
if (task.startsWith('/')) {
|
|
27
|
+
const [rawCmd] = task.trim().split(/\s+/);
|
|
28
|
+
const cmd = rawCmd.slice(1).toLowerCase();
|
|
29
|
+
|
|
30
|
+
switch (cmd) {
|
|
31
|
+
case 'new':
|
|
32
|
+
case 'clear':
|
|
33
|
+
db.prepare('DELETE FROM conversation_history WHERE user_id = ?').run(userId);
|
|
34
|
+
socket.emit('chat:cleared');
|
|
35
|
+
{
|
|
36
|
+
const resetResult = await agentEngine.run(userId, 'context was just cleared. say something very brief (1-2 sentences max) acknowledging the fresh start, in your own style. no tools needed.', {});
|
|
37
|
+
socket.emit('run:complete', { content: resetResult?.content || 'fresh start.' });
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
|
|
41
|
+
case 'stop': {
|
|
42
|
+
agentEngine.abortAll(userId);
|
|
43
|
+
const q = services.app?.locals?.userQueues;
|
|
44
|
+
if (q && q[userId]) { q[userId].pending = []; q[userId].running = false; }
|
|
45
|
+
socket.emit('run:complete', { content: 'Stopped.' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'help':
|
|
50
|
+
socket.emit('run:complete', {
|
|
51
|
+
content: '**Available commands**\n- `/new` or `/clear` — clear conversation context\n- `/stop` — abort all running tasks immediately\n- `/help` — show this message'
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
|
|
55
|
+
default:
|
|
56
|
+
socket.emit('run:complete', { content: `Unknown command: \`/${cmd}\`. Type \`/help\` for available commands.` });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
db.prepare('INSERT INTO conversation_history (user_id, role, content, metadata) VALUES (?, ?, ?, ?)')
|
|
62
|
+
.run(userId, 'user', task, JSON.stringify({ platform: 'web' }));
|
|
63
|
+
|
|
64
|
+
const priorMessages = db.prepare(
|
|
65
|
+
'SELECT role, content FROM conversation_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 30'
|
|
66
|
+
).all(userId).reverse();
|
|
67
|
+
const prior = priorMessages.filter(m => !(m.role === 'user' && m.content === task)).slice(-29);
|
|
68
|
+
|
|
69
|
+
const result = await agentEngine.run(userId, task, { ...options, priorMessages: prior });
|
|
70
|
+
|
|
71
|
+
if (result?.content) {
|
|
72
|
+
db.prepare('INSERT INTO conversation_history (user_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)')
|
|
73
|
+
.run(userId, result.runId, 'assistant', result.content, JSON.stringify({ tokens: result.totalTokens }));
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
socket.emit('run:error', { error: sanitizeError(err) });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
socket.on('agent:abort', (data) => {
|
|
81
|
+
try {
|
|
82
|
+
agentEngine.abort(data?.runId);
|
|
83
|
+
socket.emit('agent:aborted', { runId: data?.runId });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
socket.emit('error', { message: sanitizeError(err) });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── Conversation ──
|
|
90
|
+
|
|
91
|
+
socket.on('agent:history', (data) => {
|
|
92
|
+
try {
|
|
93
|
+
const runs = db.prepare(
|
|
94
|
+
'SELECT * FROM agent_runs WHERE user_id = ? ORDER BY created_at DESC LIMIT ?'
|
|
95
|
+
).all(userId, data?.limit || 20);
|
|
96
|
+
socket.emit('agent:history', runs);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
socket.emit('error', { message: sanitizeError(err) });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
socket.on('agent:run_detail', (data) => {
|
|
103
|
+
try {
|
|
104
|
+
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(data.runId, userId);
|
|
105
|
+
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_number ASC').all(data.runId);
|
|
106
|
+
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(data.runId);
|
|
107
|
+
socket.emit('agent:run_detail', { run, steps, history });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
socket.emit('error', { message: sanitizeError(err) });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ── Messaging ──
|
|
114
|
+
|
|
115
|
+
socket.on('messaging:connect', async (data) => {
|
|
116
|
+
try {
|
|
117
|
+
const result = await messagingManager.connectPlatform(userId, data.platform, data.config || {});
|
|
118
|
+
socket.emit('messaging:connect_result', result);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
socket.emit('messaging:error', { error: sanitizeError(err) });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
socket.on('messaging:disconnect', async (data) => {
|
|
125
|
+
try {
|
|
126
|
+
const result = await messagingManager.disconnectPlatform(userId, data.platform);
|
|
127
|
+
socket.emit('messaging:disconnect_result', result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
socket.emit('messaging:error', { error: sanitizeError(err) });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
socket.on('messaging:send', async (data) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await messagingManager.sendMessage(userId, data.platform, data.to, data.content, data.mediaPath);
|
|
136
|
+
socket.emit('messaging:sent', result);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
socket.emit('messaging:error', { error: sanitizeError(err) });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
socket.on('messaging:status', () => {
|
|
143
|
+
try {
|
|
144
|
+
const statuses = messagingManager.getAllStatuses(userId);
|
|
145
|
+
socket.emit('messaging:status', statuses);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
socket.emit('messaging:error', { error: sanitizeError(err) });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── MCP ──
|
|
152
|
+
|
|
153
|
+
socket.on('mcp:status', () => {
|
|
154
|
+
socket.emit('mcp:status', mcpClient.getStatus());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
socket.on('mcp:tools', async (data) => {
|
|
158
|
+
try {
|
|
159
|
+
const tools = data?.serverId
|
|
160
|
+
? await mcpClient.listTools(data.serverId)
|
|
161
|
+
: mcpClient.getAllTools();
|
|
162
|
+
socket.emit('mcp:tools', tools);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
socket.emit('mcp:error', { error: sanitizeError(err) });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── Memory ──
|
|
169
|
+
|
|
170
|
+
socket.on('memory:read', () => {
|
|
171
|
+
socket.emit('memory:data', {
|
|
172
|
+
memory: memoryManager.readMemory(),
|
|
173
|
+
soul: memoryManager.readSoul(),
|
|
174
|
+
dailyLogs: memoryManager.listDailyLogs(3)
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
socket.on('memory:search', (data) => {
|
|
179
|
+
const results = memoryManager.searchMemory(data.query);
|
|
180
|
+
socket.emit('memory:search_results', results);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── Disconnect ──
|
|
184
|
+
|
|
185
|
+
socket.on('disconnect', () => {
|
|
186
|
+
console.log(`[WS] User ${userId} disconnected (${socket.id})`);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { setupWebSocket };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities — shared helpers for input validation and output sanitization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
6
|
+
const PROJECT_ROOT = require('path').join(__dirname, '../..');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strip internal filesystem paths and module stack frames from an error message
|
|
10
|
+
* before sending it to a client. Prevents leaking absolute paths, internal
|
|
11
|
+
* directory structure, or dependency internals in API responses.
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeError(err) {
|
|
14
|
+
if (!err) return 'An unexpected error occurred';
|
|
15
|
+
const raw = typeof err === 'string' ? err : err.message || String(err);
|
|
16
|
+
|
|
17
|
+
let msg = raw;
|
|
18
|
+
|
|
19
|
+
// Replace home directory path with ~
|
|
20
|
+
if (HOME) {
|
|
21
|
+
msg = msg.split(HOME).join('~');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Replace project root path with [app]
|
|
25
|
+
if (PROJECT_ROOT) {
|
|
26
|
+
msg = msg.split(PROJECT_ROOT).join('[app]');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Strip node_modules paths
|
|
30
|
+
msg = msg.replace(/[^\s'"]+node_modules[^\s'"]+/g, '[module]');
|
|
31
|
+
|
|
32
|
+
// Strip remaining absolute Unix paths (leave relative paths intact)
|
|
33
|
+
msg = msg.replace(/\/[a-zA-Z0-9._-]+(?:\/[a-zA-Z0-9._-]+){2,}/g, '[path]');
|
|
34
|
+
|
|
35
|
+
// Strip Windows absolute paths
|
|
36
|
+
msg = msg.replace(/[A-Za-z]:\\[^\s'"]+/g, '[path]');
|
|
37
|
+
|
|
38
|
+
return msg.trim() || 'An unexpected error occurred';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate that a value is a plain string within an allowed length range.
|
|
43
|
+
*/
|
|
44
|
+
function validateString(value, { maxLength = 50000, name = 'value' } = {}) {
|
|
45
|
+
if (typeof value !== 'string') throw new Error(`${name} must be a string`);
|
|
46
|
+
if (value.length === 0) throw new Error(`${name} must not be empty`);
|
|
47
|
+
if (value.length > maxLength) throw new Error(`${name} exceeds maximum length of ${maxLength} characters`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if the string looks like it contains a prompt injection attempt.
|
|
53
|
+
* This is a heuristic for logging/alerting — NOT a hard block (context window still applies).
|
|
54
|
+
*/
|
|
55
|
+
function detectPromptInjection(text) {
|
|
56
|
+
if (typeof text !== 'string') return false;
|
|
57
|
+
const patterns = [
|
|
58
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
59
|
+
/you\s+are\s+now\s+(DAN|GPT|jailbreak)/i,
|
|
60
|
+
/system\s+prompt\s*(override|bypass|end)/i,
|
|
61
|
+
/\[SYSTEM\]/i,
|
|
62
|
+
/###\s*(SYSTEM|OVERRIDE|NEW INSTRUCTIONS)/i,
|
|
63
|
+
/<\/?system>/i,
|
|
64
|
+
/reveal\s+(your\s+)?(system\s+)?prompt/i,
|
|
65
|
+
/act\s+as\s+if\s+you\s+have\s+no\s+(rules|restrictions|guidelines)/i,
|
|
66
|
+
/forget\s+(all\s+)?(previous|prior)\s+(instructions|context|training)/i,
|
|
67
|
+
];
|
|
68
|
+
return patterns.some(p => p.test(text));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { sanitizeError, validateString, detectPromptInjection };
|