neoagent 2.3.1-beta.2 → 2.3.1-beta.21

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 (264) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +23057 -0
  66. package/flutter_app/lib/main_app_shell.dart +1682 -0
  67. package/flutter_app/lib/main_integrations.dart +931 -0
  68. package/flutter_app/lib/main_launcher.dart +959 -0
  69. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  70. package/flutter_app/lib/main_models.dart +3473 -0
  71. package/flutter_app/lib/main_shared.dart +2861 -0
  72. package/flutter_app/lib/main_theme.dart +204 -0
  73. package/flutter_app/lib/main_voice_assistant.dart +831 -0
  74. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  75. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  76. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  77. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  78. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  79. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  80. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  81. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  82. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  83. package/flutter_app/lib/src/backend_client.dart +1833 -0
  84. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  85. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  86. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  87. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  88. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  89. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  90. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  91. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  92. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  93. package/flutter_app/lib/src/health_bridge.dart +136 -0
  94. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  95. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  96. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  97. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  98. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  99. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  100. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  101. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  102. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  103. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  104. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  105. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  106. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  107. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  108. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  109. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  110. package/flutter_app/lib/src/theme/palette.dart +81 -0
  111. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  112. package/flutter_app/linux/CMakeLists.txt +128 -0
  113. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  114. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  115. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  116. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  117. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  118. package/flutter_app/linux/runner/main.cc +6 -0
  119. package/flutter_app/linux/runner/my_application.cc +144 -0
  120. package/flutter_app/linux/runner/my_application.h +18 -0
  121. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  122. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  123. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  124. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  125. package/flutter_app/macos/Podfile +42 -0
  126. package/flutter_app/macos/Podfile.lock +87 -0
  127. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  128. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  129. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  130. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  131. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  132. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  133. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  134. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  135. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  136. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  137. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  138. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  139. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  140. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  141. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  142. package/flutter_app/macos/Runner/Info.plist +36 -0
  143. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  144. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  145. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  146. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  147. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  148. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  149. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  150. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  151. package/flutter_app/patch_strings.py +12 -0
  152. package/flutter_app/pubspec.lock +1088 -0
  153. package/flutter_app/pubspec.yaml +53 -0
  154. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  155. package/flutter_app/test/recording_payloads_test.dart +53 -0
  156. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  157. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  158. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  159. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  160. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  161. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  162. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  163. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  164. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  165. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  166. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  167. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  168. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  169. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  170. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  171. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  172. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  173. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  174. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  175. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  176. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  177. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  178. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  179. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  180. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  181. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  182. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  183. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  184. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  185. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  186. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  187. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  188. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  189. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  190. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  191. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  192. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  193. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  194. package/flutter_app/web/favicon.png +0 -0
  195. package/flutter_app/web/favicon.svg +12 -0
  196. package/flutter_app/web/icons/Icon-192.png +0 -0
  197. package/flutter_app/web/icons/Icon-512.png +0 -0
  198. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  199. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  200. package/flutter_app/web/index.html +39 -0
  201. package/flutter_app/web/manifest.json +35 -0
  202. package/flutter_app/windows/CMakeLists.txt +108 -0
  203. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  204. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  205. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  206. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  207. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  208. package/flutter_app/windows/runner/Runner.rc +121 -0
  209. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  210. package/flutter_app/windows/runner/flutter_window.h +37 -0
  211. package/flutter_app/windows/runner/main.cpp +53 -0
  212. package/flutter_app/windows/runner/resource.h +16 -0
  213. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  214. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  215. package/flutter_app/windows/runner/utils.cpp +65 -0
  216. package/flutter_app/windows/runner/utils.h +19 -0
  217. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  218. package/flutter_app/windows/runner/win32_window.h +102 -0
  219. package/lib/manager.js +231 -7
  220. package/package.json +3 -1
  221. package/server/db/database.js +68 -0
  222. package/server/http/middleware.js +50 -0
  223. package/server/http/routes.js +3 -1
  224. package/server/index.js +1 -0
  225. package/server/public/.last_build_id +1 -1
  226. package/server/public/assets/NOTICES +61 -0
  227. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  228. package/server/public/flutter_bootstrap.js +1 -1
  229. package/server/public/main.dart.js +65262 -64422
  230. package/server/routes/integrations.js +86 -0
  231. package/server/routes/memory.js +11 -2
  232. package/server/routes/screenHistory.js +46 -0
  233. package/server/routes/triggers.js +81 -0
  234. package/server/services/ai/models.js +30 -0
  235. package/server/services/ai/providers/githubCopilot.js +97 -0
  236. package/server/services/ai/providers/openai.js +2 -1
  237. package/server/services/ai/providers/openaiCodex.js +31 -0
  238. package/server/services/ai/settings.js +20 -0
  239. package/server/services/ai/systemPrompt.js +1 -1
  240. package/server/services/ai/tools.js +35 -6
  241. package/server/services/browser/controller.js +47 -3
  242. package/server/services/desktop/screenRecorder.js +172 -0
  243. package/server/services/integrations/env.js +5 -0
  244. package/server/services/integrations/github/common.js +106 -0
  245. package/server/services/integrations/github/provider.js +499 -0
  246. package/server/services/integrations/github/repos.js +1124 -0
  247. package/server/services/integrations/home_assistant/provider.js +306 -26
  248. package/server/services/integrations/manager.js +63 -7
  249. package/server/services/integrations/oauth_provider.js +13 -6
  250. package/server/services/integrations/provider_config_store.js +76 -0
  251. package/server/services/integrations/registry.js +4 -0
  252. package/server/services/integrations/trello/provider.js +744 -0
  253. package/server/services/integrations/whatsapp/provider.js +6 -2
  254. package/server/services/manager.js +22 -0
  255. package/server/services/memory/manager.js +39 -2
  256. package/server/services/skills/base_catalog.js +33 -0
  257. package/server/services/tasks/adapters/index.js +1 -0
  258. package/server/services/tasks/adapters/manual.js +12 -0
  259. package/server/services/tasks/runtime.js +1 -1
  260. package/server/services/voice/openaiClient.js +4 -1
  261. package/server/services/voice/providers.js +2 -1
  262. package/server/services/widgets/service.js +49 -4
  263. package/server/utils/local_secrets.js +56 -0
  264. package/server/utils/logger.js +37 -9
