neoagent 2.3.1-beta.4 → 2.3.1-beta.41

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 (290) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +12 -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 +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 +99 -0
  66. package/flutter_app/lib/main_account_settings.dart +1250 -0
  67. package/flutter_app/lib/main_admin.dart +886 -0
  68. package/flutter_app/lib/main_app_shell.dart +1682 -0
  69. package/flutter_app/lib/main_chat.dart +3352 -0
  70. package/flutter_app/lib/main_controller.dart +6781 -0
  71. package/flutter_app/lib/main_devices.dart +2301 -0
  72. package/flutter_app/lib/main_integrations.dart +1129 -0
  73. package/flutter_app/lib/main_launcher.dart +959 -0
  74. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  75. package/flutter_app/lib/main_models.dart +3546 -0
  76. package/flutter_app/lib/main_navigation.dart +193 -0
  77. package/flutter_app/lib/main_operations.dart +4851 -0
  78. package/flutter_app/lib/main_recordings.dart +870 -0
  79. package/flutter_app/lib/main_runtime.dart +806 -0
  80. package/flutter_app/lib/main_settings.dart +2024 -0
  81. package/flutter_app/lib/main_shared.dart +2861 -0
  82. package/flutter_app/lib/main_theme.dart +204 -0
  83. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  84. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  85. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  86. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  87. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  88. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  89. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  90. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  91. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  92. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  93. package/flutter_app/lib/src/backend_client.dart +1833 -0
  94. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  95. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  96. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  97. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  98. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  99. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  100. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  101. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  102. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  103. package/flutter_app/lib/src/health_bridge.dart +136 -0
  104. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  105. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  106. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  107. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  108. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  109. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  110. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  111. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  112. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  113. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  114. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  115. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  116. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  117. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  118. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  119. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  120. package/flutter_app/lib/src/theme/palette.dart +81 -0
  121. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  122. package/flutter_app/linux/CMakeLists.txt +128 -0
  123. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  124. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  125. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  126. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  127. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  128. package/flutter_app/linux/runner/main.cc +6 -0
  129. package/flutter_app/linux/runner/my_application.cc +144 -0
  130. package/flutter_app/linux/runner/my_application.h +18 -0
  131. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  132. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  133. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  134. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  135. package/flutter_app/macos/Podfile +42 -0
  136. package/flutter_app/macos/Podfile.lock +87 -0
  137. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  138. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  139. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  140. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  141. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  142. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  143. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  144. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  145. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  146. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  147. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  148. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  150. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  151. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  152. package/flutter_app/macos/Runner/Info.plist +36 -0
  153. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  154. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  155. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  156. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  157. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  158. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  159. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  160. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  161. package/flutter_app/patch_strings.py +12 -0
  162. package/flutter_app/pubspec.lock +1088 -0
  163. package/flutter_app/pubspec.yaml +53 -0
  164. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  165. package/flutter_app/test/recording_payloads_test.dart +53 -0
  166. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  167. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  168. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  169. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  170. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  171. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  172. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  173. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  174. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  175. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  176. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  177. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  178. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  179. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  180. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  181. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  182. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  183. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  184. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  185. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  186. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  187. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  188. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  189. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  190. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  191. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  192. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  193. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  194. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  195. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  196. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  197. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  198. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  199. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  200. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  201. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  202. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  203. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  204. package/flutter_app/web/favicon.png +0 -0
  205. package/flutter_app/web/favicon.svg +12 -0
  206. package/flutter_app/web/icons/Icon-192.png +0 -0
  207. package/flutter_app/web/icons/Icon-512.png +0 -0
  208. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  209. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  210. package/flutter_app/web/index.html +39 -0
  211. package/flutter_app/web/manifest.json +35 -0
  212. package/flutter_app/windows/CMakeLists.txt +108 -0
  213. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  214. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  215. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  216. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  217. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  218. package/flutter_app/windows/runner/Runner.rc +121 -0
  219. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  220. package/flutter_app/windows/runner/flutter_window.h +37 -0
  221. package/flutter_app/windows/runner/main.cpp +53 -0
  222. package/flutter_app/windows/runner/resource.h +16 -0
  223. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  224. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  225. package/flutter_app/windows/runner/utils.cpp +65 -0
  226. package/flutter_app/windows/runner/utils.h +19 -0
  227. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  228. package/flutter_app/windows/runner/win32_window.h +102 -0
  229. package/lib/install_helpers.js +31 -0
  230. package/lib/manager.js +227 -6
  231. package/package.json +3 -1
  232. package/server/db/database.js +110 -0
  233. package/server/http/middleware.js +55 -2
  234. package/server/http/routes.js +1 -0
  235. package/server/index.js +3 -0
  236. package/server/public/.last_build_id +1 -1
  237. package/server/public/assets/NOTICES +1 -1
  238. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  239. package/server/public/canvaskit/wimp.wasm +0 -0
  240. package/server/public/flutter_bootstrap.js +2 -2
  241. package/server/public/main.dart.js +74324 -73132
  242. package/server/routes/integrations.js +108 -1
  243. package/server/routes/memory.js +11 -2
  244. package/server/{http/routes → routes}/screenHistory.js +2 -2
  245. package/server/routes/settings.js +75 -2
  246. package/server/{http/routes → routes}/triggers.js +2 -2
  247. package/server/routes/wearable.js +67 -0
  248. package/server/services/ai/models.js +30 -0
  249. package/server/services/ai/providers/githubCopilot.js +97 -0
  250. package/server/services/ai/providers/openai.js +2 -1
  251. package/server/services/ai/providers/openaiCodex.js +31 -0
  252. package/server/services/ai/settings.js +20 -0
  253. package/server/services/ai/toolSelector.js +14 -1
  254. package/server/services/ai/tools.js +77 -4
  255. package/server/services/desktop/screenRecorder.js +65 -9
  256. package/server/services/integrations/env.js +5 -0
  257. package/server/services/integrations/figma/provider.js +1 -0
  258. package/server/services/integrations/github/common.js +106 -0
  259. package/server/services/integrations/github/provider.js +499 -0
  260. package/server/services/integrations/github/repos.js +1124 -0
  261. package/server/services/integrations/google/provider.js +1 -0
  262. package/server/services/integrations/home_assistant/provider.js +325 -26
  263. package/server/services/integrations/manager.js +88 -12
  264. package/server/services/integrations/microsoft/provider.js +1 -0
  265. package/server/services/integrations/oauth_provider.js +25 -8
  266. package/server/services/integrations/provider_config_store.js +85 -0
  267. package/server/services/integrations/registry.js +4 -0
  268. package/server/services/integrations/spotify/provider.js +1 -0
  269. package/server/services/integrations/trello/provider.js +842 -0
  270. package/server/services/manager.js +46 -1
  271. package/server/services/mcp/client.js +120 -23
  272. package/server/services/memory/manager.js +39 -2
  273. package/server/services/messaging/access_policy.js +10 -0
  274. package/server/services/messaging/manager.js +49 -0
  275. package/server/services/messaging/meshtastic.js +260 -0
  276. package/server/services/messaging/meshtastic_env.js +100 -0
  277. package/server/services/messaging/meshtastic_protocol.js +476 -0
  278. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  279. package/server/services/tasks/runtime.js +1 -1
  280. package/server/services/voice/openaiClient.js +4 -1
  281. package/server/services/voice/openaiSpeech.js +6 -1
  282. package/server/services/voice/providers.js +52 -12
  283. package/server/services/voice/runtimeManager.js +136 -19
  284. package/server/services/voice/turnRunner.js +29 -9
  285. package/server/services/wearable/firmware_manifest.js +370 -0
  286. package/server/services/wearable/gateway.js +350 -0
  287. package/server/services/wearable/protocol.js +45 -0
  288. package/server/services/wearable/service.js +244 -0
  289. package/server/utils/local_secrets.js +56 -0
  290. package/server/utils/logger.js +37 -9
