neoagent 2.3.1-beta.6 → 2.3.1-beta.61

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 (312) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +6 -5
  4. package/docs/hardware.md +1 -1
  5. package/docs/integrations.md +2 -3
  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 +111 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +171 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +804 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  30. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  31. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  32. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  33. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  35. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  36. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  37. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  38. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  39. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  43. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  44. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  45. package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
  46. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  48. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  49. package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
  50. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  51. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  52. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  53. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  54. package/flutter_app/android/build.gradle.kts +24 -0
  55. package/flutter_app/android/ci-release.keystore +0 -0
  56. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  57. package/flutter_app/android/gradle.properties +3 -0
  58. package/flutter_app/android/key.properties +4 -0
  59. package/flutter_app/android/settings.gradle.kts +26 -0
  60. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  62. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  63. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  64. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  65. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  66. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  67. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  68. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  69. package/flutter_app/lib/features/location/location_service.dart +119 -0
  70. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  71. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +571 -0
  72. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +275 -0
  73. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +366 -0
  74. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
  75. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
  76. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
  77. package/flutter_app/lib/main.dart +103 -0
  78. package/flutter_app/lib/main_account_settings.dart +1250 -0
  79. package/flutter_app/lib/main_admin.dart +886 -0
  80. package/flutter_app/lib/main_app_shell.dart +2134 -0
  81. package/flutter_app/lib/main_chat.dart +3316 -0
  82. package/flutter_app/lib/main_controller.dart +6836 -0
  83. package/flutter_app/lib/main_devices.dart +2396 -0
  84. package/flutter_app/lib/main_integrations.dart +909 -0
  85. package/flutter_app/lib/main_launcher.dart +959 -0
  86. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  87. package/flutter_app/lib/main_models.dart +3539 -0
  88. package/flutter_app/lib/main_navigation.dart +188 -0
  89. package/flutter_app/lib/main_operations.dart +4851 -0
  90. package/flutter_app/lib/main_recordings.dart +920 -0
  91. package/flutter_app/lib/main_runtime.dart +838 -0
  92. package/flutter_app/lib/main_settings.dart +2040 -0
  93. package/flutter_app/lib/main_shared.dart +3147 -0
  94. package/flutter_app/lib/main_theme.dart +257 -0
  95. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  96. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  97. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  98. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  99. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  100. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  101. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  102. package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
  103. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  104. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  105. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  106. package/flutter_app/lib/src/backend_client.dart +1837 -0
  107. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  108. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  109. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  110. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  111. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  112. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  113. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  114. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  115. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  116. package/flutter_app/lib/src/health_bridge.dart +136 -0
  117. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  118. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  119. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  120. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  121. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  122. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  123. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  124. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  125. package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
  126. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  127. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  128. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  129. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  130. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  131. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  132. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  133. package/flutter_app/lib/src/theme/palette.dart +81 -0
  134. package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
  135. package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
  136. package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
  137. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  138. package/flutter_app/linux/CMakeLists.txt +128 -0
  139. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  140. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
  141. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  142. package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
  143. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  144. package/flutter_app/linux/runner/main.cc +6 -0
  145. package/flutter_app/linux/runner/my_application.cc +144 -0
  146. package/flutter_app/linux/runner/my_application.h +18 -0
  147. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  148. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  150. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +44 -0
  151. package/flutter_app/macos/Podfile +42 -0
  152. package/flutter_app/macos/Podfile.lock +87 -0
  153. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  154. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  155. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  156. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  157. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  158. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  159. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  160. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  161. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  162. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  163. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  164. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  165. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  166. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  167. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  168. package/flutter_app/macos/Runner/Info.plist +36 -0
  169. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  170. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  171. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  172. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  173. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  174. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  175. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  176. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  177. package/flutter_app/patch_strings.py +12 -0
  178. package/flutter_app/pubspec.lock +1224 -0
  179. package/flutter_app/pubspec.yaml +57 -0
  180. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  181. package/flutter_app/test/recording_payloads_test.dart +53 -0
  182. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  183. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  184. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  185. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  186. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  187. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  188. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  189. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  190. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  191. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  192. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  193. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  194. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  195. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  196. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  197. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  198. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  199. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  200. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  201. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  202. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  203. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  204. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  205. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  206. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  207. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  208. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  209. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  210. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  211. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  212. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  213. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  214. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  215. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  216. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  217. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  218. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  219. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  220. package/flutter_app/web/favicon.png +0 -0
  221. package/flutter_app/web/favicon.svg +12 -0
  222. package/flutter_app/web/icons/Icon-192.png +0 -0
  223. package/flutter_app/web/icons/Icon-512.png +0 -0
  224. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  225. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  226. package/flutter_app/web/index.html +39 -0
  227. package/flutter_app/web/manifest.json +35 -0
  228. package/flutter_app/windows/CMakeLists.txt +108 -0
  229. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  230. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
  231. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  232. package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
  233. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  234. package/flutter_app/windows/runner/Runner.rc +121 -0
  235. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  236. package/flutter_app/windows/runner/flutter_window.h +37 -0
  237. package/flutter_app/windows/runner/main.cpp +53 -0
  238. package/flutter_app/windows/runner/resource.h +16 -0
  239. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  240. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  241. package/flutter_app/windows/runner/utils.cpp +65 -0
  242. package/flutter_app/windows/runner/utils.h +19 -0
  243. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  244. package/flutter_app/windows/runner/win32_window.h +102 -0
  245. package/lib/install_helpers.js +31 -0
  246. package/lib/manager.js +201 -6
  247. package/package.json +5 -3
  248. package/server/db/database.js +125 -1
  249. package/server/http/middleware.js +55 -2
  250. package/server/http/routes.js +1 -0
  251. package/server/http/static.js +41 -1
  252. package/server/index.js +3 -0
  253. package/server/public/.last_build_id +1 -1
  254. package/server/public/assets/AssetManifest.bin +1 -1
  255. package/server/public/assets/AssetManifest.bin.json +1 -1
  256. package/server/public/assets/NOTICES +281 -1
  257. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  258. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  259. package/server/public/canvaskit/wimp.wasm +0 -0
  260. package/server/public/flutter_bootstrap.js +2 -2
  261. package/server/public/main.dart.js +86707 -82621
  262. package/server/routes/auth.js +26 -5
  263. package/server/routes/integrations.js +108 -1
  264. package/server/routes/memory.js +11 -2
  265. package/server/routes/settings.js +84 -2
  266. package/server/routes/wearable.js +67 -0
  267. package/server/services/ai/engine.js +3 -2
  268. package/server/services/ai/models.js +30 -0
  269. package/server/services/ai/providers/githubCopilot.js +97 -0
  270. package/server/services/ai/providers/openai.js +2 -1
  271. package/server/services/ai/providers/openaiCodex.js +31 -0
  272. package/server/services/ai/settings.js +20 -0
  273. package/server/services/ai/toolSelector.js +54 -1
  274. package/server/services/ai/tools.js +88 -5
  275. package/server/services/desktop/screenRecorder.js +65 -9
  276. package/server/services/integrations/env.js +5 -0
  277. package/server/services/integrations/figma/provider.js +1 -0
  278. package/server/services/integrations/github/common.js +106 -0
  279. package/server/services/integrations/github/provider.js +499 -0
  280. package/server/services/integrations/github/repos.js +1124 -0
  281. package/server/services/integrations/google/provider.js +1 -0
  282. package/server/services/integrations/manager.js +88 -12
  283. package/server/services/integrations/microsoft/provider.js +1 -0
  284. package/server/services/integrations/oauth_provider.js +25 -8
  285. package/server/services/integrations/provider_config_store.js +85 -0
  286. package/server/services/integrations/registry.js +4 -2
  287. package/server/services/integrations/spotify/provider.js +1 -0
  288. package/server/services/integrations/trello/provider.js +842 -0
  289. package/server/services/manager.js +46 -1
  290. package/server/services/mcp/client.js +120 -23
  291. package/server/services/memory/manager.js +39 -2
  292. package/server/services/messaging/access_policy.js +10 -0
  293. package/server/services/messaging/manager.js +49 -0
  294. package/server/services/messaging/meshtastic.js +277 -0
  295. package/server/services/messaging/meshtastic_env.js +40 -0
  296. package/server/services/messaging/meshtastic_protocol.js +530 -0
  297. package/server/services/messaging/meshtastic_protocol.test.js +33 -0
  298. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  299. package/server/services/tasks/runtime.js +1 -1
  300. package/server/services/voice/openaiClient.js +4 -1
  301. package/server/services/voice/openaiSpeech.js +6 -1
  302. package/server/services/voice/providers.js +144 -15
  303. package/server/services/voice/runtimeManager.js +156 -19
  304. package/server/services/voice/turnRunner.js +29 -9
  305. package/server/services/wearable/firmware_manifest.js +353 -0
  306. package/server/services/wearable/gateway.js +350 -0
  307. package/server/services/wearable/protocol.js +45 -0
  308. package/server/services/wearable/service.js +240 -0
  309. package/server/services/widgets/service.js +3 -0
  310. package/server/utils/local_secrets.js +56 -0
  311. package/server/utils/logger.js +37 -9
  312. package/server/services/integrations/home_assistant/provider.js +0 -350
