neoagent 1.6.0 → 2.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 (62) hide show
  1. package/README.md +18 -4
  2. package/docs/configuration.md +2 -2
  3. package/docs/skills.md +1 -1
  4. package/lib/manager.js +64 -2
  5. package/package.json +9 -2
  6. package/server/config/origins.js +34 -0
  7. package/server/db/database.js +0 -13
  8. package/server/http/errors.js +17 -0
  9. package/server/http/middleware.js +81 -0
  10. package/server/http/routes.js +45 -0
  11. package/server/http/socket.js +23 -0
  12. package/server/http/static.js +50 -0
  13. package/server/index.js +50 -188
  14. package/server/public/.last_build_id +1 -0
  15. package/server/public/assets/AssetManifest.bin +1 -0
  16. package/server/public/assets/AssetManifest.bin.json +1 -0
  17. package/server/public/assets/AssetManifest.json +1 -0
  18. package/server/public/assets/FontManifest.json +1 -0
  19. package/server/public/assets/NOTICES +33454 -0
  20. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  21. package/server/public/assets/packages/cupertino_icons/assets/CupertinoIcons.ttf +0 -0
  22. package/server/public/assets/shaders/ink_sparkle.frag +126 -0
  23. package/server/public/assets/web/icons/Icon-192.png +0 -0
  24. package/server/public/canvaskit/canvaskit.js +192 -0
  25. package/server/public/canvaskit/canvaskit.js.symbols +12142 -0
  26. package/server/public/canvaskit/canvaskit.wasm +0 -0
  27. package/server/public/canvaskit/chromium/canvaskit.js +192 -0
  28. package/server/public/canvaskit/chromium/canvaskit.js.symbols +11106 -0
  29. package/server/public/canvaskit/chromium/canvaskit.wasm +0 -0
  30. package/server/public/canvaskit/skwasm.js +140 -0
  31. package/server/public/canvaskit/skwasm.js.symbols +12164 -0
  32. package/server/public/canvaskit/skwasm.wasm +0 -0
  33. package/server/public/canvaskit/skwasm_heavy.js +140 -0
  34. package/server/public/canvaskit/skwasm_heavy.js.symbols +13766 -0
  35. package/server/public/canvaskit/skwasm_heavy.wasm +0 -0
  36. package/server/public/favicon.png +0 -0
  37. package/server/public/flutter.js +32 -0
  38. package/server/public/flutter_bootstrap.js +43 -0
  39. package/server/public/flutter_service_worker.js +208 -0
  40. package/server/public/icons/Icon-192.png +0 -0
  41. package/server/public/icons/Icon-512.png +0 -0
  42. package/server/public/icons/Icon-maskable-192.png +0 -0
  43. package/server/public/icons/Icon-maskable-512.png +0 -0
  44. package/server/public/index.html +38 -0
  45. package/server/public/main.dart.js +103124 -0
  46. package/server/public/manifest.json +35 -0
  47. package/server/public/version.json +1 -0
  48. package/server/services/ai/models.js +2 -8
  49. package/server/services/ai/tools.js +0 -47
  50. package/server/services/browser/controller.js +34 -0
  51. package/server/services/manager.js +49 -118
  52. package/server/services/messaging/automation.js +210 -0
  53. package/server/utils/version.js +37 -0
  54. package/server/public/app.html +0 -682
  55. package/server/public/assets/world-office-dark.png +0 -0
  56. package/server/public/assets/world-office-light.png +0 -0
  57. package/server/public/css/app.css +0 -941
  58. package/server/public/css/styles.css +0 -963
  59. package/server/public/favicon.svg +0 -17
  60. package/server/public/js/app.js +0 -4105
  61. package/server/public/login.html +0 -313
  62. package/server/routes/protocols.js +0 -87
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "NeoAgent",
3
+ "short_name": "NeoAgent",
4
+ "start_url": ".",
5
+ "display": "standalone",
6
+ "background_color": "#0175C2",
7
+ "theme_color": "#0175C2",
8
+ "description": "NeoAgent Flutter client for web and Android.",
9
+ "orientation": "portrait-primary",
10
+ "prefer_related_applications": false,
11
+ "icons": [
12
+ {
13
+ "src": "icons/Icon-192.png",
14
+ "sizes": "192x192",
15
+ "type": "image/png"
16
+ },
17
+ {
18
+ "src": "icons/Icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png"
21
+ },
22
+ {
23
+ "src": "icons/Icon-maskable-192.png",
24
+ "sizes": "192x192",
25
+ "type": "image/png",
26
+ "purpose": "maskable"
27
+ },
28
+ {
29
+ "src": "icons/Icon-maskable-512.png",
30
+ "sizes": "512x512",
31
+ "type": "image/png",
32
+ "purpose": "maskable"
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1 @@
1
+ {"app_name":"neoagent_flutter","version":"1.0.0","build_number":"1","package_name":"neoagent_flutter"}
@@ -29,16 +29,10 @@ const STATIC_MODELS = [
29
29
  purpose: 'general'
30
30
  },
31
31
  {
32
- id: 'qwen2.5-coder:7b-instruct-q4_K_M',
33
- label: 'Qwen 2.5 Coder 7B Instruct (Local / General)',
32
+ id: 'qwen3.5:4b',
33
+ label: 'Qwen 3.5 4B (Local / Ollama)',
34
34
  provider: 'ollama',
35
35
  purpose: 'general'
36
- },
37
- {
38
- id: 'qwen2.5-coder:7b',
39
- label: 'Qwen 2.5 Coder 7B (Local / Coding)',
40
- provider: 'ollama',
41
- purpose: 'coding'
42
36
  }
43
37
  ];
44
38
 
@@ -157,20 +157,6 @@ function getAvailableTools(app, options = {}) {
157
157
  required: ['query']
158
158
  }
159
159
  },
