neoagent 2.4.1-beta.9 → 2.4.1

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 (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -6,6 +6,7 @@ const { sanitizeError } = require('../utils/security');
6
6
  const { validateRemoteMcpEndpoint } = require('../services/runtime/mcp');
7
7
  const { getAgentIdFromRequest, isMainAgent, resolveAgentId } = require('../services/agents/manager');
8
8
  const { resolvePublicBaseUrl } = require('../services/integrations/env');
9
+ const { consumeOAuthState } = require('../services/mcp/client_support');
9
10
 
10
11
  const MCP_OAUTH_STATE_RE = /^(\d+)::[a-f0-9]{32}$/;
11
12
 
@@ -45,8 +46,11 @@ router.get('/', (req, res) => {
45
46
  config: JSON.parse(s.config || '{}'),
46
47
  agentId: s.agent_id || null,
47
48
  enabled: !!s.enabled,
48
- status: liveStatuses[s.id]?.status || 'stopped',
49
- toolCount: liveStatuses[s.id]?.toolCount || 0
49
+ status: liveStatuses[s.id]?.status || s.status || 'stopped',
50
+ toolCount: liveStatuses[s.id]?.toolCount || 0,
51
+ error: liveStatuses[s.id]?.error || null,
52
+ consecutiveFails: liveStatuses[s.id]?.consecutiveFails || 0,
53
+ nextRetryAt: liveStatuses[s.id]?.nextRetryAt || null,
50
54
  }));
51
55
 
52
56
  res.json(result);
@@ -70,20 +74,26 @@ router.post('/', (req, res) => {
70
74
  });
71
75
 
72
76
  // Update an MCP server
