neoagent 3.2.1-beta.2 → 3.2.1-beta.4

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 (145) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +347 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +8 -41
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +15 -28
  52. package/server/services/ai/providers/githubCopilot.js +36 -16
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +13 -22
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +102 -82
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/messaging/whatsapp.js +22 -14
  116. package/server/services/network/http.js +210 -0
  117. package/server/services/network/safe_request.js +307 -0
  118. package/server/services/runtime/backends/local-vm.js +214 -66
  119. package/server/services/runtime/manager.js +17 -12
  120. package/server/services/social_reach/channels/github.js +10 -4
  121. package/server/services/social_reach/channels/reddit.js +4 -4
  122. package/server/services/social_reach/channels/rss.js +2 -2
  123. package/server/services/social_reach/channels/social_video.js +12 -7
  124. package/server/services/social_reach/channels/v2ex.js +21 -8
  125. package/server/services/social_reach/channels/x.js +2 -2
  126. package/server/services/social_reach/channels/xueqiu.js +5 -5
  127. package/server/services/social_reach/service.js +9 -6
  128. package/server/services/social_reach/utils.js +65 -14
  129. package/server/services/social_video/service.js +160 -50
  130. package/server/services/tasks/integration_runtime.js +18 -8
  131. package/server/services/tasks/runtime.js +39 -4
  132. package/server/services/voice/agentBridge.js +17 -4
  133. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  134. package/server/services/voice/liveSession.js +31 -0
  135. package/server/services/voice/openaiSpeech.js +33 -8
  136. package/server/services/voice/providers.js +233 -151
  137. package/server/services/voice/runtimeManager.js +118 -20
  138. package/server/services/voice/turnRunner.js +6 -0
  139. package/server/services/wearable/firmware_manifest.js +51 -13
  140. package/server/services/wearable/service.js +1 -0
  141. package/server/utils/abort.js +96 -0
  142. package/server/utils/cloud-security.js +110 -3
  143. package/server/utils/files.js +31 -0
  144. package/server/utils/image_payload.js +95 -0
  145. package/server/utils/retry.js +107 -0
