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
@@ -0,0 +1,632 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const express = require('express');
5
+ const { spawn } = require('child_process');
6
+ const { requireAdminAuth } = require('../middleware/adminAuth');
7
+ const { getVersionInfo } = require('../utils/version');
8
+ const {
9
+ readUpdateStatus,
10
+ writeUpdateStatusFile: writeUpdateStatus,
11
+ } = require('../utils/update_status');
12
+ const {
13
+ parseReleaseChannel,
14
+ writeReleaseChannelToEnvFile,
15
+ getReleaseChannelBranchPolicy,
16
+ getReleaseChannelNpmPolicy,
17
+ } = require('../../runtime/release_channel');
18
+ const { APP_DIR, ENV_FILE, upsertEnvValue } = require('../../runtime/paths');
19
+ const { isManagedDeployment } = require('../utils/deployment');
20
+ const rateLimit = require('express-rate-limit');
21
+
22
+ const router = express.Router();
23
+ const ADMIN_DIR = path.join(__dirname, '..', 'admin');
24
+
25
+ const fs = require('fs');
26
+
27
+ // Admin sessions last 30 days and roll on every request.
28
+ const ADMIN_SESSION_TTL = 30 * 24 * 60 * 60 * 1000;
29
+
30
+ // Rolling refresh: touch the session on every authenticated admin request
31
+ // so the 30-day window slides forward from the last activity.
32
+ router.use((req, res, next) => {
33
+ if (req.session?.isAdmin) {
34
+ req.session.cookie.maxAge = ADMIN_SESSION_TTL;
35
+ req.session.touch();
36
+ }
37
+ next();
38
+ });
39
+
40
+ const loginLimiter = rateLimit({
41
+ windowMs: 15 * 60 * 1000,
42
+ max: 20,
43
+ standardHeaders: true,
44
+ legacyHeaders: false,
45
+ skipSuccessfulRequests: true,
46
+ message: { error: 'Too many login attempts, try again later' },
47
+ });
48
+
49
+ const updateTriggerLimiter = rateLimit({
50
+ windowMs: 15 * 60 * 1000,
51
+ max: 5,
52
+ standardHeaders: true,
53
+ legacyHeaders: false,
54
+ message: { error: 'Too many update requests, try again later' },
55
+ });
56
+
57
+ // --- Auth ---
58
+
59
+ router.get('/login', (req, res) => {
60
+ if (req.session?.isAdmin) return res.redirect('/admin');
61
+ res.sendFile(path.join(ADMIN_DIR, 'login.html'));
62
+ });
63
+
64
+ router.post('/api/login', loginLimiter, express.json(), async (req, res) => {
65
+ const { username, password } = req.body || {};
66
+ const expectedUsername = process.env.ADMIN_USERNAME || 'admin';
67
+ const expectedPassword = process.env.ADMIN_PASSWORD || '';
68
+ if (!expectedPassword) {
69
+ return res.status(503).json({ error: 'Admin credentials not configured. Run `neoagent setup`.' });
70
+ }
71
+ if (username !== expectedUsername || password !== expectedPassword) {
72
+ return res.status(401).json({ error: 'Invalid credentials' });
73
+ }
74
+ // Check whether 2FA is enabled
75
+ const adminTwoFactor = require('../services/account/admin_two_factor');
76
+ const tfStatus = adminTwoFactor.getStatus();
77
+ if (tfStatus.enabled) {
78
+ // Park the session in a "password OK, waiting for TOTP" state
79
+ req.session.adminPendingTwoFactor = true;
80
+ delete req.session.isAdmin;
81
+ return req.session.save((err) => {
82
+ if (err) return res.status(500).json({ error: 'Session error' });
83
+ res.json({ ok: false, requiresTwoFactor: true });
84
+ });
85
+ }
86
+ req.session.isAdmin = true;
87
+ req.session.cookie.maxAge = ADMIN_SESSION_TTL;
88
+ req.session.save((err) => {
89
+ if (err) return res.status(500).json({ error: 'Session error' });
90
+ res.json({ ok: true });
91
+ });
92
+ });
93
+
94
+ // Second-factor verification (called after a successful password login when 2FA is on)
95
+ router.post('/api/2fa/verify', loginLimiter, express.json(), async (req, res) => {
96
+ if (!req.session?.adminPendingTwoFactor) {
97
+ return res.status(400).json({ error: 'No pending 2FA verification' });
98
+ }
99
+ const { code } = req.body || {};
100
+ try {
101
+ const adminTwoFactor = require('../services/account/admin_two_factor');
102
+ const valid = await adminTwoFactor.verifyCode(code);
103
+ if (!valid) return res.status(401).json({ error: 'Invalid code — try again' });
104
+ req.session.adminPendingTwoFactor = false;
105
+ req.session.isAdmin = true;
106
+ req.session.cookie.maxAge = ADMIN_SESSION_TTL;
107
+ req.session.save((err) => {
108
+ if (err) return res.status(500).json({ error: 'Session error' });
109
+ res.json({ ok: true });
110
+ });
111
+ } catch (err) {
112
+ res.status(500).json({ error: err.message });
113
+ }
114
+ });
115
+
116
+ router.post('/api/logout', (req, res) => {
117
+ req.session.destroy(() => res.json({ ok: true }));
118
+ });
119
+
120
+ // --- Protected API ---
121
+
122
+ router.get('/api/logs', requireAdminAuth, (req, res) => {
123
+ const logHistory = req.app.locals.logHistory || [];
124
+ res.json({ logs: logHistory });
125
+ });
126
+
127
+ router.get('/api/version', requireAdminAuth, (req, res) => {
128
+ const version = getVersionInfo();
129
+ const status = readUpdateStatus();
130
+ res.json({
131
+ version: version.version,
132
+ installedVersion: version.installedVersion,
133
+ packageVersion: version.packageVersion,
134
+ gitVersion: version.gitVersion,
135
+ gitSha: version.gitSha,
136
+ gitBranch: version.gitBranch,
137
+ releaseChannel: status.releaseChannel || version.releaseChannel,
138
+ deploymentMode: version.deploymentMode,
139
+ deploymentProfile: version.deploymentProfile,
140
+ allowSelfUpdate: version.allowSelfUpdate,
141
+ updateStatus: {
142
+ state: status.state,
143
+ progress: status.progress,
144
+ phase: status.phase,
145
+ message: status.message,
146
+ },
147
+ uptime: process.uptime(),
148
+ nodeVersion: process.version,
149
+ });
150
+ });
151
+
152
+ router.get('/api/health', requireAdminAuth, async (req, res) => {
153
+ const runtimeManager = req.app?.locals?.runtimeManager;
154
+ const desktopRegistry = req.app?.locals?.desktopCompanionRegistry;
155
+ const extensionRegistry = req.app?.locals?.browserExtensionRegistry;
156
+ const results = [];
157
+
158
+ results.push({ id: 'backend', label: 'Backend server', passed: true, detail: 'Running' });
159
+
160
+ const version = getVersionInfo();
161
+ results.push({
162
+ id: 'version',
163
+ label: 'Server version',
164
+ passed: true,
165
+ detail: version.version || version.packageVersion || 'Unknown',
166
+ });
167
+
168
+ try {
169
+ const db = require('../db/database');
170
+ db.prepare('SELECT 1').get();
171
+ results.push({ id: 'database', label: 'Database', passed: true, detail: 'SQLite connected' });
172
+ } catch (err) {
173
+ results.push({ id: 'database', label: 'Database', passed: false, detail: String(err?.message || err).slice(0, 120) });
174
+ }
175
+
176
+ const updateStatus = readUpdateStatus();
177
+ results.push({
178
+ id: 'update',
179
+ label: 'Update status',
180
+ passed: updateStatus.state !== 'failed',
181
+ detail: updateStatus.state === 'idle'
182
+ ? 'No update running'
183
+ : `${updateStatus.state} — ${updateStatus.message || ''}`.trim(),
184
+ });
185
+
186
+ const { getRuntimeValidation } = require('../services/runtime/validation');
187
+ const runtimeValidation = getRuntimeValidation(runtimeManager);
188
+ const runtimeReady = Boolean(runtimeValidation?.ready);
189
+ results.push({
190
+ id: 'vm_runtime',
191
+ label: 'Cloud VM runtime',
192
+ passed: runtimeReady,
193
+ detail: runtimeReady ? 'Available' : String(runtimeValidation?.issues?.[0] || 'Not configured'),
194
+ });
195
+
196
+ if (desktopRegistry) {
197
+ try {
198
+ const connectedUsers = [];
199
+ for (const socket of (req.app?.locals?.io?.sockets?.sockets?.values?.() || [])) {
200
+ const uid = socket.request?.session?.userId;
201
+ if (uid && !connectedUsers.includes(uid)) connectedUsers.push(uid);
202
+ }
203
+ results.push({
204
+ id: 'desktop',
205
+ label: 'Desktop companion',
206
+ passed: true,
207
+ detail: 'Registry available',
208
+ });
209
+ } catch {
210
+ results.push({ id: 'desktop', label: 'Desktop companion', passed: false, detail: 'Registry error' });
211
+ }
212
+ }
213
+
214
+ if (extensionRegistry) {
215
+ results.push({ id: 'extension', label: 'Chrome extension registry', passed: true, detail: 'Available' });
216
+ }
217
+
218
+ const configuredProviders = [];
219
+ if (process.env.ANTHROPIC_API_KEY) configuredProviders.push('Anthropic');
220
+ if (process.env.OPENAI_API_KEY) configuredProviders.push('OpenAI');
221
+ if (process.env.XAI_API_KEY) configuredProviders.push('xAI');
222
+ if (process.env.GOOGLE_AI_KEY) configuredProviders.push('Google');
223
+ if (process.env.OPENROUTER_API_KEY) configuredProviders.push('OpenRouter');
224
+ results.push({
225
+ id: 'ai_providers',
226
+ label: 'AI providers',
227
+ passed: configuredProviders.length > 0,
228
+ detail: configuredProviders.length > 0
229
+ ? configuredProviders.join(', ')
230
+ : 'No providers configured',
231
+ });
232
+
233
+ if (process.env.DEEPGRAM_API_KEY) {
234
+ results.push({ id: 'deepgram', label: 'Deepgram (voice)', passed: true, detail: 'API key configured' });
235
+ }
236
+
237
+ const allPassed = results.every((r) => r.passed);
238
+ res.json({ passed: allPassed, results });
239
+ });
240
+
241
+ router.get('/api/config', requireAdminAuth, (req, res) => {
242
+ const safe = [
243
+ 'PORT', 'NODE_ENV', 'PUBLIC_URL', 'NEOAGENT_DEPLOYMENT_MODE',
244
+ 'NEOAGENT_PROFILE', 'NEOAGENT_RELEASE_CHANNEL', 'ALLOWED_ORIGINS',
245
+ 'SECURE_COOKIES', 'TRUST_PROXY', 'ADMIN_USERNAME',
246
+ ];
247
+ const config = {};
248
+ for (const key of safe) {
249
+ config[key] = process.env[key] || '';
250
+ }
251
+ res.json({ config });
252
+ });
253
+
254
+ router.post('/api/update', requireAdminAuth, updateTriggerLimiter, (req, res) => {
255
+ if (isManagedDeployment()) {
256
+ return res.status(403).json({ error: 'Updates are managed by this deployment.' });
257
+ }
258
+ const status = readUpdateStatus();
259
+ if (status.state === 'running') {
260
+ return res.status(409).json({ error: 'An update is already running' });
261
+ }
262
+ console.log('[Admin] Triggering update-runner...');
263
+ const child = spawn(process.execPath, ['scripts/update-runner.js'], {
264
+ detached: true,
265
+ stdio: 'ignore',
266
+ cwd: APP_DIR,
267
+ });
268
+ writeUpdateStatus({
269
+ state: 'running',
270
+ progress: 1,
271
+ phase: 'starting',
272
+ message: 'Launching update job',
273
+ startedAt: new Date().toISOString(),
274
+ completedAt: null,
275
+ versionBefore: null,
276
+ versionAfter: null,
277
+ runnerPid: child.pid,
278
+ changelog: [],
279
+ logs: [],
280
+ });
281
+ child.once('error', (error) => {
282
+ writeUpdateStatus({
283
+ state: 'failed',
284
+ progress: 100,
285
+ phase: 'failed',
286
+ message: `Failed to launch update job: ${error.message}`,
287
+ completedAt: new Date().toISOString(),
288
+ runnerPid: null,
289
+ });
290
+ });
291
+ child.unref();
292
+ res.json({ ok: true, message: 'Update triggered', pid: child.pid });
293
+ });
294
+
295
+ router.put('/api/update/channel', requireAdminAuth, (req, res) => {
296
+ if (isManagedDeployment()) {
297
+ return res.status(403).json({ error: 'Release channel changes are managed by this deployment.' });
298
+ }
299
+ const requested = req.body?.channel;
300
+ const releaseChannel = parseReleaseChannel(requested);
301
+ if (!releaseChannel) {
302
+ return res.status(400).json({ error: 'Release channel must be "stable" or "beta".' });
303
+ }
304
+ writeReleaseChannelToEnvFile(releaseChannel);
305
+ process.env.NEOAGENT_RELEASE_CHANNEL = releaseChannel;
306
+ res.json({
307
+ ok: true,
308
+ releaseChannel,
309
+ targetBranch: getReleaseChannelBranchPolicy(releaseChannel),
310
+ npmDistTag: getReleaseChannelNpmPolicy(releaseChannel),
311
+ });
312
+ });
313
+
314
+ const settingsLimiter = rateLimit({
315
+ windowMs: 15 * 60 * 1000,
316
+ max: 20,
317
+ standardHeaders: true,
318
+ legacyHeaders: false,
319
+ message: { error: 'Too many settings changes, slow down' },
320
+ });
321
+
322
+ const sqlLimiter = rateLimit({
323
+ windowMs: 15 * 60 * 1000,
324
+ max: 60,
325
+ standardHeaders: true,
326
+ legacyHeaders: false,
327
+ message: { error: 'Too many SQL queries, slow down' },
328
+ });
329
+
330
+ // --- Access settings (signup toggle + API key) ---
331
+
332
+ router.get('/api/settings', requireAdminAuth, (req, res) => {
333
+ const apiKey = process.env.ADMIN_API_KEY || '';
334
+ const hint = apiKey.length > 8
335
+ ? `${apiKey.slice(0, 4)}${'•'.repeat(4)}${apiKey.slice(-4)}`
336
+ : apiKey ? '•'.repeat(apiKey.length) : '';
337
+ const adminTwoFactor = require('../services/account/admin_two_factor');
338
+ const tfStatus = adminTwoFactor.getStatus();
339
+ res.json({
340
+ signupEnabled: process.env.NEOAGENT_ALLOW_SIGNUP !== 'false',
341
+ apiKeyConfigured: Boolean(apiKey),
342
+ apiKeyHint: hint,
343
+ twoFactor: tfStatus,
344
+ });
345
+ });
346
+
347
+ // --- 2FA management ---
348
+
349
+ router.get('/api/settings/2fa', requireAdminAuth, (req, res) => {
350
+ const adminTwoFactor = require('../services/account/admin_two_factor');
351
+ res.json(adminTwoFactor.getStatus());
352
+ });
353
+
354
+ router.post('/api/settings/2fa/setup', requireAdminAuth, settingsLimiter, async (req, res) => {
355
+ try {
356
+ const adminTwoFactor = require('../services/account/admin_two_factor');
357
+ const { otpauthUrl, manualKey } = adminTwoFactor.beginSetup();
358
+ const qrcode = require('qrcode');
359
+ const qrDataUrl = await qrcode.toDataURL(otpauthUrl, { width: 200, margin: 2 });
360
+ res.json({ qrDataUrl, manualKey });
361
+ } catch (err) {
362
+ res.status(err.statusCode || 500).json({ error: err.message });
363
+ }
364
+ });
365
+
366
+ router.post('/api/settings/2fa/enable', requireAdminAuth, settingsLimiter, express.json(), async (req, res) => {
367
+ try {
368
+ const adminTwoFactor = require('../services/account/admin_two_factor');
369
+ const { recoveryCodes } = await adminTwoFactor.enable(req.body?.code);
370
+ res.json({ ok: true, recoveryCodes });
371
+ } catch (err) {
372
+ res.status(err.statusCode || 500).json({ error: err.message });
373
+ }
374
+ });
375
+
376
+ router.delete('/api/settings/2fa', requireAdminAuth, settingsLimiter, express.json(), async (req, res) => {
377
+ try {
378
+ const adminTwoFactor = require('../services/account/admin_two_factor');
379
+ await adminTwoFactor.disable(req.body?.code);
380
+ res.json({ ok: true });
381
+ } catch (err) {
382
+ res.status(err.statusCode || 500).json({ error: err.message });
383
+ }
384
+ });
385
+
386
+ router.post('/api/settings/2fa/recovery-codes', requireAdminAuth, settingsLimiter, express.json(), async (req, res) => {
387
+ try {
388
+ const adminTwoFactor = require('../services/account/admin_two_factor');
389
+ const { recoveryCodes } = await adminTwoFactor.regenerateCodes(req.body?.code);
390
+ res.json({ ok: true, recoveryCodes });
391
+ } catch (err) {
392
+ res.status(err.statusCode || 500).json({ error: err.message });
393
+ }
394
+ });
395
+
396
+ router.put('/api/settings/signup', requireAdminAuth, settingsLimiter, express.json(), (req, res) => {
397
+ const enabled = req.body?.enabled !== false; // default true
398
+ const value = enabled ? 'true' : 'false';
399
+ upsertEnvValue(ENV_FILE, 'NEOAGENT_ALLOW_SIGNUP', value);
400
+ process.env.NEOAGENT_ALLOW_SIGNUP = value;
401
+ res.json({ ok: true, signupEnabled: enabled });
402
+ });
403
+
404
+ router.post('/api/settings/apikey/rotate', requireAdminAuth, settingsLimiter, (req, res) => {
405
+ const newKey = require('crypto').randomBytes(32).toString('hex');
406
+ upsertEnvValue(ENV_FILE, 'ADMIN_API_KEY', newKey);
407
+ process.env.ADMIN_API_KEY = newKey;
408
+ // Return the key once — it will not be shown again
409
+ res.json({ ok: true, apiKey: newKey });
410
+ });
411
+
412
+ router.delete('/api/settings/apikey', requireAdminAuth, settingsLimiter, (req, res) => {
413
+ upsertEnvValue(ENV_FILE, 'ADMIN_API_KEY', '');
414
+ delete process.env.ADMIN_API_KEY;
415
+ res.json({ ok: true });
416
+ });
417
+
418
+ // --- Analytics ---
419
+
420
+ router.get('/api/analytics', requireAdminAuth, (req, res) => {
421
+ const db = require('../db/database');
422
+ const now = new Date().toISOString();
423
+ const dayAgo = new Date(Date.now() - 86_400_000).toISOString();
424
+ const weekAgo = new Date(Date.now() - 7 * 86_400_000).toISOString();
425
+ try {
426
+ const stats = {
427
+ totalUsers: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
428
+ activeToday: db.prepare('SELECT COUNT(*) AS n FROM users WHERE last_login > ?').get(dayAgo).n,
429
+ newThisWeek: db.prepare('SELECT COUNT(*) AS n FROM users WHERE created_at > ?').get(weekAgo).n,
430
+ totalRuns: db.prepare('SELECT COUNT(*) AS n FROM agent_runs').get().n,
431
+ runsToday: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(dayAgo).n,
432
+ totalTokens: db.prepare('SELECT COALESCE(SUM(total_tokens),0) AS n FROM agent_runs').get().n,
433
+ activeSessions:db.prepare('SELECT COUNT(*) AS n FROM user_sessions WHERE revoked_at IS NULL AND expires_at > ?').get(now).n,
434
+ totalStorage: db.prepare('SELECT COALESCE(SUM(byte_size),0) AS n FROM artifacts').get().n,
435
+ };
436
+ const topUsers = db.prepare(`
437
+ SELECT u.id, u.username, u.display_name,
438
+ COALESCE(r.runs, 0) AS runs,
439
+ COALESCE(r.tokens, 0) AS tokens,
440
+ COALESCE(a.storage, 0) AS storage
441
+ FROM users u
442
+ LEFT JOIN (
443
+ SELECT user_id,
444
+ COUNT(*) AS runs,
445
+ COALESCE(SUM(total_tokens),0) AS tokens
446
+ FROM agent_runs GROUP BY user_id
447
+ ) r ON r.user_id = u.id
448
+ LEFT JOIN (
449
+ SELECT user_id, COALESCE(SUM(byte_size),0) AS storage
450
+ FROM artifacts GROUP BY user_id
451
+ ) a ON a.user_id = u.id
452
+ ORDER BY runs DESC LIMIT 8
453
+ `).all();
454
+ const recentRuns = db.prepare(`
455
+ SELECT r.id, u.username, r.title, r.status, r.total_tokens,
456
+ r.created_at, r.completed_at
457
+ FROM agent_runs r
458
+ JOIN users u ON u.id = r.user_id
459
+ ORDER BY r.created_at DESC LIMIT 20
460
+ `).all();
461
+ res.json({ stats, topUsers, recentRuns });
462
+ } catch (err) {
463
+ res.status(500).json({ error: String(err.message || err) });
464
+ }
465
+ });
466
+
467
+ // --- User management ---
468
+
469
+ router.get('/api/users', requireAdminAuth, (req, res) => {
470
+ const db = require('../db/database');
471
+ const q = req.query.q ? `%${req.query.q}%` : null;
472
+ try {
473
+ const sql = `
474
+ SELECT u.id, u.username, u.display_name, u.email, u.email_verified_at,
475
+ u.created_at, u.last_login,
476
+ COALESCE(r.run_count, 0) AS run_count,
477
+ COALESCE(a.storage_bytes,0) AS storage_bytes
478
+ FROM users u
479
+ LEFT JOIN (
480
+ SELECT user_id, COUNT(*) AS run_count
481
+ FROM agent_runs GROUP BY user_id
482
+ ) r ON r.user_id = u.id
483
+ LEFT JOIN (
484
+ SELECT user_id, COALESCE(SUM(byte_size),0) AS storage_bytes
485
+ FROM artifacts GROUP BY user_id
486
+ ) a ON a.user_id = u.id
487
+ ${q ? 'WHERE u.username LIKE ? OR u.email LIKE ?' : ''}
488
+ ORDER BY u.created_at DESC LIMIT 200`;
489
+ const users = q ? db.prepare(sql).all(q, q) : db.prepare(sql).all();
490
+ res.json({ users });
491
+ } catch (err) {
492
+ res.status(500).json({ error: String(err.message || err) });
493
+ }
494
+ });
495
+
496
+ router.delete('/api/users/:id', requireAdminAuth, (req, res) => {
497
+ const db = require('../db/database');
498
+ const { DATA_DIR } = require('../../runtime/paths');
499
+ const { id } = req.params;
500
+ if (!id) return res.status(400).json({ error: 'Missing user id' });
501
+ try {
502
+ // Collect artifact paths before deletion
503
+ const artifacts = db.prepare('SELECT storage_path FROM artifacts WHERE user_id = ?').all(id);
504
+
505
+ const erase = db.transaction((uid) => {
506
+ db.prepare('DELETE FROM conversation_messages WHERE conversation_id IN (SELECT id FROM conversations WHERE user_id = ?)').run(uid);
507
+ db.prepare('DELETE FROM conversations WHERE user_id = ?').run(uid);
508
+ db.prepare('DELETE FROM agent_steps WHERE run_id IN (SELECT id FROM agent_runs WHERE user_id = ?)').run(uid);
509
+ db.prepare('DELETE FROM agent_runs WHERE user_id = ?').run(uid);
510
+ db.prepare('DELETE FROM messages WHERE user_id = ?').run(uid);
511
+ db.prepare('DELETE FROM memories WHERE user_id = ?').run(uid);
512
+ db.prepare('DELETE FROM integration_connections WHERE user_id = ?').run(uid);
513
+ db.prepare('DELETE FROM platform_connections WHERE user_id = ?').run(uid);
514
+ db.prepare('DELETE FROM mcp_servers WHERE user_id = ?').run(uid);
515
+ db.prepare('DELETE FROM user_settings WHERE user_id = ?').run(uid);
516
+ db.prepare('DELETE FROM user_sessions WHERE user_id = ?').run(uid);
517
+ db.prepare('DELETE FROM desktop_companion_devices WHERE user_id = ?').run(uid);
518
+ db.prepare('DELETE FROM scheduled_tasks WHERE user_id = ?').run(uid);
519
+ db.prepare('DELETE FROM recording_sessions WHERE user_id = ?').run(uid);
520
+ db.prepare('DELETE FROM screen_history WHERE user_id = ?').run(uid);
521
+ db.prepare('DELETE FROM agents WHERE user_id = ?').run(uid);
522
+ db.prepare('DELETE FROM artifacts WHERE user_id = ?').run(uid);
523
+ db.prepare('DELETE FROM users WHERE id = ?').run(uid);
524
+ });
525
+ erase(id);
526
+
527
+ // Clean up artifact files on disk
528
+ for (const artifact of artifacts) {
529
+ try {
530
+ const abs = path.isAbsolute(artifact.storage_path)
531
+ ? artifact.storage_path
532
+ : path.join(DATA_DIR, artifact.storage_path);
533
+ fs.rmSync(abs, { force: true });
534
+ } catch {}
535
+ }
536
+ try { fs.rmSync(path.join(DATA_DIR, 'artifacts', id), { recursive: true, force: true }); } catch {}
537
+
538
+ res.json({ ok: true });
539
+ } catch (err) {
540
+ res.status(500).json({ error: String(err.message || err) });
541
+ }
542
+ });
543
+
544
+ router.delete('/api/users/:id/sessions', requireAdminAuth, (req, res) => {
545
+ const db = require('../db/database');
546
+ const { id } = req.params;
547
+ try {
548
+ db.prepare('UPDATE user_sessions SET revoked_at = ? WHERE user_id = ?').run(new Date().toISOString(), id);
549
+ res.json({ ok: true });
550
+ } catch (err) {
551
+ res.status(500).json({ error: String(err.message || err) });
552
+ }
553
+ });
554
+
555
+ // --- SQL editor (read-only SELECT only) ---
556
+
557
+ const SQL_BLOCKED = /\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|ATTACH|DETACH|TRUNCATE|VACUUM|REINDEX|REPLACE|UPSERT|PRAGMA)\b/i;
558
+
559
+ router.post('/api/sql', requireAdminAuth, sqlLimiter, express.json(), (req, res) => {
560
+ const { query } = req.body || {};
561
+ if (!query || typeof query !== 'string') return res.status(400).json({ error: 'No query provided' });
562
+ const trimmed = query.trim();
563
+ if (!/^(SELECT|WITH)\b/i.test(trimmed)) return res.status(400).json({ error: 'Only SELECT (or WITH …) queries are allowed' });
564
+ if (SQL_BLOCKED.test(trimmed)) return res.status(400).json({ error: 'Query contains a blocked SQL keyword' });
565
+ try {
566
+ const db = require('../db/database');
567
+ const rows = db.prepare(trimmed).all();
568
+ const limited = rows.slice(0, 500);
569
+ const columns = limited.length ? Object.keys(limited[0]) : [];
570
+ res.json({ rows: limited, columns, truncated: rows.length > 500, total: rows.length });
571
+ } catch (err) {
572
+ res.status(400).json({ error: String(err.message || err) });
573
+ }
574
+ });
575
+
576
+ // --- Providers ---
577
+
578
+ const PROVIDERS = [
579
+ { key: 'ANTHROPIC_API_KEY', label: 'Anthropic (Claude)', type: 'key' },
580
+ { key: 'OPENAI_API_KEY', label: 'OpenAI', type: 'key' },
581
+ { key: 'XAI_API_KEY', label: 'xAI (Grok)', type: 'key' },
582
+ { key: 'GOOGLE_AI_KEY', label: 'Google (Gemini)', type: 'key' },
583
+ { key: 'MINIMAX_API_KEY', label: 'MiniMax', type: 'key' },
584
+ { key: 'NVIDIA_API_KEY', label: 'NVIDIA NIM', type: 'key' },
585
+ { key: 'OPENROUTER_API_KEY', label: 'OpenRouter', type: 'key' },
586
+ { key: 'BRAVE_SEARCH_API_KEY', label: 'Brave Search', type: 'key' },
587
+ { key: 'DEEPGRAM_API_KEY', label: 'Deepgram (Voice)', type: 'key' },
588
+ { key: 'OLLAMA_URL', label: 'Ollama (Local)', type: 'url' },
589
+ ];
590
+
591
+ const ALLOWED_PROVIDER_KEYS = new Set(PROVIDERS.map((p) => p.key));
592
+
593
+ router.get('/api/providers', requireAdminAuth, (req, res) => {
594
+ const result = PROVIDERS.map(({ key, label, type }) => {
595
+ const value = process.env[key] || '';
596
+ let hint = '';
597
+ if (value) {
598
+ hint = type === 'url'
599
+ ? value
600
+ : value.length > 8
601
+ ? `${value.slice(0, 4)}${'•'.repeat(4)}${value.slice(-4)}`
602
+ : '•'.repeat(value.length);
603
+ }
604
+ return { key, label, type, configured: Boolean(value), hint };
605
+ });
606
+ res.json({ providers: result });
607
+ });
608
+
609
+ router.put('/api/providers', requireAdminAuth, express.json(), (req, res) => {
610
+ const { key, value } = req.body || {};
611
+ if (!ALLOWED_PROVIDER_KEYS.has(key)) {
612
+ return res.status(400).json({ error: 'Unknown provider key' });
613
+ }
614
+ const trimmed = String(value || '').trim().replace(/[\r\n]/g, '');
615
+ upsertEnvValue(ENV_FILE, key, trimmed);
616
+ if (trimmed) {
617
+ process.env[key] = trimmed;
618
+ } else {
619
+ delete process.env[key];
620
+ }
621
+ res.json({ ok: true });
622
+ });
623
+
624
+ // --- Static files ---
625
+
626
+ router.use(express.static(ADMIN_DIR));
627
+
628
+ router.get('*', requireAdminAuth, (req, res) => {
629
+ res.sendFile(path.join(ADMIN_DIR, 'index.html'));
630
+ });
631
+
632
+ module.exports = router;
@@ -41,6 +41,7 @@ router.put('/:id', (req, res) => {
41
41
  try {
42
42
  res.json(updateAgent(req.session.userId, req.params.id, req.body || {}));
43
43
  } catch (err) {
44
+ if (err.message === 'Agent not found.') return res.status(404).json({ error: err.message });
44
45
  res.status(400).json({ error: sanitizeError(err) });
45
46
  }
46
47
  });