73
- router.put('/:id', (req, res) => {
77
+ router.put('/:id', async (req, res) => {
74
78
  try {
75
79
  const server = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
76
80
  if (!server) return res.status(404).json({ error: 'Server not found' });
77
81
 
82
+ const mcpClient = req.app.locals.mcpClient;
83
+ const wasLive = Boolean(mcpClient.getStatus(req.session.userId)[server.id]);
78
84
  const { name, command, config, enabled } = req.body;
79
85
  const agentId = (req.body.agentId !== undefined || req.body.agent_id !== undefined)
80
86
  ? resolveAgentId(req.session.userId, getAgentIdFromRequest(req))
81
87
  : (server.agent_id || resolveAgentId(req.session.userId, null));
82
88
  const endpoint = command ? validateRemoteMcpEndpoint(command) : server.command;
83
- db.prepare('UPDATE mcp_servers SET agent_id = ?, name = ?, command = ?, config = ?, enabled = ? WHERE id = ?')
89
+ db.prepare("UPDATE mcp_servers SET agent_id = ?, name = ?, command = ?, config = ?, enabled = ?, status = 'stopped' WHERE id = ?")
84
90
  .run(agentId, name || server.name, endpoint, JSON.stringify(config || JSON.parse(server.config)), enabled !== undefined ? (enabled ? 1 : 0) : server.enabled, server.id);
85
91
 
86
- res.json({ success: true });
92
+ if (wasLive) {
93
+ await mcpClient.stopServer(server.id);
94
+ }
95
+
96
+ res.json({ success: true, status: 'stopped' });
87
97
  } catch (err) {
88
98
  res.status(400).json({ error: sanitizeError(err) });
89
99
  }
@@ -109,9 +119,8 @@ router.post('/:id/start', async (req, res) => {
109
119
 
110
120
  const mcpClient = req.app.locals.mcpClient;
111
121
  const result = await mcpClient.startServer(server.id, server.command, server.name, req.session.userId, { agentId: server.agent_id });
112
- const tools = await mcpClient.listTools(server.id, req.session.userId);
113
122
 
114
- res.json({ ...result, tools });
123
+ res.json(result);
115
124
  } catch (err) {
116
125
  if (err.message && err.message.startsWith('OAUTH_REDIRECT:')) {
117
126
  const url = err.message.substring(15);
@@ -152,20 +161,27 @@ router.get('/:id/tools', async (req, res) => {
152
161
  // OAuth Callback
153
162
  router.get('/oauth/callback', async (req, res) => {
154
163
  const { code, state, error } = req.query;
155
- if (!state) return res.status(400).send('Missing state parameter');
156
- if (!error && !code) return res.status(400).send('Missing code parameter');
157
- if (error) return res.status(400).send(`OAuth Error: ${error}`);
164
+ if (!state) return res.status(400).type('text/plain').send('Missing state parameter');
158
165
 
159
166
  const stateMatch = String(state).match(MCP_OAUTH_STATE_RE);
160
- if (!stateMatch) return res.status(400).send('Invalid state format');
167
+ if (!stateMatch) return res.status(400).type('text/plain').send('Invalid state format');
161
168
 
162
169
  const serverId = Number.parseInt(stateMatch[1], 10);
163
170
  if (!Number.isInteger(serverId) || serverId <= 0) {
164
- return res.status(400).send('Invalid state format');
171
+ return res.status(400).type('text/plain').send('Invalid state format');
165
172
  }
166
173
 
167
174
  const server = db.prepare('SELECT id FROM mcp_servers WHERE id = ? AND user_id = ?').get(serverId, req.session.userId);
168
- if (!server) return res.status(404).send('Server not found');
175
+ if (!server) return res.status(404).type('text/plain').send('Server not found');
176
+ if (!error && !code) {
177
+ return res.status(400).type('text/plain').send('Missing code parameter');
178
+ }
179
+ if (!consumeOAuthState(serverId, state)) {
180
+ return res.status(400).type('text/plain').send('Invalid or expired OAuth state');
181
+ }
182
+ if (error) {
183
+ return res.status(400).type('text/plain').send(`OAuth error: ${String(error)}`);
184
+ }
169
185
 
170
186
  const mcpClient = req.app.locals.mcpClient;
171
187
  const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
@@ -111,13 +111,19 @@ router.get('/', (req, res) => {
111
111
  const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
112
112
  const coreMemory = { ...(mm.getCoreMemory(userId, { agentId }) || {}) };
113
113
  delete coreMemory.active_context;
114
+ const knowledgeViews = mm.listKnowledgeViews(userId, { agentId, limit: 12 });
114
115
  res.json({
115
116
  agentId,
116
117
  assistantBehaviorNotes: mm.getAssistantBehaviorNotes(userId, { agentId }),
117
118
  assistantSelfState: mm.getAssistantSelfState(userId, { agentId }),
118
119
  dailyLogs: mm.listDailyLogs(7, userId),
119
120
  apiKeys: Object.keys(mm.readApiKeys(userId)),
120
- coreMemory
121
+ coreMemory,
122
+ stats: mm.getMemoryStats(userId, { agentId }),
123
+ entities: mm.listEntities(userId, { agentId, limit: 12 }),
124
+ knowledgeViews,
125
+ ingestionOverview: mm.getIngestionOverview(userId, { agentId }),
126
+ recentKnowledgeChanges: mm.listRecentKnowledgeChanges(userId, { agentId, limit: 8 }),
121
127
  });
122
128
  });
123
129
 
@@ -157,6 +163,7 @@ router.get('/ingestion/status', (req, res) => {
157
163
  jobs: mm.listIngestionJobs(userId, { agentId }),
158
164
  recentChanges: mm.listRecentKnowledgeChanges(userId, { agentId }),
159
165
  connections: ingestionService?.listConnectionStatuses?.(userId, { agentId }) || [],
166
+ service: ingestionService?.getStatus?.() || { state: 'unavailable' },
160
167
  });
161
168
  } catch (err) {
162
169
  res.status(500).json({ error: sanitizeError(err) });
@@ -204,6 +211,21 @@ router.get('/knowledge-views', (req, res) => {
204
211
  }
205
212
  });
206
213
 
214
+ router.get('/entities', (req, res) => {
215
+ const mm = req.app.locals.memoryManager;
216
+ const userId = req.session.userId;
217
+ const agentId = resolveAgentId(userId, getAgentIdFromRequest(req));
218
+ try {
219
+ res.json(mm.listEntities(userId, {
220
+ agentId,
221
+ query: req.query.query || null,
222
+ limit: req.query.limit,
223
+ }));
224
+ } catch (err) {
225
+ res.status(500).json({ error: sanitizeError(err) });
226
+ }
227
+ });
228
+
207
229
  router.post('/knowledge-views/materialize', (req, res) => {
208
230
  const mm = req.app.locals.memoryManager;
209
231
  const userId = req.session.userId;
@@ -80,6 +80,8 @@ router.post('/disconnect', async (req, res) => {
80
80
  try {
81
81
  const { platform } = req.body;
82
82
  const agentId = resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
83
+ if (!platform) return res.status(400).json({ error: 'Platform is required' });
84
+
83
85
  const manager = req.app.locals.messagingManager;
84
86
  const result = await manager.disconnectPlatform(req.session.userId, platform, { agentId });
85
87
  res.json(result);
@@ -2,21 +2,24 @@
2
2
 
3
3
  const express = require('express');
4
4
  const db = require('../db/database');
5
+ const { buildFtsQuery } = require('../db/ftsQuery');
6
+ const { requireAuth } = require('../middleware/auth');
5
7
  const { getErrorMessage } = require('../services/bootstrap_helpers');
6
8
 
7
9
  const router = express.Router();
8
10
 
11
+ router.use(requireAuth);
12
+
9
13
  router.get('/search', (req, res) => {
10
14
  const { q, limit = 50, offset = 0 } = req.query;
11
-
12
- if (!req.user || !req.user.id) {
13
- return res.status(401).json({ error: 'Unauthorized' });
14
- }
15
+ const userId = req.session.userId;
15
16
 
16
17
  try {
17
18
  let results = [];
18
- if (q) {
19
- // Full text search
19
+ const ftsQuery = q ? buildFtsQuery(q) : null;
20
+ if (ftsQuery) {
21
+ // Full text search. buildFtsQuery sanitizes user input so FTS5 operator
22
+ // characters (hyphens, AND/OR/NOT) don't throw and 500 the request.
20
23
  results = db.prepare(`
21
24
  SELECT s.id, s.timestamp, s.app_name, s.text_content
22
25
  FROM screen_history_fts fts
@@ -24,7 +27,10 @@ router.get('/search', (req, res) => {
24
27
  WHERE screen_history_fts MATCH ? AND s.user_id = ?
25
28
  ORDER BY s.timestamp DESC
26
29
  LIMIT ? OFFSET ?
27
- `).all(q, req.user.id, Number(limit), Number(offset));
30
+ `).all(ftsQuery, userId, Number(limit), Number(offset));
31
+ } else if (q) {
32
+ // Query had no usable search tokens — return no matches rather than error.
33
+ results = [];
28
34
  } else {
29
35
  // Recent history
30
36
  results = db.prepare(`
@@ -33,7 +39,7 @@ router.get('/search', (req, res) => {
33
39
  WHERE user_id = ?
34
40
  ORDER BY timestamp DESC
35
41
  LIMIT ? OFFSET ?
36
- `).all(req.user.id, Number(limit), Number(offset));
42
+ `).all(userId, Number(limit), Number(offset));
37
43
  }
38
44
 
39
45
  res.json({ results });
@@ -562,8 +562,16 @@ router.delete('/:key', (req, res) => {
562
562
  res.json({ success: true });
563
563
  });
564
564
 
565
+ function requireAdminSession(req, res, next) {
566
+ if (req.session?.isAdmin === true) return next();
567
+ return res.status(403).json({
568
+ success: false,
569
+ error: 'Server updates are only available from the admin dashboard.',
570
+ });
571
+ }
572
+
565
573
  // Trigger auto-update script
566
- router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
574
+ router.post('/update', requireAuth, requireAdminSession, updateTriggerLimiter, (req, res) => {
567
575
  if (isManagedDeployment()) {
568
576
  return res.status(403).json({
569
577
  success: false,
@@ -611,7 +619,7 @@ router.post('/update', requireAuth, updateTriggerLimiter, (req, res) => {
611
619
  res.json({ success: true, message: 'Update triggered', pid: child.pid });
612
620
  });
613
621
 
614
- router.put('/update/channel', (req, res) => {
622
+ router.put('/update/channel', requireAuth, requireAdminSession, (req, res) => {
615
623
  if (isManagedDeployment()) {
616
624
  return res.status(403).json({
617
625
  success: false,
@@ -111,7 +111,7 @@ router.post('/start', async (req, res) => {
111
111
  const platform = normalizePlatform(req.body?.platform);
112
112
  const resolved = await resolveStartDeviceId(platform, req);
113
113
  const deviceId = resolved.deviceId;
114
- const fps = boundedInt(req.body?.fps, platform === 'android' ? 10 : 15, 1, platform === 'android' ? 15 : 20);
114
+ const fps = boundedInt(req.body?.fps, platform === 'android' ? 12 : 15, 1, platform === 'android' ? 30 : 20);
115
115
  const quality = boundedInt(req.body?.quality, platform === 'android' ? 75 : 80, 30, 95);
116
116
  const hub = streamHub(req);
117
117
  await hub.stopStream(userId, platform, deviceId, 'restart');
@@ -13,10 +13,10 @@ router.post('/geofence', async (req, res) => {
13
13
  const { label, latitude, longitude, radius_meters, action } = req.body;
14
14
 
15
15
  try {
16
- const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.user.id);
16
+ const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.session.userId);
17
17
  if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
18
18
 
19
- console.log(`[Triggers] Geofence entered: ${label} by user ${req.user.id}`);
19
+ console.log(`[Triggers] Geofence entered: ${label} by user ${req.session.userId}`);
20
20
 
21
21
  res.json({ success: true, message: 'Geofence trigger processed' });
22
22
  } catch (err) {
@@ -29,14 +29,14 @@ router.post('/geofence', async (req, res) => {
29
29
  Promise.resolve().then(() => {
30
30
  const agentEngine = req.app.locals.agentEngine;
31
31
  if (!agentEngine) return;
32
- const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.user.id)?.id;
32
+ const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.session.userId)?.id;
33
33
  if (!defaultAgentId) return;
34
34
  const prompt = [
35
35
  `A geofence event was triggered.`,
36
36
  `Location label: ${label || 'unknown'}`,
37
37
  action ? `Suggested action: ${action}` : 'Check if there are any active reminders or tasks related to this location.',
38
38
  ].join('\n');
39
- return agentEngine.run(req.user.id, prompt, {
39
+ return agentEngine.run(req.session.userId, prompt, {
40
40
  agentId: defaultAgentId,
41
41
  triggerSource: 'tasks',
42
42
  context: { source: 'geofence', label, latitude, longitude, radius_meters },
@@ -4,7 +4,8 @@ const { requireAuth } = require('../middleware/auth');
4
4
  const { sanitizeError } = require('../utils/security');
5
5
  const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
6
6
  const { TurnCoordinator } = require('../services/voice/turnCoordinator');
7
- const { normalizeVoiceSynthesisOptions } = require('../services/voice/providers');
7
+ const { normalizeVoiceSynthesisOptions, transcribeVoiceInput } = require('../services/voice/providers');
8
+ const { writeTempAudioFile, removeTempFile } = require('../services/voice/liveAudio');
8
9
  const { runVoiceTranscriptTurn } = require('../services/voice/turnRunner');
9
10
 
10
11
  const router = express.Router();
@@ -135,4 +136,38 @@ router.post('/respond', async (req, res) => {
135
136
  }
136
137
  });
137
138
 
139
+ router.post('/transcribe', async (req, res) => {
140
+ try {
141
+ const audioBase64 = String(req.body?.audioBase64 || '').trim();
142
+ const mimeType = String(req.body?.mimeType || 'audio/pcm;rate=16000;channels=1').trim();
143
+
144
+ if (!audioBase64) {
145
+ return res.status(400).json({ error: 'audioBase64 is required.' });
146
+ }
147
+
148
+ const approxBytes = (audioBase64.length * 3) / 4;
149
+ if (approxBytes > 25 * 1024 * 1024) {
150
+ return res.status(400).json({ error: 'Audio exceeds maximum size of 25MB.' });
151
+ }
152
+
153
+ const audioBytes = Buffer.from(audioBase64, 'base64');
154
+ const { filePath, mimeType: fileMimeType } = await writeTempAudioFile(audioBytes, mimeType);
155
+ let transcript = '';
156
+ try {
157
+ transcript = await transcribeVoiceInput(filePath, {
158
+ mimeType: fileMimeType,
159
+ userId: req.session.userId,
160
+ timeoutMs: 30000,
161
+ });
162
+ } finally {
163
+ await removeTempFile(filePath);
164
+ }
165
+
166
+ return res.json({ transcript: String(transcript || '').trim() });
167
+ } catch (err) {
168
+ const message = sanitizeError(err);
169
+ return res.status(500).json({ error: message });
170
+ }
171
+ });
172
+
138
173
  module.exports = router;
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const { requireAuth } = require('../middleware/auth');
5
+ const { sanitizeError } = require('../utils/security');
6
+
7
+ const router = express.Router();
8
+
9
+ const MAX_PATH_LENGTH = 2048;
10
+ const MAX_EDIT_BYTES = 1024 * 1024;
11
+
12
+ router.use(requireAuth);
13
+
14
+ function badRequest(message) {
15
+ const error = new Error(message);
16
+ error.status = 400;
17
+ return error;
18
+ }
19
+
20
+ function parseWorkspacePath(value, fallback = '.') {
21
+ const normalized = String(value ?? fallback).trim();
22
+ if (normalized.length > MAX_PATH_LENGTH) {
23
+ throw badRequest('path is too long.');
24
+ }
25
+ return normalized || fallback;
26
+ }
27
+
28
+ function getWorkspace(req) {
29
+ const workspace = req.app?.locals?.workspaceManager;
30
+ if (!workspace) {
31
+ const error = new Error('Workspace service is unavailable.');
32
+ error.status = 503;
33
+ throw error;
34
+ }
35
+ return workspace;
36
+ }
37
+
38
+ function handleWorkspaceAction(req, res, action) {
39
+ try {
40
+ const result = action(getWorkspace(req));
41
+ return res.json(result);
42
+ } catch (err) {
43
+ return res.status(err.status || 500).json({ error: sanitizeError(err), code: err.code || null });
44
+ }
45
+ }
46
+
47
+ router.get('/files', (req, res) => handleWorkspaceAction(req, res, (workspace) =>
48
+ workspace.listExplorerDirectory(req.session.userId, {
49
+ path: parseWorkspacePath(req.query?.path, '.'),
50
+ })));
51
+
52
+ router.get('/files/content', (req, res) => handleWorkspaceAction(req, res, (workspace) =>
53
+ workspace.readExplorerFile(req.session.userId, {
54
+ path: parseWorkspacePath(req.query?.path, ''),
55
+ maxBytes: MAX_EDIT_BYTES,
56
+ })));
57
+
58
+ router.put('/files/content', (req, res) => handleWorkspaceAction(req, res, (workspace) => {
59
+ const content = String(req.body?.content ?? '');
60
+ if (Buffer.byteLength(content, 'utf8') > MAX_EDIT_BYTES) {
61
+ throw badRequest('content is too large.');
62
+ }
63
+ return workspace.writeExplorerFile(req.session.userId, {
64
+ path: parseWorkspacePath(req.body?.path, ''),
65
+ content,
66
+ });
67
+ }));
68
+
69
+ router.get('/files/download', (req, res) => {
70
+ try {
71
+ const workspace = getWorkspace(req);
72
+ const file = workspace.getExplorerDownload(req.session.userId, {
73
+ path: parseWorkspacePath(req.query?.path, ''),
74
+ });
75
+ res.setHeader('Cache-Control', 'private, no-store');
76
+ return res.download(file.absolutePath, file.filename, (err) => {
77
+ if (!err || res.headersSent) return;
78
+ const status = err.code === 'ENOENT' || err.code === 'EACCES' ? 404 : 500;
79
+ res.status(status).json({ error: status === 404 ? 'File not found' : 'Failed to download file' });
80
+ });
81
+ } catch (err) {
82
+ return res.status(err.status || 500).json({ error: sanitizeError(err), code: err.code || null });
83
+ }
84
+ });
85
+
86
+ module.exports = router;
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+
3
+ const bcrypt = require('bcrypt');
4
+ const crypto = require('crypto');
5
+ const { generateSecret, generateURI, verifySync } = require('otplib');
6
+ const db = require('../../db/database');
7
+ const { encryptValue, decryptValue } = require('../integrations/secrets');
8
+
9
+ const TOTP_OPTS = { strategy: 'totp', digits: 6, period: 30, epochTolerance: 30 };
10
+ const RECOVERY_ALPHA = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
11
+
12
+ // ── Helpers ────────────────────────────────────────────────────────────────
13
+
14
+ function getRow() {
15
+ return db.prepare('SELECT * FROM admin_two_factor WHERE id = 1').get() || null;
16
+ }
17
+
18
+ function normalizeCode(v) {
19
+ return String(v || '').trim().replace(/\s+/g, '').toUpperCase();
20
+ }
21
+
22
+ function verifyTotpCode(secret, code) {
23
+ const token = String(code || '').trim().replace(/\s+/g, '');
24
+ if (!/^\d{6}$/.test(token)) return false;
25
+ return verifySync({ ...TOTP_OPTS, secret, token })?.valid === true;
26
+ }
27
+
28
+ function makeRecoveryCode() {
29
+ let raw = '';
30
+ while (raw.length < 10) raw += RECOVERY_ALPHA[crypto.randomInt(RECOVERY_ALPHA.length)];
31
+ return `${raw.slice(0, 5)}-${raw.slice(5)}`;
32
+ }
33
+
34
+ // ── Public API ─────────────────────────────────────────────────────────────
35
+
36
+ function getStatus() {
37
+ const row = getRow();
38
+ const codesLeft = db.prepare('SELECT COUNT(*) AS n FROM admin_recovery_codes WHERE used_at IS NULL').get().n;
39
+ return {
40
+ enabled: row?.enabled === 1,
41
+ pending: Boolean(row?.pending_secret),
42
+ enabledAt: row?.enabled_at || null,
43
+ recoveryCodesRemaining: codesLeft,
44
+ };
45
+ }
46
+
47
+ function beginSetup() {
48
+ if (!String(process.env.SESSION_SECRET || '').trim()) {
49
+ throw Object.assign(new Error('SESSION_SECRET must be configured before enabling 2FA'), { statusCode: 500 });
50
+ }
51
+ const secret = generateSecret();
52
+ const otpauthUrl = generateURI({ ...TOTP_OPTS, issuer: 'NeoAgent Admin', label: 'admin', secret });
53
+ db.prepare(`
54
+ INSERT INTO admin_two_factor (id, pending_secret, enabled, created_at, updated_at)
55
+ VALUES (1, ?, COALESCE((SELECT enabled FROM admin_two_factor WHERE id = 1), 0), datetime('now'), datetime('now'))
56
+ ON CONFLICT(id) DO UPDATE SET pending_secret = excluded.pending_secret, updated_at = datetime('now')
57
+ `).run(encryptValue(secret));
58
+ return { otpauthUrl, manualKey: secret };
59
+ }
60
+
61
+ async function enable(code) {
62
+ const row = getRow();
63
+ if (!row?.pending_secret) throw Object.assign(new Error('No 2FA setup pending'), { statusCode: 400 });
64
+ const secret = decryptValue(row.pending_secret);
65
+ if (!verifyTotpCode(secret, code)) throw Object.assign(new Error('Invalid code — try again'), { statusCode: 401 });
66
+
67
+ const codes = Array.from({ length: 10 }, makeRecoveryCode);
68
+ const hashes = await Promise.all(codes.map((c) => bcrypt.hash(normalizeCode(c).replace(/-/g, ''), 12)));
69
+
70
+ db.transaction(() => {
71
+ db.prepare('DELETE FROM admin_recovery_codes').run();
72
+ const ins = db.prepare("INSERT INTO admin_recovery_codes (code_hash, created_at) VALUES (?, datetime('now'))");
73
+ for (const h of hashes) ins.run(h);
74
+ db.prepare(`
75
+ UPDATE admin_two_factor
76
+ SET secret = pending_secret, pending_secret = NULL, enabled = 1,
77
+ enabled_at = datetime('now'), updated_at = datetime('now')
78
+ WHERE id = 1
79
+ `).run();
80
+ })();
81
+
82
+ return { recoveryCodes: codes };
83
+ }
84
+
85
+ /**
86
+ * Verifies a TOTP code or recovery code.
87
+ * Returns true if valid (or if 2FA is not enabled).
88
+ */
89
+ async function verifyCode(code) {
90
+ const row = getRow();
91
+ if (!row?.enabled || !row.secret) return true; // 2FA not configured
92
+
93
+ const secret = decryptValue(row.secret);
94
+ if (verifyTotpCode(secret, code)) return true;
95
+
96
+ // Try recovery code
97
+ const normalized = normalizeCode(String(code || '')).replace(/-/g, '');
98
+ if (normalized.length < 6) return false;
99
+ const rows = db.prepare('SELECT id, code_hash FROM admin_recovery_codes WHERE used_at IS NULL ORDER BY id').all();
100
+ for (const r of rows) {
101
+ if (await bcrypt.compare(normalized, r.code_hash)) {
102
+ db.prepare("UPDATE admin_recovery_codes SET used_at = datetime('now') WHERE id = ?").run(r.id);
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+
109
+ async function disable(code) {
110
+ const row = getRow();
111
+ if (!row?.enabled) return;
112
+ if (!await verifyCode(code)) throw Object.assign(new Error('Invalid 2FA code'), { statusCode: 401 });
113
+ db.transaction(() => {
114
+ db.prepare(`UPDATE admin_two_factor SET enabled = 0, secret = NULL, pending_secret = NULL,
115
+ enabled_at = NULL, updated_at = datetime('now') WHERE id = 1`).run();
116
+ db.prepare('DELETE FROM admin_recovery_codes').run();
117
+ })();
118
+ }
119
+
120
+ async function regenerateCodes(code) {
121
+ if (!await verifyCode(code)) throw Object.assign(new Error('Invalid 2FA code'), { statusCode: 401 });
122
+ const codes = Array.from({ length: 10 }, makeRecoveryCode);
123
+ const hashes = await Promise.all(codes.map((c) => bcrypt.hash(normalizeCode(c).replace(/-/g, ''), 12)));
124
+ db.transaction(() => {
125
+ db.prepare('DELETE FROM admin_recovery_codes').run();
126
+ const ins = db.prepare("INSERT INTO admin_recovery_codes (code_hash, created_at) VALUES (?, datetime('now'))");
127
+ for (const h of hashes) ins.run(h);
128
+ })();
129
+ return { recoveryCodes: codes };
130
+ }
131
+
132
+ module.exports = { getStatus, beginSetup, enable, verifyCode, disable, regenerateCodes };
@@ -113,7 +113,12 @@ function evaluatePasswordStrength(password, context = {}) {
113
113
  label,
114
114
  length,
115
115
  hasMinimumLength: length >= MIN_PASSWORD_LENGTH,
116
- isAcceptable: length >= MIN_PASSWORD_LENGTH && score >= MIN_PASSWORD_SCORE,
116
+ isAcceptable: length >= MIN_PASSWORD_LENGTH
117
+ && score >= MIN_PASSWORD_SCORE
118
+ && !containsPersonalInfo
119
+ && !usesCommonPattern
120
+ && !sequential
121
+ && !repeatedRuns,
117
122
  feedback,
118
123
  };
119
124
  }
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ // Decides whether the agent may accept a `task_complete` signal, balancing the
4
+ // model's self-reported confidence against the confidence the run requires.
5
+
6
+ function normalizeCompletionConfidence(value) {
7
+ const normalized = String(value || '').trim().toLowerCase();
8
+ if (normalized === 'high' || normalized === 'medium' || normalized === 'low') return normalized;
9
+ return 'medium';
10
+ }
11
+
12
+ function completionConfidenceRank(value) {
13
+ const normalized = normalizeCompletionConfidence(value);
14
+ if (normalized === 'high') return 3;
15
+ if (normalized === 'medium') return 2;
16
+ return 1;
17
+ }
18
+
19
+ function shouldAcceptTaskComplete({ confidence, requiredConfidence, iteration, maxIterations }) {
20
+ const required = normalizeCompletionConfidence(requiredConfidence || 'medium');
21
+ const actual = normalizeCompletionConfidence(confidence || 'medium');
22
+ if (completionConfidenceRank(actual) >= completionConfidenceRank(required)) {
23
+ return { accept: true, reason: '' };
24
+ }
25
+
26
+ const iterationsRemaining = Math.max(0, Number(maxIterations || 0) - Number(iteration || 0));
27
+ if (iterationsRemaining <= 1) {
28
+ return {
29
+ accept: true,
30
+ reason: `Accepted ${actual}-confidence completion at the iteration limit; final verifier will calibrate the answer.`,
31
+ };
32
+ }
33
+
34
+ return {
35
+ accept: false,
36
+ reason: `Completion confidence "${actual}" is below required "${required}". Continue with verification, recovery, or a narrower truthful result before completing.`,
37
+ };
38
+ }
39
+
40
+ module.exports = {
41
+ normalizeCompletionConfidence,
42
+ completionConfidenceRank,
43
+ shouldAcceptTaskComplete,
44
+ };