@@ -1,10 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('crypto');
4
+ const net = require('net');
5
+ const ipaddr = require('ipaddr.js');
4
6
  const {
5
7
  describeEnvStatus,
6
8
  resolveHomeAssistantOAuthConfig,
7
9
  } = require('../env');
10
+ const {
11
+ deleteProviderConfig,
12
+ getProviderConfig,
13
+ setProviderConfig,
14
+ } = require('../provider_config_store');
8
15
  const {
9
16
  appendQuery,
10
17
  createOAuthProvider,
@@ -93,6 +100,248 @@ const homeAssistantToolDefinitions = [
93
100
  },
94
101
  ];
95
102
 
103
+ function trimText(value) {
104
+ return String(value || '').trim();
105
+ }
106
+
107
+ function isTruthyEnv(name) {
108
+ const value = String(process.env[name] || '').trim().toLowerCase();
109
+ return value === '1' || value === 'true' || value === 'yes' || value === 'on';
110
+ }
111
+
112
+ function isLikelyLocalHostname(hostname) {
113
+ const host = String(hostname || '').trim().toLowerCase();
114
+ if (!host) return false;
115
+ if (host === 'localhost' || host === 'host.docker.internal') return true;
116
+ if (host.endsWith('.localhost')) return true;
117
+ if (host.endsWith('.local') || host.endsWith('.lan') || host.endsWith('.internal')) {
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+
123
+ function isPrivateIpv4(hostname) {
124
+ const parts = String(hostname || '').split('.').map((part) => Number(part));
125
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
126
+ return false;
127
+ }
128
+ const [a, b] = parts;
129
+ if (a === 10) return true;
130
+ if (a === 127) return true;
131
+ if (a === 169 && b === 254) return true;
132
+ if (a === 172 && b >= 16 && b <= 31) return true;
133
+ if (a === 192 && b === 168) return true;
134
+ if (a === 100 && b >= 64 && b <= 127) return true;
135
+ if (a === 0) return true;
136
+ return false;
137
+ }
138
+
139
+ function isPrivateIpv6(hostname) {
140
+ const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
141
+ if (!host) return false;
142
+ try {
143
+ const parsed = ipaddr.parse(host);
144
+ if (parsed.kind() !== 'ipv6') return false;
145
+
146
+ // Normalize IPv4-mapped IPv6 literals and enforce the same local/private rules.
147
+ if (parsed.isIPv4MappedAddress()) {
148
+ return isPrivateIpv4(parsed.toIPv4Address().toString());
149
+ }
150
+
151
+ const range = parsed.range();
152
+ return range === 'loopback' || range === 'uniqueLocal' || range === 'linkLocal' || range === 'unspecified';
153
+ } catch {
154
+ return false;
155
+ }
156
+ }
157
+
158
+ function isPrivateOrLocalIp(hostname) {
159
+ const host = String(hostname || '').trim().replace(/^\[|\]$/g, '');
160
+ const kind = net.isIP(host);
161
+ if (kind === 4) return isPrivateIpv4(host);
162
+ if (kind === 6) return isPrivateIpv6(host);
163
+ return false;
164
+ }
165
+
166
+ function validateHomeAssistantBaseUrlSafety(parsedUrl) {
167
+ const allowPrivate = isTruthyEnv('HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL');
168
+ const host = String(parsedUrl.hostname || '').trim();
169
+ const localHostname = isLikelyLocalHostname(host);
170
+ const localIp = isPrivateOrLocalIp(host);
171
+ const isLocalTarget = localHostname || localIp;
172
+
173
+ if (isLocalTarget && !allowPrivate) {
174
+ throw new Error(
175
+ 'Home Assistant base URL cannot target localhost/private network addresses unless HOME_ASSISTANT_ALLOW_PRIVATE_BASE_URL=1 is set on the server.',
176
+ );
177
+ }
178
+
179
+ if (parsedUrl.protocol === 'http:' && !isLocalTarget) {
180
+ throw new Error('Home Assistant base URL must use HTTPS for non-local hosts.');
181
+ }
182
+ }
183
+
184
+ function normalizeBaseUrl(value) {
185
+ const text = trimText(value);
186
+ if (!text) return '';
187
+ return text.replace(/\/$/, '');
188
+ }
189
+
190
+ function normalizeOptionalAbsoluteUrl(value, label) {
191
+ const text = trimText(value);
192
+ if (!text) return '';
193
+ try {
194
+ const parsed = new URL(text);
195
+ if (!/^https?:$/.test(parsed.protocol)) {
196
+ throw new Error(`${label} must use http or https.`);
197
+ }
198
+ return parsed.toString().replace(/\/$/, '');
199
+ } catch {
200
+ throw new Error(`${label} must be a valid absolute URL.`);
201
+ }
202
+ }
203
+
204
+ function normalizeHomeAssistantBaseUrl(value) {
205
+ const text = trimText(value);
206
+ if (!text) return '';
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(text);
210
+ } catch {
211
+ throw new Error('Home Assistant base URL must be a valid absolute URL.');
212
+ }
213
+ if (!/^https?:$/.test(parsed.protocol)) {
214
+ throw new Error('Home Assistant base URL must use http or https.');
215
+ }
216
+ validateHomeAssistantBaseUrlSafety(parsed);
217
+ return parsed.toString().replace(/\/$/, '');
218
+ }
219
+
220
+ function normalizeUserHomeAssistantConfig(rawConfig) {
221
+ const source = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
222
+ return {
223
+ baseUrl: normalizeBaseUrl(source.baseUrl),
224
+ clientId: trimText(source.clientId),
225
+ clientSecret: trimText(source.clientSecret),
226
+ redirectUri: trimText(source.redirectUri),
227
+ };
228
+ }
229
+
230
+ function resolveUserHomeAssistantConfig(userId) {
231
+ const userConfig = normalizeUserHomeAssistantConfig(
232
+ Number.isInteger(Number(userId)) && Number(userId) > 0
233
+ ? getProviderConfig(Number(userId), 'home_assistant')
234
+ : {},
235
+ );
236
+ const envConfig = resolveHomeAssistantOAuthConfig();
237
+ return {
238
+ baseUrl: userConfig.baseUrl || envConfig.baseUrl,
239
+ clientId: userConfig.clientId || envConfig.clientId,
240
+ clientSecret: userConfig.clientSecret || envConfig.clientSecret,
241
+ redirectUri: userConfig.redirectUri || envConfig.redirectUri,
242
+ };
243
+ }
244
+
245
+ function validateResolvedConfig(config) {
246
+ const missing = [];
247
+ if (!trimText(config.baseUrl)) missing.push('baseUrl');
248
+ if (!trimText(config.clientId)) missing.push('clientId');
249
+ if (!trimText(config.clientSecret)) missing.push('clientSecret');
250
+ return {
251
+ configured: missing.length === 0,
252
+ missing,
253
+ };
254
+ }
255
+
256
+ function resolveHomeAssistantConfigForUser(userId) {
257
+ const merged = resolveUserHomeAssistantConfig(userId);
258
+ const validatedBaseUrl = merged.baseUrl
259
+ ? normalizeHomeAssistantBaseUrl(merged.baseUrl)
260
+ : '';
261
+ const validatedRedirectUri = merged.redirectUri
262
+ ? normalizeOptionalAbsoluteUrl(
263
+ merged.redirectUri,
264
+ 'Home Assistant OAuth redirect URI',
265
+ )
266
+ : '';
267
+ const result = {
268
+ baseUrl: validatedBaseUrl,
269
+ clientId: trimText(merged.clientId),
270
+ clientSecret: trimText(merged.clientSecret),
271
+ redirectUri: validatedRedirectUri,
272
+ };
273
+ const status = validateResolvedConfig(result);
274
+ return {
275
+ ...result,
276
+ configured: status.configured,
277
+ missing: status.missing,
278
+ };
279
+ }
280
+
281
+ function sanitizeHomeAssistantUserConfigForClient(rawConfig) {
282
+ const config = normalizeUserHomeAssistantConfig(rawConfig);
283
+ return {
284
+ baseUrl: config.baseUrl,
285
+ clientId: config.clientId,
286
+ redirectUri: config.redirectUri,
287
+ hasClientSecret: Boolean(config.clientSecret),
288
+ };
289
+ }
290
+
291
+ function parseHomeAssistantConfigInput(input, existingConfig = {}) {
292
+ const source = input && typeof input === 'object' ? input : {};
293
+ const baseUrl = normalizeHomeAssistantBaseUrl(source.baseUrl);
294
+ const clientId = trimText(source.clientId);
295
+ const clientSecret =
296
+ trimText(source.clientSecret) || trimText(existingConfig.clientSecret);
297
+ const redirectUri = source.redirectUri
298
+ ? normalizeOptionalAbsoluteUrl(
299
+ source.redirectUri,
300
+ 'Home Assistant OAuth redirect URI',
301
+ )
302
+ : '';
303
+
304
+ if (!baseUrl) {
305
+ throw new Error('Home Assistant base URL is required.');
306
+ }
307
+ if (!clientId) {
308
+ throw new Error('Home Assistant OAuth client ID is required.');
309
+ }
310
+ if (!clientSecret) {
311
+ throw new Error('Home Assistant OAuth client secret is required.');
312
+ }
313
+
314
+ return {
315
+ baseUrl,
316
+ clientId,
317
+ clientSecret,
318
+ redirectUri,
319
+ };
320
+ }
321
+
322
+ function saveHomeAssistantUserConfig(userId, input) {
323
+ const normalizedUserId = Number(userId);
324
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
325
+ throw new Error('A valid user is required to save Home Assistant configuration.');
326
+ }
327
+ const existingConfig = normalizeUserHomeAssistantConfig(
328
+ getProviderConfig(normalizedUserId, 'home_assistant'),
329
+ );
330
+ const config = parseHomeAssistantConfigInput(input, existingConfig);
331
+ setProviderConfig(normalizedUserId, 'home_assistant', config);
332
+ return sanitizeHomeAssistantUserConfigForClient(config);
333
+ }
334
+
335
+ function getHomeAssistantUserConfig(userId) {
336
+ const normalizedUserId = Number(userId);
337
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
338
+ return sanitizeHomeAssistantUserConfigForClient({});
339
+ }
340
+ return sanitizeHomeAssistantUserConfigForClient(
341
+ getProviderConfig(normalizedUserId, 'home_assistant'),
342
+ );
343
+ }
344
+
96
345
  function requireText(value, label) {
97
346
  const text = String(value || '').trim();
98
347
  if (!text) throw new Error(`${label} is required.`);
@@ -102,7 +351,7 @@ function requireText(value, label) {
102
351
  function homeAssistantUrl(baseUrl, path, query) {
103
352
  const normalizedBase = String(baseUrl || '').trim().replace(/\/$/, '');
104
353
  if (!normalizedBase) {
105
- throw new Error('HOME_ASSISTANT_BASE_URL is required.');
354
+ throw new Error('Home Assistant base URL is required.');
106
355
  }
107
356
  const url = new URL(
108
357
  String(path || '').startsWith('http')
@@ -119,7 +368,7 @@ function homeAssistantUrl(baseUrl, path, query) {
119
368
  }
120
369
 
121
370
  async function homeAssistantRequest(credentials, options = {}) {
122
- const config = resolveHomeAssistantOAuthConfig();
371
+ const config = resolveHomeAssistantConfigForUser(options.userId);
123
372
  const accessToken = String(credentials?.access_token || '').trim();
124
373
  if (!accessToken) {
125
374
  throw new Error('Home Assistant access token is missing. Reconnect this integration account.');
@@ -138,14 +387,20 @@ async function homeAssistantRequest(credentials, options = {}) {
138
387
  );
139
388
  }
140
389
 
141
- async function executeHomeAssistantTool(toolName, args, { credentials }) {
390
+ async function executeHomeAssistantTool(toolName, args, { connection, credentials }) {
142
391
  switch (toolName) {
143
392
  case 'home_assistant_get_config':
144
393
  return {
145
- result: await homeAssistantRequest(credentials, { path: '/api/config' }),
394
+ result: await homeAssistantRequest(credentials, {
395
+ path: '/api/config',
396
+ userId: connection?.user_id,
397
+ }),
146
398
  };
147
399
  case 'home_assistant_list_states': {
148
- const states = await homeAssistantRequest(credentials, { path: '/api/states' });
400
+ const states = await homeAssistantRequest(credentials, {
401
+ path: '/api/states',
402
+ userId: connection?.user_id,
403
+ });
149
404
  const domain = String(args.domain || '').trim().toLowerCase();
150
405
  const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
151
406
  const filtered = Array.isArray(states)
@@ -163,6 +418,7 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
163
418
  return {
164
419
  result: await homeAssistantRequest(credentials, {
165
420
  path: `/api/states/${encodeURIComponent(requireText(args.entity_id, 'entity_id'))}`,
421
+ userId: connection?.user_id,
166
422
  }),
167
423
  };
168
424
  case 'home_assistant_call_service':
@@ -171,6 +427,7 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
171
427
  method: 'POST',
172
428
  path: `/api/services/${encodeURIComponent(requireText(args.domain, 'domain'))}/${encodeURIComponent(requireText(args.service, 'service'))}`,
173
429
  body: args.service_data || {},
430
+ userId: connection?.user_id,
174
431
  }),
175
432
  };
176
433
  case 'home_assistant_api_request':
@@ -180,6 +437,7 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
180
437
  path: requireText(args.path, 'path'),
181
438
  query: args.query,
182
439
  body: args.body,
440
+ userId: connection?.user_id,
183
441
  }),
184
442
  };
185
443
  default:
@@ -187,19 +445,25 @@ async function executeHomeAssistantTool(toolName, args, { credentials }) {
187
445
  }
188
446
  }
189
447
 
190
- function resolveHomeAssistantEnvStatus() {
191
- const config = resolveHomeAssistantOAuthConfig();
192
- const missing = config.missing.slice();
193
- if (!config.baseUrl) {
194
- missing.push('HOME_ASSISTANT_BASE_URL');
448
+ function resolveHomeAssistantEnvStatus(userId) {
449
+ try {
450
+ const config = resolveHomeAssistantConfigForUser(userId);
451
+ return {
452
+ configured: config.configured,
453
+ missing: config.missing,
454
+ summary: config.configured
455
+ ? 'Home Assistant is ready for account connections.'
456
+ : 'Complete your personal Home Assistant setup to connect an account.',
457
+ setupMode: 'user',
458
+ };
459
+ } catch (error) {
460
+ return {
461
+ configured: false,
462
+ missing: ['baseUrl'],
463
+ summary: `Home Assistant setup is invalid: ${error?.message || 'unknown error'}`,
464
+ setupMode: 'user',
465
+ };
195
466
  }
196
- return describeEnvStatus(
197
- {
198
- configured: missing.length === 0,
199
- missing,
200
- },
201
- { label: 'Home Assistant' },
202
- );
203
467
  }
204
468
 
205
469
  function normalizeCurrentUser(currentUser) {
@@ -225,8 +489,8 @@ function stableAccountEmailLikeIdentifier(user, config) {
225
489
  return `homeassistant@${host}`;
226
490
  }
227
491
 
228
- async function fetchCurrentUser(token) {
229
- const config = resolveHomeAssistantOAuthConfig();
492
+ async function fetchCurrentUser(token, userId) {
493
+ const config = resolveHomeAssistantConfigForUser(userId);
230
494
  return fetchJson(
231
495
  homeAssistantUrl(config.baseUrl, '/api/auth/current_user'),
232
496
  {
@@ -250,11 +514,11 @@ function createHomeAssistantProvider() {
250
514
  toolDefinitions: homeAssistantToolDefinitions,
251
515
  connectPrompt:
252
516
  'Connect your Home Assistant account to let the agent read entity states and control services with structured tools.',
253
- getEnvStatus() {
254
- return resolveHomeAssistantEnvStatus();
517
+ getEnvStatus(context = {}) {
518
+ return resolveHomeAssistantEnvStatus(context.userId);
255
519
  },
256
- async beginOAuth({ state, codeVerifier, app }) {
257
- const config = resolveHomeAssistantOAuthConfig();
520
+ async beginOAuth({ state, codeVerifier, app, userId }) {
521
+ const config = resolveHomeAssistantConfigForUser(userId);
258
522
  const codeChallenge = String(codeChallengeForVerifier(codeVerifier));
259
523
  return {
260
524
  url: appendQuery(homeAssistantUrl(config.baseUrl, '/auth/authorize'), {
@@ -269,8 +533,8 @@ function createHomeAssistantProvider() {
269
533
  appId: app.id,
270
534
  };
271
535
  },
272
- async finishOAuth({ code, codeVerifier, app }) {
273
- const config = resolveHomeAssistantOAuthConfig();
536
+ async finishOAuth({ code, codeVerifier, app, userId }) {
537
+ const config = resolveHomeAssistantConfigForUser(userId);
274
538
  const token = await fetchJson(
275
539
  homeAssistantUrl(config.baseUrl, '/auth/token'),
276
540
  {
@@ -297,7 +561,7 @@ function createHomeAssistantProvider() {
297
561
  throw new Error('Home Assistant OAuth did not return a refresh token.');
298
562
  }
299
563
 
300
- const currentUser = await fetchCurrentUser(accessToken);
564
+ const currentUser = await fetchCurrentUser(accessToken, userId);
301
565
  const normalizedUser = normalizeCurrentUser(currentUser);
302
566
  const accountEmail = stableAccountEmailLikeIdentifier(normalizedUser, config);
303
567
 
@@ -328,6 +592,20 @@ function createHomeAssistantProvider() {
328
592
  };
329
593
  },
330
594
  executeTool: executeHomeAssistantTool,
595
+ getUserConfig({ userId }) {
596
+ return getHomeAssistantUserConfig(userId);
597
+ },
598
+ saveUserConfig({ userId, config }) {
599
+ return saveHomeAssistantUserConfig(userId, config);
600
+ },
601
+ clearUserConfig({ userId }) {
602
+ const normalizedUserId = Number(userId);
603
+ if (!Number.isInteger(normalizedUserId) || normalizedUserId <= 0) {
604
+ throw new Error('A valid user is required to clear Home Assistant configuration.');
605
+ }
606
+ deleteProviderConfig(normalizedUserId, 'home_assistant');
607
+ return { cleared: true };
608
+ },
331
609
  });
332
610
  }
333
611
 
@@ -346,5 +624,7 @@ function codeChallengeForVerifier(codeVerifier) {
346
624
  }
347
625
 
348
626
  module.exports = {
627
+ getHomeAssistantUserConfig,
628
+ saveHomeAssistantUserConfig,
349
629
  createHomeAssistantProvider,
350
630
  };
@@ -13,6 +13,22 @@ const {
13
13
 
14
14
  const OAUTH_STATE_PATTERN = /^[a-f0-9]{32,128}$/i;
15
15
 
16
+ function isLikelyExpiredConnectionError(error) {
17
+ const message = String(error?.message || error || '').toLowerCase();
18
+ if (!message) return false;
19
+ return [
20
+ 'invalid_grant',
21
+ 'token refresh failed',
22
+ 'token expired',
23
+ 'access token is missing',
24
+ 'refresh token is missing',
25
+ 'reconnect this integration account',
26
+ 'account is no longer authorized',
27
+ 'reauthorize',
28
+ 're-authorize',
29
+ ].some((hint) => message.includes(hint));
30
+ }
31
+
16
32
  class IntegrationManager {
17
33
  constructor(options = {}) {
18
34
  this.app = options.app || null;
@@ -147,7 +163,12 @@ class IntegrationManager {
147
163
 
148
164
  return this.registry
149
165
  .list()
150
- .map((provider) => provider.buildSnapshot(rowsByProvider.get(provider.key) || []));
166
+ .map((provider) =>
167
+ provider.buildSnapshot(rowsByProvider.get(provider.key) || [], {
168
+ userId,
169
+ agentId: scopedAgentId,
170
+ }),
171
+ );
151
172
  }
152
173
 
153
174
  async beginOAuth(userId, providerKey, options = {}) {
@@ -163,7 +184,10 @@ class IntegrationManager {
163
184
  throw new Error(`Unknown ${provider.label} app: ${appKey || 'missing app key'}`);
164
185
  }
165
186
 
166
- const env = provider.getEnvStatus();
187
+ const env = provider.getEnvStatus({
188
+ userId,
189
+ agentId,
190
+ });
167
191
  if (!env.configured) {
168
192
  throw new Error(env.summary);
169
193
  }
@@ -389,7 +413,10 @@ class IntegrationManager {
389
413
  getToolDefinitions(userId, agentId = null) {
390
414
  const definitions = [];
391
415
  for (const provider of this.registry.list()) {
392
- const env = provider.getEnvStatus();
416
+ const env = provider.getEnvStatus({
417
+ userId,
418
+ agentId,
419
+ });
393
420
  if (!env.configured) continue;
394
421
  const connections = this.listConnections(userId, provider.key, agentId);
395
422
  const connectedAppIds = Array.from(
@@ -411,9 +438,15 @@ class IntegrationManager {
411
438
  if (!provider) {
412
439
  throw new Error(`Unknown integration provider: ${providerKey}`);
413
440
  }
414
- const env = provider.getEnvStatus();
441
+ const env = provider.getEnvStatus({
442
+ userId,
443
+ agentId,
444
+ });
415
445
  const connections = this.listConnections(userId, provider.key, agentId);
416
- const snapshot = provider.buildSnapshot(connections);
446
+ const snapshot = provider.buildSnapshot(connections, {
447
+ userId,
448
+ agentId,
449
+ });
417
450
  const connectedAppIds = snapshot.apps
418
451
  .filter((app) => app.connection.connected)
419
452
  .map((app) => app.id);
@@ -582,7 +615,10 @@ class IntegrationManager {
582
615
  for (const provider of this.registry.list()) {
583
616
  if (!provider.supportsTool(toolName)) continue;
584
617
  foundSupportingProvider = true;
585
- const env = provider.getEnvStatus();
618
+ const env = provider.getEnvStatus({
619
+ userId,
620
+ agentId,
621
+ });
586
622
  if (!env.configured) {
587
623
  return { error: env.summary };
588
624
  }
@@ -607,6 +643,17 @@ class IntegrationManager {
607
643
  selection.connection,
608
644
  );
609
645
  } catch (err) {
646
+ if (isLikelyExpiredConnectionError(err)) {
647
+ db.prepare(
648
+ `UPDATE integration_connections
649
+ SET status = 'expired', updated_at = datetime('now')
650
+ WHERE id = ? AND user_id = ? AND agent_id = ?`,
651
+ ).run(
652
+ selection.connection.id,
653
+ userId,
654
+ resolveAgentId(userId, agentId),
655
+ );
656
+ }
610
657
  return { error: err?.message || 'execution_error' };
611
658
  }
612
659
  if (!execution) {
@@ -641,7 +688,13 @@ class IntegrationManager {
641
688
  summarizeConnectedProviders(userId, agentId = null) {
642
689
  const providers = this.registry.list().map((provider) => ({
643
690
  provider,
644
- snapshot: provider.buildSnapshot(this.listConnections(userId, provider.key, agentId)),
691
+ snapshot: provider.buildSnapshot(
692
+ this.listConnections(userId, provider.key, agentId),
693
+ {
694
+ userId,
695
+ agentId,
696
+ },
697
+ ),
645
698
  }));
646
699
 
647
700
  if (providers.length === 0) {
@@ -655,6 +708,9 @@ class IntegrationManager {
655
708
  }
656
709
 
657
710
  if (!snapshot?.env?.configured) {
711
+ if (snapshot?.env?.setupMode === 'user') {
712
+ return `${provider.label}: setup is not complete for this user yet. If the user wants to use it, tell them to finish setup in Official Integrations first.`;
713
+ }
658
714
  return `${provider.label}: available but not configured on the server yet. If the user wants to use it, tell them to finish setup in Official Integrations first.`;
659
715
  }
660
716
 
@@ -247,8 +247,8 @@ function createOAuthProvider(options = {}) {
247
247
  getToolAppId(toolName) {
248
248
  return toolAppMap.get(String(toolName || '').trim()) || null;
249
249
  },
250
- getEnvStatus() {
251
- return options.getEnvStatus();
250
+ getEnvStatus(context = {}) {
251
+ return options.getEnvStatus(context);
252
252
  },
253
253
  getToolDefinitions(toolOptions = {}) {
254
254
  const connectedAppIds = new Set(toolOptions.connectedAppIds || []);
@@ -257,8 +257,8 @@ function createOAuthProvider(options = {}) {
257
257
  supportsTool(toolName) {
258
258
  return toolAppMap.has(String(toolName || '').trim());
259
259
  },
260
- buildSnapshot(connectionRows) {
261
- const env = this.getEnvStatus();
260
+ buildSnapshot(connectionRows, context = {}) {
261
+ const env = this.getEnvStatus(context);
262
262
  const byApp = new Map();
263
263
  for (const row of Array.isArray(connectionRows) ? connectionRows : []) {
264
264
  const appId = String(row.app_key || '').trim();
@@ -319,7 +319,7 @@ function createOAuthProvider(options = {}) {
319
319
  throw new Error(`Unknown ${this.label} app: ${appKey}`);
320
320
  }
321
321
  const normalizedState = assertValidOAuthState(state);
322
- const env = this.getEnvStatus();
322
+ const env = this.getEnvStatus({ userId });
323
323
  if (!env.configured) {
324
324
  throw new Error(env.summary);
325
325
  }
@@ -343,7 +343,7 @@ function createOAuthProvider(options = {}) {
343
343
  userId,
344
344
  state: normalizedState,
345
345
  app,
346
- env: this.getEnvStatus(),
346
+ env: this.getEnvStatus({ userId }),
347
347
  });
348
348
  },
349
349
  async disconnect(connectionRow) {
@@ -368,12 +368,16 @@ function createOAuthProvider(options = {}) {
368
368
  appId: toolAppMap.get(String(toolName || '').trim()) || connectionRow.app_key,
369
369
  connection: connectionRow,
370
370
  credentials,
371
+ env: this.getEnvStatus({ userId: connectionRow?.user_id }),
371
372
  });
372
373
  },
373
374
  summarizeConnection(connectionRows) {
374
375
  const snapshot = this.buildSnapshot(connectionRows);
375
376
  if (!snapshot.connection.connected) {
376
377
  if (snapshot.connection.status === 'env_not_configured') {
378
+ if (snapshot?.env?.setupMode === 'user') {
379
+ return `${this.label} still needs per-user setup before accounts can connect.`;
380
+ }
377
381
  return `${this.label} still needs administrator setup before accounts can connect.`;
378
382
  }
379
383
  return `${this.label} is not connected.`;
@@ -392,6 +396,9 @@ function createOAuthProvider(options = {}) {
392
396
  },
393
397
  summarizeForModel(snapshot) {
394
398
  if (!snapshot?.env?.configured) {
399
+ if (snapshot?.env?.setupMode === 'user') {
400
+ return `${this.label}: setup is user-managed and is not complete yet. If the user wants to use it, tell them to open Official Integrations and finish setup in their account first.`;
401
+ }
395
402
  return `${this.label}: workspace setup is not complete yet. If the user wants to use it, tell them to open Official Integrations and ask an administrator to finish setup first.`;
396
403
  }
397
404