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,290 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NeoAgent Admin</title>
7
+ <link rel="icon" href="/admin/logo.svg" type="image/svg+xml">
8
+ <link rel="stylesheet" href="/admin/admin.css">
9
+ <style>
10
+ body {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ min-height: 100vh;
15
+ }
16
+
17
+ .login-wrap {
18
+ width: 100%;
19
+ max-width: 380px;
20
+ padding: 20px;
21
+ }
22
+
23
+ .login-card {
24
+ background: var(--bg-card);
25
+ border: 1px solid var(--border);
26
+ border-radius: var(--radius);
27
+ padding: 36px 32px 32px;
28
+ }
29
+
30
+ .brand {
31
+ margin-bottom: 32px;
32
+ }
33
+
34
+ .brand-mark {
35
+ width: 32px;
36
+ height: 32px;
37
+ border-radius: 9px;
38
+ }
39
+
40
+ .brand-mark img {
41
+ width: 32px;
42
+ height: 32px;
43
+ }
44
+
45
+ .login-heading {
46
+ font-size: 20px;
47
+ font-weight: 700;
48
+ color: var(--text);
49
+ letter-spacing: -0.3px;
50
+ margin-bottom: 4px;
51
+ }
52
+
53
+ .login-sub {
54
+ font-size: 13px;
55
+ color: var(--text-muted);
56
+ margin-bottom: 28px;
57
+ }
58
+
59
+ .login-sub code {
60
+ font-family: var(--font-mono);
61
+ font-size: 12px;
62
+ background: var(--bg-secondary);
63
+ padding: 1px 5px;
64
+ border-radius: 4px;
65
+ color: var(--accent);
66
+ }
67
+
68
+ .submit-btn {
69
+ width: 100%;
70
+ padding: 11px;
71
+ font-size: 14px;
72
+ margin-top: 4px;
73
+ }
74
+
75
+ /* ── Admin hero mark ────────────────────────────── */
76
+ .admin-hero {
77
+ display: flex;
78
+ flex-direction: column;
79
+ align-items: center;
80
+ margin-bottom: 28px;
81
+ gap: 14px;
82
+ }
83
+
84
+ .admin-a-mark {
85
+ width: 80px;
86
+ height: 80px;
87
+ border-radius: 22px;
88
+ background: var(--bg-secondary);
89
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
90
+ box-shadow:
91
+ 0 0 0 4px color-mix(in srgb, var(--accent) 8%, transparent),
92
+ 0 12px 32px rgba(0,0,0,0.45);
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ font-size: 42px;
97
+ font-weight: 800;
98
+ color: var(--accent);
99
+ letter-spacing: -2px;
100
+ font-family: var(--font-sans);
101
+ line-height: 1;
102
+ user-select: none;
103
+ }
104
+
105
+ .admin-hero .brand {
106
+ margin-bottom: 0;
107
+ justify-content: center;
108
+ }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div class="login-wrap">
113
+ <div class="login-card">
114
+ <div class="admin-hero">
115
+ <div class="admin-a-mark" aria-hidden="true">A</div>
116
+ <div class="brand">
117
+ <div class="brand-mark">
118
+ <img src="/admin/logo.svg" width="32" height="32" alt="">
119
+ </div>
120
+ <div>
121
+ <div class="brand-name">NeoAgent</div>
122
+ <div class="brand-sub">Admin</div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Step 1: credentials -->
128
+ <div id="step-credentials">
129
+ <h1 class="login-heading">Sign in</h1>
130
+ <p class="login-sub">
131
+ Credentials are in your <code>.env</code> or via <code>neoagent admin</code>
132
+ </p>
133
+
134
+ <div class="alert alert-error" id="error" role="alert"></div>
135
+
136
+ <form id="form" novalidate>
137
+ <div class="field">
138
+ <label for="username">Username</label>
139
+ <input
140
+ id="username"
141
+ type="text"
142
+ autocomplete="username"
143
+ autofocus
144
+ required
145
+ spellcheck="false"
146
+ autocorrect="off"
147
+ autocapitalize="none"
148
+ >
149
+ </div>
150
+ <div class="field">
151
+ <label for="password">Password</label>
152
+ <input
153
+ id="password"
154
+ type="password"
155
+ autocomplete="current-password"
156
+ required
157
+ >
158
+ </div>
159
+ <button type="submit" class="btn btn-primary submit-btn" id="submit-btn">
160
+ Sign in
161
+ </button>
162
+ </form>
163
+ </div>
164
+
165
+ <!-- Step 2: 2FA code (shown after password is accepted) -->
166
+ <div id="step-totp" style="display:none;">
167
+ <h1 class="login-heading">Two-factor authentication</h1>
168
+ <p class="login-sub">Enter the 6-digit code from your authenticator app, or a recovery code.</p>
169
+
170
+ <div class="alert alert-error" id="totp-error" role="alert"></div>
171
+
172
+ <form id="totp-form" novalidate>
173
+ <div class="field">
174
+ <label for="totp-code">Authenticator code</label>
175
+ <input
176
+ id="totp-code"
177
+ type="text"
178
+ inputmode="numeric"
179
+ autocomplete="one-time-code"
180
+ maxlength="11"
181
+ placeholder="123 456"
182
+ spellcheck="false"
183
+ autocorrect="off"
184
+ autocapitalize="none"
185
+ required
186
+ >
187
+ </div>
188
+ <button type="submit" class="btn btn-primary submit-btn" id="totp-btn">
189
+ Verify
190
+ </button>
191
+ </form>
192
+ <div style="margin-top:14px;text-align:center;">
193
+ <button type="button" class="btn btn-ghost" style="font-size:12px;padding:4px 10px;"
194
+ onclick="backToCredentials()">← Back</button>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ <script>
201
+ const form = document.getElementById('form');
202
+ const submitBtn = document.getElementById('submit-btn');
203
+ const errorEl = document.getElementById('error');
204
+ const totpForm = document.getElementById('totp-form');
205
+ const totpBtn = document.getElementById('totp-btn');
206
+ const totpError = document.getElementById('totp-error');
207
+
208
+ function showStep(step) {
209
+ document.getElementById('step-credentials').style.display = step === 1 ? '' : 'none';
210
+ document.getElementById('step-totp').style.display = step === 2 ? '' : 'none';
211
+ if (step === 2) setTimeout(() => document.getElementById('totp-code')?.focus(), 50);
212
+ if (step === 1) setTimeout(() => document.getElementById('username')?.focus(), 50);
213
+ }
214
+
215
+ function backToCredentials() { showStep(1); }
216
+
217
+ form.addEventListener('submit', async (e) => {
218
+ e.preventDefault();
219
+ errorEl.classList.remove('visible');
220
+ submitBtn.disabled = true;
221
+ submitBtn.textContent = 'Signing in…';
222
+
223
+ try {
224
+ const res = await fetch('/admin/api/login', {
225
+ method: 'POST',
226
+ headers: { 'Content-Type': 'application/json' },
227
+ body: JSON.stringify({
228
+ username: document.getElementById('username').value.trim(),
229
+ password: document.getElementById('password').value,
230
+ }),
231
+ });
232
+
233
+ const body = await res.json().catch(() => ({}));
234
+
235
+ if (res.ok || body.requiresTwoFactor) {
236
+ if (body.requiresTwoFactor) {
237
+ // Password accepted — go to 2FA step
238
+ showStep(2);
239
+ } else {
240
+ window.location.replace('/admin');
241
+ }
242
+ return;
243
+ }
244
+
245
+ errorEl.textContent = body.error || 'Invalid credentials';
246
+ errorEl.classList.add('visible');
247
+ } catch {
248
+ errorEl.textContent = 'Network error — is the server running?';
249
+ errorEl.classList.add('visible');
250
+ } finally {
251
+ submitBtn.disabled = false;
252
+ submitBtn.textContent = 'Sign in';
253
+ }
254
+ });
255
+
256
+ totpForm.addEventListener('submit', async (e) => {
257
+ e.preventDefault();
258
+ totpError.classList.remove('visible');
259
+ totpBtn.disabled = true;
260
+ totpBtn.textContent = 'Verifying…';
261
+
262
+ try {
263
+ const code = document.getElementById('totp-code').value.trim();
264
+ const res = await fetch('/admin/api/2fa/verify', {
265
+ method: 'POST',
266
+ headers: { 'Content-Type': 'application/json' },
267
+ body: JSON.stringify({ code }),
268
+ });
269
+
270
+ if (res.ok) {
271
+ window.location.replace('/admin');
272
+ return;
273
+ }
274
+
275
+ const body = await res.json().catch(() => ({}));
276
+ totpError.textContent = body.error || 'Invalid code';
277
+ totpError.classList.add('visible');
278
+ document.getElementById('totp-code').value = '';
279
+ document.getElementById('totp-code').focus();
280
+ } catch {
281
+ totpError.textContent = 'Network error';
282
+ totpError.classList.add('visible');
283
+ } finally {
284
+ totpBtn.disabled = false;
285
+ totpBtn.textContent = 'Verify';
286
+ }
287
+ });
288
+ </script>
289
+ </body>
290
+ </html>
@@ -0,0 +1,43 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 100 100">
2
+ <defs>
3
+ <linearGradient id="tile" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" stop-color="#705331"></stop>
5
+ <stop offset="100%" stop-color="#22564c"></stop>
6
+ </linearGradient>
7
+ <radialGradient id="shine" cx="26%" cy="14%" r="72%">
8
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.22"></stop>
9
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"></stop>
10
+ </radialGradient>
11
+ <radialGradient id="core" cx="38%" cy="32%" r="75%">
12
+ <stop offset="0%" stop-color="#fffdf6"></stop>
13
+ <stop offset="48%" stop-color="#eee3cc"></stop>
14
+ <stop offset="100%" stop-color="#b5a888"></stop>
15
+ </radialGradient>
16
+ <radialGradient id="node" cx="36%" cy="30%" r="80%">
17
+ <stop offset="0%" stop-color="#f8dca8"></stop>
18
+ <stop offset="52%" stop-color="#d3a85f"></stop>
19
+ <stop offset="100%" stop-color="#9b6f2f"></stop>
20
+ </radialGradient>
21
+ <linearGradient id="tube" x1="0%" y1="0%" x2="0%" y2="100%">
22
+ <stop offset="0%" stop-color="#fffaf0"></stop>
23
+ <stop offset="52%" stop-color="#efe7d6"></stop>
24
+ <stop offset="100%" stop-color="#cabf9f"></stop>
25
+ </linearGradient>
26
+ <filter id="sh" x="-30%" y="-30%" width="160%" height="160%">
27
+ <feDropShadow dx="0" dy="1.1" stdDeviation="1.1" flood-color="#000000" flood-opacity="0.45"></feDropShadow>
28
+ </filter>
29
+ <clipPath id="tc"><rect x="0" y="0" width="100" height="100" rx="22.5"></rect></clipPath>
30
+ </defs>
31
+ <g clip-path="url(#tc)">
32
+ <rect x="0" y="0" width="100" height="100" fill="url(#tile)"></rect>
33
+ <rect x="0" y="0" width="100" height="100" fill="url(#shine)"></rect>
34
+ <g fill="none" stroke="url(#tube)" stroke-linecap="round" filter="url(#sh)">
35
+ <circle cx="50" cy="50" r="17" stroke-width="5" stroke-dasharray="78 29" transform="rotate(-35 50 50)" stroke-opacity="0.92"></circle>
36
+ <circle cx="50" cy="50" r="30" stroke-width="5" stroke-dasharray="150 39" transform="rotate(70 50 50)" stroke-opacity="0.8"></circle>
37
+ </g>
38
+ <circle cx="50" cy="50" r="7" fill="url(#core)"></circle>
39
+ <circle cx="50" cy="20" r="5.6" fill="url(#node)"></circle>
40
+ <circle cx="48" cy="18.4" r="1.7" fill="#fff7e6" fill-opacity="0.8"></circle>
41
+ </g>
42
+ <rect x="0.6" y="0.6" width="98.8" height="98.8" rx="22" fill="none" stroke="rgba(255,255,255,0.16)" stroke-width="1.2"></rect>
43
+ </svg>
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ // ── SQL Editor ─────────────────────────────────────────────────────────────
4
+
5
+ const SQL_TEMPLATES = [
6
+ { label: '— Select a template —', query: '' },
7
+ { label: 'User summary', query: `SELECT u.id, u.username, u.email,\n u.created_at, u.last_login,\n COUNT(DISTINCT r.id) AS runs,\n COALESCE(SUM(r.total_tokens),0) AS tokens\nFROM users u\nLEFT JOIN agent_runs r ON r.user_id = u.id\nGROUP BY u.id\nORDER BY runs DESC\nLIMIT 50` },
8
+ { label: 'Recent failed runs', query: `SELECT r.id, u.username, r.title, r.status,\n r.error, r.created_at\nFROM agent_runs r\nJOIN users u ON u.id = r.user_id\nWHERE r.status = 'failed'\nORDER BY r.created_at DESC\nLIMIT 50` },
9
+ { label: 'Artifact storage by user', query: `SELECT u.username,\n COUNT(a.id) AS files,\n SUM(a.byte_size) AS bytes,\n ROUND(SUM(a.byte_size) / 1048576.0, 2) AS mb\nFROM users u\nLEFT JOIN artifacts a ON a.user_id = u.id\nGROUP BY u.id\nORDER BY bytes DESC` },
10
+ { label: 'Active sessions', query: `SELECT u.username, s.ip_address,\n s.user_agent, s.created_at, s.last_seen_at\nFROM user_sessions s\nJOIN users u ON u.id = s.user_id\nWHERE s.revoked_at IS NULL\n AND s.expires_at > datetime('now')\nORDER BY s.last_seen_at DESC\nLIMIT 50` },
11
+ { label: 'Runs per day (30 days)', query: `SELECT DATE(created_at) AS day,\n COUNT(*) AS runs,\n COALESCE(SUM(total_tokens),0) AS tokens\nFROM agent_runs\nWHERE created_at >= datetime('now', '-30 days')\nGROUP BY day\nORDER BY day DESC` },
12
+ { label: 'Most used agents', query: `SELECT a.slug, a.display_name, u.username AS owner,\n COUNT(r.id) AS runs\nFROM agents a\nJOIN users u ON u.id = a.user_id\nLEFT JOIN agent_runs r ON r.agent_id = a.id\nGROUP BY a.id\nORDER BY runs DESC\nLIMIT 20` },
13
+ { label: 'Integration connections', query: `SELECT u.username, ic.provider_key,\n ic.status, ic.account_email, ic.last_connected_at\nFROM integration_connections ic\nJOIN users u ON u.id = ic.user_id\nORDER BY ic.last_connected_at DESC\nLIMIT 50` },
14
+ { label: 'All tables', query: `SELECT name FROM sqlite_master WHERE type='table' ORDER BY name` },
15
+ { label: 'Table info (users)', query: `SELECT * FROM pragma_table_info('users')` },
16
+ ];
17
+
18
+ function loadSql() {
19
+ // Only initialize the editor once
20
+ if (document.getElementById('sql-editor')?.dataset.ready === '1') return;
21
+ initSqlEditor();
22
+ }
23
+
24
+ function initSqlEditor() {
25
+ const el = document.getElementById('sql-content');
26
+ if (!el) return;
27
+
28
+ const templateOptions = SQL_TEMPLATES.map((t, i) =>
29
+ `<option value="${i}">${esc(t.label)}</option>`).join('');
30
+
31
+ el.innerHTML = `
32
+ <div class="sql-toolbar">
33
+ <select id="sql-template" onchange="onSqlTemplate(this)" style="width:auto;min-width:220px;" aria-label="Query template">
34
+ ${templateOptions}
35
+ </select>
36
+ <span class="spacer"></span>
37
+ <button class="btn btn-primary" id="sql-run-btn" onclick="runSql()" style="padding:7px 16px;">
38
+ <svg viewBox="0 0 20 20" fill="currentColor" style="width:13px;height:13px;" aria-hidden="true">
39
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/>
40
+ </svg>
41
+ Run
42
+ </button>
43
+ </div>
44
+ <textarea id="sql-editor" data-ready="1" class="sql-textarea" spellcheck="false" autocorrect="off" autocapitalize="off"
45
+ placeholder="SELECT * FROM users LIMIT 10"
46
+ onkeydown="onSqlKeydown(event)"
47
+ aria-label="SQL query editor"
48
+ ></textarea>
49
+ <div id="sql-results"></div>`;
50
+ }
51
+
52
+ function onSqlTemplate(sel) {
53
+ const idx = parseInt(sel.value, 10);
54
+ const q = SQL_TEMPLATES[idx]?.query || '';
55
+ const editor = document.getElementById('sql-editor');
56
+ if (editor && q) { editor.value = q; editor.focus(); }
57
+ }
58
+
59
+ function onSqlKeydown(e) {
60
+ // Ctrl+Enter / Cmd+Enter to run
61
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
62
+ e.preventDefault();
63
+ runSql();
64
+ }
65
+ // Tab → insert 2 spaces
66
+ if (e.key === 'Tab') {
67
+ e.preventDefault();
68
+ const ta = e.target;
69
+ const s = ta.selectionStart, end = ta.selectionEnd;
70
+ ta.value = ta.value.slice(0, s) + ' ' + ta.value.slice(end);
71
+ ta.selectionStart = ta.selectionEnd = s + 2;
72
+ }
73
+ }
74
+
75
+ async function runSql() {
76
+ const query = document.getElementById('sql-editor')?.value?.trim();
77
+ if (!query) return;
78
+ const resultsEl = document.getElementById('sql-results');
79
+ const runBtn = document.getElementById('sql-run-btn');
80
+ if (!resultsEl || !runBtn) return;
81
+
82
+ runBtn.disabled = true;
83
+ resultsEl.innerHTML = '<div class="empty"><span class="spinner"></span></div>';
84
+
85
+ try {
86
+ const res = await api('/admin/api/sql', {
87
+ method: 'POST',
88
+ headers: { 'Content-Type': 'application/json' },
89
+ body: JSON.stringify({ query }),
90
+ });
91
+ const data = await res.json();
92
+
93
+ if (!res.ok) {
94
+ resultsEl.innerHTML = `<div class="sql-error">${esc(data.error || 'Query failed')}</div>`;
95
+ return;
96
+ }
97
+
98
+ if (!data.rows?.length) {
99
+ resultsEl.innerHTML = '<div class="empty">Query returned 0 rows</div>';
100
+ return;
101
+ }
102
+
103
+ resultsEl.innerHTML = `
104
+ <div class="sql-meta">
105
+ ${fmtNum(data.rows.length)}${data.truncated ? ` of ${fmtNum(data.total)}` : ''} row${data.rows.length === 1 ? '' : 's'}
106
+ ${data.truncated ? '<span class="badge badge-warn" style="margin-left:6px;">truncated at 500</span>' : ''}
107
+ <button class="btn btn-ghost" style="margin-left:auto;padding:4px 10px;font-size:11px;" onclick="copySqlResults()">Copy CSV</button>
108
+ </div>
109
+ <div class="sql-result-wrap">
110
+ <table class="sql-result-table">
111
+ <thead><tr>${data.columns.map((c) => `<th>${esc(c)}</th>`).join('')}</tr></thead>
112
+ <tbody>${data.rows.map((row) =>
113
+ `<tr>${data.columns.map((c) => `<td>${esc(row[c] == null ? 'NULL' : String(row[c]))}</td>`).join('')}</tr>`
114
+ ).join('')}</tbody>
115
+ </table>
116
+ </div>`;
117
+ // stash for copy
118
+ window._lastSqlData = data;
119
+ } catch (err) {
120
+ if (err.message !== 'unauthorized') {
121
+ resultsEl.innerHTML = `<div class="sql-error">Network error</div>`;
122
+ }
123
+ } finally {
124
+ runBtn.disabled = false;
125
+ }
126
+ }
127
+
128
+ function copySqlResults() {
129
+ const d = window._lastSqlData;
130
+ if (!d) return;
131
+ const header = d.columns.join(',');
132
+ const rows = d.rows.map((r) => d.columns.map((c) => JSON.stringify(r[c] ?? '')).join(','));
133
+ navigator.clipboard.writeText([header, ...rows].join('\n')).catch(() => {});
134
+ }
@@ -0,0 +1,147 @@
1
+ 'use strict';
2
+
3
+ // ── User Management ────────────────────────────────────────────────────────
4
+
5
+ let _userSearchTimer = null;
6
+
7
+ async function loadUsers() {
8
+ const q = document.getElementById('user-search')?.value?.trim() || '';
9
+ await fetchUsers(q);
10
+ }
11
+
12
+ async function fetchUsers(q = '') {
13
+ const el = document.getElementById('users-table-wrap');
14
+ if (!el) return;
15
+ el.innerHTML = '<div class="empty"><span class="spinner"></span></div>';
16
+ try {
17
+ const url = q ? `/admin/api/users?q=${encodeURIComponent(q)}` : '/admin/api/users';
18
+ const data = await api(url).then((r) => r.json());
19
+ renderUsersTable(el, data.users || []);
20
+ const cnt = document.getElementById('users-count');
21
+ if (cnt) cnt.textContent = `${(data.users || []).length} user${(data.users || []).length === 1 ? '' : 's'}`;
22
+ } catch (err) {
23
+ if (err.message !== 'unauthorized') el.innerHTML = '<div class="empty">Failed to load users</div>';
24
+ }
25
+ }
26
+
27
+ function onUserSearch() {
28
+ clearTimeout(_userSearchTimer);
29
+ _userSearchTimer = setTimeout(() => {
30
+ const q = document.getElementById('user-search')?.value?.trim() || '';
31
+ fetchUsers(q);
32
+ }, 300);
33
+ }
34
+
35
+ function renderUsersTable(el, users) {
36
+ if (!users.length) {
37
+ el.innerHTML = '<div class="empty">No users found</div>';
38
+ return;
39
+ }
40
+ el.innerHTML = `
41
+ <table class="users-table">
42
+ <thead><tr>
43
+ <th>User</th>
44
+ <th>Email</th>
45
+ <th>Joined</th>
46
+ <th>Last Login</th>
47
+ <th>Runs</th>
48
+ <th>Storage</th>
49
+ <th style="text-align:right;">Actions</th>
50
+ </tr></thead>
51
+ <tbody>${users.map((u) => `
52
+ <tr data-uid="${esc(u.id)}">
53
+ <td>
54
+ <div style="display:flex;align-items:center;gap:9px;">
55
+ <span class="user-avatar">${esc((u.display_name || u.username || '?')[0]).toUpperCase()}</span>
56
+ <div>
57
+ <div style="font-weight:600;color:var(--text);">${esc(u.display_name || u.username)}</div>
58
+ ${u.display_name && u.display_name !== u.username ? `<div style="font-size:11px;color:var(--text-muted);">@${esc(u.username)}</div>` : ''}
59
+ </div>
60
+ </div>
61
+ </td>
62
+ <td>
63
+ <span style="font-size:12px;">${esc(u.email || '—')}</span>
64
+ ${u.email_verified_at
65
+ ? '<span class="badge badge-ok" style="font-size:10px;margin-left:5px;">verified</span>'
66
+ : u.email ? '<span class="badge badge-warn" style="font-size:10px;margin-left:5px;">unverified</span>' : ''}
67
+ </td>
68
+ <td style="font-size:12px;color:var(--text-muted);">${fmtDate(u.created_at)}</td>
69
+ <td style="font-size:12px;color:var(--text-muted);">${u.last_login ? fmtDate(u.last_login) : '—'}</td>
70
+ <td style="font-family:var(--font-mono);font-size:12px;">${fmtNum(u.run_count)}</td>
71
+ <td style="font-family:var(--font-mono);font-size:12px;">${fmtBytes(u.storage_bytes)}</td>
72
+ <td>
73
+ <div style="display:flex;gap:6px;justify-content:flex-end;">
74
+ <button class="btn btn-ghost" style="padding:5px 10px;font-size:11px;"
75
+ onclick="forceLogout('${esc(u.id)}','${esc(u.username)}',this)" title="Revoke all sessions">
76
+ Logout
77
+ </button>
78
+ <button class="btn btn-danger" style="padding:5px 10px;font-size:11px;"
79
+ onclick="deleteUser('${esc(u.id)}','${esc(u.display_name || u.username)}',this)" title="GDPR: delete all user data">
80
+ Delete
81
+ </button>
82
+ </div>
83
+ </td>
84
+ </tr>`).join('')}
85
+ </tbody>
86
+ </table>
87
+ <p class="gdpr-note">
88
+ ⚠ <strong>Delete</strong> permanently erases all user data (runs, messages, memories, artifacts, sessions) — irreversible. Required by GDPR Art. 17 right to erasure.
89
+ </p>`;
90
+ }
91
+
92
+ async function forceLogout(id, username, btn) {
93
+ if (!confirm(`Force logout ${username}?\n\nThis will revoke all active sessions. They can log back in.`)) return;
94
+ btn.disabled = true;
95
+ try {
96
+ const res = await api(`/admin/api/users/${id}/sessions`, { method: 'DELETE' });
97
+ if (res.ok) {
98
+ btn.textContent = 'Done';
99
+ setTimeout(loadUsers, 800);
100
+ } else {
101
+ const b = await res.json().catch(() => ({}));
102
+ alert(b.error || 'Failed');
103
+ btn.disabled = false;
104
+ }
105
+ } catch (err) {
106
+ if (err.message !== 'unauthorized') alert('Network error');
107
+ btn.disabled = false;
108
+ }
109
+ }
110
+
111
+ async function deleteUser(id, displayName, btn) {
112
+ if (!confirm(
113
+ `⚠ GDPR Erasure — permanently delete "${displayName}"?\n\n` +
114
+ 'This will delete the account and ALL associated data:\n' +
115
+ '• Agent runs, steps, and messages\n• Memories and conversations\n• Integrations and platform connections\n• Artifacts and files on disk\n• All sessions\n\n' +
116
+ 'This action CANNOT be undone.'
117
+ )) return;
118
+ btn.disabled = true;
119
+ btn.textContent = 'Deleting…';
120
+ try {
121
+ const res = await api(`/admin/api/users/${id}`, { method: 'DELETE' });
122
+ if (res.ok) {
123
+ btn.closest('tr')?.remove();
124
+ const cnt = document.getElementById('users-count');
125
+ if (cnt) {
126
+ const n = parseInt(cnt.textContent) - 1;
127
+ cnt.textContent = `${n} user${n === 1 ? '' : 's'}`;
128
+ }
129
+ } else {
130
+ const b = await res.json().catch(() => ({}));
131
+ alert(b.error || 'Failed to delete user');
132
+ btn.disabled = false;
133
+ btn.textContent = 'Delete';
134
+ }
135
+ } catch (err) {
136
+ if (err.message !== 'unauthorized') alert('Network error');
137
+ btn.disabled = false;
138
+ btn.textContent = 'Delete';
139
+ }
140
+ }
141
+
142
+ function fmtDate(iso) {
143
+ if (!iso) return '—';
144
+ try {
145
+ return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
146
+ } catch { return iso; }
147
+ }
@@ -35,7 +35,9 @@ function isAllowedOrigin(origin, options = {}) {
35
35
 
36
36
  function validateOrigin(origin, callback, options = {}) {
37
37
  if (isAllowedOrigin(origin, options)) return callback(null, true);
38
- return callback(new Error(`Origin not allowed: ${origin || 'unknown'}`));
38
+ const error = new Error(`Origin not allowed: ${origin || 'unknown'}`);
39
+ error.statusCode = 403;
40
+ return callback(error);
39
41
  }
40
42
 
41
43
  module.exports = {