@@ -13,9 +13,11 @@ const {
13
13
  const { getConnectionAccessMode } = require("../access");
14
14
  const { decryptValue } = require("../secrets");
15
15
  const { appendQuery, fetchJson } = require("../oauth_provider");
16
+ const { fetchResponseText } = require("../http");
16
17
  const { resolvePublicBaseUrl } = require("../env");
17
18
 
18
19
  const KEY = "neoarchive";
20
+ const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
19
21
  const APP = Object.freeze({
20
22
  id: "archive",
21
23
  label: "Archive",
@@ -277,24 +279,25 @@ function snapshot(provider, rows, context) {
277
279
  connectionMethod: "oauth",
278
280
  };
279
281
  }
280
- async function bootstrap(baseUrl) {
282
+ async function bootstrap(baseUrl, options = {}) {
281
283
  return fetchJson(
282
284
  `${baseUrl}/api/oauth/companion/neoagent/bootstrap`,
283
285
  {
284
286
  method: "POST",
285
287
  json: { redirectUri: callbackUrl(), appName: "NeoAgent" },
288
+ signal: options.signal,
286
289
  },
287
290
  { serviceName: "NeoArchive companion bootstrap" },
288
291
  );
289
292
  }
290
- async function token(baseUrl, form) {
293
+ async function token(baseUrl, form, options = {}) {
291
294
  return fetchJson(
292
295
  `${baseUrl}/oauth/token`,
293
- { method: "POST", form },
296
+ { method: "POST", form, signal: options.signal },
294
297
  { serviceName: "NeoArchive OAuth token" },
295
298
  );
296
299
  }
297
- async function access(connection) {
300
+ async function access(connection, options = {}) {
298
301
  const saved = credentials(connection);
299
302
  const baseUrl = normalizeBaseUrl(saved.baseUrl);
300
303
  if (saved.access_token && Number(saved.expires_at_ms) > Date.now() + 60000)
@@ -307,7 +310,7 @@ async function access(connection) {
307
310
  grant_type: "refresh_token",
308
311
  client_id: saved.client_id,
309
312
  refresh_token: saved.refresh_token,
310
- });
313
+ }, options);
311
314
  const next = {
312
315
  ...saved,
313
316
  access_token: text(refreshed.access_token),
@@ -319,13 +322,14 @@ async function access(connection) {
319
322
  return { baseUrl, accessToken: next.access_token, credentials: next };
320
323
  }
321
324
  async function request(connection, apiPath, options = {}) {
322
- const auth = await access(connection);
325
+ const auth = await access(connection, options);
323
326
  const response = await fetchJson(
324
327
  `${auth.baseUrl}${apiPath}`,
325
328
  {
326
329
  method: options.method || "GET",
327
330
  headers: { Authorization: `Bearer ${auth.accessToken}` },
328
331
  ...(options.json === undefined ? {} : { json: options.json }),
332
+ signal: options.signal,
329
333
  },
330
334
  { serviceName: "NeoArchive API" },
331
335
  );
@@ -336,7 +340,7 @@ function required(value, name) {
336
340
  if (!normalized) throw new Error(`${name} is required.`);
337
341
  return normalized;
338
342
  }
339
- async function upload(connection, filePath) {
343
+ async function upload(connection, filePath, options = {}) {
340
344
  const raw = text(filePath);
341
345
  if (!raw || raw.split(/[\\/]+/).includes(".."))
342
346
  throw new Error(
@@ -346,27 +350,40 @@ async function upload(connection, filePath) {
346
350
  const stats = await fs.promises.stat(resolved);
347
351
  if (!stats.isFile())
348
352
  throw new Error("file_path must point to a readable file.");
353
+ if (stats.size > MAX_UPLOAD_BYTES)
354
+ throw new Error(`file_path exceeds the ${MAX_UPLOAD_BYTES}-byte upload limit.`);
349
355
  await fs.promises.access(resolved, fs.constants.R_OK);
350
- const auth = await access(connection);
356
+ const auth = await access(connection, options);
351
357
  const form = new FormData();
352
358
  form.append(
353
359
  "files",
354
- new Blob([await fs.promises.readFile(resolved)]),
360
+ new Blob([await fs.promises.readFile(resolved, { signal: options.signal })]),
355
361
  path.basename(resolved),
356
362
  );
357
- const response = await fetch(`${auth.baseUrl}/api/v1/documents`, {
358
- method: "POST",
359
- headers: { Authorization: `Bearer ${auth.accessToken}` },
360
- body: form,
361
- });
362
- const result = await response.json().catch(() => null);
363
+ const { response, text: responseText } = await fetchResponseText(
364
+ `${auth.baseUrl}/api/v1/documents`,
365
+ {
366
+ method: "POST",
367
+ headers: { Authorization: `Bearer ${auth.accessToken}` },
368
+ body: form,
369
+ signal: options.signal,
370
+ timeoutMs: 120000,
371
+ },
372
+ { serviceName: "NeoArchive upload" },
373
+ );
374
+ let result = null;
375
+ try {
376
+ result = responseText ? JSON.parse(responseText) : null;
377
+ } catch {
378
+ result = null;
379
+ }
363
380
  if (!response.ok)
364
381
  throw new Error(
365
382
  `NeoArchive upload request failed: ${result?.error || response.statusText}`,
366
383
  );
367
384
  return { result, credentials: auth.credentials };
368
385
  }
369
- async function execute(toolName, args, connection) {
386
+ async function execute(toolName, args, connection, options = {}) {
370
387
  let pathName;
371
388
  let method = "GET";
372
389
  let json;
@@ -401,7 +418,7 @@ async function execute(toolName, args, connection) {
401
418
  pathName = "/api/v1/document-types";
402
419
  break;
403
420
  case "neoarchive_upload_document":
404
- return upload(connection, args.file_path);
421
+ return upload(connection, args.file_path, options);
405
422
  case "neoarchive_update_document":
406
423
  pathName = `/api/v1/documents/${encodeURIComponent(required(args.document_id, "document_id"))}`;
407
424
  method = "PATCH";
@@ -426,7 +443,11 @@ async function execute(toolName, args, connection) {
426
443
  default:
427
444
  throw new Error(`Unsupported NeoArchive tool: ${toolName}`);
428
445
  }
429
- const result = await request(connection, pathName, { method, json });
446
+ const result = await request(connection, pathName, {
447
+ method,
448
+ json,
449
+ signal: options.signal,
450
+ });
430
451
  return { result: result.response, credentials: result.credentials };
431
452
  }
432
453
 
@@ -483,7 +504,7 @@ function createNeoArchiveProvider() {
483
504
  ? "NeoArchive: setup is ready, but no archive account is connected."
484
505
  : "NeoArchive: connected with document search, metadata, text, upload, archive, and reprocessing tools.";
485
506
  },
486
- async beginOAuth({ state, codeVerifier, userId, agentId, appKey }) {
507
+ async beginOAuth({ state, codeVerifier, userId, agentId, appKey, signal }) {
487
508
  if (text(appKey) !== APP.id) throw new Error("Unknown NeoArchive app.");
488
509
  const stored = config(
489
510
  getProviderConfig(
@@ -493,7 +514,7 @@ function createNeoArchiveProvider() {
493
514
  ),
494
515
  );
495
516
  const baseUrl = normalizeBaseUrl(stored.baseUrl);
496
- const boot = await bootstrap(baseUrl);
517
+ const boot = await bootstrap(baseUrl, { signal });
497
518
  const challenge = crypto
498
519
  .createHash("sha256")
499
520
  .update(String(codeVerifier))
@@ -515,7 +536,7 @@ function createNeoArchiveProvider() {
515
536
  ),
516
537
  };
517
538
  },
518
- async finishOAuth({ userId, agentId, code, codeVerifier }) {
539
+ async finishOAuth({ userId, agentId, code, codeVerifier, signal }) {
519
540
  const stored = config(
520
541
  getProviderConfig(
521
542
  Number(userId),
@@ -524,21 +545,24 @@ function createNeoArchiveProvider() {
524
545
  ),
525
546
  );
526
547
  const baseUrl = normalizeBaseUrl(stored.baseUrl);
527
- const boot = await bootstrap(baseUrl);
548
+ const boot = await bootstrap(baseUrl, { signal });
528
549
  const issued = await token(baseUrl, {
529
550
  grant_type: "authorization_code",
530
551
  client_id: boot.clientId,
531
552
  code: text(code),
532
553
  redirect_uri: text(boot.redirectUri) || callbackUrl(),
533
554
  code_verifier: text(codeVerifier),
534
- });
555
+ }, { signal });
535
556
  const accessToken = text(issued.access_token);
536
557
  const refreshToken = text(issued.refresh_token);
537
558
  if (!accessToken || !refreshToken)
538
559
  throw new Error("NeoArchive did not return durable OAuth credentials.");
539
560
  const info = await fetchJson(
540
561
  `${baseUrl}/oauth/userinfo`,
541
- { headers: { Authorization: `Bearer ${accessToken}` } },
562
+ {
563
+ headers: { Authorization: `Bearer ${accessToken}` },
564
+ signal,
565
+ },
542
566
  { serviceName: "NeoArchive userinfo" },
543
567
  );
544
568
  const host = new URL(baseUrl).host;
@@ -565,8 +589,10 @@ function createNeoArchiveProvider() {
565
589
  },
566
590
  };
567
591
  },
568
- async executeTool(toolName, args, connection) {
569
- return execute(toolName, args || {}, connection);
592
+ async executeTool(toolName, args, connection, executionOptions = {}) {
593
+ return execute(toolName, args || {}, connection, {
594
+ signal: executionOptions.signal || null,
595
+ });
570
596
  },
571
597
  getUserConfig({ userId, agentId }) {
572
598
  const scoped = resolveAgentId(Number(userId), agentId);
@@ -584,17 +610,17 @@ function createNeoArchiveProvider() {
584
610
  hasConnectedAccount: accountCount > 0,
585
611
  };
586
612
  },
587
- async saveUserConfig({ userId, agentId, config: input }) {
613
+ async saveUserConfig({ userId, agentId, config: input, signal }) {
588
614
  const scoped = resolveAgentId(Number(userId), agentId);
589
615
  const existing = config(getProviderConfig(Number(userId), KEY, scoped));
590
616
  const parsed = config(input, existing);
591
617
  const baseUrl = normalizeBaseUrl(parsed.baseUrl);
592
618
  await fetchJson(
593
619
  `${baseUrl}/api/v1/health`,
594
- { method: "GET" },
620
+ { method: "GET", signal },
595
621
  { serviceName: "NeoArchive health check" },
596
622
  );
597
- await bootstrap(baseUrl);
623
+ await bootstrap(baseUrl, { signal });
598
624
  setProviderConfig(Number(userId), KEY, { baseUrl }, scoped);
599
625
  if (existing.baseUrl && existing.baseUrl !== baseUrl)
600
626
  db.prepare(
@@ -29,17 +29,22 @@ function normalizeBaseUrl(value) {
29
29
  return url.toString().replace(/\/+$/, '');
30
30
  }
31
31
 
32
- async function bootstrap(baseUrl, callbackUrl) {
32
+ async function bootstrap(baseUrl, callbackUrl, options = {}) {
33
33
  return fetchJson(`${baseUrl}/api/oauth/companion/neoagent/bootstrap`, {
34
34
  method: 'POST', json: { redirectUri: callbackUrl, appName: 'NeoAgent' },
35
+ signal: options.signal,
35
36
  }, { serviceName: 'NeoRecall companion bootstrap' });
36
37
  }
37
38
 
38
- async function token(baseUrl, form) {
39
- return fetchJson(`${baseUrl}/oauth/token`, { method: 'POST', form }, { serviceName: 'NeoRecall OAuth token' });
39
+ async function token(baseUrl, form, options = {}) {
40
+ return fetchJson(
41
+ `${baseUrl}/oauth/token`,
42
+ { method: 'POST', form, signal: options.signal },
43
+ { serviceName: 'NeoRecall OAuth token' },
44
+ );
40
45
  }
41
46
 
42
- async function revoke(credentials) {
47
+ async function revoke(credentials, options = {}) {
43
48
  const saved = credentials && typeof credentials === 'object' ? credentials : {};
44
49
  const baseUrl = normalizeBaseUrl(saved.baseUrl);
45
50
  const clientId = text(saved.client_id);
@@ -47,12 +52,13 @@ async function revoke(credentials) {
47
52
  const tokens = [saved.refresh_token, saved.access_token].map(text).filter(Boolean);
48
53
  const outcomes = await Promise.allSettled(tokens.map((value) => fetchJson(`${baseUrl}/oauth/revoke`, {
49
54
  method: 'POST', form: { client_id: clientId, token: value },
55
+ signal: options.signal,
50
56
  }, { serviceName: 'NeoRecall OAuth revocation' })));
51
57
  const failed = outcomes.find((outcome) => outcome.status === 'rejected');
52
58
  if (failed) throw failed.reason;
53
59
  }
54
60
 
55
- async function authenticated(credentials) {
61
+ async function authenticated(credentials, options = {}) {
56
62
  const saved = credentials && typeof credentials === 'object' ? credentials : {};
57
63
  const baseUrl = normalizeBaseUrl(saved.baseUrl);
58
64
  if (text(saved.access_token) && Number(saved.expires_at_ms) > Date.now() + 60_000) {
@@ -63,7 +69,7 @@ async function authenticated(credentials) {
63
69
  }
64
70
  const refreshed = await token(baseUrl, {
65
71
  grant_type: 'refresh_token', client_id: saved.client_id, refresh_token: saved.refresh_token,
66
- });
72
+ }, options);
67
73
  const next = {
68
74
  ...saved,
69
75
  access_token: text(refreshed.access_token),
@@ -97,15 +103,16 @@ function appendApiQuery(path, query) {
97
103
  return encoded ? `${path}?${encoded}` : path;
98
104
  }
99
105
 
100
- async function request(credentials, apiPath) {
101
- const authorization = await authenticated(credentials);
106
+ async function request(credentials, apiPath, options = {}) {
107
+ const authorization = await authenticated(credentials, options);
102
108
  const response = await fetchJson(`${authorization.baseUrl}${apiPath}`, {
103
109
  headers: { Authorization: `Bearer ${authorization.accessToken}` },
110
+ signal: options.signal,
104
111
  }, { serviceName: 'NeoRecall API' });
105
112
  return { result: response, credentials: authorization.credentials };
106
113
  }
107
114
 
108
- async function executeTool(toolName, args, credentials) {
115
+ async function executeTool(toolName, args, credentials, options = {}) {
109
116
  let apiPath;
110
117
  switch (toolName) {
111
118
  case 'neorecall_search': {
@@ -134,7 +141,7 @@ async function executeTool(toolName, args, credentials) {
134
141
  default:
135
142
  throw new Error(`Unsupported NeoRecall tool: ${toolName}`);
136
143
  }
137
- return request(credentials, apiPath);
144
+ return request(credentials, apiPath, options);
138
145
  }
139
146
 
140
147
  module.exports = { bootstrap, executeTool, normalizeBaseUrl, revoke, text, token };
@@ -71,9 +71,9 @@ function createNeoRecallProvider() {
71
71
  connectionMethod: 'user_config',
72
72
  requiresRefreshToken: true,
73
73
  getEnvStatus: envStatus,
74
- async beginOAuth({ state, codeVerifier, userId, agentId }) {
74
+ async beginOAuth({ state, codeVerifier, userId, agentId, signal }) {
75
75
  const baseUrl = normalizeBaseUrl(storedConfig(userId, resolveAgentId(userId, agentId)).baseUrl);
76
- const boot = await bootstrap(baseUrl, callbackUrl());
76
+ const boot = await bootstrap(baseUrl, callbackUrl(), { signal });
77
77
  const challenge = crypto.createHash('sha256').update(String(codeVerifier)).digest('base64url');
78
78
  return {
79
79
  url: appendQuery(text(boot.authorizationEndpoint) || `${baseUrl}/oauth/authorize`, {
@@ -83,18 +83,19 @@ function createNeoRecallProvider() {
83
83
  }),
84
84
  };
85
85
  },
86
- async finishOAuth({ userId, agentId, code, codeVerifier }) {
86
+ async finishOAuth({ userId, agentId, code, codeVerifier, signal }) {
87
87
  const baseUrl = normalizeBaseUrl(storedConfig(userId, resolveAgentId(userId, agentId)).baseUrl);
88
- const boot = await bootstrap(baseUrl, callbackUrl());
88
+ const boot = await bootstrap(baseUrl, callbackUrl(), { signal });
89
89
  const issued = await token(baseUrl, {
90
90
  grant_type: 'authorization_code', client_id: boot.clientId, code: text(code),
91
91
  redirect_uri: text(boot.redirectUri) || callbackUrl(), code_verifier: text(codeVerifier),
92
- });
92
+ }, { signal });
93
93
  const accessToken = text(issued.access_token);
94
94
  const refreshToken = text(issued.refresh_token);
95
95
  if (!accessToken || !refreshToken) throw new Error('NeoRecall did not return durable OAuth credentials.');
96
96
  const info = await fetchJson(`${baseUrl}/oauth/userinfo`, {
97
97
  headers: { Authorization: `Bearer ${accessToken}` },
98
+ signal,
98
99
  }, { serviceName: 'NeoRecall userinfo' });
99
100
  const host = new URL(baseUrl).host;
100
101
  const accountEmail = text(info.email) || text(info.preferred_username) || `neorecall:${text(info.sub) || host}`;
@@ -109,10 +110,14 @@ function createNeoRecallProvider() {
109
110
  };
110
111
  },
111
112
  executeTool(toolName, args, context) {
112
- return executeTool(toolName, args || {}, context.credentials);
113
+ return executeTool(toolName, args || {}, context.credentials, {
114
+ signal: context.signal,
115
+ });
113
116
  },
114
- disconnect(connection) {
115
- return revoke(connectionCredentials(connection));
117
+ disconnect(connection, executionOptions = {}) {
118
+ return revoke(connectionCredentials(connection), {
119
+ signal: executionOptions.signal || null,
120
+ });
116
121
  },
117
122
  });
118
123
 
@@ -122,16 +127,20 @@ function createNeoRecallProvider() {
122
127
  const accountCount = connectedAccountCount(Number(userId), scoped);
123
128
  return { baseUrl: stored.baseUrl, configured: Boolean(stored.baseUrl), accountCount, hasConnectedAccount: accountCount > 0 };
124
129
  };
125
- provider.saveUserConfig = async ({ userId, agentId, config }) => {
130
+ provider.saveUserConfig = async ({ userId, agentId, config, signal }) => {
126
131
  const normalizedUserId = Number(userId);
127
132
  const scoped = resolveAgentId(normalizedUserId, agentId);
128
133
  const existing = storedConfig(normalizedUserId, scoped);
129
134
  const baseUrl = normalizeBaseUrl(parseConfig(config, existing).baseUrl);
130
- const health = await fetchJson(`${baseUrl}/health`, { method: 'GET' }, { serviceName: 'NeoRecall health check' });
135
+ const health = await fetchJson(
136
+ `${baseUrl}/health`,
137
+ { method: 'GET', signal },
138
+ { serviceName: 'NeoRecall health check' },
139
+ );
131
140
  if (text(health?.status) !== 'ok' || text(health?.process) !== 'http') {
132
141
  throw new Error('The configured endpoint did not identify itself as a healthy NeoRecall HTTP service.');
133
142
  }
134
- const boot = await bootstrap(baseUrl, callbackUrl());
143
+ const boot = await bootstrap(baseUrl, callbackUrl(), { signal });
135
144
  if (text(boot?.companion) !== 'neoagent' || !text(boot?.clientId)) {
136
145
  throw new Error('The NeoRecall server does not expose the NeoAgent companion OAuth contract.');
137
146
  }
@@ -187,7 +187,8 @@ function notionUrl(path, query) {
187
187
  return url.toString();
188
188
  }
189
189
 
190
- async function notionRequest(credentials, { method = 'GET', path, query, body }) {
190
+ async function notionRequest(context, { method = 'GET', path, query, body }) {
191
+ const { credentials, signal } = context;
191
192
  return fetchJson(
192
193
  notionUrl(path, query),
193
194
  {
@@ -197,16 +198,17 @@ async function notionRequest(credentials, { method = 'GET', path, query, body })
197
198
  'Notion-Version': NOTION_VERSION,
198
199
  },
199
200
  ...(body === undefined ? {} : { json: body }),
201
+ signal,
200
202
  },
201
203
  { serviceName: 'Notion' },
202
204
  );
203
205
  }
204
206
 
205
- async function executeNotionTool(toolName, args, { credentials }) {
207
+ async function executeNotionTool(toolName, args, context) {
206
208
  switch (toolName) {
207
209
  case 'notion_search':
208
210
  return {
209
- result: await notionRequest(credentials, {
211
+ result: await notionRequest(context, {
210
212
  method: 'POST',
211
213
  path: '/v1/search',
212
214
  body: {
@@ -218,13 +220,13 @@ async function executeNotionTool(toolName, args, { credentials }) {
218
220
  };
219
221
  case 'notion_get_page':
220
222
  return {
221
- result: await notionRequest(credentials, {
223
+ result: await notionRequest(context, {
222
224
  path: `/v1/pages/${encodeURIComponent(requireText(args.page_id, 'page_id'))}`,
223
225
  }),
224
226
  };
225
227
  case 'notion_create_page':
226
228
  return {
227
- result: await notionRequest(credentials, {
229
+ result: await notionRequest(context, {
228
230
  method: 'POST',
229
231
  path: '/v1/pages',
230
232
  body: {
@@ -236,7 +238,7 @@ async function executeNotionTool(toolName, args, { credentials }) {
236
238
  };
237
239
  case 'notion_update_page':
238
240
  return {
239
- result: await notionRequest(credentials, {
241
+ result: await notionRequest(context, {
240
242
  method: 'PATCH',
241
243
  path: `/v1/pages/${encodeURIComponent(requireText(args.page_id, 'page_id'))}`,
242
244
  body: {
@@ -249,7 +251,7 @@ async function executeNotionTool(toolName, args, { credentials }) {
249
251
  };
250
252
  case 'notion_query_database':
251
253
  return {
252
- result: await notionRequest(credentials, {
254
+ result: await notionRequest(context, {
253
255
  method: 'POST',
254
256
  path: `/v1/databases/${encodeURIComponent(requireText(args.database_id, 'database_id'))}/query`,
255
257
  body: {
@@ -261,14 +263,14 @@ async function executeNotionTool(toolName, args, { credentials }) {
261
263
  };
262
264
  case 'notion_get_block_children':
263
265
  return {
264
- result: await notionRequest(credentials, {
266
+ result: await notionRequest(context, {
265
267
  path: `/v1/blocks/${encodeURIComponent(requireText(args.block_id, 'block_id'))}/children`,
266
268
  query: { page_size: Math.max(1, Math.min(Number(args.page_size) || 25, 100)) },
267
269
  }),
268
270
  };
269
271
  case 'notion_append_block_children':
270
272
  return {
271
- result: await notionRequest(credentials, {
273
+ result: await notionRequest(context, {
272
274
  method: 'PATCH',
273
275
  path: `/v1/blocks/${encodeURIComponent(requireText(args.block_id, 'block_id'))}/children`,
274
276
  body: { children: Array.isArray(args.children) ? args.children : [] },
@@ -276,7 +278,7 @@ async function executeNotionTool(toolName, args, { credentials }) {
276
278
  };
277
279
  case 'notion_update_block':
278
280
  return {
279
- result: await notionRequest(credentials, {
281
+ result: await notionRequest(context, {
280
282
  method: 'PATCH',
281
283
  path: `/v1/blocks/${encodeURIComponent(requireText(args.block_id, 'block_id'))}`,
282
284
  body: args.body || {},
@@ -284,14 +286,14 @@ async function executeNotionTool(toolName, args, { credentials }) {
284
286
  };
285
287
  case 'notion_delete_block':
286
288
  return {
287
- result: await notionRequest(credentials, {
289
+ result: await notionRequest(context, {
288
290
  method: 'DELETE',
289
291
  path: `/v1/blocks/${encodeURIComponent(requireText(args.block_id, 'block_id'))}`,
290
292
  }),
291
293
  };
292
294
  case 'notion_api_request':
293
295
  return {
294
- result: await notionRequest(credentials, {
296
+ result: await notionRequest(context, {
295
297
  method: args.method,
296
298
  path: requireText(args.path, 'path'),
297
299
  query: args.query,
@@ -332,7 +334,7 @@ function createNotionProvider() {
332
334
  appId: app.id,
333
335
  };
334
336
  },
335
- async finishOAuth({ code, app }) {
337
+ async finishOAuth({ code, app, signal }) {
336
338
  const config = resolveNotionOAuthConfig();
337
339
  const basic = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
338
340
  'base64',
@@ -349,6 +351,7 @@ function createNotionProvider() {
349
351
  code,
350
352
  redirect_uri: config.redirectUri,
351
353
  },
354
+ signal,
352
355
  },
353
356
  { serviceName: 'Notion' },
354
357
  );