@@ -49,6 +50,7 @@ router.post('/:id/default', (req, res) => {
49
50
  try {
50
51
  res.json(setDefaultAgent(req.session.userId, req.params.id));
51
52
  } catch (err) {
53
+ if (err.message === 'Agent not found.') return res.status(404).json({ error: err.message });
52
54
  res.status(400).json({ error: sanitizeError(err) });
53
55
  }
54
56
  });
@@ -57,6 +59,7 @@ router.delete('/:id', (req, res) => {
57
59
  try {
58
60
  res.json(archiveAgent(req.session.userId, req.params.id));
59
61
  } catch (err) {
62
+ if (err.message === 'Agent not found.') return res.status(404).json({ error: err.message });
60
63
  res.status(400).json({ error: sanitizeError(err) });
61
64
  }
62
65
  });
@@ -186,7 +186,7 @@ router.post('/:id/abort', (req, res) => {
186
186
  const run = db.prepare('SELECT id FROM agent_runs WHERE id = ? AND user_id = ?').get(req.params.id, req.session.userId);
187
187
  if (!run) return res.status(404).json({ error: 'Run not found' });
188
188
  const engine = req.app.locals.agentEngine;
189
- engine.abort(req.params.id);
189
+ engine.abort(req.params.id, { userId: req.session.userId });
190
190
  res.json({ success: true });
191
191
  } catch (err) {
192
192
  res.status(500).json({ error: sanitizeError(err) });
@@ -129,7 +129,9 @@ function readAuthenticatedUser(req) {
129
129
  if (!user) {
130
130
  try {
131
131
  req.session.destroy(() => {});
132
- } catch (_) {}
132
+ } catch (destroyError) {
133
+ console.warn('Auth stale-session cleanup failed:', destroyError.message);
134
+ }
133
135
  return null;
134
136
  }
135
137
  return user;