@@ -0,0 +1,353 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_GITHUB_REPOSITORY = 'NeoLabs-Systems/NeoAgent';
4
+ const DEFAULT_ASSET_NAME = 'neoagent-wearable-firmware.bin';
5
+ const MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000;
6
+ const manifestCache = new Map();
7
+
8
+ function trimString(value, maxLength = 512) {
9
+ return String(value || '').trim().slice(0, maxLength);
10
+ }
11
+
12
+ function normalizeChannel(value) {
13
+ return trimString(value).toLowerCase() === 'beta' ? 'beta' : 'stable';
14
+ }
15
+
16
+ function parseRepositorySlug(value) {
17
+ const raw = trimString(value, 256);
18
+ if (!raw) {
19
+ return null;
20
+ }
21
+ const slugMatch = raw.match(/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/);
22
+ if (slugMatch) {
23
+ return slugMatch[1].replace(/\.git$/, '');
24
+ }
25
+ const urlMatch = raw.match(/github\.com[/:]([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?(?:\/.*)?$/i);
26
+ if (urlMatch) {
27
+ return urlMatch[1].replace(/\.git$/, '');
28
+ }
29
+ return null;
30
+ }
31
+
32
+ function getGithubRepository() {
33
+ return (
34
+ parseRepositorySlug(process.env.NEOAGENT_WEARABLE_FIRMWARE_GITHUB_REPOSITORY)
35
+ || DEFAULT_GITHUB_REPOSITORY
36
+ );
37
+ }
38
+
39
+ function getFirmwareAssetName() {
40
+ return trimString(process.env.NEOAGENT_WEARABLE_FIRMWARE_ASSET_NAME, 128) || DEFAULT_ASSET_NAME;
41
+ }
42
+
43
+ function getGithubToken() {
44
+ return trimString(
45
+ process.env.NEOAGENT_WEARABLE_GITHUB_TOKEN
46
+ || process.env.GITHUB_TOKEN
47
+ || process.env.GH_TOKEN,
48
+ 2048
49
+ ) || null;
50
+ }
51
+
52
+ function stripHashPrefix(value) {
53
+ const text = trimString(value, 512);
54
+ if (!text) {
55
+ return null;
56
+ }
57
+ return text.replace(/^sha256:/i, '').toLowerCase();
58
+ }
59
+
60
+ function toBoolean(value, fallback = false) {
61
+ const normalized = String(value || '').trim().toLowerCase();
62
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
63
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
64
+ return fallback;
65
+ }
66
+
67
+ function releaseHasAsset(release, assetName) {
68
+ return Boolean(selectReleaseAsset(release, assetName));
69
+ }
70
+
71
+ function selectGithubRelease(releases, channel, assetName = null) {
72
+ const normalizedChannel = normalizeChannel(channel);
73
+ if (normalizedChannel === 'stable') {
74
+ const candidates = Array.isArray(releases)
75
+ ? releases.filter((release) => release && release.prerelease === false && release.draft === false)
76
+ : [];
77
+ return candidates.find((release) => !assetName || releaseHasAsset(release, assetName)) || candidates[0] || null;
78
+ }
79
+
80
+ const betaPattern = /-beta(?:\.\d+)?$/i;
81
+ const candidates = Array.isArray(releases)
82
+ ? releases.filter((release) => release && release.prerelease === true && betaPattern.test(String(release.tag_name || '')))
83
+ : [];
84
+ candidates.sort((left, right) => {
85
+ const rightPublished = Date.parse(right?.published_at ?? right?.created_at ?? '') || 0;
86
+ const leftPublished = Date.parse(left?.published_at ?? left?.created_at ?? '') || 0;
87
+ return rightPublished - leftPublished;
88
+ });
89
+ return candidates.find((release) => !assetName || releaseHasAsset(release, assetName)) || candidates[0] || null;
90
+ }
91
+
92
+ function selectReleaseAsset(release, assetName) {
93
+ if (!release || !Array.isArray(release.assets) || release.assets.length === 0) {
94
+ return null;
95
+ }
96
+ const expectedName = trimString(assetName, 128) || DEFAULT_ASSET_NAME;
97
+ return release.assets.find((asset) => asset && asset.name === expectedName) || null;
98
+ }
99
+
100
+ function selectChecksumAsset(release, assetName) {
101
+ if (!release || !Array.isArray(release.assets) || release.assets.length === 0) {
102
+ return null;
103
+ }
104
+ const expectedName = trimString(assetName, 128) || DEFAULT_ASSET_NAME;
105
+ return release.assets.find((asset) => {
106
+ const name = String(asset?.name || '');
107
+ if (!name || name === expectedName) {
108
+ return false;
109
+ }
110
+ const lower = name.toLowerCase();
111
+ return (
112
+ lower === `${expectedName.toLowerCase()}.sha256`
113
+ || lower === `${expectedName.toLowerCase()}.sha256sum`
114
+ || lower.includes('checksum')
115
+ || lower.endsWith('.sha256')
116
+ || lower.endsWith('.sha256sum')
117
+ );
118
+ }) || null;
119
+ }
120
+
121
+ async function fetchGithubJson(fetchImpl, url, token) {
122
+ const response = await fetchImpl(url, {
123
+ headers: {
124
+ Accept: 'application/vnd.github+json',
125
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
126
+ },
127
+ });
128
+ if (!response.ok) {
129
+ const body = await response.text().catch(() => '');
130
+ const error = new Error(`GitHub API request failed with ${response.status}`);
131
+ error.status = response.status;
132
+ error.body = body;
133
+ throw error;
134
+ }
135
+ return response.json();
136
+ }
137
+
138
+ async function fetchText(fetchImpl, url, token) {
139
+ const response = await fetchImpl(url, {
140
+ headers: {
141
+ Accept: 'text/plain, application/octet-stream;q=0.9, */*;q=0.1',
142
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
143
+ },
144
+ });
145
+ if (!response.ok) {
146
+ const error = new Error(`Asset request failed with ${response.status}`);
147
+ error.status = response.status;
148
+ throw error;
149
+ }
150
+ return response.text();
151
+ }
152
+
153
+ function parseChecksumBody(body, assetName) {
154
+ const normalizedAssetName = String(assetName || '').trim();
155
+ const lines = String(body || '')
156
+ .split(/\r?\n/)
157
+ .map((line) => line.trim())
158
+ .filter(Boolean);
159
+ for (const line of lines) {
160
+ const firstToken = line.split(/\s+/)[0];
161
+ const normalized = stripHashPrefix(firstToken);
162
+ if (normalized && /^[a-f0-9]{64}$/i.test(normalized)) {
163
+ if (!normalizedAssetName || line.includes(normalizedAssetName) || lines.length === 1) {
164
+ return normalized;
165
+ }
166
+ }
167
+ }
168
+ return null;
169
+ }
170
+
171
+ async function fetchGithubRelease(fetchImpl, repository, channel, token, assetName = null) {
172
+ const normalizedChannel = normalizeChannel(channel);
173
+ const releases = await fetchGithubJson(fetchImpl, `https://api.github.com/repos/${repository}/releases?per_page=100`, token);
174
+ const release = selectGithubRelease(releases, normalizedChannel, assetName);
175
+ if (!release) {
176
+ const error = new Error(`No ${normalizedChannel} firmware release found for ${repository}`);
177
+ error.status = 404;
178
+ throw error;
179
+ }
180
+ return release;
181
+ }
182
+
183
+ function cacheKey({ repository, channel, assetName, downloadUrlOverride }) {
184
+ return [repository, channel, assetName, downloadUrlOverride || ''].join('|');
185
+ }
186
+
187
+ function getCachedManifest(key) {
188
+ const cached = manifestCache.get(key);
189
+ if (!cached) {
190
+ return null;
191
+ }
192
+ if (cached.expiresAt <= Date.now()) {
193
+ manifestCache.delete(key);
194
+ return null;
195
+ }
196
+ return cached.manifest;
197
+ }
198
+
199
+ function setCachedManifest(key, manifest) {
200
+ manifestCache.set(key, {
201
+ manifest,
202
+ expiresAt: Date.now() + MANIFEST_CACHE_TTL_MS,
203
+ });
204
+ }
205
+
206
+ async function resolveFirmwareManifest({
207
+ channel,
208
+ downloadUrlOverride,
209
+ currentVersionOverride,
210
+ releaseNotesUrlOverride,
211
+ sha256Override,
212
+ repositoryOverride,
213
+ assetNameOverride,
214
+ fetchImpl = fetch,
215
+ } = {}) {
216
+ const normalizedChannel = normalizeChannel(channel);
217
+ const repository = parseRepositorySlug(repositoryOverride) || getGithubRepository();
218
+ const assetName = trimString(assetNameOverride, 128) || getFirmwareAssetName();
219
+ const downloadUrl = trimString(downloadUrlOverride, 2000);
220
+ const cacheId = cacheKey({ repository, channel: normalizedChannel, assetName, downloadUrlOverride: downloadUrl });
221
+ const cached = getCachedManifest(cacheId);
222
+ if (cached) {
223
+ return cached;
224
+ }
225
+
226
+ if (downloadUrl) {
227
+ const manifest = {
228
+ configured: true,
229
+ source: 'static',
230
+ manifestVersion: 2,
231
+ channel: normalizedChannel,
232
+ currentVersion: trimString(currentVersionOverride, 120) || null,
233
+ releaseName: trimString(currentVersionOverride, 120) || null,
234
+ releaseTag: trimString(currentVersionOverride, 120) || null,
235
+ minimumServerVersion: trimString(process.env.NEOAGENT_WEARABLE_MIN_SERVER_VERSION, 120) || null,
236
+ downloadUrl,
237
+ releaseNotesUrl: trimString(releaseNotesUrlOverride, 2000) || null,
238
+ sha256: stripHashPrefix(sha256Override),
239
+ assetName,
240
+ repository,
241
+ generatedAt: new Date().toISOString(),
242
+ mandatory: toBoolean(process.env.NEOAGENT_WEARABLE_FIRMWARE_MANDATORY, false),
243
+ };
244
+ setCachedManifest(cacheId, manifest);
245
+ return manifest;
246
+ }
247
+
248
+ if (!repository) {
249
+ return {
250
+ configured: false,
251
+ source: 'github',
252
+ manifestVersion: 2,
253
+ channel: normalizedChannel,
254
+ downloadUrl: null,
255
+ releaseNotesUrl: null,
256
+ sha256: null,
257
+ assetName,
258
+ repository: null,
259
+ generatedAt: new Date().toISOString(),
260
+ error: 'GitHub repository is not configured.',
261
+ mandatory: false,
262
+ };
263
+ }
264
+
265
+ try {
266
+ const token = getGithubToken();
267
+ const release = await fetchGithubRelease(fetchImpl, repository, normalizedChannel, token, assetName);
268
+ const asset = selectReleaseAsset(release, assetName);
269
+ if (!asset || !asset.browser_download_url) {
270
+ return {
271
+ configured: false,
272
+ source: 'github',
273
+ manifestVersion: 2,
274
+ channel: normalizedChannel,
275
+ currentVersion: trimString(release?.tag_name, 120) || null,
276
+ releaseName: trimString(release?.name, 128) || trimString(release?.tag_name, 120) || null,
277
+ releaseTag: trimString(release?.tag_name, 120) || null,
278
+ minimumServerVersion: trimString(process.env.NEOAGENT_WEARABLE_MIN_SERVER_VERSION, 120) || null,
279
+ downloadUrl: null,
280
+ releaseNotesUrl: trimString(release?.html_url, 2000) || null,
281
+ sha256: null,
282
+ assetName,
283
+ repository,
284
+ generatedAt: new Date().toISOString(),
285
+ error: `Release asset ${assetName} was not found.`,
286
+ mandatory: false,
287
+ prerelease: Boolean(release?.prerelease),
288
+ };
289
+ }
290
+
291
+ let checksum = null;
292
+ const checksumAsset = selectChecksumAsset(release, asset.name);
293
+ if (checksumAsset?.browser_download_url) {
294
+ try {
295
+ checksum = parseChecksumBody(
296
+ await fetchText(fetchImpl, checksumAsset.browser_download_url, token),
297
+ asset.name,
298
+ );
299
+ } catch {
300
+ checksum = null;
301
+ }
302
+ }
303
+
304
+ const manifest = {
305
+ configured: true,
306
+ source: 'github',
307
+ manifestVersion: 2,
308
+ channel: normalizedChannel,
309
+ currentVersion: trimString(release?.tag_name, 120) || null,
310
+ releaseName: trimString(release?.name, 128) || trimString(release?.tag_name, 120) || null,
311
+ releaseTag: trimString(release?.tag_name, 120) || null,
312
+ minimumServerVersion: trimString(process.env.NEOAGENT_WEARABLE_MIN_SERVER_VERSION, 120) || null,
313
+ downloadUrl: asset.browser_download_url,
314
+ releaseNotesUrl: trimString(release?.html_url, 2000) || null,
315
+ sha256: checksum,
316
+ assetName: asset.name,
317
+ repository,
318
+ generatedAt: new Date().toISOString(),
319
+ mandatory: toBoolean(process.env.NEOAGENT_WEARABLE_FIRMWARE_MANDATORY, false),
320
+ prerelease: Boolean(release?.prerelease),
321
+ };
322
+ setCachedManifest(cacheId, manifest);
323
+ return manifest;
324
+ } catch (error) {
325
+ return {
326
+ configured: false,
327
+ source: 'github',
328
+ manifestVersion: 2,
329
+ channel: normalizedChannel,
330
+ downloadUrl: null,
331
+ releaseNotesUrl: null,
332
+ sha256: null,
333
+ assetName,
334
+ repository,
335
+ generatedAt: new Date().toISOString(),
336
+ error: error.message,
337
+ mandatory: false,
338
+ };
339
+ }
340
+ }
341
+
342
+ module.exports = {
343
+ DEFAULT_ASSET_NAME,
344
+ DEFAULT_GITHUB_REPOSITORY,
345
+ getFirmwareAssetName,
346
+ getGithubRepository,
347
+ normalizeChannel,
348
+ parseRepositorySlug,
349
+ resolveFirmwareManifest,
350
+ selectGithubRelease,
351
+ selectReleaseAsset,
352
+ stripHashPrefix,
353
+ };
@@ -0,0 +1,350 @@
1
+ 'use strict';
2
+
3
+ const { WebSocketServer } = require('ws');
4
+ const { sanitizeError } = require('../../utils/security');
5
+ const {
6
+ WEARABLE_WS_PATH,
7
+ isSupportedClientMessageType,
8
+ isWearableHello,
9
+ parseWearableMessage,
10
+ } = require('./protocol');
11
+
12
+ const UPGRADE_WINDOW_MS = 60 * 1000;
13
+ const UPGRADE_MAX_ATTEMPTS = 30;
14
+ const HELLO_TIMEOUT_MS = 5000;
15
+
16
+ function rejectUpgrade(socket, statusCode, message) {
17
+ try {
18
+ socket.write(
19
+ `HTTP/1.1 ${statusCode} ${message}\r\n` +
20
+ 'Connection: close\r\n' +
21
+ '\r\n',
22
+ );
23
+ } catch {}
24
+ try {
25
+ socket.destroy();
26
+ } catch {}
27
+ }
28
+
29
+ function remoteAddressFromRequest(req) {
30
+ const forwarded = req.headers?.['x-forwarded-for'];
31
+ if (typeof forwarded === 'string' && forwarded.trim()) {
32
+ return forwarded.split(',')[0].trim();
33
+ }
34
+ return req.socket?.remoteAddress || 'unknown';
35
+ }
36
+
37
+ function createUpgradeLimiter() {
38
+ const attempts = new Map();
39
+ return (remoteAddress) => {
40
+ const key = String(remoteAddress || 'unknown');
41
+ const now = Date.now();
42
+ for (const [entryKey, stats] of attempts.entries()) {
43
+ if (!stats?.windowStart || stats.windowStart + UPGRADE_WINDOW_MS <= now) {
44
+ attempts.delete(entryKey);
45
+ }
46
+ }
47
+ const current = attempts.get(key);
48
+ if (!current) {
49
+ attempts.set(key, { windowStart: now, count: 1 });
50
+ return true;
51
+ }
52
+ if (now - current.windowStart >= UPGRADE_WINDOW_MS) {
53
+ attempts.set(key, { windowStart: now, count: 1 });
54
+ return true;
55
+ }
56
+ if (current.count >= UPGRADE_MAX_ATTEMPTS) {
57
+ return false;
58
+ }
59
+ current.count += 1;
60
+ return true;
61
+ };
62
+ }
63
+
64
+ function sendJson(ws, payload) {
65
+ if (!ws || ws.readyState !== 1) return;
66
+ ws.send(JSON.stringify(payload));
67
+ }
68
+
69
+ function asObject(value) {
70
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
71
+ }
72
+
73
+ function toOptionalString(value, maxLength = 512) {
74
+ if (value == null) return '';
75
+ return String(value).trim().slice(0, maxLength);
76
+ }
77
+
78
+ function toBoundedInt(value, fallback, min, max) {
79
+ const parsed = Number(value);
80
+ if (!Number.isFinite(parsed)) return fallback;
81
+ return Math.min(max, Math.max(min, Math.floor(parsed)));
82
+ }
83
+
84
+ function createWearableVoiceSink(ws, voiceRuntimeManager) {
85
+ return {
86
+ publishReady: async (session, extra = {}) => {
87
+ sendJson(ws, {
88
+ type: 'voice:session_ready',
89
+ sessionId: session.id,
90
+ ...extra,
91
+ });
92
+ },
93
+ setState: async (session, state, extra = {}) => {
94
+ sendJson(ws, {
95
+ type: 'voice:assistant_state',
96
+ sessionId: session.id,
97
+ state,
98
+ ...extra,
99
+ });
100
+ },
101
+ publishTranscriptPartial: async (session, content) => {
102
+ sendJson(ws, {
103
+ type: 'voice:transcript_partial',
104
+ sessionId: session.id,
105
+ content,
106
+ });
107
+ },
108
+ publishTranscriptFinal: async (session, content) => {
109
+ sendJson(ws, {
110
+ type: 'voice:transcript_final',
111
+ sessionId: session.id,
112
+ content,
113
+ });
114
+ },
115
+ publishAssistantOutput: async (session, content, options = {}) => {
116
+ await voiceRuntimeManager.deliverWearableAssistantOutput(ws, session.id, content, options);
117
+ },
118
+ interruptOutput: async (session) => {
119
+ sendJson(ws, {
120
+ type: 'voice:assistant_state',
121
+ sessionId: session.id,
122
+ state: 'interrupted',
123
+ });
124
+ },
125
+ publishError: async (session, message, extra = {}) => {
126
+ sendJson(ws, {
127
+ type: 'voice:error',
128
+ sessionId: session.id,
129
+ error: message,
130
+ ...extra,
131
+ });
132
+ },
133
+ close: async (session, reason = 'closed') => {
134
+ sendJson(ws, {
135
+ type: 'voice:assistant_state',
136
+ sessionId: session.id,
137
+ state: 'closed',
138
+ reason,
139
+ });
140
+ },
141
+ };
142
+ }
143
+
144
+ function bindWearableGateway(httpServer, app, sessionMiddleware) {
145
+ const wss = new WebSocketServer({ noServer: true });
146
+ const allowUpgradeAttempt = createUpgradeLimiter();
147
+
148
+ httpServer.on('upgrade', (req, socket, head) => {
149
+ let url;
150
+ try {
151
+ url = new URL(req.url, 'http://localhost');
152
+ } catch {
153
+ return;
154
+ }
155
+ if (url.pathname !== WEARABLE_WS_PATH) {
156
+ return;
157
+ }
158
+ const remoteAddress = remoteAddressFromRequest(req);
159
+ if (!allowUpgradeAttempt(remoteAddress)) {
160
+ rejectUpgrade(socket, 429, 'Too Many Requests');
161
+ return;
162
+ }
163
+ sessionMiddleware(req, {}, (err) => {
164
+ if (err) {
165
+ rejectUpgrade(socket, 500, 'Session Error');
166
+ return;
167
+ }
168
+ if (!req.session?.userId) {
169
+ rejectUpgrade(socket, 401, 'Unauthorized');
170
+ return;
171
+ }
172
+ const wearableService = app?.locals?.wearableService;
173
+ const voiceRuntimeManager = app?.locals?.voiceRuntimeManager;
174
+ if (!wearableService || !voiceRuntimeManager) {
175
+ rejectUpgrade(socket, 503, 'Service Unavailable');
176
+ return;
177
+ }
178
+
179
+ wss.handleUpgrade(req, socket, head, (ws) => {
180
+ ws.isAlive = true;
181
+ ws.on('pong', () => {
182
+ ws.isAlive = true;
183
+ });
184
+
185
+ let initialized = false;
186
+ let deviceId = '';
187
+ const activeSessionIds = new Set();
188
+ const helloTimer = setTimeout(() => {
189
+ if (!initialized) {
190
+ try {
191
+ ws.close(1008, 'Wearable hello timed out');
192
+ } catch {}
193
+ }
194
+ }, HELLO_TIMEOUT_MS);
195
+
196
+ const teardown = async () => {
197
+ clearTimeout(helloTimer);
198
+ if (deviceId) {
199
+ wearableService.unregisterConnection(req.session.userId, deviceId);
200
+ }
201
+ await Promise.allSettled(
202
+ Array.from(activeSessionIds).map((sessionId) =>
203
+ voiceRuntimeManager.closeSession(sessionId, 'wearable_socket_closed'),
204
+ ),
205
+ );
206
+ };
207
+
208
+ ws.on('close', () => {
209
+ void teardown();
210
+ });
211
+
212
+ ws.on('message', async (data) => {
213
+ try {
214
+ const message = parseWearableMessage(data);
215
+ if (!initialized) {
216
+ if (!isWearableHello(message)) {
217
+ throw new Error('wearable:hello is required before other messages.');
218
+ }
219
+ const connection = wearableService.registerConnection({
220
+ userId: req.session.userId,
221
+ ws,
222
+ remoteAddress,
223
+ userAgent: req.headers['user-agent'] || null,
224
+ hello: message,
225
+ });
226
+ initialized = true;
227
+ deviceId = connection.deviceId;
228
+ sendJson(ws, {
229
+ type: 'wearable:hello',
230
+ ok: true,
231
+ deviceId,
232
+ userId: req.session.userId,
233
+ serverTime: new Date().toISOString(),
234
+ });
235
+ return;
236
+ }
237
+
238
+ if (!isSupportedClientMessageType(message.type)) {
239
+ throw new Error(`Unsupported wearable message type "${message.type}".`);
240
+ }
241
+ wearableService.touchConnection(req.session.userId, deviceId);
242
+ const payload = asObject(message);
243
+ const sessionId = toOptionalString(payload.sessionId, 128);
244
+
245
+ switch (message.type) {
246
+ case 'voice:session_open': {
247
+ const resolvedSessionId = sessionId || null;
248
+ const session = await voiceRuntimeManager.openWearableSession({
249
+ userId: req.session.userId,
250
+ agentId: payload.agentId || payload.agent_id || null,
251
+ sessionId: resolvedSessionId,
252
+ sink: createWearableVoiceSink(ws, voiceRuntimeManager),
253
+ });
254
+ activeSessionIds.add(session.id);
255
+ break;
256
+ }
257
+ case 'voice:input_start':
258
+ if (!sessionId) throw new Error('sessionId is required');
259
+ await voiceRuntimeManager.beginInput(sessionId, {
260
+ mimeType: toOptionalString(payload.mimeType, 128),
261
+ turnId: toOptionalString(payload.turnId, 128),
262
+ });
263
+ break;
264
+ case 'voice:audio_chunk': {
265
+ if (!sessionId) throw new Error('sessionId is required');
266
+ const audioBase64 = toOptionalString(payload.audioBase64, 800000);
267
+ if (!audioBase64) throw new Error('audioBase64 is required');
268
+ const sequence = toBoundedInt(payload.sequence, -1, -1, 1000000);
269
+ if (sequence < 0) throw new Error('sequence is required');
270
+ const turnId = toOptionalString(payload.turnId, 128);
271
+ if (!turnId) throw new Error('turnId is required');
272
+ const audioBytes = Buffer.from(audioBase64, 'base64');
273
+ const appendResult = await voiceRuntimeManager.appendInputAudio(sessionId, audioBytes, {
274
+ mimeType: toOptionalString(payload.mimeType, 128),
275
+ turnId,
276
+ sequence,
277
+ });
278
+ sendJson(ws, {
279
+ type: 'voice:chunk_ack',
280
+ sessionId,
281
+ turnId,
282
+ sequence,
283
+ receivedThrough: appendResult?.receivedThrough ?? sequence,
284
+ });
285
+ break;
286
+ }
287
+ case 'voice:input_commit': {
288
+ if (!sessionId) throw new Error('sessionId is required');
289
+ await voiceRuntimeManager.commitInput(sessionId, {
290
+ turnId: toOptionalString(payload.turnId, 128),
291
+ finalSequence: toBoundedInt(payload.finalSequence, -1, -1, 1000000),
292
+ promptHint: toOptionalString(payload.promptHint, 2000),
293
+ });
294
+ break;
295
+ }
296
+ case 'voice:interrupt':
297
+ if (!sessionId) throw new Error('sessionId is required');
298
+ await voiceRuntimeManager.interruptSession(sessionId);
299
+ break;
300
+ case 'voice:session_close':
301
+ if (!sessionId) throw new Error('sessionId is required');
302
+ activeSessionIds.delete(sessionId);
303
+ await voiceRuntimeManager.closeSession(sessionId, 'wearable_client_closed');
304
+ break;
305
+ default:
306
+ break;
307
+ }
308
+ } catch (error) {
309
+ sendJson(ws, {
310
+ type: 'voice:error',
311
+ error: sanitizeError(error),
312
+ });
313
+ }
314
+ });
315
+ });
316
+ });
317
+ });
318
+
319
+ const heartbeat = setInterval(() => {
320
+ for (const ws of wss.clients) {
321
+ if (ws.isAlive === false) {
322
+ try {
323
+ ws.terminate();
324
+ } catch {}
325
+ continue;
326
+ }
327
+ ws.isAlive = false;
328
+ try {
329
+ ws.ping();
330
+ } catch {}
331
+ }
332
+ }, 30000);
333
+ heartbeat.unref?.();
334
+
335
+ if (app?.locals) {
336
+ app.locals.wearableGateway = {
337
+ close: () =>
338
+ new Promise((resolve) => {
339
+ clearInterval(heartbeat);
340
+ wss.close(() => resolve());
341
+ }),
342
+ };
343
+ }
344
+
345
+ return wss;
346
+ }
347
+
348
+ module.exports = {
349
+ bindWearableGateway,
350
+ };