@@ -0,0 +1,370 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { APP_DIR } = require('../../../runtime/paths');
7
+
8
+ const DEFAULT_ASSET_NAME = 'neoagent-wearable-firmware.bin';
9
+ const MANIFEST_CACHE_TTL_MS = 5 * 60 * 1000;
10
+ const manifestCache = new Map();
11
+
12
+ function trimString(value, maxLength = 512) {
13
+ return String(value || '').trim().slice(0, maxLength);
14
+ }
15
+
16
+ function normalizeChannel(value) {
17
+ return trimString(value).toLowerCase() === 'beta' ? 'beta' : 'stable';
18
+ }
19
+
20
+ function parseRepositorySlug(value) {
21
+ const raw = trimString(value, 256);
22
+ if (!raw) {
23
+ return null;
24
+ }
25
+ const slugMatch = raw.match(/^([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)$/);
26
+ if (slugMatch) {
27
+ return slugMatch[1].replace(/\.git$/, '');
28
+ }
29
+ const urlMatch = raw.match(/github\.com[/:]([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?(?:\/.*)?$/i);
30
+ if (urlMatch) {
31
+ return urlMatch[1].replace(/\.git$/, '');
32
+ }
33
+ return null;
34
+ }
35
+
36
+ function readPackageRepositorySlug() {
37
+ try {
38
+ const packageJsonPath = path.join(APP_DIR, 'package.json');
39
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
40
+ const repository = pkg?.repository;
41
+ if (typeof repository === 'string') {
42
+ return parseRepositorySlug(repository);
43
+ }
44
+ if (repository && typeof repository.url === 'string') {
45
+ return parseRepositorySlug(repository.url);
46
+ }
47
+ } catch {
48
+ return null;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function getGithubRepository() {
54
+ return (
55
+ parseRepositorySlug(process.env.NEOAGENT_WEARABLE_FIRMWARE_GITHUB_REPOSITORY)
56
+ || parseRepositorySlug(process.env.GITHUB_REPOSITORY)
57
+ || readPackageRepositorySlug()
58
+ );
59
+ }
60
+
61
+ function getFirmwareAssetName() {
62
+ return trimString(process.env.NEOAGENT_WEARABLE_FIRMWARE_ASSET_NAME, 128) || DEFAULT_ASSET_NAME;
63
+ }
64
+
65
+ function getGithubToken() {
66
+ return trimString(
67
+ process.env.NEOAGENT_WEARABLE_GITHUB_TOKEN
68
+ || process.env.GITHUB_TOKEN
69
+ || process.env.GH_TOKEN,
70
+ 2048
71
+ ) || null;
72
+ }
73
+
74
+ function stripHashPrefix(value) {
75
+ const text = trimString(value, 512);
76
+ if (!text) {
77
+ return null;
78
+ }
79
+ return text.replace(/^sha256:/i, '').toLowerCase();
80
+ }
81
+
82
+ function toBoolean(value, fallback = false) {
83
+ const normalized = String(value || '').trim().toLowerCase();
84
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
85
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
86
+ return fallback;
87
+ }
88
+
89
+ function selectGithubRelease(releases, channel) {
90
+ const normalizedChannel = normalizeChannel(channel);
91
+ if (normalizedChannel === 'stable') {
92
+ return Array.isArray(releases) ? releases.find((release) => release && release.prerelease === false && release.draft === false) || null : null;
93
+ }
94
+
95
+ const betaPattern = /-beta(?:\.\d+)?$/i;
96
+ const candidates = Array.isArray(releases)
97
+ ? releases.filter((release) => release && release.prerelease === true && betaPattern.test(String(release.tag_name || '')))
98
+ : [];
99
+ candidates.sort((left, right) => {
100
+ const rightPublished = Date.parse(right?.published_at ?? right?.created_at ?? '') || 0;
101
+ const leftPublished = Date.parse(left?.published_at ?? left?.created_at ?? '') || 0;
102
+ return rightPublished - leftPublished;
103
+ });
104
+ return candidates[0] || null;
105
+ }
106
+
107
+ function selectReleaseAsset(release, assetName) {
108
+ if (!release || !Array.isArray(release.assets) || release.assets.length === 0) {
109
+ return null;
110
+ }
111
+ const expectedName = trimString(assetName, 128) || DEFAULT_ASSET_NAME;
112
+ return release.assets.find((asset) => asset && asset.name === expectedName) || null;
113
+ }
114
+
115
+ function selectChecksumAsset(release, assetName) {
116
+ if (!release || !Array.isArray(release.assets) || release.assets.length === 0) {
117
+ return null;
118
+ }
119
+ const expectedName = trimString(assetName, 128) || DEFAULT_ASSET_NAME;
120
+ return release.assets.find((asset) => {
121
+ const name = String(asset?.name || '');
122
+ if (!name || name === expectedName) {
123
+ return false;
124
+ }
125
+ const lower = name.toLowerCase();
126
+ return (
127
+ lower === `${expectedName.toLowerCase()}.sha256`
128
+ || lower === `${expectedName.toLowerCase()}.sha256sum`
129
+ || lower.includes('checksum')
130
+ || lower.endsWith('.sha256')
131
+ || lower.endsWith('.sha256sum')
132
+ );
133
+ }) || null;
134
+ }
135
+
136
+ async function fetchGithubJson(fetchImpl, url, token) {
137
+ const response = await fetchImpl(url, {
138
+ headers: {
139
+ Accept: 'application/vnd.github+json',
140
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
141
+ },
142
+ });
143
+ if (!response.ok) {
144
+ const body = await response.text().catch(() => '');
145
+ const error = new Error(`GitHub API request failed with ${response.status}`);
146
+ error.status = response.status;
147
+ error.body = body;
148
+ throw error;
149
+ }
150
+ return response.json();
151
+ }
152
+
153
+ async function fetchText(fetchImpl, url, token) {
154
+ const response = await fetchImpl(url, {
155
+ headers: {
156
+ Accept: 'text/plain, application/octet-stream;q=0.9, */*;q=0.1',
157
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
158
+ },
159
+ });
160
+ if (!response.ok) {
161
+ const error = new Error(`Asset request failed with ${response.status}`);
162
+ error.status = response.status;
163
+ throw error;
164
+ }
165
+ return response.text();
166
+ }
167
+
168
+ function parseChecksumBody(body, assetName) {
169
+ const normalizedAssetName = String(assetName || '').trim();
170
+ const lines = String(body || '')
171
+ .split(/\r?\n/)
172
+ .map((line) => line.trim())
173
+ .filter(Boolean);
174
+ for (const line of lines) {
175
+ const firstToken = line.split(/\s+/)[0];
176
+ const normalized = stripHashPrefix(firstToken);
177
+ if (normalized && /^[a-f0-9]{64}$/i.test(normalized)) {
178
+ if (!normalizedAssetName || line.includes(normalizedAssetName) || lines.length === 1) {
179
+ return normalized;
180
+ }
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ async function fetchGithubRelease(fetchImpl, repository, channel, token) {
187
+ const normalizedChannel = normalizeChannel(channel);
188
+ if (normalizedChannel === 'stable') {
189
+ return fetchGithubJson(fetchImpl, `https://api.github.com/repos/${repository}/releases/latest`, token);
190
+ }
191
+ const releases = await fetchGithubJson(fetchImpl, `https://api.github.com/repos/${repository}/releases?per_page=100`, token);
192
+ const release = selectGithubRelease(releases, normalizedChannel);
193
+ if (!release) {
194
+ const error = new Error(`No ${normalizedChannel} firmware release found for ${repository}`);
195
+ error.status = 404;
196
+ throw error;
197
+ }
198
+ return release;
199
+ }
200
+
201
+ function cacheKey({ repository, channel, assetName, downloadUrlOverride }) {
202
+ return [repository, channel, assetName, downloadUrlOverride || ''].join('|');
203
+ }
204
+
205
+ function getCachedManifest(key) {
206
+ const cached = manifestCache.get(key);
207
+ if (!cached) {
208
+ return null;
209
+ }
210
+ if (cached.expiresAt <= Date.now()) {
211
+ manifestCache.delete(key);
212
+ return null;
213
+ }
214
+ return cached.manifest;
215
+ }
216
+
217
+ function setCachedManifest(key, manifest) {
218
+ manifestCache.set(key, {
219
+ manifest,
220
+ expiresAt: Date.now() + MANIFEST_CACHE_TTL_MS,
221
+ });
222
+ }
223
+
224
+ async function resolveFirmwareManifest({
225
+ channel,
226
+ downloadUrlOverride,
227
+ currentVersionOverride,
228
+ releaseNotesUrlOverride,
229
+ sha256Override,
230
+ repositoryOverride,
231
+ assetNameOverride,
232
+ fetchImpl = fetch,
233
+ } = {}) {
234
+ const normalizedChannel = normalizeChannel(channel);
235
+ const repository = parseRepositorySlug(repositoryOverride) || getGithubRepository();
236
+ const assetName = trimString(assetNameOverride, 128) || getFirmwareAssetName();
237
+ const downloadUrl = trimString(downloadUrlOverride, 2000);
238
+ const cacheId = cacheKey({ repository, channel: normalizedChannel, assetName, downloadUrlOverride: downloadUrl });
239
+ const cached = getCachedManifest(cacheId);
240
+ if (cached) {
241
+ return cached;
242
+ }
243
+
244
+ if (downloadUrl) {
245
+ const manifest = {
246
+ configured: true,
247
+ source: 'static',
248
+ manifestVersion: 2,
249
+ channel: normalizedChannel,
250
+ currentVersion: trimString(currentVersionOverride, 120) || null,
251
+ releaseName: trimString(currentVersionOverride, 120) || null,
252
+ releaseTag: trimString(currentVersionOverride, 120) || null,
253
+ minimumServerVersion: trimString(process.env.NEOAGENT_WEARABLE_MIN_SERVER_VERSION, 120) || null,
254
+ downloadUrl,
255
+ releaseNotesUrl: trimString(releaseNotesUrlOverride, 2000) || null,
256
+ sha256: stripHashPrefix(sha256Override),
257
+ assetName,
258
+ repository,
259
+ generatedAt: new Date().toISOString(),
260
+ mandatory: toBoolean(process.env.NEOAGENT_WEARABLE_FIRMWARE_MANDATORY, false),
261
+ };
262
+ setCachedManifest(cacheId, manifest);
263
+ return manifest;
264
+ }
265
+
266
+ if (!repository) {
267
+ return {
268
+ configured: false,
269
+ source: 'github',
270
+ manifestVersion: 2,
271
+ channel: normalizedChannel,
272
+ downloadUrl: null,
273
+ releaseNotesUrl: null,
274
+ sha256: null,
275
+ assetName,
276
+ repository: null,
277
+ generatedAt: new Date().toISOString(),
278
+ error: 'GitHub repository is not configured.',
279
+ mandatory: false,
280
+ };
281
+ }
282
+
283
+ try {
284
+ const token = getGithubToken();
285
+ const release = await fetchGithubRelease(fetchImpl, repository, normalizedChannel, token);
286
+ const asset = selectReleaseAsset(release, assetName);
287
+ if (!asset || !asset.browser_download_url) {
288
+ return {
289
+ configured: false,
290
+ source: 'github',
291
+ manifestVersion: 2,
292
+ channel: normalizedChannel,
293
+ currentVersion: trimString(release?.tag_name, 120) || null,
294
+ releaseName: trimString(release?.name, 128) || trimString(release?.tag_name, 120) || null,
295
+ releaseTag: trimString(release?.tag_name, 120) || null,
296
+ minimumServerVersion: trimString(process.env.NEOAGENT_WEARABLE_MIN_SERVER_VERSION, 120) || null,
297
+ downloadUrl: null,
298
+ releaseNotesUrl: trimString(release?.html_url, 2000) || null,
299
+ sha256: null,
300
+ assetName,
301
+ repository,
302
+ generatedAt: new Date().toISOString(),
303
+ error: `Release asset ${assetName} was not found.`,
304
+ mandatory: false,
305
+ prerelease: Boolean(release?.prerelease),
306
+ };
307
+ }
308
+
309
+ let checksum = null;
310
+ const checksumAsset = selectChecksumAsset(release, asset.name);
311
+ if (checksumAsset?.browser_download_url) {
312
+ try {
313
+ checksum = parseChecksumBody(
314
+ await fetchText(fetchImpl, checksumAsset.browser_download_url, token),
315
+ asset.name,
316
+ );
317
+ } catch {
318
+ checksum = null;
319
+ }
320
+ }
321
+
322
+ const manifest = {
323
+ configured: true,
324
+ source: 'github',
325
+ manifestVersion: 2,
326
+ channel: normalizedChannel,
327
+ currentVersion: trimString(release?.tag_name, 120) || null,
328
+ releaseName: trimString(release?.name, 128) || trimString(release?.tag_name, 120) || null,
329
+ releaseTag: trimString(release?.tag_name, 120) || null,
330
+ minimumServerVersion: trimString(process.env.NEOAGENT_WEARABLE_MIN_SERVER_VERSION, 120) || null,
331
+ downloadUrl: asset.browser_download_url,
332
+ releaseNotesUrl: trimString(release?.html_url, 2000) || null,
333
+ sha256: checksum,
334
+ assetName: asset.name,
335
+ repository,
336
+ generatedAt: new Date().toISOString(),
337
+ mandatory: toBoolean(process.env.NEOAGENT_WEARABLE_FIRMWARE_MANDATORY, false),
338
+ prerelease: Boolean(release?.prerelease),
339
+ };
340
+ setCachedManifest(cacheId, manifest);
341
+ return manifest;
342
+ } catch (error) {
343
+ return {
344
+ configured: false,
345
+ source: 'github',
346
+ manifestVersion: 2,
347
+ channel: normalizedChannel,
348
+ downloadUrl: null,
349
+ releaseNotesUrl: null,
350
+ sha256: null,
351
+ assetName,
352
+ repository,
353
+ generatedAt: new Date().toISOString(),
354
+ error: error.message,
355
+ mandatory: false,
356
+ };
357
+ }
358
+ }
359
+
360
+ module.exports = {
361
+ DEFAULT_ASSET_NAME,
362
+ getFirmwareAssetName,
363
+ getGithubRepository,
364
+ normalizeChannel,
365
+ parseRepositorySlug,
366
+ resolveFirmwareManifest,
367
+ selectGithubRelease,
368
+ selectReleaseAsset,
369
+ stripHashPrefix,
370
+ };
@@ -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
+ };