neoagent 3.1.0 → 3.1.1-beta.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.
@@ -11,6 +11,7 @@ const { createSpotifyProvider } = require('./spotify/provider');
11
11
  const { createSlackProvider } = require('./slack/provider');
12
12
  const { createWeatherProvider } = require('./weather/provider');
13
13
  const { createWhatsAppPersonalProvider } = require('./whatsapp');
14
+ const { createNeoMailProvider } = require('./neomail/provider');
14
15
 
15
16
  function createIntegrationRegistry(options = {}) {
16
17
  const providers = [
@@ -23,6 +24,7 @@ function createIntegrationRegistry(options = {}) {
23
24
  createTrelloProvider(),
24
25
  createWeatherProvider(),
25
26
  createSpotifyProvider(),
27
+ createNeoMailProvider(),
26
28
  createHomeAssistantProvider(),
27
29
  createWhatsAppPersonalProvider(options),
28
30
  ];
@@ -5,11 +5,11 @@ module.exports = [
5
5
  require('./manual'),
6
6
  require('./webhook'),
7
7
  require('./gmail_message_received'),
8
+ require('./neomail_email_received'),
8
9
  require('./outlook_email_received'),
9
10
  require('./slack_message_received'),
10
11
  require('./teams_message_received'),
11
12
  require('./weather_event'),
12
- require('./webhook'),
13
13
  require('./whatsapp_personal_message_received'),
14
14
  require('./android_notification_received'),
15
15
  ];
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ ensureOwnedIntegrationConnection,
5
+ normalizeBoolean,
6
+ normalizeTrimmedText,
7
+ } = require('../security');
8
+
9
+ module.exports = {
10
+ type: 'neomail_email_received',
11
+ label: 'NeoMail Email Received',
12
+ providerKey: 'neomail',
13
+ appKey: 'mailbox',
14
+ async validateConfig(config = {}, context = {}) {
15
+ const connection = ensureOwnedIntegrationConnection(context.integrationManager, {
16
+ userId: context.userId,
17
+ agentId: context.agentId,
18
+ connectionId: config.connectionId || config.connection_id,
19
+ providerKey: 'neomail',
20
+ appKey: 'mailbox',
21
+ });
22
+ return {
23
+ connectionId: connection.id,
24
+ accountEmail: connection.account_email || null,
25
+ mailAccountId: normalizeTrimmedText(
26
+ config.mailAccountId || config.mail_account_id,
27
+ 120,
28
+ ),
29
+ folder: normalizeTrimmedText(config.folder, 200),
30
+ query: normalizeTrimmedText(config.query, 500),
31
+ unreadOnly: normalizeBoolean(
32
+ config.unreadOnly ?? config.unread_only,
33
+ false,
34
+ ),
35
+ };
36
+ },
37
+ summarize(config = {}) {
38
+ const parts = ['NeoMail'];
39
+ if (config.accountEmail) parts.push(config.accountEmail);
40
+ if (config.mailAccountId) parts.push(`mailbox: ${config.mailAccountId}`);
41
+ if (config.folder) parts.push(`folder: ${config.folder}`);
42
+ if (config.query) parts.push(`query: ${config.query}`);
43
+ if (config.unreadOnly) parts.push('unread only');
44
+ return parts.join(' · ');
45
+ },
46
+ };
@@ -5,6 +5,7 @@ const { normalizeJsonObject } = require('./utils');
5
5
 
6
6
  const POLLED_TRIGGER_TYPES = Object.freeze([
7
7
  'gmail_message_received',
8
+ 'neomail_email_received',
8
9
  'outlook_email_received',
9
10
  'slack_message_received',
10
11
  'teams_message_received',
@@ -13,10 +14,19 @@ const POLLED_TRIGGER_TYPES = Object.freeze([
13
14
  ]);
14
15
 
15
16
  function sortByTimestamp(left, right) {
16
- return String(left.timestamp).localeCompare(String(right.timestamp));
17
+ const timeOrder = String(left.timestamp).localeCompare(String(right.timestamp));
18
+ if (timeOrder !== 0) return timeOrder;
19
+ return String(left.fingerprint).localeCompare(String(right.fingerprint));
17
20
  }
18
21
 
19
- async function fetchTriggerRows({ integrationManager, userId, agentId, triggerType, config }) {
22
+ async function fetchTriggerRows({
23
+ integrationManager,
24
+ userId,
25
+ agentId,
26
+ triggerType,
27
+ config,
28
+ lastTriggeredAt,
29
+ }) {
20
30
  if (!integrationManager) return [];
21
31
  const scopedAgentId = resolveAgentId(userId, agentId);
22
32
  const connectionArg = {
@@ -80,6 +90,53 @@ async function fetchTriggerRows({ integrationManager, userId, agentId, triggerTy
80
90
  .sort(sortByTimestamp);
81
91
  }
82
92
 
93
+ if (triggerType === 'neomail_email_received') {
94
+ const provider = integrationManager.getProvider('neomail');
95
+ if (!provider || typeof provider.fetchTriggerEvents !== 'function') {
96
+ return [];
97
+ }
98
+ const connectionId = Number(config.connectionId);
99
+ if (!Number.isInteger(connectionId) || connectionId <= 0) {
100
+ return [];
101
+ }
102
+ const connection = integrationManager.getConnectionById(
103
+ userId,
104
+ connectionId,
105
+ scopedAgentId,
106
+ );
107
+ if (
108
+ !connection ||
109
+ String(connection.provider_key || '').trim() !== 'neomail' ||
110
+ String(connection.app_key || '').trim() !== 'mailbox' ||
111
+ String(connection.status || '').trim() !== 'connected'
112
+ ) {
113
+ return [];
114
+ }
115
+ const result = await provider.fetchTriggerEvents({
116
+ connection,
117
+ config,
118
+ since: lastTriggeredAt,
119
+ limit: 100,
120
+ });
121
+ if (result?.credentials) {
122
+ const existingCredentials = integrationManager.parseCredentials(
123
+ connection.credentials_json,
124
+ );
125
+ const mergedCredentials = integrationManager.mergeUpdatedCredentials(
126
+ existingCredentials,
127
+ result.credentials,
128
+ );
129
+ integrationManager.persistSharedCredentials(
130
+ userId,
131
+ scopedAgentId,
132
+ 'neomail',
133
+ connection.account_email,
134
+ mergedCredentials,
135
+ );
136
+ }
137
+ return Array.isArray(result?.rows) ? result.rows.slice().sort(sortByTimestamp) : [];
138
+ }
139
+
83
140
  if (triggerType === 'slack_message_received') {
84
141
  const result = await integrationManager.executeTool(userId, 'slack_get_conversation_history', {
85
142
  ...connectionArg,
@@ -262,13 +319,20 @@ async function pollIntegrationTask(runtime, task) {
262
319
  agentId: task.agent_id,
263
320
  triggerType: task.trigger_type,
264
321
  config,
322
+ lastTriggeredAt: task.last_triggered_at,
265
323
  });
266
324
  if (!rows.length) return;
267
325
 
268
326
  const existingFingerprint = String(task.last_trigger_fingerprint || '');
269
327
  const latestFingerprint = rows[rows.length - 1].fingerprint;
328
+ const latestTimestamp = rows[rows.length - 1].timestamp;
270
329
  if (!existingFingerprint) {
271
- runtime.taskRepository.markTaskTriggerCheckpoint(task.id, latestFingerprint, task.user_id);
330
+ runtime.taskRepository.markTaskTriggerCheckpoint(
331
+ task.id,
332
+ latestFingerprint,
333
+ task.user_id,
334
+ latestTimestamp,
335
+ );
272
336
  return;
273
337
  }
274
338
 
@@ -332,7 +332,12 @@ class TaskRuntime {
332
332
  triggerPayload: triggerPayload.context || {},
333
333
  });
334
334
  if (!result?.error && !result?.skipped) {
335
- this.taskRepository.markTaskTriggered(taskId, userId, fingerprint);
335
+ this.taskRepository.markTaskTriggered(
336
+ taskId,
337
+ userId,
338
+ fingerprint,
339
+ triggerPayload.timestamp,
340
+ );
336
341
  }
337
342
  return result;
338
343
  }
@@ -3,6 +3,13 @@
3
3
  const db = require('../../db/database');
4
4
 
5
5
  class TaskRepository {
6
+ normalizeTriggerTimestamp(value) {
7
+ const text = String(value || '').trim();
8
+ if (!text) return new Date().toISOString();
9
+ const parsed = new Date(text);
10
+ return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
11
+ }
12
+
6
13
  createTask(userId, normalizedTask) {
7
14
  const result = db.prepare(
8
15
  `INSERT INTO scheduled_tasks (
@@ -124,20 +131,20 @@ class TaskRepository {
124
131
  ).all(userId, agentId);
125
132
  }
126
133
 
127
- markTaskTriggered(taskId, userId, fingerprint) {
134
+ markTaskTriggered(taskId, userId, fingerprint, triggeredAt = null) {
128
135
  db.prepare(
129
136
  `UPDATE scheduled_tasks
130
- SET last_triggered_at = datetime('now'), last_trigger_fingerprint = ?
137
+ SET last_triggered_at = ?, last_trigger_fingerprint = ?
131
138
  WHERE id = ? AND user_id = ?`
132
- ).run(fingerprint, taskId, userId);
139
+ ).run(this.normalizeTriggerTimestamp(triggeredAt), fingerprint, taskId, userId);
133
140
  }
134
141
 
135
- markTaskTriggerCheckpoint(taskId, fingerprint, userId) {
142
+ markTaskTriggerCheckpoint(taskId, fingerprint, userId, triggeredAt = null) {
136
143
  db.prepare(
137
144
  `UPDATE scheduled_tasks
138
- SET last_triggered_at = datetime('now'), last_trigger_fingerprint = ?
145
+ SET last_triggered_at = ?, last_trigger_fingerprint = ?
139
146
  WHERE id = ? AND user_id = ?`
140
- ).run(fingerprint, taskId, userId);
147
+ ).run(this.normalizeTriggerTimestamp(triggeredAt), fingerprint, taskId, userId);
141
148
  }
142
149
 
143
150
  markTaskRun(taskId, userId) {