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,260 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
|
|
5
|
+
const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js');
|
|
6
|
+
|
|
7
|
+
class DBAuthProvider {
|
|
8
|
+
constructor(serverId, clientId, authServerUrl) {
|
|
9
|
+
this.serverId = serverId;
|
|
10
|
+
this.clientId = clientId;
|
|
11
|
+
this.authServerUrl = authServerUrl;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get redirectUrl() {
|
|
15
|
+
const baseUrl = process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 8000}`;
|
|
16
|
+
return `${baseUrl}/api/mcp/oauth/callback`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get clientMetadata() {
|
|
20
|
+
return { client_id: this.clientId };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
state() {
|
|
24
|
+
return `${this.serverId}::${crypto.randomBytes(16).toString('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
clientInformation() {
|
|
28
|
+
return { client_id: this.clientId };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_getConfig() {
|
|
32
|
+
const row = db.prepare('SELECT config FROM mcp_servers WHERE id = ?').get(this.serverId);
|
|
33
|
+
return row ? JSON.parse(row.config || '{}') : {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_saveConfig(config) {
|
|
37
|
+
db.prepare('UPDATE mcp_servers SET config = ? WHERE id = ?').run(JSON.stringify(config), this.serverId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
tokens() {
|
|
41
|
+
return this._getConfig().auth?.tokens;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
saveTokens(tokens) {
|
|
45
|
+
const config = this._getConfig();
|
|
46
|
+
config.auth = config.auth || {};
|
|
47
|
+
config.auth.tokens = tokens;
|
|
48
|
+
this._saveConfig(config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
redirectToAuthorization(authorizationUrl) {
|
|
52
|
+
// Throw error so the API route catches it and returns the URL to the frontend
|
|
53
|
+
throw new Error(`OAUTH_REDIRECT:${authorizationUrl.toString()}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
saveCodeVerifier(codeVerifier) {
|
|
57
|
+
const config = this._getConfig();
|
|
58
|
+
config.auth = config.auth || {};
|
|
59
|
+
config.auth.codeVerifier = codeVerifier;
|
|
60
|
+
this._saveConfig(config);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
codeVerifier() {
|
|
64
|
+
return this._getConfig().auth?.codeVerifier;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class MCPClient extends EventEmitter {
|
|
69
|
+
constructor() {
|
|
70
|
+
super();
|
|
71
|
+
this.servers = new Map();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async startServer(serverId, url, name = '') {
|
|
75
|
+
if (this.servers.has(serverId)) {
|
|
76
|
+
await this.stopServer(serverId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const slug = (name || String(serverId)).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
80
|
+
try {
|
|
81
|
+
const serverRow = db.prepare('SELECT config FROM mcp_servers WHERE id = ?').get(serverId);
|
|
82
|
+
let configObj = {};
|
|
83
|
+
let authObj = {};
|
|
84
|
+
if (serverRow) {
|
|
85
|
+
configObj = JSON.parse(serverRow.config || '{}');
|
|
86
|
+
authObj = configObj.auth || {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const transportOpts = {
|
|
90
|
+
requestInit: { headers: {} },
|
|
91
|
+
eventSourceInit: { headers: {} }
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (authObj.type === 'bearer' && authObj.token) {
|
|
95
|
+
const h = `Bearer ${authObj.token}`;
|
|
96
|
+
transportOpts.requestInit.headers['Authorization'] = h;
|
|
97
|
+
// Native EventSource doesn't support headers well in browsers, but Node.js EventSource / sse.js might
|
|
98
|
+
transportOpts.eventSourceInit.headers['Authorization'] = h;
|
|
99
|
+
} else if (authObj.type === 'oauth') {
|
|
100
|
+
transportOpts.authProvider = new DBAuthProvider(serverId, authObj.clientId, authObj.authServerUrl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const transport = new SSEClientTransport(new URL(url), transportOpts);
|
|
104
|
+
const client = new Client(
|
|
105
|
+
{ name: 'NeoAgent', version: '1.0.0' },
|
|
106
|
+
{ capabilities: { tools: {} } }
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const serverObj = {
|
|
110
|
+
id: serverId,
|
|
111
|
+
url,
|
|
112
|
+
slug,
|
|
113
|
+
name: name || String(serverId),
|
|
114
|
+
command: url,
|
|
115
|
+
client,
|
|
116
|
+
transport,
|
|
117
|
+
tools: [],
|
|
118
|
+
status: 'starting'
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.servers.set(serverId, serverObj);
|
|
122
|
+
|
|
123
|
+
await client.connect(transport);
|
|
124
|
+
|
|
125
|
+
const server = this.servers.get(serverId);
|
|
126
|
+
if (server) {
|
|
127
|
+
server.status = 'running';
|
|
128
|
+
this.emit('server_status', { serverId, status: 'running' });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { status: 'running' };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
const server = this.servers.get(serverId);
|
|
134
|
+
if (server) {
|
|
135
|
+
server.status = 'error';
|
|
136
|
+
this.emit('server_status', { serverId, status: 'error', error: err.message });
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async finishOAuth(serverId, code) {
|
|
143
|
+
const server = this.servers.get(serverId);
|
|
144
|
+
if (!server || !server.transport) {
|
|
145
|
+
throw new Error(`Server ${serverId} transport not initialized`);
|
|
146
|
+
}
|
|
147
|
+
await server.transport.finishAuth(code);
|
|
148
|
+
await server.client.connect(server.transport).catch(() => { }); // Reconnect using tokens
|
|
149
|
+
|
|
150
|
+
server.status = 'running';
|
|
151
|
+
this.emit('server_status', { serverId, status: 'running' });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async stopServer(serverId) {
|
|
155
|
+
const server = this.servers.get(serverId);
|
|
156
|
+
if (!server) return;
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
if (server.client) await server.client.close();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(`Error closing MCP client ${serverId}:`, err);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.servers.delete(serverId);
|
|
165
|
+
this.emit('server_status', { serverId, status: 'stopped' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async listTools(serverId) {
|
|
169
|
+
const server = this.servers.get(serverId);
|
|
170
|
+
if (!server || server.status !== 'running') {
|
|
171
|
+
throw new Error(`Server ${serverId} not running`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const response = await server.client.listTools();
|
|
175
|
+
server.tools = response.tools || [];
|
|
176
|
+
return server.tools;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async callTool(serverId, toolName, args = {}) {
|
|
180
|
+
const server = this.servers.get(serverId);
|
|
181
|
+
if (!server || server.status !== 'running') {
|
|
182
|
+
throw new Error(`Server ${serverId} not running`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return await server.client.callTool({
|
|
186
|
+
name: toolName,
|
|
187
|
+
arguments: args
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async callToolByName(fullName, args = {}) {
|
|
192
|
+
for (const [serverId, server] of this.servers) {
|
|
193
|
+
const prefix = `mcp_${server.slug}_`;
|
|
194
|
+
if (fullName.startsWith(prefix)) {
|
|
195
|
+
const originalName = fullName.substring(prefix.length);
|
|
196
|
+
return await this.callTool(serverId, originalName, args);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
getAllTools() {
|
|
203
|
+
const allTools = [];
|
|
204
|
+
for (const [serverId, server] of this.servers) {
|
|
205
|
+
if (server.status !== 'running') continue;
|
|
206
|
+
for (const tool of server.tools) {
|
|
207
|
+
allTools.push({
|
|
208
|
+
...tool,
|
|
209
|
+
name: `mcp_${server.slug}_${tool.name}`,
|
|
210
|
+
originalName: tool.name,
|
|
211
|
+
parameters: tool.inputSchema || tool.parameters,
|
|
212
|
+
serverId
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return allTools;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getStatus() {
|
|
220
|
+
const statuses = {};
|
|
221
|
+
for (const [serverId, server] of this.servers) {
|
|
222
|
+
statuses[serverId] = {
|
|
223
|
+
status: server.status,
|
|
224
|
+
command: server.url,
|
|
225
|
+
args: [],
|
|
226
|
+
toolCount: server.tools.length,
|
|
227
|
+
serverInfo: null
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return statuses;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async loadFromDB(userId) {
|
|
234
|
+
const servers = db.prepare('SELECT * FROM mcp_servers WHERE user_id = ? AND enabled = 1').all(userId);
|
|
235
|
+
const results = [];
|
|
236
|
+
|
|
237
|
+
for (const srv of servers) {
|
|
238
|
+
try {
|
|
239
|
+
await this.startServer(srv.id, srv.command, srv.name);
|
|
240
|
+
await this.listTools(srv.id);
|
|
241
|
+
results.push({ id: srv.id, name: srv.name, status: 'running' });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error(`Failed to start MCP server ${srv.name}:`, err.message);
|
|
244
|
+
results.push({ id: srv.id, name: srv.name, status: 'error', error: err.message });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async shutdown() {
|
|
252
|
+
const promises = [];
|
|
253
|
+
for (const serverId of this.servers.keys()) {
|
|
254
|
+
promises.push(this.stopServer(serverId));
|
|
255
|
+
}
|
|
256
|
+
await Promise.allSettled(promises);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = { MCPClient };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Embedding helpers for the semantic memory system.
|
|
5
|
+
* Uses OpenAI text-embedding-3-small (1536 dims) when available.
|
|
6
|
+
* Gracefully degrades to keyword search if OPENAI_API_KEY is missing.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const https = require('https');
|
|
10
|
+
|
|
11
|
+
const EMBEDDING_MODEL = 'text-embedding-3-small';
|
|
12
|
+
const EMBED_DIM = 1536;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get an embedding vector for a piece of text.
|
|
16
|
+
* Returns a Float32Array of length EMBED_DIM, or null if unavailable.
|
|
17
|
+
*/
|
|
18
|
+
async function getEmbedding(text) {
|
|
19
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
20
|
+
if (!apiKey) return null;
|
|
21
|
+
if (!text || !text.trim()) return null;
|
|
22
|
+
|
|
23
|
+
// Truncate very long text to stay within token limits (~8k tokens)
|
|
24
|
+
const truncated = text.slice(0, 25000);
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const body = JSON.stringify({
|
|
28
|
+
model: EMBEDDING_MODEL,
|
|
29
|
+
input: truncated,
|
|
30
|
+
encoding_format: 'float'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const options = {
|
|
34
|
+
hostname: 'api.openai.com',
|
|
35
|
+
path: '/v1/embeddings',
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'Content-Length': Buffer.byteLength(body)
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const req = https.request(options, (res) => {
|
|
45
|
+
let data = '';
|
|
46
|
+
res.on('data', chunk => { data += chunk; });
|
|
47
|
+
res.on('end', () => {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(data);
|
|
50
|
+
if (parsed.error) return resolve(null);
|
|
51
|
+
const vec = parsed.data?.[0]?.embedding;
|
|
52
|
+
if (!vec) return resolve(null);
|
|
53
|
+
resolve(new Float32Array(vec));
|
|
54
|
+
} catch {
|
|
55
|
+
resolve(null);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
req.on('error', () => resolve(null));
|
|
61
|
+
req.setTimeout(15000, () => { req.destroy(); resolve(null); });
|
|
62
|
+
req.write(body);
|
|
63
|
+
req.end();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Cosine similarity between two Float32Arrays.
|
|
69
|
+
* Returns a value in [-1, 1]; higher = more similar.
|
|
70
|
+
*/
|
|
71
|
+
function cosineSimilarity(a, b) {
|
|
72
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
73
|
+
let dot = 0, magA = 0, magB = 0;
|
|
74
|
+
for (let i = 0; i < a.length; i++) {
|
|
75
|
+
dot += a[i] * b[i];
|
|
76
|
+
magA += a[i] * a[i];
|
|
77
|
+
magB += b[i] * b[i];
|
|
78
|
+
}
|
|
79
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
80
|
+
return denom === 0 ? 0 : dot / denom;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Serialize a Float32Array to a JSON string for SQLite TEXT storage.
|
|
85
|
+
*/
|
|
86
|
+
function serializeEmbedding(vec) {
|
|
87
|
+
if (!vec) return null;
|
|
88
|
+
return JSON.stringify(Array.from(vec));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Deserialize a JSON string back to a Float32Array.
|
|
93
|
+
*/
|
|
94
|
+
function deserializeEmbedding(str) {
|
|
95
|
+
if (!str) return null;
|
|
96
|
+
try {
|
|
97
|
+
const arr = JSON.parse(str);
|
|
98
|
+
return new Float32Array(arr);
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Keyword-based fallback similarity when embeddings are unavailable.
|
|
106
|
+
* Returns 0–1 based on term overlap.
|
|
107
|
+
*/
|
|
108
|
+
function keywordSimilarity(query, text) {
|
|
109
|
+
if (!query || !text) return 0;
|
|
110
|
+
const tokens = (s) => s.toLowerCase().split(/\W+/).filter(t => t.length > 2);
|
|
111
|
+
const qTokens = new Set(tokens(query));
|
|
112
|
+
const tTokens = tokens(text);
|
|
113
|
+
if (!qTokens.size || !tTokens.length) return 0;
|
|
114
|
+
let hits = 0;
|
|
115
|
+
for (const t of tTokens) { if (qTokens.has(t)) hits++; }
|
|
116
|
+
return hits / Math.max(qTokens.size, tTokens.length);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
getEmbedding,
|
|
121
|
+
cosineSimilarity,
|
|
122
|
+
serializeEmbedding,
|
|
123
|
+
deserializeEmbedding,
|
|
124
|
+
keywordSimilarity,
|
|
125
|
+
EMBED_DIM
|
|
126
|
+
};
|