neoagent 3.1.1-beta.9 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/.env.example +2 -2
  2. package/README.md +5 -1
  3. package/docs/architecture.md +1 -1
  4. package/docs/benchmarking.md +68 -87
  5. package/docs/capabilities.md +1 -1
  6. package/docs/clients-and-devices.md +3 -4
  7. package/docs/configuration.md +1 -1
  8. package/docs/devices.md +1 -2
  9. package/docs/health.md +24 -0
  10. package/docs/index.md +2 -2
  11. package/docs/integrations-architecture.md +6 -0
  12. package/docs/integrations.md +15 -1
  13. package/docs/models.md +1 -1
  14. package/docs/persistence.md +1 -1
  15. package/docs/why-neoagent.md +1 -1
  16. package/flutter_app/README.md +0 -5
  17. package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
  19. package/flutter_app/lib/main.dart +0 -6
  20. package/flutter_app/lib/main_app_shell.dart +0 -2
  21. package/flutter_app/lib/main_controller.dart +2 -867
  22. package/flutter_app/lib/main_devices.dart +0 -73
  23. package/flutter_app/lib/main_integrations.dart +339 -187
  24. package/flutter_app/lib/main_launcher.dart +2 -212
  25. package/flutter_app/lib/main_models.dart +0 -259
  26. package/flutter_app/lib/main_navigation.dart +2 -14
  27. package/flutter_app/lib/main_operations.dart +322 -364
  28. package/flutter_app/lib/main_runtime.dart +9 -208
  29. package/flutter_app/lib/main_settings.dart +17 -199
  30. package/flutter_app/lib/main_shared.dart +20 -591
  31. package/flutter_app/lib/main_timeline.dart +26 -122
  32. package/flutter_app/lib/main_voice_assistant.dart +0 -5
  33. package/flutter_app/lib/src/backend_client.dart +0 -102
  34. package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
  35. package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
  36. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
  37. package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
  38. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  39. package/flutter_app/macos/Podfile.lock +56 -5
  40. package/flutter_app/pubspec.lock +0 -7
  41. package/flutter_app/pubspec.yaml +0 -2
  42. package/flutter_app/web/index.html +174 -0
  43. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
  44. package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
  45. package/landing/index.html +0 -2
  46. package/lib/manager.js +1 -1
  47. package/lib/schema_migrations.js +75 -0
  48. package/package.json +1 -4
  49. package/server/db/database.js +1 -133
  50. package/server/http/middleware.js +2 -16
  51. package/server/http/routes.js +0 -2
  52. package/server/public/.last_build_id +1 -1
  53. package/server/public/assets/NOTICES +0 -25
  54. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  55. package/server/public/flutter_bootstrap.js +1 -1
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +88081 -90036
  58. package/server/routes/auth.js +11 -2
  59. package/server/routes/settings.js +0 -5
  60. package/server/routes/voice_assistant.js +1 -128
  61. package/server/services/account/erasure.js +0 -3
  62. package/server/services/ai/deliverables/artifact_helpers.js +1 -1
  63. package/server/services/ai/loop/conversation_loop.js +6 -2
  64. package/server/services/ai/loop/tool_dispatch.js +0 -3
  65. package/server/services/ai/messagingFallback.js +1 -1
  66. package/server/services/ai/settings.js +0 -6
  67. package/server/services/ai/toolEvidence.js +1 -4
  68. package/server/services/ai/tools.js +0 -234
  69. package/server/services/desktop/auth.js +0 -3
  70. package/server/services/desktop/registry.js +2 -50
  71. package/server/services/integrations/neoarchive/provider.js +616 -0
  72. package/server/services/integrations/neorecall/client.js +140 -0
  73. package/server/services/integrations/neorecall/constants.js +87 -0
  74. package/server/services/integrations/neorecall/provider.js +163 -0
  75. package/server/services/integrations/registry.js +4 -2
  76. package/server/services/manager.js +0 -22
  77. package/server/services/memory/manager.js +44 -19
  78. package/server/services/memory/retrieval_reasoning.js +36 -14
  79. package/server/services/messaging/whatsapp.js +18 -0
  80. package/server/services/security/tool_categories.js +0 -5
  81. package/server/services/social_video/service.js +2 -20
  82. package/server/services/tasks/adapters/index.js +1 -1
  83. package/server/services/tasks/integration_runtime.js +3 -67
  84. package/server/services/tasks/runtime.js +1 -6
  85. package/server/services/tasks/task_repository.js +6 -13
  86. package/server/services/timeline/service.js +0 -224
  87. package/server/services/voice/message.js +0 -24
  88. package/server/services/voice/providers.js +42 -1
  89. package/server/services/voice/runtimeManager.js +9 -5
  90. package/server/services/voice/turnRunner.js +0 -22
  91. package/server/services/wearable/service.js +0 -5
  92. package/docs/recordings-and-health.md +0 -41
  93. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  97. package/flutter_app/lib/main_recordings.dart +0 -920
  98. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  99. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  100. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  101. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  102. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  103. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  104. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  105. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  106. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  107. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  108. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  109. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  110. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  111. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  112. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  114. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  115. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  118. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  119. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  120. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  121. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  126. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  130. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  131. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  132. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  133. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  136. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  138. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  140. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  142. package/server/routes/recordings.js +0 -335
  143. package/server/routes/screenHistory.js +0 -190
  144. package/server/services/ai/recordingInsights.js +0 -105
  145. package/server/services/integrations/neomail/provider.js +0 -993
  146. package/server/services/recordings/deepgram.js +0 -58
  147. package/server/services/recordings/manager.js +0 -1273
  148. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  149. package/server/services/voice/screenshotContext.js +0 -73
  150. package/server/services/voice/turnCoordinator.js +0 -51
