neoagent 2.3.1-beta.26 → 2.3.1-beta.28

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.3.1-beta.26",
3
+ "version": "2.3.1-beta.28",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"59aa584fdf100e6c78c785d8a5b565d1de4b48
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "753731712" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "2330022069" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });
@@ -192,6 +192,54 @@ function isProactiveTrigger(triggerSource) {
192
192
  return triggerSource === 'schedule' || triggerSource === 'tasks';
193
193
  }
194
194
 
195
+ function normalizeSendMessagePurpose(value) {
196
+ const normalized = String(value || '').trim().toLowerCase();
197
+ if (normalized === 'final_result' || normalized === 'blocker' || normalized === 'no_response') {
198
+ return normalized;
199
+ }
200
+ return '';
201
+ }
202
+
203
+ function validateProactiveSendMessageArgs({ purpose, normalizedMessage }) {
204
+ const normalizedPurpose = normalizeSendMessagePurpose(purpose);
205
+ if (!normalizedPurpose) {
206
+ return {
207
+ ok: false,
208
+ error: 'Background send_message requires purpose=final_result, blocker, or no_response.',
209
+ reason: 'Background send_message requires purpose=final_result, blocker, or no_response.',
210
+ };
211
+ }
212
+
213
+ if (normalizedPurpose === 'no_response') {
214
+ if (normalizedMessage !== '[NO RESPONSE]') {
215
+ return {
216
+ ok: false,
217
+ error: 'purpose=no_response requires content "[NO RESPONSE]".',
218
+ reason: 'purpose=no_response requires content "[NO RESPONSE]".',
219
+ };
220
+ }
221
+ return {
222
+ ok: false,
223
+ skipped: true,
224
+ suppressed: true,
225
+ reason: 'no_response',
226
+ };
227
+ }
228
+
229
+ if (normalizedMessage === '[NO RESPONSE]') {
230
+ return {
231
+ ok: false,
232
+ error: `purpose=${normalizedPurpose} cannot use content "[NO RESPONSE]".`,
233
+ reason: `purpose=${normalizedPurpose} cannot use content "[NO RESPONSE]".`,
234
+ };
235
+ }
236
+
237
+ return {
238
+ ok: true,
239
+ purpose: normalizedPurpose,
240
+ };
241
+ }
242
+
195
243
  function getRunState(engine, runId) {
196
244
  if (!engine || !runId) return null;
197
245
  return engine.activeRuns.get(runId) || null;
@@ -742,14 +790,15 @@ function getAvailableTools(app, options = {}) {
742
790
  },
743
791
  {
744
792
  name: 'send_message',
745
- description: `Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, Telegram, Slack, Google Chat, Microsoft Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, WebChat, and configurable webhook bridges. ${buildSendMessageFormattingReference()} For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence or no reply.`,
793
+ description: `Send a message on a connected messaging platform. Supports WhatsApp (text/media), Telnyx Voice (phone calls — TTS), Discord, Telegram, Slack, Google Chat, Microsoft Teams, Matrix, Signal, iMessage/BlueBubbles, IRC, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, WeChat, WebChat, and configurable webhook bridges. ${buildSendMessageFormattingReference()} For WhatsApp: use media_path to attach files. Use content "[NO RESPONSE]" only when the user explicitly asked for silence or no reply. For background task or schedule runs, set purpose to final_result, blocker, or no_response.`,
746
794
  parameters: {
747
795
  type: 'object',
748
796
  properties: {
749
797
  platform: { type: 'string', description: 'Platform name, for example whatsapp, telnyx, discord, telegram, slack, google_chat, teams, matrix, signal, imessage, bluebubbles, irc, line, mattermost, or webchat' },
750
798
  to: { type: 'string', description: 'Recipient/chat ID for the connected platform, such as a WhatsApp chat ID, Telnyx call_control_id, Slack channel ID, Matrix room ID, Discord channel snowflake / "dm_<userId>", Telegram "dm_<userId>" / raw group chat ID, IRC channel, or webhook target' },
751
799
  content: { type: 'string', description: 'Message text. Write one compact natural chat reply; the runtime adapts final formatting for the destination platform.' },
752
- media_path: { type: 'string', description: 'WhatsApp only: absolute path to a local file to attach. Leave empty for text-only or Telnyx.' }
800
+ media_path: { type: 'string', description: 'WhatsApp only: absolute path to a local file to attach. Leave empty for text-only or Telnyx.' },
801
+ purpose: { type: 'string', enum: ['final_result', 'blocker', 'no_response'], description: 'For background task or schedule runs, required intent for this outbound message. Use final_result for a concrete useful outcome, blocker for a real issue the user should know about, or no_response to intentionally send nothing.' }
753
802
  },
754
803
  required: ['platform', 'to', 'content']
755
804
  }
@@ -1986,6 +2035,25 @@ async function executeTool(toolName, args, context, engine) {
1986
2035
  stripNoResponseMarker: false
1987
2036
  });
1988
2037
  const suppressReply = normalizedMessage === '[NO RESPONSE]';
2038
+ if (isProactiveTrigger(triggerSource)) {
2039
+ const proactiveValidation = validateProactiveSendMessageArgs({
2040
+ purpose: args.purpose,
2041
+ normalizedMessage,
2042
+ });
2043
+ if (!proactiveValidation.ok) {
2044
+ if (proactiveValidation.error) {
2045
+ return {
2046
+ error: proactiveValidation.error,
2047
+ };
2048
+ }
2049
+ return {
2050
+ sent: false,
2051
+ suppressed: proactiveValidation.suppressed === true,
2052
+ skipped: proactiveValidation.skipped === true,
2053
+ reason: proactiveValidation.reason,
2054
+ };
2055
+ }
2056
+ }
1989
2057
  if (!suppressReply && hasAlreadySentProactiveMessage({
1990
2058
  triggerSource,
1991
2059
  runState,
@@ -2701,4 +2769,4 @@ async function executeTool(toolName, args, context, engine) {
2701
2769
  }
2702
2770
  }
2703
2771
 
2704
- module.exports = { getAvailableTools, executeTool };
2772
+ module.exports = { getAvailableTools, executeTool, validateProactiveSendMessageArgs };
@@ -240,6 +240,7 @@ function createFigmaProvider() {
240
240
  description:
241
241
  'Official Figma OAuth account connections for future design file and collaboration workflows.',
242
242
  icon: 'figma',
243
+ requiresRefreshToken: true,
243
244
  apps: FIGMA_APPS,
244
245
  toolDefinitions: figmaToolDefinitions,
245
246
  connectPrompt:
@@ -337,15 +337,15 @@ function createGithubProvider() {
337
337
  method: 'POST',
338
338
  headers: {
339
339
  'Accept': 'application/json',
340
- 'Content-Type': 'application/json',
340
+ 'Content-Type': 'application/x-www-form-urlencoded',
341
341
  },
342
- body: JSON.stringify({
342
+ body: new URLSearchParams({
343
343
  client_id: config.clientId,
344
344
  client_secret: config.clientSecret,
345
345
  code,
346
346
  code_verifier: codeVerifier,
347
347
  redirect_uri: config.redirectUri,
348
- }),
348
+ }).toString(),
349
349
  });
350
350
 
351
351
  const tokenBody = await tokenResponse.text();
@@ -300,6 +300,7 @@ function createGoogleWorkspaceProvider() {
300
300
  description:
301
301
  'Official Gmail, Calendar, Drive, Docs, and Sheets integrations with app-specific accounts.',
302
302
  icon: 'google',
303
+ requiresRefreshToken: true,
303
304
  apps: GOOGLE_WORKSPACE_APPS.map(({ id, label, description }) => ({
304
305
  id,
305
306
  label,
@@ -524,6 +524,7 @@ function createHomeAssistantProvider() {
524
524
  description:
525
525
  'Official Home Assistant account connections for entity state reads, service control, and automation support.',
526
526
  icon: 'home_assistant',
527
+ requiresRefreshToken: true,
527
528
  apps: HOME_ASSISTANT_APPS,
528
529
  toolDefinitions: homeAssistantToolDefinitions,
529
530
  connectPrompt:
@@ -29,6 +29,27 @@ function isLikelyExpiredConnectionError(error) {
29
29
  ].some((hint) => message.includes(hint));
30
30
  }
31
31
 
32
+ function assertDurableOAuthCredentials(provider, credentials) {
33
+ const label = String(provider?.label || 'This integration').trim() || 'This integration';
34
+ const normalizedCredentials =
35
+ credentials && typeof credentials === 'object' ? credentials : {};
36
+
37
+ if (!String(normalizedCredentials.access_token || '').trim()) {
38
+ throw new Error(
39
+ `${label} did not return an access token, so the connection could not be completed.`,
40
+ );
41
+ }
42
+
43
+ if (
44
+ provider?.requiresRefreshToken === true &&
45
+ !String(normalizedCredentials.refresh_token || '').trim()
46
+ ) {
47
+ throw new Error(
48
+ `${label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
49
+ );
50
+ }
51
+ }
52
+
32
53
  class IntegrationManager {
33
54
  constructor(options = {}) {
34
55
  this.app = options.app || null;
@@ -303,11 +324,7 @@ class IntegrationManager {
303
324
  result.accountEmail,
304
325
  result.credentials,
305
326
  );
306
- if (!mergedCredentials.refresh_token) {
307
- throw new Error(
308
- `${provider.label} did not return a refresh token, so the connection would expire. Revoke the existing app grant for this provider and reconnect it so offline access is granted.`,
309
- );
310
- }
327
+ assertDurableOAuthCredentials(provider, mergedCredentials);
311
328
 
312
329
  db.prepare(
313
330
  `INSERT INTO integration_connections (
@@ -727,5 +744,6 @@ class IntegrationManager {
727
744
  }
728
745
 
729
746
  module.exports = {
747
+ assertDurableOAuthCredentials,
730
748
  IntegrationManager,
731
749
  };
@@ -346,6 +346,7 @@ function createMicrosoftProvider() {
346
346
  description:
347
347
  'Official Microsoft 365 OAuth account connections for Outlook, Calendar, OneDrive, and Teams.',
348
348
  icon: 'microsoft',
349
+ requiresRefreshToken: true,
349
350
  apps: MICROSOFT_APPS,
350
351
  toolDefinitions: microsoftToolDefinitions,
351
352
  connectPrompt:
@@ -241,6 +241,7 @@ function createOAuthProvider(options = {}) {
241
241
  label: options.label,
242
242
  description: options.description,
243
243
  icon: options.icon,
244
+ requiresRefreshToken: options.requiresRefreshToken === true,
244
245
  apps: apps.map(({ id, label, description }) => ({ id, label, description })),
245
246
  connectPrompt: options.connectPrompt || null,
246
247
  supportsMultipleAccounts: options.supportsMultipleAccounts !== false,
@@ -394,6 +394,7 @@ function createSpotifyProvider() {
394
394
  label: 'Spotify',
395
395
  description: 'Official Spotify account integration for music search and playback control.',
396
396
  icon: 'spotify',
397
+ requiresRefreshToken: true,
397
398
  apps: SPOTIFY_APPS,
398
399
  toolDefinitions: spotifyToolDefinitions,
399
400
  connectPrompt:
@@ -296,7 +296,7 @@ class TaskRuntime {
296
296
  if (normalizedConfig.callTo) {
297
297
  notifyHint = `\n\nThis task is configured to notify the user by phone. Use the make_call tool to call "${normalizedConfig.callTo}" with an appropriate greeting based on your findings. The configured greeting hint is: "${normalizedConfig.callGreeting || 'Hello, this is your task reminder.'}"`;
298
298
  } else if (normalizedConfig.notifyPlatform && normalizedConfig.notifyTo) {
299
- notifyHint = `\n\nIf your task result is worth notifying the user about, send it proactively via send_message to platform="${normalizedConfig.notifyPlatform}" to="${normalizedConfig.notifyTo}".`;
299
+ notifyHint = `\n\nIf your task result is worth notifying the user about, send it proactively via send_message to platform="${normalizedConfig.notifyPlatform}" to="${normalizedConfig.notifyTo}" and set purpose="final_result" for a concrete useful outcome or purpose="blocker" for a real issue the user should know about. If nothing important or actionable changed, call send_message with purpose="no_response" and content="[NO RESPONSE]".`;
300
300
  }
301
301
 
302
302
  const triggerPayloadText = executionMeta.triggerPayload