160
- {
161
- name: 'manage_protocols',
162
- description: 'Read, list, create, update, or delete text-based protocols (a pre-set list of instructions/actions). If user asks to execute a protocol, you should read it and follow its instructions.',
163
- parameters: {
164
- type: 'object',
165
- properties: {
166
- action: { type: 'string', enum: ['list', 'read', 'create', 'update', 'delete'], description: 'The protocol action to perform.' },
167
- name: { type: 'string', description: 'Name of the protocol (required for read, create, update, delete)' },
168
- description: { type: 'string', description: 'Description of the protocol (optional for create/update)' },
169
- content: { type: 'string', description: 'Text content/instructions of the protocol (required for create/update)' }
170
- },
171
- required: ['action']
172
- }
173
- },
174
160
  {
175
161
  name: 'memory_save',
176
162
  description: 'Save ONE specific, self-contained fact to long-term semantic memory. RULES: (1) One discrete fact per call — if you have 10 facts, call this 10 times. (2) The ENTIRE value must be IN the content string itself — never write a pointer/reference like "user shared a profile" or "see chat history for details". That is useless. (3) Content must be a complete statement a stranger could read cold and understand. GOOD: "Neo lives in Braunschweig, Germany" / "Neo prefers dark mode" / "Neo\'s project WorldEndArchive crawls and compresses websites to offline JSON archives". BAD: "User pasted a profile dump" / "Neo shared lots of details — see chat history" / "Neo gave a big list of projects".',
@@ -747,39 +733,6 @@ async function executeTool(toolName, args, context, engine) {
747
733
  }
748
734
  }
749
735
 