@@ -1,993 +0,0 @@
1
- 'use strict';
2
-
3
- const crypto = require('crypto');
4
- const db = require('../../../db/database');
5
- const { resolveAgentId } = require('../../agents/manager');
6
- const {
7
- deleteProviderConfig,
8
- getProviderConfig,
9
- setProviderConfig,
10
- } = require('../provider_config_store');
11
- const { getConnectionAccessMode } = require('../access');
12
- const { decryptValue } = require('../secrets');
13
- const { appendQuery, fetchJson } = require('../oauth_provider');
14
- const { resolvePublicBaseUrl } = require('../env');
15
-
16
- const NEOMAIL_PROVIDER_KEY = 'neomail';
17
- const NEOMAIL_APP = Object.freeze({
18
- id: 'mailbox',
19
- label: 'Mailbox',
20
- description:
21
- 'Search threads, read mail, draft replies, send messages, and trigger tasks from a connected NeoMail inbox.',
22
- });
23
- const NEOMAIL_COMPANION_SCOPES = Object.freeze([
24
- 'mail:read',
25
- 'mail:write',
26
- 'drafts:write',
27
- 'send:write',
28
- 'ai:use',
29
- ]);
30
- const TOOL_DEFINITIONS = Object.freeze([
31
- {
32
- appId: NEOMAIL_APP.id,
33
- name: 'neomail_list_accounts',
34
- access: 'read',
35
- description: 'List mailboxes available in the connected NeoMail account.',
36
- parameters: { type: 'object', properties: {} },
37
- },
38
- {
39
- appId: NEOMAIL_APP.id,
40
- name: 'neomail_list_threads',
41
- access: 'read',
42
- description: 'List threads from the connected NeoMail account.',
43
- parameters: {
44
- type: 'object',
45
- properties: {
46
- mail_account_id: {
47
- type: 'string',
48
- description: 'Optional NeoMail mailbox account ID.',
49
- },
50
- mail_account_email: {
51
- type: 'string',
52
- description: 'Optional NeoMail mailbox email address.',
53
- },
54
- folder: { type: 'string', description: 'Optional folder path like INBOX or archive.' },
55
- query: { type: 'string', description: 'Optional text query.' },
56
- unread_only: { type: 'boolean', description: 'Whether to return only unread threads.' },
57
- limit: { type: 'number', description: 'Maximum threads to return, default 25.' },
58
- },
59
- },
60
- },
61
- {
62
- appId: NEOMAIL_APP.id,
63
- name: 'neomail_get_thread',
64
- access: 'read',
65
- description: 'Read a full thread from NeoMail.',
66
- parameters: {
67
- type: 'object',
68
- properties: {
69
- thread_id: { type: 'string', description: 'NeoMail thread ID.' },
70
- },
71
- required: ['thread_id'],
72
- },
73
- },
74
- {
75
- appId: NEOMAIL_APP.id,
76
- name: 'neomail_search_messages',
77
- access: 'read',
78
- description: 'Search messages in NeoMail.',
79
- parameters: {
80
- type: 'object',
81
- properties: {
82
- query: { type: 'string', description: 'Search query.' },
83
- mail_account_id: {
84
- type: 'string',
85
- description: 'Optional NeoMail mailbox account ID.',
86
- },
87
- mail_account_email: {
88
- type: 'string',
89
- description: 'Optional NeoMail mailbox email address.',
90
- },
91
- limit: { type: 'number', description: 'Maximum results to return, default 20.' },
92
- },
93
- required: ['query'],
94
- },
95
- },
96
- {
97
- appId: NEOMAIL_APP.id,
98
- name: 'neomail_save_draft',
99
- access: 'write',
100
- description: 'Create or update a NeoMail draft.',
101
- parameters: {
102
- type: 'object',
103
- properties: {
104
- draft_id: { type: 'string', description: 'Optional existing draft ID.' },
105
- mail_account_id: {
106
- type: 'string',
107
- description: 'Optional NeoMail mailbox account ID.',
108
- },
109
- mail_account_email: {
110
- type: 'string',
111
- description: 'Optional NeoMail mailbox email address.',
112
- },
113
- thread_id: { type: 'string', description: 'Optional NeoMail thread ID.' },
114
- to: {
115
- type: 'array',
116
- items: {
117
- anyOf: [
118
- { type: 'string' },
119
- {
120
- type: 'object',
121
- properties: {
122
- name: { type: 'string' },
123
- address: { type: 'string' },
124
- },
125
- required: ['address'],
126
- },
127
- ],
128
- },
129
- description: 'Recipient list.',
130
- },
131
- cc: {
132
- type: 'array',
133
- items: {
134
- anyOf: [
135
- { type: 'string' },
136
- {
137
- type: 'object',
138
- properties: {
139
- name: { type: 'string' },
140
- address: { type: 'string' },
141
- },
142
- required: ['address'],
143
- },
144
- ],
145
- },
146
- },
147
- bcc: {
148
- type: 'array',
149
- items: {
150
- anyOf: [
151
- { type: 'string' },
152
- {
153
- type: 'object',
154
- properties: {
155
- name: { type: 'string' },
156
- address: { type: 'string' },
157
- },
158
- required: ['address'],
159
- },
160
- ],
161
- },
162
- },
163
- subject: { type: 'string', description: 'Draft subject.' },
164
- body_text: { type: 'string', description: 'Plain-text draft body.' },
165
- body_html: { type: 'string', description: 'Optional HTML draft body.' },
166
- scheduled_for: { type: 'string', description: 'Optional ISO datetime for send-later.' },
167
- },
168
- required: ['to'],
169
- },
170
- },
171
- {
172
- appId: NEOMAIL_APP.id,
173
- name: 'neomail_send_draft',
174
- access: 'write',
175
- description: 'Send a NeoMail draft immediately or leave it queued.',
176
- parameters: {
177
- type: 'object',
178
- properties: {
179
- draft_id: { type: 'string', description: 'NeoMail draft ID.' },
180
- immediate: { type: 'boolean', description: 'Whether to send immediately, default true.' },
181
- },
182
- required: ['draft_id'],
183
- },
184
- },
185
- {
186
- appId: NEOMAIL_APP.id,
187
- name: 'neomail_update_thread',
188
- access: 'write',
189
- description: 'Update NeoMail thread flags and labels.',
190
- parameters: {
191
- type: 'object',
192
- properties: {
193
- thread_id: { type: 'string', description: 'NeoMail thread ID.' },
194
- is_read: { type: 'boolean', description: 'Read state.' },
195
- starred: { type: 'boolean', description: 'Starred state.' },
196
- archived: { type: 'boolean', description: 'Archived state.' },
197
- trashed: { type: 'boolean', description: 'Trash state.' },
198
- labels: { type: 'array', items: { type: 'string' }, description: 'Optional labels.' },
199
- },
200
- required: ['thread_id'],
201
- },
202
- },
203
- {
204
- appId: NEOMAIL_APP.id,
205
- name: 'neomail_ai_summarize_thread',
206
- access: 'read',
207
- description: 'Generate an AI summary for a NeoMail thread.',
208
- parameters: {
209
- type: 'object',
210
- properties: {
211
- thread_id: { type: 'string', description: 'NeoMail thread ID.' },
212
- },
213
- required: ['thread_id'],
214
- },
215
- },
216
- {
217
- appId: NEOMAIL_APP.id,
218
- name: 'neomail_ai_improve_draft',
219
- access: 'write',
220
- description: 'Improve a NeoMail draft with NeoMail AI.',
221
- parameters: {
222
- type: 'object',
223
- properties: {
224
- draft_id: { type: 'string', description: 'NeoMail draft ID.' },
225
- instruction: { type: 'string', description: 'Optional rewrite instruction.' },
226
- },
227
- required: ['draft_id'],
228
- },
229
- },
230
- {
231
- appId: NEOMAIL_APP.id,
232
- name: 'neomail_ai_ask_inbox',
233
- access: 'read',
234
- description: 'Ask NeoMail AI a question about the inbox.',
235
- parameters: {
236
- type: 'object',
237
- properties: {
238
- query: { type: 'string', description: 'Inbox question.' },
239
- },
240
- required: ['query'],
241
- },
242
- },
243
- ]);
244
-
245
- const toolAppMap = new Map(TOOL_DEFINITIONS.map((tool) => [tool.name, tool.appId]));
246
-
247
- function trimText(value) {
248
- return String(value || '').trim();
249
- }
250
-
251
- function isPrivateHost(hostname) {
252
- const lower = String(hostname || '').trim().toLowerCase();
253
- if (!lower) return false;
254
- return lower === 'localhost'
255
- || lower === '::1'
256
- || lower === '[::1]'
257
- || lower.startsWith('127.')
258
- || lower.startsWith('10.')
259
- || lower.startsWith('192.168.')
260
- || /^172\.(1[6-9]|2\d|3[0-1])\./.test(lower);
261
- }
262
-
263
- function normalizeNeoMailBaseUrl(value) {
264
- const raw = trimText(value);
265
- if (!raw) {
266
- throw new Error('NeoMail backend URL is required.');
267
- }
268
- const withScheme = raw.includes('://')
269
- ? raw
270
- : `${isPrivateHost(raw.split('/')[0]) ? 'http' : 'https'}://${raw}`;
271
- let parsed;
272
- try {
273
- parsed = new URL(withScheme);
274
- } catch {
275
- throw new Error('NeoMail backend URL must be a valid HTTP or HTTPS URL.');
276
- }
277
- if (!['http:', 'https:'].includes(parsed.protocol)) {
278
- throw new Error('NeoMail backend URL must use HTTP or HTTPS.');
279
- }
280
- if (parsed.hash) {
281
- throw new Error('NeoMail backend URL must not contain a fragment.');
282
- }
283
- parsed.pathname = parsed.pathname.replace(/\/+$/, '');
284
- return parsed.toString().replace(/\/+$/, '');
285
- }
286
-
287
- function parseConfigInput(rawConfig, existingConfig = {}) {
288
- const source = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
289
- return {
290
- baseUrl: trimText(source.baseUrl) || trimText(existingConfig.baseUrl),
291
- };
292
- }
293
-
294
- function getCallbackUrl() {
295
- return `${resolvePublicBaseUrl()}/api/integrations/oauth/callback`;
296
- }
297
-
298
- function getCompanionBootstrapUrl(baseUrl) {
299
- return `${baseUrl}/api/oauth/companion/neoagent/bootstrap`;
300
- }
301
-
302
- async function fetchNeoMailAuthStatus(baseUrl) {
303
- return fetchJson(
304
- `${baseUrl}/api/auth/status`,
305
- { method: 'GET' },
306
- { serviceName: 'NeoMail auth status' },
307
- );
308
- }
309
-
310
- async function bootstrapNeoMailCompanion(baseUrl) {
311
- return fetchJson(
312
- getCompanionBootstrapUrl(baseUrl),
313
- {
314
- method: 'POST',
315
- json: {
316
- redirectUri: getCallbackUrl(),
317
- appName: 'NeoAgent',
318
- },
319
- },
320
- { serviceName: 'NeoMail companion bootstrap' },
321
- );
322
- }
323
-
324
- async function exchangeNeoMailToken(baseUrl, form) {
325
- return fetchJson(
326
- `${baseUrl}/oauth/token`,
327
- {
328
- method: 'POST',
329
- form,
330
- },
331
- { serviceName: 'NeoMail OAuth token' },
332
- );
333
- }
334
-
335
- function buildApiPath(path, query = {}) {
336
- return appendQuery(path, query);
337
- }
338
-
339
- async function fetchAccounts(baseUrl, accessToken) {
340
- const response = await fetchJson(
341
- `${baseUrl}/api/v1/mail/accounts`,
342
- {
343
- method: 'GET',
344
- headers: {
345
- Authorization: `Bearer ${accessToken}`,
346
- Accept: 'application/json',
347
- },
348
- },
349
- { serviceName: 'NeoMail accounts' },
350
- );
351
- return Array.isArray(response?.accounts) ? response.accounts : [];
352
- }
353
-
354
- async function fetchUserInfo(baseUrl, accessToken) {
355
- return fetchJson(
356
- `${baseUrl}/oauth/userinfo`,
357
- {
358
- method: 'GET',
359
- headers: {
360
- Authorization: `Bearer ${accessToken}`,
361
- Accept: 'application/json',
362
- },
363
- },
364
- { serviceName: 'NeoMail userinfo' },
365
- );
366
- }
367
-
368
- function connectionLabelForUser(info = {}, baseUrl, accounts = []) {
369
- const email = trimText(info.email);
370
- if (email) return email;
371
- const username = trimText(info.preferred_username || info.username);
372
- const host = new URL(baseUrl).host;
373
- if (username) return `${username}@${host}`;
374
- if (Array.isArray(accounts) && accounts.length === 1) {
375
- const emailAddress = trimText(accounts[0]?.emailAddress);
376
- if (emailAddress) return emailAddress;
377
- }
378
- return `neomail:${host}`;
379
- }
380
-
381
- function decodeCredentials(connection) {
382
- try {
383
- return JSON.parse(decryptValue(connection.credentials_json || '{}') || '{}');
384
- } catch {
385
- return {};
386
- }
387
- }
388
-
389
- function summarizeAccountRow(row, envStatus) {
390
- if (!envStatus.configured) {
391
- return {
392
- id: row?.id || null,
393
- status: 'env_not_configured',
394
- connected: false,
395
- accountEmail: row?.account_email || null,
396
- lastConnectedAt: row?.last_connected_at || null,
397
- accessMode: 'read_write',
398
- };
399
- }
400
- if (!row) {
401
- return {
402
- id: null,
403
- status: 'not_connected',
404
- connected: false,
405
- accountEmail: null,
406
- lastConnectedAt: null,
407
- accessMode: 'read_write',
408
- };
409
- }
410
- return {
411
- id: row.id || null,
412
- status: row.status || 'not_connected',
413
- connected: row.status === 'connected',
414
- accountEmail: row.account_email || null,
415
- lastConnectedAt: row.last_connected_at || null,
416
- accessMode: getConnectionAccessMode(row),
417
- };
418
- }
419
-
420
- function buildSnapshot(provider, connectionRows, context = {}) {
421
- const env = provider.getEnvStatus(context);
422
- const accounts = (Array.isArray(connectionRows) ? connectionRows : [])
423
- .slice()
424
- .sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')))
425
- .map((row) => summarizeAccountRow(row, env));
426
- const connectedAccounts = accounts.filter((account) => account.connected);
427
- return {
428
- id: provider.key,
429
- label: provider.label,
430
- description: provider.description,
431
- icon: provider.icon,
432
- apps: [
433
- {
434
- id: NEOMAIL_APP.id,
435
- label: NEOMAIL_APP.label,
436
- description: NEOMAIL_APP.description,
437
- accounts,
438
- connection: {
439
- status: !env.configured ? 'env_not_configured' : connectedAccounts.length > 0 ? 'connected' : 'not_connected',
440
- connected: connectedAccounts.length > 0,
441
- accountCount: connectedAccounts.length,
442
- accountEmail: connectedAccounts.length === 1 ? connectedAccounts[0].accountEmail : null,
443
- lastConnectedAt: connectedAccounts.map((account) => account.lastConnectedAt).filter(Boolean).sort().reverse()[0] || null,
444
- },
445
- availableToolCount:
446
- env.configured && connectedAccounts.length > 0 ? TOOL_DEFINITIONS.length : 0,
447
- },
448
- ],
449
- env,
450
- connection: {
451
- status: !env.configured ? 'env_not_configured' : connectedAccounts.length > 0 ? 'connected' : 'not_connected',
452
- connected: connectedAccounts.length > 0,
453
- accountEmail: connectedAccounts.length === 1 ? connectedAccounts[0].accountEmail : null,
454
- accountCount: connectedAccounts.length,
455
- appCount: connectedAccounts.length > 0 ? 1 : 0,
456
- lastConnectedAt: connectedAccounts.map((account) => account.lastConnectedAt).filter(Boolean).sort().reverse()[0] || null,
457
- },
458
- availableToolCount:
459
- env.configured && connectedAccounts.length > 0 ? TOOL_DEFINITIONS.length : 0,
460
- connectPrompt: provider.connectPrompt,
461
- supportsMultipleAccounts: provider.supportsMultipleAccounts,
462
- connectionMethod: provider.connectionMethod,
463
- };
464
- }
465
-
466
- async function ensureValidAccessToken(connection) {
467
- const credentials = decodeCredentials(connection);
468
- const baseUrl = normalizeNeoMailBaseUrl(credentials.baseUrl);
469
- const accessToken = trimText(credentials.access_token);
470
- const refreshToken = trimText(credentials.refresh_token);
471
- const clientId = trimText(credentials.client_id);
472
- const expiresAtMs = Number(credentials.expires_at_ms || 0);
473
- const needsRefresh = !accessToken || !expiresAtMs || Date.now() >= (expiresAtMs - 60 * 1000);
474
-
475
- if (!needsRefresh) {
476
- return { baseUrl, accessToken, credentials };
477
- }
478
- if (!refreshToken || !clientId) {
479
- throw new Error('NeoMail refresh token is missing. Reconnect the NeoMail account.');
480
- }
481
-
482
- const refreshed = await exchangeNeoMailToken(baseUrl, {
483
- grant_type: 'refresh_token',
484
- client_id: clientId,
485
- refresh_token: refreshToken,
486
- });
487
- const nextCredentials = {
488
- ...credentials,
489
- access_token: trimText(refreshed.access_token),
490
- refresh_token: trimText(refreshed.refresh_token) || refreshToken,
491
- scope: trimText(refreshed.scope) || credentials.scope,
492
- token_type: trimText(refreshed.token_type) || 'Bearer',
493
- expires_at_ms: Date.now() + (Math.max(1, Number(refreshed.expires_in) || 3600) * 1000),
494
- };
495
- return {
496
- baseUrl,
497
- accessToken: nextCredentials.access_token,
498
- credentials: nextCredentials,
499
- };
500
- }
501
-
502
- async function requestNeoMail(connection, path, options = {}) {
503
- const { baseUrl, accessToken, credentials } = await ensureValidAccessToken(connection);
504
- const response = await fetchJson(
505
- `${baseUrl}${path.startsWith('/') ? '' : '/'}${path}`,
506
- {
507
- method: options.method || 'GET',
508
- headers: {
509
- Authorization: `Bearer ${accessToken}`,
510
- Accept: 'application/json',
511
- },
512
- ...(options.json === undefined ? {} : { json: options.json }),
513
- },
514
- { serviceName: 'NeoMail API' },
515
- );
516
- return { response, credentials, baseUrl, accessToken };
517
- }
518
-
519
- async function fetchNeoMailTriggerEvents(connection, config = {}, options = {}) {
520
- const path = buildApiPath('/api/v1/mail/events', {
521
- accountId: trimText(config.mailAccountId || config.mail_account_id) || undefined,
522
- folder: trimText(config.folder) || undefined,
523
- q: trimText(config.query) || undefined,
524
- unread: config.unreadOnly === true ? 'true' : undefined,
525
- since: trimText(options.since) || undefined,
526
- limit:
527
- Number(options.limit) > 0
528
- ? String(Math.min(Number(options.limit), 200))
529
- : '100',
530
- });
531
- const { response, credentials } = await requestNeoMail(connection, path);
532
- const events = Array.isArray(response?.events) ? response.events : [];
533
- const rows = events.map((event) => {
534
- const messageId = trimText(event?.id);
535
- return {
536
- fingerprint: `neomail:${connection.id}:${messageId}`,
537
- timestamp: trimText(event?.createdAt) || new Date().toISOString(),
538
- context: {
539
- triggerEvent: {
540
- provider: 'neomail',
541
- connectionId: connection.id,
542
- messageId,
543
- threadId: trimText(event?.threadId) || null,
544
- accountId: trimText(event?.accountId) || null,
545
- accountEmail: trimText(event?.accountEmail) || null,
546
- accountLabel: trimText(event?.accountLabel) || null,
547
- remoteMessageId: trimText(event?.remoteMessageId) || null,
548
- from: event?.from && typeof event.from === 'object'
549
- ? {
550
- name: trimText(event.from.name),
551
- address: trimText(event.from.address),
552
- }
553
- : { name: '', address: '' },
554
- subject: trimText(event?.subject),
555
- snippet: trimText(event?.snippet),
556
- folderPath: trimText(event?.folderPath) || 'INBOX',
557
- receivedAt: trimText(event?.receivedAt) || null,
558
- sentAt: trimText(event?.sentAt) || null,
559
- createdAt: trimText(event?.createdAt) || null,
560
- updatedAt: trimText(event?.updatedAt) || null,
561
- isRead: event?.isRead === true,
562
- },
563
- },
564
- };
565
- });
566
- return { rows, credentials };
567
- }
568
-
569
- function normalizeAddressList(values) {
570
- const list = Array.isArray(values) ? values : [];
571
- return list
572
- .map((entry) => {
573
- if (typeof entry === 'string') {
574
- const address = trimText(entry);
575
- return address ? { address } : null;
576
- }
577
- if (entry && typeof entry === 'object') {
578
- const address = trimText(entry.address);
579
- if (!address) return null;
580
- const name = trimText(entry.name);
581
- return name ? { name, address } : { address };
582
- }
583
- return null;
584
- })
585
- .filter(Boolean);
586
- }
587
-
588
- async function resolveMailAccountSelector(connection, args, options = {}) {
589
- const directId = trimText(args.mail_account_id || args.account_id);
590
- if (directId) {
591
- return { id: directId, accounts: null, credentials: null };
592
- }
593
-
594
- const selectorEmail = trimText(args.mail_account_email).toLowerCase();
595
- const { response, credentials } = await requestNeoMail(connection, '/api/v1/mail/accounts');
596
- const accounts = Array.isArray(response?.accounts) ? response.accounts : [];
597
- if (selectorEmail) {
598
- const match = accounts.find(
599
- (account) => trimText(account?.emailAddress).toLowerCase() === selectorEmail,
600
- );
601
- if (!match) {
602
- throw new Error(`No NeoMail mailbox matches ${selectorEmail}.`);
603
- }
604
- return { id: String(match.id), accounts, credentials };
605
- }
606
- if (options.required === true) {
607
- if (accounts.length === 0) {
608
- throw new Error('The connected NeoMail account has no mailboxes yet.');
609
- }
610
- if (accounts.length > 1) {
611
- const labels = accounts
612
- .map((account) => trimText(account?.emailAddress) || String(account?.id || 'unknown'))
613
- .join(', ');
614
- throw new Error(
615
- `Multiple NeoMail mailboxes are available (${labels}). Re-run the tool with mail_account_id or mail_account_email.`,
616
- );
617
- }
618
- return { id: String(accounts[0].id), accounts, credentials };
619
- }
620
- return { id: null, accounts, credentials };
621
- }
622
-
623
- async function executeNeoMailTool(toolName, args, connection) {
624
- if (toolName === 'neomail_list_accounts') {
625
- const { response, credentials } = await requestNeoMail(connection, '/api/v1/mail/accounts');
626
- return { result: response, credentials };
627
- }
628
-
629
- if (toolName === 'neomail_list_threads') {
630
- const resolved = await resolveMailAccountSelector(connection, args, { required: false });
631
- const { response, credentials } = await requestNeoMail(
632
- connection,
633
- buildApiPath('/api/v1/mail/threads', {
634
- accountId: resolved.id || undefined,
635
- folder: trimText(args.folder) || undefined,
636
- q: trimText(args.query) || undefined,
637
- unread: args.unread_only === true ? 'true' : undefined,
638
- limit: Number(args.limit) > 0 ? String(Math.min(Number(args.limit), 100)) : undefined,
639
- }),
640
- );
641
- return { result: response, credentials: resolved.credentials || credentials };
642
- }
643
-
644
- if (toolName === 'neomail_get_thread') {
645
- const threadId = trimText(args.thread_id);
646
- if (!threadId) throw new Error('thread_id is required.');
647
- const { response, credentials } = await requestNeoMail(connection, `/api/v1/mail/threads/${encodeURIComponent(threadId)}`);
648
- return { result: response, credentials };
649
- }
650
-
651
- if (toolName === 'neomail_search_messages') {
652
- const query = trimText(args.query);
653
- if (!query) throw new Error('query is required.');
654
- const resolved = await resolveMailAccountSelector(connection, args, { required: false });
655
- const { response, credentials } = await requestNeoMail(
656
- connection,
657
- buildApiPath('/api/v1/mail/search', {
658
- accountId: resolved.id || undefined,
659
- q: query,
660
- limit: Number(args.limit) > 0 ? String(Math.min(Number(args.limit), 100)) : undefined,
661
- }),
662
- );
663
- return { result: response, credentials: resolved.credentials || credentials };
664
- }
665
-
666
- if (toolName === 'neomail_save_draft') {
667
- const resolved = await resolveMailAccountSelector(connection, args, { required: true });
668
- const payload = {
669
- ...(trimText(args.thread_id) ? { threadId: trimText(args.thread_id) } : {}),
670
- accountId: resolved.id,
671
- to: normalizeAddressList(args.to),
672
- cc: normalizeAddressList(args.cc),
673
- bcc: normalizeAddressList(args.bcc),
674
- subject: trimText(args.subject),
675
- bodyText: String(args.body_text || ''),
676
- ...(trimText(args.body_html) ? { bodyHtml: String(args.body_html) } : {}),
677
- ...(trimText(args.scheduled_for) ? { scheduledFor: trimText(args.scheduled_for) } : {}),
678
- };
679
- if (payload.to.length === 0) {
680
- throw new Error('At least one recipient is required in to.');
681
- }
682
- const draftId = trimText(args.draft_id);
683
- const { response, credentials } = await requestNeoMail(
684
- connection,
685
- draftId
686
- ? `/api/v1/mail/drafts/${encodeURIComponent(draftId)}`
687
- : '/api/v1/mail/drafts',
688
- {
689
- method: draftId ? 'PUT' : 'POST',
690
- json: payload,
691
- },
692
- );
693
- return { result: response, credentials: resolved.credentials || credentials };
694
- }
695
-
696
- if (toolName === 'neomail_send_draft') {
697
- const draftId = trimText(args.draft_id);
698
- if (!draftId) throw new Error('draft_id is required.');
699
- const { response, credentials } = await requestNeoMail(
700
- connection,
701
- `/api/v1/mail/send/draft/${encodeURIComponent(draftId)}`,
702
- {
703
- method: 'POST',
704
- json: { immediate: args.immediate !== false },
705
- },
706
- );
707
- return { result: response, credentials };
708
- }
709
-
710
- if (toolName === 'neomail_update_thread') {
711
- const threadId = trimText(args.thread_id);
712
- if (!threadId) throw new Error('thread_id is required.');
713
- const payload = {};
714
- if (args.is_read !== undefined) payload.isRead = args.is_read === true;
715
- if (args.starred !== undefined) payload.starred = args.starred === true;
716
- if (args.archived !== undefined) payload.archived = args.archived === true;
717
- if (args.trashed !== undefined) payload.trashed = args.trashed === true;
718
- if (Array.isArray(args.labels)) payload.labels = args.labels.map((label) => String(label || '')).filter(Boolean);
719
- const { response, credentials } = await requestNeoMail(
720
- connection,
721
- `/api/v1/mail/threads/${encodeURIComponent(threadId)}`,
722
- {
723
- method: 'PATCH',
724
- json: payload,
725
- },
726
- );
727
- return { result: response, credentials };
728
- }
729
-
730
- if (toolName === 'neomail_ai_summarize_thread') {
731
- const threadId = trimText(args.thread_id);
732
- if (!threadId) throw new Error('thread_id is required.');
733
- const { response, credentials } = await requestNeoMail(
734
- connection,
735
- `/api/v1/mail/ai/thread/${encodeURIComponent(threadId)}/summary`,
736
- { method: 'POST', json: {} },
737
- );
738
- return { result: response, credentials };
739
- }
740
-
741
- if (toolName === 'neomail_ai_improve_draft') {
742
- const draftId = trimText(args.draft_id);
743
- if (!draftId) throw new Error('draft_id is required.');
744
- const { response, credentials } = await requestNeoMail(
745
- connection,
746
- `/api/v1/mail/ai/draft/${encodeURIComponent(draftId)}/improve`,
747
- {
748
- method: 'POST',
749
- json: trimText(args.instruction)
750
- ? { instruction: trimText(args.instruction) }
751
- : {},
752
- },
753
- );
754
- return { result: response, credentials };
755
- }
756
-
757
- if (toolName === 'neomail_ai_ask_inbox') {
758
- const query = trimText(args.query);
759
- if (!query) throw new Error('query is required.');
760
- const { response, credentials } = await requestNeoMail(
761
- connection,
762
- '/api/v1/mail/ai/ask',
763
- {
764
- method: 'POST',
765
- json: { query },
766
- },
767
- );
768
- return { result: response, credentials };
769
- }
770
-
771
- throw new Error(`Unsupported NeoMail tool: ${toolName}`);
772
- }
773
-
774
- function createNeoMailProvider() {
775
- return {
776
- key: NEOMAIL_PROVIDER_KEY,
777
- label: 'NeoMail',
778
- description:
779
- 'Connect a self-hosted NeoMail server so NeoAgent can search inboxes, draft replies, send mail, and trigger automations from new email.',
780
- icon: 'neomail',
781
- apps: [NEOMAIL_APP],
782
- connectPrompt:
783
- 'Add the NeoMail backend URL once, then connect with OAuth. NeoAgent can work across every mailbox visible to that NeoMail user.',
784
- supportsMultipleAccounts: true,
785
- connectionMethod: 'oauth',
786
- getApp(appId) {
787
- return String(appId || '').trim() === NEOMAIL_APP.id ? NEOMAIL_APP : null;
788
- },
789
- getToolAppId(toolName) {
790
- return toolAppMap.get(String(toolName || '').trim()) || null;
791
- },
792
- getEnvStatus(context = {}) {
793
- const normalizedUserId = Number(context.userId);
794
- const config = Number.isInteger(normalizedUserId) && normalizedUserId > 0
795
- ? parseConfigInput(getProviderConfig(normalizedUserId, NEOMAIL_PROVIDER_KEY, context.agentId))
796
- : { baseUrl: '' };
797
- const configured = Boolean(trimText(config.baseUrl));
798
- return {
799
- configured,
800
- missing: configured ? [] : ['baseUrl'],
801
- summary: configured
802
- ? 'NeoMail is ready for account connections.'
803
- : 'Add the NeoMail backend URL to enable account connections.',
804
- setupMode: 'user',
805
- };
806
- },
807
- getToolDefinitions(options = {}) {
808
- const connectedAppIds = new Set(options.connectedAppIds || []);
809
- return connectedAppIds.has(NEOMAIL_APP.id) ? TOOL_DEFINITIONS.slice() : [];
810
- },
811
- supportsTool(toolName) {
812
- return toolAppMap.has(String(toolName || '').trim());
813
- },
814
- buildSnapshot(connectionRows, context = {}) {
815
- return buildSnapshot(this, connectionRows, context);
816
- },
817
- summarizeForModel(snapshot) {
818
- if (!snapshot?.env?.configured) {
819
- return 'NeoMail: setup is not complete for this user yet. Tell them to add the NeoMail backend URL in Official Integrations first.';
820
- }
821
- if (!snapshot.connection?.connected) {
822
- return 'NeoMail: setup is ready, but no NeoMail account is connected yet. Tell the user to connect NeoMail in Official Integrations first.';
823
- }
824
- return 'NeoMail: native inbox access is connected in this run with tools for account listing, inbox search, thread reads, drafts, sending, thread updates, and inbox AI.';
825
- },
826
- async beginOAuth({ state, codeVerifier, userId, agentId, appKey }) {
827
- if (String(appKey || '').trim() !== NEOMAIL_APP.id) {
828
- throw new Error(`Unknown NeoMail app: ${appKey || 'missing app key'}`);
829
- }
830
- const config = parseConfigInput(
831
- getProviderConfig(Number(userId), NEOMAIL_PROVIDER_KEY, resolveAgentId(Number(userId), agentId || null)),
832
- );
833
- const baseUrl = normalizeNeoMailBaseUrl(config.baseUrl);
834
- const bootstrap = await bootstrapNeoMailCompanion(baseUrl);
835
- const clientId = trimText(bootstrap.clientId);
836
- if (!clientId) {
837
- throw new Error('NeoMail companion bootstrap did not return a client ID.');
838
- }
839
- const scope = Array.isArray(bootstrap.scopes) && bootstrap.scopes.length > 0
840
- ? bootstrap.scopes.join(' ')
841
- : NEOMAIL_COMPANION_SCOPES.join(' ');
842
- const authorizeUrl = appendQuery(
843
- trimText(bootstrap.authorizationEndpoint) || `${baseUrl}/oauth/authorize`,
844
- {
845
- response_type: 'code',
846
- client_id: clientId,
847
- redirect_uri: trimText(bootstrap.redirectUri) || getCallbackUrl(),
848
- state,
849
- code_challenge: crypto
850
- .createHash('sha256')
851
- .update(String(codeVerifier || ''))
852
- .digest('base64')
853
- .replace(/\+/g, '-')
854
- .replace(/\//g, '_')
855
- .replace(/=+$/g, ''),
856
- code_challenge_method: 'S256',
857
- scope,
858
- },
859
- );
860
- return { url: authorizeUrl };
861
- },
862
- async finishOAuth({ userId, agentId, code, codeVerifier }) {
863
- const config = parseConfigInput(
864
- getProviderConfig(Number(userId), NEOMAIL_PROVIDER_KEY, resolveAgentId(Number(userId), agentId || null)),
865
- );
866
- const baseUrl = normalizeNeoMailBaseUrl(config.baseUrl);
867
- const bootstrap = await bootstrapNeoMailCompanion(baseUrl);
868
- const tokenSet = await exchangeNeoMailToken(baseUrl, {
869
- grant_type: 'authorization_code',
870
- client_id: trimText(bootstrap.clientId),
871
- code: trimText(code),
872
- redirect_uri: trimText(bootstrap.redirectUri) || getCallbackUrl(),
873
- code_verifier: trimText(codeVerifier),
874
- });
875
- const accessToken = trimText(tokenSet.access_token);
876
- const refreshToken = trimText(tokenSet.refresh_token);
877
- if (!accessToken || !refreshToken) {
878
- throw new Error('NeoMail did not return durable OAuth credentials.');
879
- }
880
-
881
- const [userInfo, mailAccounts] = await Promise.all([
882
- fetchUserInfo(baseUrl, accessToken),
883
- fetchAccounts(baseUrl, accessToken).catch(() => []),
884
- ]);
885
-
886
- const metadata = {
887
- baseUrl,
888
- username: trimText(userInfo?.preferred_username),
889
- email: trimText(userInfo?.email) || null,
890
- mailboxCount: Array.isArray(mailAccounts) ? mailAccounts.length : 0,
891
- mailboxes: Array.isArray(mailAccounts)
892
- ? mailAccounts.slice(0, 20).map((account) => ({
893
- id: account.id,
894
- emailAddress: account.emailAddress || null,
895
- label: account.label || null,
896
- }))
897
- : [],
898
- };
899
-
900
- return {
901
- accountEmail: connectionLabelForUser(userInfo, baseUrl, mailAccounts),
902
- scopes: trimText(tokenSet.scope).split(/\s+/g).filter(Boolean),
903
- credentials: {
904
- baseUrl,
905
- client_id: trimText(bootstrap.clientId),
906
- access_token: accessToken,
907
- refresh_token: refreshToken,
908
- scope: trimText(tokenSet.scope),
909
- token_type: trimText(tokenSet.token_type) || 'Bearer',
910
- expires_at_ms: Date.now() + (Math.max(1, Number(tokenSet.expires_in) || 3600) * 1000),
911
- },
912
- metadata,
913
- };
914
- },
915
- async executeTool(toolName, args, connection) {
916
- return executeNeoMailTool(toolName, args || {}, connection);
917
- },
918
- async fetchTriggerEvents({ connection, config, since, limit }) {
919
- return fetchNeoMailTriggerEvents(connection, config, { since, limit });
920
- },
921
- getUserConfig({ userId, agentId }) {
922
- const normalizedUserId = Number(userId);
923
- const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
924
- const storedConfig = parseConfigInput(
925
- getProviderConfig(normalizedUserId, NEOMAIL_PROVIDER_KEY, scopedAgentId),
926
- );
927
- const accountCount = db.prepare(
928
- `SELECT COUNT(*) AS count
929
- FROM integration_connections
930
- WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND status = 'connected'`,
931
- ).get(normalizedUserId, scopedAgentId, NEOMAIL_PROVIDER_KEY)?.count || 0;
932
- return {
933
- baseUrl: storedConfig.baseUrl,
934
- configured: Boolean(storedConfig.baseUrl),
935
- accountCount,
936
- hasConnectedAccount: accountCount > 0,
937
- };
938
- },
939
- async saveUserConfig({ userId, agentId, config }) {
940
- const normalizedUserId = Number(userId);
941
- if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
942
- throw new Error('A valid user is required to save NeoMail configuration.');
943
- }
944
- const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
945
- const existingConfig = parseConfigInput(
946
- getProviderConfig(normalizedUserId, NEOMAIL_PROVIDER_KEY, scopedAgentId),
947
- );
948
- const parsedConfig = parseConfigInput(config, existingConfig);
949
- const baseUrl = normalizeNeoMailBaseUrl(parsedConfig.baseUrl);
950
- const authStatus = await fetchNeoMailAuthStatus(baseUrl);
951
- await bootstrapNeoMailCompanion(baseUrl);
952
-
953
- setProviderConfig(normalizedUserId, NEOMAIL_PROVIDER_KEY, { baseUrl }, scopedAgentId);
954
-
955
- if (existingConfig.baseUrl && existingConfig.baseUrl !== baseUrl) {
956
- db.prepare(
957
- 'DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ?',
958
- ).run(normalizedUserId, scopedAgentId, NEOMAIL_PROVIDER_KEY);
959
- }
960
-
961
- const accountCount = db.prepare(
962
- `SELECT COUNT(*) AS count
963
- FROM integration_connections
964
- WHERE user_id = ? AND agent_id = ? AND provider_key = ? AND status = 'connected'`,
965
- ).get(normalizedUserId, scopedAgentId, NEOMAIL_PROVIDER_KEY)?.count || 0;
966
-
967
- return {
968
- baseUrl,
969
- configured: true,
970
- hasConnectedAccount: accountCount > 0,
971
- accountCount,
972
- authenticatedOnServer: authStatus?.authenticated === true,
973
- hasUser: authStatus?.hasUser === true,
974
- };
975
- },
976
- clearUserConfig({ userId, agentId }) {
977
- const normalizedUserId = Number(userId);
978
- if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
979
- throw new Error('A valid user is required to clear NeoMail configuration.');
980
- }
981
- const scopedAgentId = resolveAgentId(normalizedUserId, agentId || null);
982
- deleteProviderConfig(normalizedUserId, NEOMAIL_PROVIDER_KEY, scopedAgentId);
983
- db.prepare(
984
- 'DELETE FROM integration_connections WHERE user_id = ? AND agent_id = ? AND provider_key = ?',
985
- ).run(normalizedUserId, scopedAgentId, NEOMAIL_PROVIDER_KEY);
986
- return { cleared: true };
987
- },
988
- };
989
- }
990
-
991
- module.exports = {
992
- createNeoMailProvider,
993
- };