750
- case 'manage_protocols': {
751
- try {
752
- if (args.action === 'list') {
753
- const list = db.prepare('SELECT name, description, updated_at FROM protocols WHERE user_id = ?').all(userId);
754
- return { protocols: list };
755
- } else if (args.action === 'read') {
756
- if (!args.name) return { error: "name is required" };
757
- const p = db.prepare('SELECT * FROM protocols WHERE name = ? AND user_id = ?').get(args.name, userId);
758
- return p ? { name: p.name, description: p.description, content: p.content } : { error: `Protocol '${args.name}' not found` };
759
- } else if (args.action === 'create') {
760
- if (!args.name || !args.content) return { error: "name and content are required" };
761
- db.prepare('INSERT INTO protocols (user_id, name, description, content) VALUES (?, ?, ?, ?)').run(userId, args.name, args.description || '', args.content);
762
- return { success: true, message: `Protocol '${args.name}' created.` };
763
- } else if (args.action === 'update') {
764
- if (!args.name || !args.content) return { error: "name and content are required" };
765
- const p = db.prepare('SELECT id FROM protocols WHERE name = ? AND user_id = ?').get(args.name, userId);
766
- if (!p) return { error: `Protocol '${args.name}' not found` };
767
- db.prepare("UPDATE protocols SET description = ?, content = ?, updated_at = datetime('now') WHERE id = ?").run(args.description || '', args.content, p.id);
768
- return { success: true, message: `Protocol '${args.name}' updated.` };
769
- } else if (args.action === 'delete') {
770
- if (!args.name) return { error: "name is required" };
771
- const p = db.prepare('SELECT id FROM protocols WHERE name = ? AND user_id = ?').get(args.name, userId);
772
- if (!p) return { error: `Protocol '${args.name}' not found` };
773
- db.prepare('DELETE FROM protocols WHERE id = ?').run(p.id);
774
- return { success: true, message: `Protocol '${args.name}' deleted.` };
775
- }
776
- return { error: 'Invalid action' };
777
- } catch (err) {
778
- if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') return { error: 'Protocol with this name already exists' };
779
- return { error: err.message };
780
- }
781
- }
782
-
783
736
  case 'memory_save': {
784
737
  const { MemoryManager } = require('../memory/manager');
785
738
  const mm = new MemoryManager();
@@ -20,6 +20,39 @@ const VIEWPORTS = [
20
20
  { width: 1920, height: 1080 },
21
21
  ];
22
22
 
23
+ function resolveBrowserExecutablePath() {
24
+ const explicitPath =
25
+ process.env.PUPPETEER_EXECUTABLE_PATH ||
26
+ process.env.CHROME_BIN ||
27
+ process.env.CHROMIUM_BIN;
28
+
29
+ if (explicitPath && fs.existsSync(explicitPath)) return explicitPath;
30
+
31
+ const platformCandidates = process.platform === 'darwin'
32
+ ? [
33
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
34
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
35
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
36
+ ]
37
+ : process.platform === 'win32'
38
+ ? [
39
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
40
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
41
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
42
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
43
+ ]
44
+ : [
45
+ '/usr/bin/google-chrome',
46
+ '/usr/bin/google-chrome-stable',
47
+ '/usr/bin/chromium',
48
+ '/usr/bin/chromium-browser',
49
+ '/snap/bin/chromium',
50
+ '/usr/bin/microsoft-edge',
51
+ ];
52
+
53
+ return platformCandidates.find((candidate) => fs.existsSync(candidate)) || null;
54
+ }
55
+
23
56
  function rand(min, max) {
24
57
  return Math.floor(Math.random() * (max - min + 1)) + min;
25
58
  }
@@ -127,6 +160,7 @@ class BrowserController {
127
160
 
128
161
  this.browser = await puppeteer.launch({
129
162
  headless: this.headless ? 'new' : false,
163
+ executablePath: resolveBrowserExecutablePath() || undefined,
130
164
  args: [
131
165
  '--no-sandbox',
132
166
  '--disable-setuid-sandbox',
@@ -11,9 +11,7 @@ const { SkillRunner } = require('./ai/toolRunner');
11
11
  const { MessagingManager } = require('./messaging/manager');
12
12
  const { Scheduler } = require('./scheduler/cron');
13
13
  const { setupWebSocket } = require('./websocket');
14
- const { detectPromptInjection } = require('../utils/security');
15
- const { normalizeWhatsAppId } = require('../utils/whatsapp');
16
- const { randomUUID } = require('crypto');
14
+ const { registerMessagingAutomation } = require('./messaging/automation');
17
15
 
18
16
  async function startServices(app, io) {
19
17
  try {
@@ -62,120 +60,11 @@ async function startServices(app, io) {
62
60
  mcpClient.loadFromDB(u.id).catch(err => console.error('[MCP] Auto-start error:', err.message));
63
61
  }
64
62
 
65
- const userQueues = {};
66
- app.locals.userQueues = userQueues;
67
-
68
- async function processMessage(userId, msg) {
69
- if (!userQueues[userId]) userQueues[userId] = { running: false, pending: [] };
70
- const q = userQueues[userId];
71
-
72
- if (q.running) {
73
- const last = q.pending[q.pending.length - 1];
74
- if (last && last.platform === msg.platform && last.chatId === msg.chatId) {
75
- last.content += '\n' + msg.content;
76
- last.messageId = msg.messageId;
77
- } else {
78
- q.pending.push({ ...msg });
79
- }
80
- return;
81
- }
82
-
83
- q.running = true;
84
- try {
85
- await messagingManager.markRead(userId, msg.platform, msg.chatId, msg.messageId).catch(() => { });
86
- await messagingManager.sendTyping(userId, msg.platform, msg.chatId, true).catch(() => { });
87
-
88
- const mediaNote = msg.localMediaPath
89
- ? `\nMedia attached at: ${msg.localMediaPath} (type: ${msg.mediaType}). You can reference or forward it with send_message media_path.`
90
- : '';
91
-
92
- if (detectPromptInjection(msg.content)) {
93
- console.warn(`[Security] Possible prompt injection attempt from ${msg.sender} on ${msg.platform}: ${msg.content.slice(0, 200)}`);
94
- }
95
-
96
- const isVoiceCall = msg.platform === 'telnyx' && msg.mediaType === 'voice';
97
- const isVoiceNote = !isVoiceCall && msg.mediaType === 'audio';
98
- const isDiscordGuild = msg.platform === 'discord' && msg.isGroup;
99
-
100
- const discordContext = (isDiscordGuild && Array.isArray(msg.channelContext) && msg.channelContext.length)
101
- ? '\n\nRecent channel context (oldest → newest):\n' +
102
- msg.channelContext.map(m => `[${m.author}]: ${m.content}`).join('\n')
103
- : '';
104
-
105
- const sttNote = isVoiceNote
106
- ? '\n[Note: This message was sent as a voice note and transcribed via speech-to-text. The transcription may not be perfectly accurate.]'
107
- : '';
108
-
109
- const prompt = isVoiceCall
110
- ? `You are on a live phone call. The caller (${msg.senderName || msg.sender}) said:\n<caller_speech>\n${msg.content}\n</caller_speech>\n\nRespond via send_message with platform="telnyx" and to="${msg.chatId}".`
111
- : `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}".`;
112
-
113
- let convRow = db.prepare(
114
- 'SELECT id FROM conversations WHERE user_id = ? AND platform = ? AND platform_chat_id = ?'
115
- ).get(userId, msg.platform, msg.chatId);
116
-
117
- if (!convRow) {
118
- const convId = randomUUID();
119
- db.prepare(
120
- 'INSERT INTO conversations (id, user_id, platform, platform_chat_id, title) VALUES (?, ?, ?, ?, ?)'
121
- ).run(convId, userId, msg.platform, msg.chatId, `${msg.platform} — ${msg.senderName || msg.sender || msg.chatId}`);
122
- convRow = { id: convId };
123
- }
124
-
125
- const runOpts = { triggerSource: 'messaging', conversationId: convRow.id, source: msg.platform, chatId: msg.chatId, context: { rawUserMessage: msg.content } };
126
- if (msg.localMediaPath) runOpts.mediaAttachments = [{ path: msg.localMediaPath, type: msg.mediaType }];
127
-
128
- await agentEngine.run(userId, prompt, runOpts);
129
- } finally {
130
- await messagingManager.sendTyping(userId, msg.platform, msg.chatId, false).catch(() => { });
131
- q.running = false;
132
- if (q.pending.length > 0) {
133
- const next = q.pending.shift();
134
- processMessage(userId, next);
135
- }
136
- }
137
- }
138
-
139
- messagingManager.registerHandler(async (userId, msg) => {
140
- if (msg.platform !== 'discord' && msg.platform !== 'telegram') {
141
- const whitelistRow = db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
142
- .get(userId, `platform_whitelist_${msg.platform}`);
143
- const normalize = msg.platform === 'whatsapp'
144
- ? normalizeWhatsAppId
145
- : (id) => String(id || '').replace(/[^0-9+]/g, '');
146
-
147
- let whitelist = [];
148
- if (whitelistRow) {
149
- try {
150
- const parsed = JSON.parse(whitelistRow.value);
151
- if (Array.isArray(parsed)) whitelist = parsed;
152
- } catch { }
153
- }
154
-
155
- const enforceEmptyWhitelist = msg.platform === 'whatsapp';
156
- const shouldCheckWhitelist = whitelist.length > 0 || enforceEmptyWhitelist;
157
-
158
- if (shouldCheckWhitelist) {
159
- const senderNorm = normalize(msg.sender || msg.chatId);
160
- const allowed = whitelist.some((n) => normalize(n) === senderNorm);
161
- if (!allowed) {
162
- console.log(`[Messaging] Blocked ${msg.platform} message from ${msg.sender} (not in whitelist)`);
163
- io.to(`user:${userId}`).emit('messaging:blocked_sender', {
164
- platform: msg.platform,
165
- sender: msg.sender,
166
- chatId: msg.chatId,
167
- senderName: msg.senderName || null
168
- });
169
- return;
170
- }
171
- }
172
- }
173
-
174
- const upsertSetting = db.prepare('INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)');
175
- upsertSetting.run(userId, 'last_platform', msg.platform);
176
- upsertSetting.run(userId, 'last_chat_id', msg.chatId);
177
-
178
- await processMessage(userId, msg);
63
+ registerMessagingAutomation({
64
+ app,
65
+ io,
66
+ messagingManager,
67
+ agentEngine,
179
68
  });
180
69
 
181
70
  const scheduler = new Scheduler(io, agentEngine, app);
@@ -199,4 +88,46 @@ async function startServices(app, io) {
199
88
  }
200
89
  }
201
90
 
202
- module.exports = { startServices };
91
+ async function stopServices(app) {
92
+ const tasks = [];
93
+
94
+ if (app.locals.scheduler) {
95
+ try {
96
+ app.locals.scheduler.stop();
97
+ } catch (err) {
98
+ console.error('[Scheduler] Stop error:', err.message);
99
+ }
100
+ }
101
+
102
+ if (app.locals.mcpClient) {
103
+ tasks.push(
104
+ app.locals.mcpClient.shutdown().catch((err) => {
105
+ console.error('[MCP] Shutdown error:', err.message);
106
+ }),
107
+ );
108
+ }
109
+
110
+ if (app.locals.browserController) {
111
+ tasks.push(
112
+ app.locals.browserController.closeBrowser().catch((err) => {
113
+ console.error('[Browser] Shutdown error:', err.message);
114
+ }),
115
+ );
116
+ }
117
+
118
+ if (app.locals.messagingManager?.platforms instanceof Map) {
119
+ for (const platform of app.locals.messagingManager.platforms.values()) {
120
+ if (typeof platform.disconnect === 'function') {
121
+ tasks.push(
122
+ platform.disconnect().catch((err) => {
123
+ console.error('[Messaging] Disconnect error:', err.message);
124
+ }),
125
+ );
126
+ }
127
+ }
128
+ }
129
+
130
+ await Promise.allSettled(tasks);
131
+ }
132
+
133
+ module.exports = { startServices, stopServices };
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const db = require('../../db/database');
4
+ const { detectPromptInjection } = require('../../utils/security');
5
+ const { normalizeWhatsAppId } = require('../../utils/whatsapp');
6
+ const { randomUUID } = require('crypto');
7
+
8
+ function registerMessagingAutomation({ app, io, messagingManager, agentEngine }) {
9
+ const userQueues = {};
10
+ app.locals.userQueues = userQueues;
11
+
12
+ messagingManager.registerHandler(async (userId, msg) => {
13
+ if (!(await isAllowedMessagingSender({ io, userId, msg }))) {
14
+ return;
15
+ }
16
+
17
+ const upsertSetting = db.prepare(
18
+ 'INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)'
19
+ );
20
+ upsertSetting.run(userId, 'last_platform', msg.platform);
21
+ upsertSetting.run(userId, 'last_chat_id', msg.chatId);
22
+
23
+ await processQueuedMessage({
24
+ userQueues,
25
+ messagingManager,
26
+ agentEngine,
27
+ userId,
28
+ msg
29
+ });
30
+ });
31
+ }
32
+
33
+ async function processQueuedMessage({
34
+ userQueues,
35
+ messagingManager,
36
+ agentEngine,
37
+ userId,
38
+ msg
39
+ }) {
40
+ if (!userQueues[userId]) {
41
+ userQueues[userId] = { running: false, pending: [] };
42
+ }
43
+ const queue = userQueues[userId];
44
+
45
+ if (queue.running) {
46
+ const last = queue.pending[queue.pending.length - 1];
47
+ if (last && last.platform === msg.platform && last.chatId === msg.chatId) {
48
+ last.content += `\n${msg.content}`;
49
+ last.messageId = msg.messageId;
50
+ } else {
51
+ queue.pending.push({ ...msg });
52
+ }
53
+ return;
54
+ }
55
+
56
+ queue.running = true;
57
+ try {
58
+ await messagingManager
59
+ .markRead(userId, msg.platform, msg.chatId, msg.messageId)
60
+ .catch(() => {});
61
+ await messagingManager
62
+ .sendTyping(userId, msg.platform, msg.chatId, true)
63
+ .catch(() => {});
64
+
65
+ const prompt = buildIncomingPrompt(msg);
66
+ const conversationId = ensureConversation(userId, msg);
67
+ const runOptions = {
68
+ triggerSource: 'messaging',
69
+ conversationId,
70
+ source: msg.platform,
71
+ chatId: msg.chatId,
72
+ context: { rawUserMessage: msg.content }
73
+ };
74
+
75
+ if (msg.localMediaPath) {
76
+ runOptions.mediaAttachments = [
77
+ { path: msg.localMediaPath, type: msg.mediaType }
78
+ ];
79
+ }
80
+
81
+ await agentEngine.run(userId, prompt, runOptions);
82
+ } finally {
83
+ await messagingManager
84
+ .sendTyping(userId, msg.platform, msg.chatId, false)
85
+ .catch(() => {});
86
+ queue.running = false;
87
+ if (queue.pending.length > 0) {
88
+ const next = queue.pending.shift();
89
+ await processQueuedMessage({
90
+ userQueues,
91
+ messagingManager,
92
+ agentEngine,
93
+ userId,
94
+ msg: next
95
+ });
96
+ }
97
+ }
98
+ }
99
+
100
+ function ensureConversation(userId, msg) {
101
+ let conversation = db
102
+ .prepare(
103
+ 'SELECT id FROM conversations WHERE user_id = ? AND platform = ? AND platform_chat_id = ?'
104
+ )
105
+ .get(userId, msg.platform, msg.chatId);
106
+
107
+ if (conversation) {
108
+ return conversation.id;
109
+ }
110
+
111
+ const conversationId = randomUUID();
112
+ db.prepare(
113
+ 'INSERT INTO conversations (id, user_id, platform, platform_chat_id, title) VALUES (?, ?, ?, ?, ?)'
114
+ ).run(
115
+ conversationId,
116
+ userId,
117
+ msg.platform,
118
+ msg.chatId,
119
+ `${msg.platform} — ${msg.senderName || msg.sender || msg.chatId}`
120
+ );
121
+
122
+ return conversationId;
123
+ }
124
+
125
+ function buildIncomingPrompt(msg) {
126
+ const mediaNote = msg.localMediaPath
127
+ ? `\nMedia attached at: ${msg.localMediaPath} (type: ${msg.mediaType}). You can reference or forward it with send_message media_path.`
128
+ : '';
129
+
130
+ if (detectPromptInjection(msg.content)) {
131
+ console.warn(
132
+ `[Security] Possible prompt injection attempt from ${msg.sender} on ${msg.platform}: ${msg.content.slice(0, 200)}`
133
+ );
134
+ }
135
+
136
+ const isVoiceCall = msg.platform === 'telnyx' && msg.mediaType === 'voice';
137
+ const isVoiceNote = !isVoiceCall && msg.mediaType === 'audio';
138
+ const isDiscordGuild = msg.platform === 'discord' && msg.isGroup;
139
+
140
+ const discordContext =
141
+ isDiscordGuild &&
142
+ Array.isArray(msg.channelContext) &&
143
+ msg.channelContext.length
144
+ ? '\n\nRecent channel context (oldest → newest):\n' +
145
+ msg.channelContext.map((item) => `[${item.author}]: ${item.content}`).join('\n')
146
+ : '';
147
+
148
+ const sttNote = isVoiceNote
149
+ ? '\n[Note: This message was sent as a voice note and transcribed via speech-to-text. The transcription may not be perfectly accurate.]'
150
+ : '';
151
+
152
+ if (isVoiceCall) {
153
+ return `You are on a live phone call. The caller (${msg.senderName || msg.sender}) said:\n<caller_speech>\n${msg.content}\n</caller_speech>\n\nRespond via send_message with platform="telnyx" and to="${msg.chatId}".`;
154
+ }
155
+
156
+ return `You received a ${msg.platform} message from ${msg.senderName || msg.sender} (chat: ${msg.chatId}):\n<external_message>\n${msg.content}\n</external_message>${mediaNote}${discordContext}${sttNote}\n\nReply via send_message with platform="${msg.platform}" and to="${msg.chatId}".`;
157
+ }
158
+
159
+ async function isAllowedMessagingSender({ io, userId, msg }) {
160
+ if (msg.platform === 'discord' || msg.platform === 'telegram') {
161
+ return true;
162
+ }
163
+
164
+ const whitelistRow = db
165
+ .prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?')
166
+ .get(userId, `platform_whitelist_${msg.platform}`);
167
+
168
+ const normalize =
169
+ msg.platform === 'whatsapp'
170
+ ? normalizeWhatsAppId
171
+ : (id) => String(id || '').replace(/[^0-9+]/g, '');
172
+
173
+ let whitelist = [];
174
+ if (whitelistRow) {
175
+ try {
176
+ const parsed = JSON.parse(whitelistRow.value);
177
+ if (Array.isArray(parsed)) whitelist = parsed;
178
+ } catch {
179
+ whitelist = [];
180
+ }
181
+ }
182
+
183
+ const enforceEmptyWhitelist = msg.platform === 'whatsapp';
184
+ const shouldCheckWhitelist = whitelist.length > 0 || enforceEmptyWhitelist;
185
+
186
+ if (!shouldCheckWhitelist) {
187
+ return true;
188
+ }
189
+
190
+ const senderNorm = normalize(msg.sender || msg.chatId);
191
+ const allowed = whitelist.some((entry) => normalize(entry) === senderNorm);
192
+ if (allowed) {
193
+ return true;
194
+ }
195
+
196
+ console.log(
197
+ `[Messaging] Blocked ${msg.platform} message from ${msg.sender} (not in whitelist)`
198
+ );
199
+ io.to(`user:${userId}`).emit('messaging:blocked_sender', {
200
+ platform: msg.platform,
201
+ sender: msg.sender,
202
+ chatId: msg.chatId,
203
+ senderName: msg.senderName || null
204
+ });
205
+ return false;
206
+ }
207
+
208
+ module.exports = {
209
+ registerMessagingAutomation
210
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const { APP_DIR } = require('../../runtime/paths');
5
+ const packageJson = require('../../package.json');
6
+
7
+ function getVersionInfo() {
8
+ let version = packageJson.version;
9
+ let gitSha = null;
10
+
11
+ try {
12
+ version =
13
+ execSync('git describe --tags --always --dirty', {
14
+ cwd: APP_DIR,
15
+ encoding: 'utf8',
16
+ stdio: ['ignore', 'pipe', 'ignore']
17
+ })
18
+ .trim()
19
+ .replace(/^v/, '') || packageJson.version;
20
+ gitSha = execSync('git rev-parse --short HEAD', {
21
+ cwd: APP_DIR,
22
+ encoding: 'utf8',
23
+ stdio: ['ignore', 'pipe', 'ignore']
24
+ }).trim();
25
+ } catch {
26
+ gitSha = process.env.GIT_SHA || null;
27
+ }
28
+
29
+ return {
30
+ name: packageJson.name,
31
+ version,
32
+ packageVersion: packageJson.version,
33
+ gitSha
34
+ };
35
+ }
36
+
37
+ module.exports = { getVersionInfo };