neoagent 2.3.1-beta.11 → 2.3.1-beta.111

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 (390) hide show
  1. package/.env.example +53 -5
  2. package/README.md +16 -7
  3. package/com.neoagent.plist +1 -1
  4. package/docs/automation.md +82 -41
  5. package/docs/capabilities.md +85 -89
  6. package/docs/configuration.md +87 -94
  7. package/docs/getting-started.md +46 -27
  8. package/docs/hardware.md +1 -1
  9. package/docs/index.md +24 -30
  10. package/docs/integrations.md +46 -64
  11. package/docs/migration.md +41 -146
  12. package/docs/operations.md +23 -20
  13. package/docs/skills.md +20 -20
  14. package/docs/why-neoagent.md +15 -13
  15. package/extensions/chrome-browser/background.mjs +10 -2
  16. package/flutter_app/.metadata +42 -0
  17. package/flutter_app/DESIGN_SYSTEM.md +166 -0
  18. package/flutter_app/README.md +21 -0
  19. package/flutter_app/analysis_options.yaml +32 -0
  20. package/flutter_app/android/app/build.gradle.kts +111 -0
  21. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  22. package/flutter_app/android/app/src/main/AndroidManifest.xml +185 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +888 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/auto/NeoAgentCarAppService.kt +114 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  30. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  31. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  32. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  33. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  34. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  35. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnection.kt +118 -0
  36. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/telecom/NeoAgentConnectionService.kt +86 -0
  37. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  38. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  39. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  40. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  41. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  42. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  43. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  44. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  45. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  46. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  47. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  48. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  49. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  50. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  51. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  52. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  53. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  54. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  55. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  56. package/flutter_app/android/app/src/main/res/values/arrays.xml +8 -0
  57. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  58. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  59. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  60. package/flutter_app/android/app/src/main/res/xml/automotive_app_desc.xml +4 -0
  61. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  62. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  63. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  64. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  65. package/flutter_app/android/build.gradle.kts +24 -0
  66. package/flutter_app/android/ci-release.keystore +0 -0
  67. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  68. package/flutter_app/android/gradle.properties +3 -0
  69. package/flutter_app/android/key.properties +4 -0
  70. package/flutter_app/android/settings.gradle.kts +26 -0
  71. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  72. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  73. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  74. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  75. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  76. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  77. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  78. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  79. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  80. package/flutter_app/lib/features/location/location_service.dart +117 -0
  81. package/flutter_app/lib/features/notifications/notification_interceptor.dart +73 -0
  82. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +571 -0
  83. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +275 -0
  84. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +366 -0
  85. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +57 -0
  86. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +218 -0
  87. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +46 -0
  88. package/flutter_app/lib/main.dart +107 -0
  89. package/flutter_app/lib/main_account_settings.dart +1340 -0
  90. package/flutter_app/lib/main_admin.dart +900 -0
  91. package/flutter_app/lib/main_app_shell.dart +2162 -0
  92. package/flutter_app/lib/main_chat.dart +3902 -0
  93. package/flutter_app/lib/main_controller.dart +7229 -0
  94. package/flutter_app/lib/main_devices.dart +1738 -0
  95. package/flutter_app/lib/main_integrations.dart +917 -0
  96. package/flutter_app/lib/main_launcher.dart +959 -0
  97. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  98. package/flutter_app/lib/main_models.dart +3807 -0
  99. package/flutter_app/lib/main_navigation.dart +206 -0
  100. package/flutter_app/lib/main_operations.dart +5249 -0
  101. package/flutter_app/lib/main_recordings.dart +920 -0
  102. package/flutter_app/lib/main_runtime.dart +869 -0
  103. package/flutter_app/lib/main_settings.dart +2288 -0
  104. package/flutter_app/lib/main_shared.dart +3393 -0
  105. package/flutter_app/lib/main_spacing.dart +33 -0
  106. package/flutter_app/lib/main_theme.dart +276 -0
  107. package/flutter_app/lib/main_unified.dart +388 -0
  108. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  109. package/flutter_app/lib/src/analytics_service.dart +294 -0
  110. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  111. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  112. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  113. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  114. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  115. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  116. package/flutter_app/lib/src/android_auto_bridge.dart +59 -0
  117. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  118. package/flutter_app/lib/src/app_launch_bridge.dart +58 -0
  119. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  120. package/flutter_app/lib/src/backend_client.dart +1870 -0
  121. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  122. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  123. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  124. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  125. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  126. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  127. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  128. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  129. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  130. package/flutter_app/lib/src/health_bridge.dart +136 -0
  131. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  132. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  133. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  134. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  135. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  136. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  137. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  138. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  139. package/flutter_app/lib/src/oauth_launcher_io.dart +95 -0
  140. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  141. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  142. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  143. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  144. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  145. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  146. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  147. package/flutter_app/lib/src/theme/palette.dart +81 -0
  148. package/flutter_app/lib/src/web_app_update_monitor.dart +17 -0
  149. package/flutter_app/lib/src/web_app_update_monitor_stub.dart +24 -0
  150. package/flutter_app/lib/src/web_app_update_monitor_web.dart +123 -0
  151. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  152. package/flutter_app/linux/CMakeLists.txt +128 -0
  153. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  154. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +47 -0
  155. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  156. package/flutter_app/linux/flutter/generated_plugins.cmake +32 -0
  157. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  158. package/flutter_app/linux/runner/main.cc +6 -0
  159. package/flutter_app/linux/runner/my_application.cc +144 -0
  160. package/flutter_app/linux/runner/my_application.h +18 -0
  161. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  162. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  163. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  164. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +48 -0
  165. package/flutter_app/macos/Podfile +42 -0
  166. package/flutter_app/macos/Podfile.lock +87 -0
  167. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  168. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  169. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  170. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  171. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  172. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  173. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  174. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  175. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  176. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  177. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  178. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  179. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  180. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  181. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  182. package/flutter_app/macos/Runner/Info.plist +36 -0
  183. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  184. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  185. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  186. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  187. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  188. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  189. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  190. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  191. package/flutter_app/patch_strings.py +12 -0
  192. package/flutter_app/pubspec.lock +1256 -0
  193. package/flutter_app/pubspec.yaml +59 -0
  194. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  195. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  196. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  197. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  198. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  199. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  200. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  201. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  202. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  203. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  204. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  205. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  206. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  207. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  208. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  209. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  210. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  211. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  212. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  213. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  214. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  215. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  216. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  217. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  218. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  219. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  220. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  221. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  222. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  223. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  224. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  225. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  226. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  227. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  228. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  229. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  230. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  231. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  232. package/flutter_app/web/favicon.png +0 -0
  233. package/flutter_app/web/favicon.svg +12 -0
  234. package/flutter_app/web/icons/Icon-192.png +0 -0
  235. package/flutter_app/web/icons/Icon-512.png +0 -0
  236. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  237. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  238. package/flutter_app/web/index.html +39 -0
  239. package/flutter_app/web/manifest.json +35 -0
  240. package/flutter_app/windows/CMakeLists.txt +108 -0
  241. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  242. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +50 -0
  243. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  244. package/flutter_app/windows/flutter/generated_plugins.cmake +36 -0
  245. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  246. package/flutter_app/windows/runner/Runner.rc +121 -0
  247. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  248. package/flutter_app/windows/runner/flutter_window.h +37 -0
  249. package/flutter_app/windows/runner/main.cpp +53 -0
  250. package/flutter_app/windows/runner/resource.h +16 -0
  251. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  252. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  253. package/flutter_app/windows/runner/utils.cpp +65 -0
  254. package/flutter_app/windows/runner/utils.h +19 -0
  255. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  256. package/flutter_app/windows/runner/win32_window.h +102 -0
  257. package/lib/install_helpers.js +31 -0
  258. package/lib/manager.js +579 -122
  259. package/package.json +10 -3
  260. package/runtime/paths.js +137 -0
  261. package/server/config/analytics.js +30 -0
  262. package/server/db/database.js +296 -22
  263. package/server/guest-agent.android.package.json +13 -0
  264. package/server/guest-agent.browser.package.json +14 -0
  265. package/server/guest_agent.js +98 -52
  266. package/server/http/middleware.js +5 -2
  267. package/server/http/routes.js +3 -0
  268. package/server/http/static.js +41 -1
  269. package/server/index.js +15 -16
  270. package/server/public/.last_build_id +1 -1
  271. package/server/public/assets/AssetManifest.bin +1 -1
  272. package/server/public/assets/AssetManifest.bin.json +1 -1
  273. package/server/public/assets/AssetManifest.json +1 -1
  274. package/server/public/assets/NOTICES +644 -155
  275. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  276. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  277. package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
  278. package/server/public/canvaskit/wimp.wasm +0 -0
  279. package/server/public/flutter_bootstrap.js +2 -2
  280. package/server/public/main.dart.js +96067 -88139
  281. package/server/routes/account.js +26 -1
  282. package/server/routes/agents.js +6 -2
  283. package/server/routes/android.js +22 -6
  284. package/server/routes/auth.js +25 -5
  285. package/server/routes/browser.js +47 -13
  286. package/server/routes/integrations.js +22 -1
  287. package/server/routes/memory.js +168 -6
  288. package/server/routes/runtime.js +14 -0
  289. package/server/routes/settings.js +102 -24
  290. package/server/routes/skills.js +12 -0
  291. package/server/routes/social_video.js +66 -0
  292. package/server/routes/tasks.js +22 -2
  293. package/server/routes/triggers.js +44 -35
  294. package/server/routes/wearable.js +67 -0
  295. package/server/routes/widgets.js +4 -4
  296. package/server/services/ai/capabilityHealth.js +23 -51
  297. package/server/services/ai/compaction.js +1 -1
  298. package/server/services/ai/deliverables/artifact_helpers.js +190 -0
  299. package/server/services/ai/deliverables/contracts.js +113 -0
  300. package/server/services/ai/deliverables/index.js +20 -0
  301. package/server/services/ai/deliverables/selector.js +94 -0
  302. package/server/services/ai/deliverables/validator.js +63 -0
  303. package/server/services/ai/deliverables/workflows.js +207 -0
  304. package/server/services/ai/engine.js +363 -27
  305. package/server/services/ai/hooks.js +127 -0
  306. package/server/services/ai/integrated_tools/index.js +98 -0
  307. package/server/services/ai/integrated_tools/remotion.js +373 -0
  308. package/server/services/ai/integrated_tools/shared.js +204 -0
  309. package/server/services/ai/integrated_tools/slidev.js +207 -0
  310. package/server/services/ai/loopPolicy.js +146 -0
  311. package/server/services/ai/models.js +31 -4
  312. package/server/services/ai/providers/claudeCode.js +49 -0
  313. package/server/services/ai/providers/openai.js +2 -1
  314. package/server/services/ai/providers/openaiCodex.js +422 -15
  315. package/server/services/ai/recordingInsights.js +11 -13
  316. package/server/services/ai/runEvents.js +100 -0
  317. package/server/services/ai/settings.js +12 -2
  318. package/server/services/ai/systemPrompt.js +15 -1
  319. package/server/services/ai/taskAnalysis.js +2 -0
  320. package/server/services/ai/toolResult.js +20 -0
  321. package/server/services/ai/toolRunner.js +46 -6
  322. package/server/services/ai/toolSelector.js +54 -1
  323. package/server/services/ai/tools.js +210 -116
  324. package/server/services/android/android_bootstrap_worker.js +63 -0
  325. package/server/services/android/controller.js +442 -1760
  326. package/server/services/bootstrap_helpers.js +0 -34
  327. package/server/services/browser/controller.js +279 -49
  328. package/server/services/cli/executor.js +40 -1
  329. package/server/services/desktop/screenRecorder.js +65 -9
  330. package/server/services/integrations/figma/provider.js +1 -0
  331. package/server/services/integrations/github/provider.js +3 -3
  332. package/server/services/integrations/google/provider.js +1 -0
  333. package/server/services/integrations/manager.js +25 -5
  334. package/server/services/integrations/microsoft/provider.js +1 -0
  335. package/server/services/integrations/oauth_provider.js +15 -5
  336. package/server/services/integrations/provider_config_store.js +22 -13
  337. package/server/services/integrations/registry.js +2 -4
  338. package/server/services/integrations/spotify/provider.js +1 -0
  339. package/server/services/integrations/trello/provider.js +842 -0
  340. package/server/services/manager.js +88 -155
  341. package/server/services/mcp/client.js +120 -23
  342. package/server/services/memory/llm_transfer.js +218 -0
  343. package/server/services/memory/manager.js +260 -33
  344. package/server/services/messaging/access_policy.js +10 -0
  345. package/server/services/messaging/automation.js +1 -1
  346. package/server/services/messaging/manager.js +64 -1
  347. package/server/services/messaging/meshtastic.js +277 -0
  348. package/server/services/messaging/meshtastic_env.js +40 -0
  349. package/server/services/messaging/meshtastic_protocol.js +530 -0
  350. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  351. package/server/services/runtime/backends/local-vm.js +221 -158
  352. package/server/services/runtime/docker-vm-manager.js +392 -0
  353. package/server/services/runtime/guest_bootstrap.js +654 -0
  354. package/server/services/runtime/manager.js +85 -29
  355. package/server/services/runtime/settings.js +22 -31
  356. package/server/services/runtime/validation.js +16 -31
  357. package/server/services/social_video/adapters/base.js +26 -0
  358. package/server/services/social_video/adapters/index.js +27 -0
  359. package/server/services/social_video/adapters/instagram.js +17 -0
  360. package/server/services/social_video/adapters/tiktok.js +17 -0
  361. package/server/services/social_video/adapters/x.js +17 -0
  362. package/server/services/social_video/adapters/youtube.js +17 -0
  363. package/server/services/social_video/captions.js +187 -0
  364. package/server/services/social_video/frame.js +42 -0
  365. package/server/services/social_video/index.js +7 -0
  366. package/server/services/social_video/metadata.js +63 -0
  367. package/server/services/social_video/result.js +63 -0
  368. package/server/services/social_video/service.js +720 -0
  369. package/server/services/social_video/url.js +83 -0
  370. package/server/services/tasks/integration_runtime.js +1 -1
  371. package/server/services/tasks/runtime.js +1 -1
  372. package/server/services/tasks/task_repository.js +3 -6
  373. package/server/services/voice/openaiSpeech.js +6 -1
  374. package/server/services/voice/providers.js +142 -14
  375. package/server/services/voice/runtime.js +8 -8
  376. package/server/services/voice/runtimeManager.js +156 -19
  377. package/server/services/voice/turnRunner.js +29 -9
  378. package/server/services/wearable/firmware_manifest.js +353 -0
  379. package/server/services/wearable/gateway.js +350 -0
  380. package/server/services/wearable/protocol.js +45 -0
  381. package/server/services/wearable/service.js +240 -0
  382. package/server/services/websocket.js +3 -1
  383. package/server/services/widgets/focus_widget.js +137 -0
  384. package/server/services/widgets/service.js +133 -2
  385. package/server/services/workspace/manager.js +301 -0
  386. package/server/utils/deployment.js +16 -9
  387. package/server/services/integrations/home_assistant/provider.js +0 -630
  388. package/server/services/messaging/access_policy.test.js +0 -228
  389. package/server/services/runtime/backends/host.js +0 -37
  390. package/server/services/runtime/qemu.js +0 -391
package/lib/manager.js CHANGED
@@ -18,6 +18,7 @@ const {
18
18
  LOG_DIR,
19
19
  ENV_FILE,
20
20
  PID_FILE,
21
+ getDefaultVmBaseImageUrl,
21
22
  ensureRuntimeDirs,
22
23
  migrateLegacyRuntime
23
24
  } = require('../runtime/paths');
@@ -118,28 +119,45 @@ function readEnvFileRaw() {
118
119
  return fs.readFileSync(ENV_FILE, 'utf8');
119
120
  }
120
121
 
122
+ function sanitizeEnvKey(key) {
123
+ return String(key).replace(/[\r\n]/g, '');
124
+ }
125
+
126
+ function sanitizeEnvValue(value) {
127
+ return String(value).replace(/[\r\n]/g, '');
128
+ }
129
+
130
+ function validateEnvKey(key) {
131
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
132
+ throw new Error(`Invalid env key "${key}". Keys must be uppercase letters, digits, and underscores (e.g. PORT, ANTHROPIC_API_KEY).`);
133
+ }
134
+ }
135
+
121
136
  function upsertEnvValue(key, value) {
137
+ const safeKey = sanitizeEnvKey(key);
138
+ const safeValue = sanitizeEnvValue(value);
122
139
  const raw = readEnvFileRaw();
123
140
  const lines = raw ? raw.split('\n') : [];
124
141
  let replaced = false;
125
142
 
126
143
  for (let i = 0; i < lines.length; i++) {
127
- if (lines[i].startsWith(`${key}=`)) {
128
- lines[i] = `${key}=${value}`;
144
+ if (lines[i].startsWith(`${safeKey}=`)) {
145
+ lines[i] = `${safeKey}=${safeValue}`;
129
146
  replaced = true;
130
147
  break;
131
148
  }
132
149
  }
133
150
 
134
- if (!replaced) lines.push(`${key}=${value}`);
151
+ if (!replaced) lines.push(`${safeKey}=${safeValue}`);
135
152
  const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
136
153
  fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
137
154
  }
138
155
 
139
156
  function removeEnvValue(key) {
157
+ const safeKey = sanitizeEnvKey(key);
140
158
  const raw = readEnvFileRaw();
141
159
  if (!raw) return false;
142
- const lines = raw.split('\n').filter((line) => !line.startsWith(`${key}=`));
160
+ const lines = raw.split('\n').filter((line) => !line.startsWith(`${safeKey}=`));
143
161
  const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
144
162
  fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
145
163
  return true;
@@ -287,6 +305,38 @@ function ensureLogDir() {
287
305
  ensureRuntimeDirs();
288
306
  }
289
307
 
308
+ function pruneOldRuntimeBackups(backupsDir, keepLatest = 3) {
309
+ if (!fs.existsSync(backupsDir) || keepLatest < 0) return;
310
+
311
+ const backupDirs = fs
312
+ .readdirSync(backupsDir, { withFileTypes: true })
313
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('pre-update-'))
314
+ .map((entry) => {
315
+ const fullPath = path.join(backupsDir, entry.name);
316
+ let mtimeMs = 0;
317
+ try {
318
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
319
+ } catch {
320
+ // Skip entries that disappear or cannot be statted.
321
+ return null;
322
+ }
323
+ return { name: entry.name, fullPath, mtimeMs };
324
+ })
325
+ .filter(Boolean)
326
+ .sort((a, b) => {
327
+ if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
328
+ return b.name.localeCompare(a.name);
329
+ });
330
+
331
+ for (const backup of backupDirs.slice(keepLatest)) {
332
+ try {
333
+ fs.rmSync(backup.fullPath, { recursive: true, force: true });
334
+ } catch {
335
+ // Best-effort cleanup only.
336
+ }
337
+ }
338
+ }
339
+
290
340
  function backupRuntimeData() {
291
341
  const backupsDir = path.join(RUNTIME_HOME, 'backups');
292
342
  fs.mkdirSync(backupsDir, { recursive: true });
@@ -296,6 +346,7 @@ function backupRuntimeData() {
296
346
 
297
347
  if (fs.existsSync(ENV_FILE)) fs.copyFileSync(ENV_FILE, path.join(target, '.env'));
298
348
  if (fs.existsSync(DATA_DIR)) fs.cpSync(DATA_DIR, path.join(target, 'data'), { recursive: true, force: false, errorOnExist: false });
349
+ pruneOldRuntimeBackups(backupsDir, 3);
299
350
  }
300
351
 
301
352
  function killByPort(port) {
@@ -347,15 +398,19 @@ function listNeoAgentServerProcesses() {
347
398
  };
348
399
  })
349
400
  .filter(Boolean)
350
- .filter((entry) =>
351
- entry.pid !== process.pid &&
352
- /(^|\s)node(\s|$)/.test(entry.command) &&
353
- (
354
- appIndexPattern.test(String(entry.command || '').replace(/\\/g, '/')) ||
355
- genericNeoAgentPattern.test(String(entry.command || '').replace(/\\/g, '/')) ||
356
- repoNamePattern.test(String(entry.command || ''))
357
- )
358
- );
401
+ .filter((entry) => {
402
+ if (entry.pid === process.pid) return false;
403
+ const cmd = String(entry.command || '');
404
+ const cmdNormalized = cmd.replace(/\\/g, '/');
405
+ const executablePart = cmd.split(/\s+/)[0] || '';
406
+ const executableBase = path.basename(executablePart);
407
+ const isNode = /^node\d*$/.test(executableBase) || /(^|\s)node\d*(\s|$)/.test(cmd);
408
+ return isNode && (
409
+ appIndexPattern.test(cmdNormalized) ||
410
+ genericNeoAgentPattern.test(cmdNormalized) ||
411
+ repoNamePattern.test(cmd)
412
+ );
413
+ });
359
414
  }
360
415
 
361
416
  function killNeoAgentServerProcesses() {
@@ -425,18 +480,24 @@ async function cmdSetup() {
425
480
  ensureRuntimeDirs();
426
481
 
427
482
  const current = Object.fromEntries(parseEnv(readEnvFileRaw()).entries());
483
+ const defaultVmBaseImageUrl = getDefaultVmBaseImageUrl();
428
484
 
429
485
  logInfo('Press Enter to keep the current value shown in brackets.');
430
486
 
431
487
  heading('Core');
432
- const port = await ask('Server port', current.PORT || '3333');
488
+ const portRaw = await ask('Server port', current.PORT || '3333');
489
+ const portNum = Number(portRaw);
490
+ if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
491
+ throw new Error(`Invalid port "${portRaw}". Must be an integer between 1 and 65535.`);
492
+ }
493
+ const port = String(portNum);
433
494
  const publicUrl = await ask('Public base URL', current.PUBLIC_URL || '');
434
495
  const secureCookiesDefault = current.SECURE_COOKIES ||
435
496
  (String(publicUrl || '').trim().startsWith('https://') ? 'true' : 'false');
436
497
  const secureCookies = await ask('Secure cookies (true/false)', secureCookiesDefault);
437
498
  const trustProxyDefault = current.TRUST_PROXY || secureCookiesDefault;
438
499
  const trustProxy = await ask('Trust reverse proxy headers (true/false)', trustProxyDefault);
439
- const sessionSecret = await askSecret('Session secret', current.SESSION_SECRET || randomSecret());
500
+ const sessionSecret = current.SESSION_SECRET || randomSecret();
440
501
  const deploymentMode = await ask(
441
502
  'Deployment mode (self_hosted/managed)',
442
503
  current.NEOAGENT_DEPLOYMENT_MODE || 'self_hosted'
@@ -446,6 +507,11 @@ async function cmdSetup() {
446
507
  current.NEOAGENT_RELEASE_CHANNEL || 'stable'
447
508
  );
448
509
  const origins = await ask('Allowed CORS origins', current.ALLOWED_ORIGINS || '');
510
+ const deploymentProfile = current.NEOAGENT_PROFILE || 'prod';
511
+ const vmBaseImageUrl = current.NEOAGENT_VM_BASE_IMAGE_URL || defaultVmBaseImageUrl;
512
+ const vmMemoryMb = current.NEOAGENT_VM_MEMORY_MB || '4096';
513
+ const vmCpus = current.NEOAGENT_VM_CPUS || '2';
514
+ const vmGuestToken = current.NEOAGENT_VM_GUEST_TOKEN || randomSecret();
449
515
 
450
516
  heading('AI Providers');
451
517
  const anthropic = await askSecret('Anthropic API key', current.ANTHROPIC_API_KEY || '');
@@ -542,6 +608,19 @@ async function cmdSetup() {
542
608
  const normalizedDeploymentMode = parseDeploymentMode(deploymentMode);
543
609
  const normalizedReleaseChannel = parseReleaseChannel(releaseChannel) || 'stable';
544
610
 
611
+ const githubOauthClientId = await askSecret(
612
+ 'GitHub OAuth client ID',
613
+ current.GITHUB_OAUTH_CLIENT_ID || ''
614
+ );
615
+ const githubOauthClientSecret = await askSecret(
616
+ 'GitHub OAuth client secret',
617
+ current.GITHUB_OAUTH_CLIENT_SECRET || ''
618
+ );
619
+ const githubOauthRedirectUri = await ask(
620
+ 'GitHub OAuth redirect URI',
621
+ current.GITHUB_OAUTH_REDIRECT_URI || ''
622
+ );
623
+
545
624
  const lines = [
546
625
  `NODE_ENV=production`,
547
626
  `PORT=${port}`,
@@ -549,8 +628,13 @@ async function cmdSetup() {
549
628
  `SECURE_COOKIES=${normalizedSecureCookies}`,
550
629
  `TRUST_PROXY=${normalizedTrustProxy}`,
551
630
  `SESSION_SECRET=${sessionSecret}`,
631
+ `NEOAGENT_PROFILE=${deploymentProfile}`,
552
632
  `NEOAGENT_DEPLOYMENT_MODE=${normalizedDeploymentMode}`,
553
633
  `NEOAGENT_RELEASE_CHANNEL=${normalizedReleaseChannel}`,
634
+ `NEOAGENT_VM_BASE_IMAGE_URL=${vmBaseImageUrl}`,
635
+ `NEOAGENT_VM_MEMORY_MB=${vmMemoryMb}`,
636
+ `NEOAGENT_VM_CPUS=${vmCpus}`,
637
+ `NEOAGENT_VM_GUEST_TOKEN=${vmGuestToken}`,
554
638
  anthropic ? `ANTHROPIC_API_KEY=${anthropic}` : '',
555
639
  anthropicBaseUrl ? `ANTHROPIC_BASE_URL=${anthropicBaseUrl}` : '',
556
640
  openai ? `OPENAI_API_KEY=${openai}` : '',
@@ -576,6 +660,9 @@ async function cmdSetup() {
576
660
  figmaOauthClientId ? `FIGMA_OAUTH_CLIENT_ID=${figmaOauthClientId}` : '',
577
661
  figmaOauthClientSecret ? `FIGMA_OAUTH_CLIENT_SECRET=${figmaOauthClientSecret}` : '',
578
662
  figmaOauthRedirectUri ? `FIGMA_OAUTH_REDIRECT_URI=${figmaOauthRedirectUri}` : '',
663
+ githubOauthClientId ? `GITHUB_OAUTH_CLIENT_ID=${githubOauthClientId}` : '',
664
+ githubOauthClientSecret ? `GITHUB_OAUTH_CLIENT_SECRET=${githubOauthClientSecret}` : '',
665
+ githubOauthRedirectUri ? `GITHUB_OAUTH_REDIRECT_URI=${githubOauthRedirectUri}` : '',
579
666
  deepgramApiKey ? `DEEPGRAM_API_KEY=${deepgramApiKey}` : '',
580
667
  deepgramBaseUrl ? `DEEPGRAM_BASE_URL=${deepgramBaseUrl}` : '',
581
668
  deepgramModel ? `DEEPGRAM_MODEL=${deepgramModel}` : '',
@@ -674,10 +761,192 @@ async function cmdMigrate(args = []) {
674
761
  });
675
762
  }
676
763
 
764
+ async function pollDeviceCode({ pollUrl, pollBody, pollHeaders = {}, intervalMs, timeoutMs, onToken }) {
765
+ const start = Date.now();
766
+ let currentInterval = intervalMs;
767
+ while (Date.now() - start < timeoutMs) {
768
+ await new Promise((r) => setTimeout(r, currentInterval));
769
+ const res = await fetch(pollUrl, {
770
+ method: 'POST',
771
+ headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...pollHeaders },
772
+ body: JSON.stringify(pollBody()),
773
+ });
774
+ if (res.status === 403 || res.status === 404) continue;
775
+ if (!res.ok) {
776
+ const text = await res.text().catch(() => 'Unknown error');
777
+ throw new Error(`Token poll failed: HTTP ${res.status} — ${text}`);
778
+ }
779
+ const data = await res.json();
780
+ const done = await onToken(data);
781
+ if (done) return;
782
+ if (data.error === 'authorization_pending') continue;
783
+ if (data.error === 'slow_down') { currentInterval += 5000; continue; }
784
+ if (data.error) throw new Error(`Authentication failed: ${data.error_description || data.error}`);
785
+ }
786
+ throw new Error('Authentication timed out after 15 minutes.');
787
+ }
788
+
789
+ async function cmdLoginClaudeCode() {
790
+ heading('Claude Code Login');
791
+
792
+ // Check for Claude CLI credential file first (set by `claude login`)
793
+ const cliCredsPath = path.join(os.homedir(), '.claude', '.credentials.json');
794
+ if (fs.existsSync(cliCredsPath)) {
795
+ try {
796
+ const raw = fs.readFileSync(cliCredsPath, 'utf8');
797
+ const data = JSON.parse(raw);
798
+ const token = data?.claudeAiOauthTokens?.accessToken;
799
+ if (token) {
800
+ upsertEnvValue('CLAUDE_CODE_OAUTH_TOKEN', token);
801
+ logOk('Imported access token from Claude CLI credentials store');
802
+ logInfo('Restarting NeoAgent to apply credentials...');
803
+ cmdRestart();
804
+ return;
805
+ }
806
+ } catch { }
807
+ }
808
+
809
+ // Browser-based PKCE OAuth flow.
810
+ // client_id is the metadata URL per claude.ai's dynamic client registration.
811
+ // Redirect URIs registered: http://localhost/callback and http://127.0.0.1/callback (port 80).
812
+ // Per RFC 8252 §7.3, servers SHOULD allow any loopback port — we try high ports first
813
+ // and fall back to 80 if everything else is occupied.
814
+ const http = require('http');
815
+ const { URL: NodeURL } = require('url');
816
+
817
+ const clientId = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
818
+ const SCOPES = 'user:inference user:profile org:create_api_key user:sessions:claude_code user:mcp_servers';
819
+
820
+ // The registered redirect URIs are http://localhost/callback and http://127.0.0.1/callback
821
+ // (port 80). The OAuth server validates the URI exactly, so we must use port 80.
822
+ // Dynamic high port — the server accepts http://localhost:{any-port}/callback per RFC 8252.
823
+ const redirectPort = Math.floor(Math.random() * 10000) + 49152;
824
+ const redirectUri = `http://localhost:${redirectPort}/callback`;
825
+
826
+ // Generate PKCE verifier and challenge
827
+ const codeVerifier = crypto.randomBytes(48).toString('base64url');
828
+ const codeChallenge = crypto
829
+ .createHash('sha256')
830
+ .update(codeVerifier)
831
+ .digest('base64url');
832
+
833
+ const state = crypto.randomBytes(16).toString('hex');
834
+
835
+ const authUrl = new URL('https://platform.claude.com/oauth/authorize');
836
+ authUrl.searchParams.set('response_type', 'code');
837
+ authUrl.searchParams.set('client_id', clientId);
838
+ authUrl.searchParams.set('redirect_uri', redirectUri);
839
+ authUrl.searchParams.set('scope', SCOPES);
840
+ authUrl.searchParams.set('code_challenge', codeChallenge);
841
+ authUrl.searchParams.set('code_challenge_method', 'S256');
842
+ authUrl.searchParams.set('state', state);
843
+
844
+ console.log(`\n ${COLORS.cyan}Opening browser for Claude Code authorization...${COLORS.reset}`);
845
+ console.log(` ${COLORS.dim}If the browser doesn't open, visit:${COLORS.reset}`);
846
+ console.log(` ${authUrl.toString()}\n`);
847
+
848
+ // Open browser
849
+ const openCmd = process.platform === 'darwin' ? 'open'
850
+ : process.platform === 'win32' ? 'start'
851
+ : 'xdg-open';
852
+ spawnSync(openCmd, [authUrl.toString()], { stdio: 'ignore' });
853
+
854
+ // Start local redirect server to capture authorization code
855
+ const authCode = await new Promise((resolve, reject) => {
856
+ const timeout = setTimeout(() => {
857
+ server.close();
858
+ reject(new Error('Claude Code authorization timed out after 5 minutes.'));
859
+ }, 5 * 60 * 1000);
860
+
861
+ const server = http.createServer((req, res) => {
862
+ try {
863
+ const reqUrl = new NodeURL(req.url, redirectUri);
864
+ const code = reqUrl.searchParams.get('code');
865
+ const returnedState = reqUrl.searchParams.get('state');
866
+ const error = reqUrl.searchParams.get('error');
867
+
868
+ if (error) {
869
+ res.writeHead(200, { 'Content-Type': 'text/html' });
870
+ res.end('<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>');
871
+ clearTimeout(timeout);
872
+ server.close();
873
+ reject(new Error(`OAuth error: ${error}`));
874
+ return;
875
+ }
876
+
877
+ if (returnedState && returnedState !== state) {
878
+ res.writeHead(200, { 'Content-Type': 'text/html' });
879
+ res.end('<html><body><h2>Authorization failed.</h2><p>State mismatch. You can close this tab.</p></body></html>');
880
+ clearTimeout(timeout);
881
+ server.close();
882
+ reject(new Error('OAuth state mismatch — possible CSRF attempt.'));
883
+ return;
884
+ }
885
+
886
+ if (code) {
887
+ res.writeHead(200, { 'Content-Type': 'text/html' });
888
+ res.end('<html><body><h2>Authorization successful!</h2><p>You can close this tab and return to the terminal.</p></body></html>');
889
+ clearTimeout(timeout);
890
+ server.close();
891
+ resolve(code);
892
+ }
893
+ } catch (err) {
894
+ res.writeHead(500);
895
+ res.end('Internal error');
896
+ }
897
+ });
898
+
899
+ server.listen(redirectPort, 'localhost', () => {
900
+ logInfo(`Waiting for OAuth callback on ${redirectUri} ...`);
901
+ });
902
+ server.on('error', (err) => {
903
+ clearTimeout(timeout);
904
+ reject(new Error(`Could not start OAuth callback server: ${err.message}`));
905
+ });
906
+ });
907
+
908
+ logInfo('Exchanging authorization code for access token...');
909
+ const tokenRes = await fetch('https://platform.claude.com/v1/oauth/token', {
910
+ method: 'POST',
911
+ headers: {
912
+ 'Content-Type': 'application/json',
913
+ 'Accept': 'application/json',
914
+ 'anthropic-version': '2023-06-01',
915
+ },
916
+ body: JSON.stringify({
917
+ grant_type: 'authorization_code',
918
+ code: authCode,
919
+ redirect_uri: redirectUri,
920
+ client_id: clientId,
921
+ code_verifier: codeVerifier,
922
+ state,
923
+ }),
924
+ });
925
+
926
+ if (!tokenRes.ok) {
927
+ const text = await tokenRes.text().catch(() => 'Unknown error');
928
+ throw new Error(`Token exchange failed: HTTP ${tokenRes.status} — ${text}`);
929
+ }
930
+
931
+ const tokenData = await tokenRes.json();
932
+ const accessToken = tokenData.access_token;
933
+ if (!accessToken) {
934
+ throw new Error('Token exchange succeeded but no access_token was returned.');
935
+ }
936
+
937
+ upsertEnvValue('CLAUDE_CODE_OAUTH_TOKEN', accessToken);
938
+ if (tokenData.refresh_token) {
939
+ upsertEnvValue('CLAUDE_CODE_REFRESH_TOKEN', tokenData.refresh_token);
940
+ }
941
+ logOk('Saved Claude Code OAuth token to .env');
942
+ logInfo('Restarting NeoAgent to apply credentials...');
943
+ cmdRestart();
944
+ }
945
+
677
946
  async function cmdLogin(args = []) {
678
947
  const provider = args[0];
679
- if (provider !== 'github-copilot' && provider !== 'openai-codex') {
680
- throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex`);
948
+ if (provider !== 'github-copilot' && provider !== 'openai-codex' && provider !== 'claude-code') {
949
+ throw new Error(`Unsupported login provider: ${provider || 'none'}. Available: github-copilot, openai-codex, claude-code`);
681
950
  }
682
951
 
683
952
  if (provider === 'github-copilot') {
@@ -690,52 +959,28 @@ async function cmdLogin(args = []) {
690
959
  headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
691
960
  body: JSON.stringify({ client_id: clientId, scope: 'user:email' })
692
961
  });
693
-
694
- if (!reqRes.ok) {
695
- throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
696
- }
962
+ if (!reqRes.ok) throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
697
963
 
698
964
  const { device_code, user_code, verification_uri, interval } = await reqRes.json();
699
-
700
965
  console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
701
- console.log(` ${COLORS.cyan}And enter the code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
702
-
966
+ console.log(` ${COLORS.cyan}Enter code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
703
967
  logInfo('Waiting for authorization (timeout in 15m)...');
704
- const startTime = Date.now();
705
- const timeoutMs = 15 * 60 * 1000;
706
- let currentPollInterval = (interval || 5) * 1000;
707
-
708
- while (Date.now() - startTime < timeoutMs) {
709
- await new Promise((r) => setTimeout(r, currentPollInterval));
710
- const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
711
- method: 'POST',
712
- headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
713
- body: JSON.stringify({
714
- client_id: clientId,
715
- device_code,
716
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
717
- })
718
- });
719
-
720
- if (!tokenRes.ok) {
721
- const errorText = await tokenRes.text().catch(() => 'Unknown error');
722
- throw new Error(`GitHub token request failed: HTTP ${tokenRes.status} - ${errorText}`);
723
- }
724
968
 
725
- const data = await tokenRes.json();
726
- if (data.access_token) {
969
+ await pollDeviceCode({
970
+ pollUrl: 'https://github.com/login/oauth/access_token',
971
+ pollBody: () => ({ client_id: clientId, device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }),
972
+ intervalMs: (interval || 5) * 1000,
973
+ timeoutMs: 15 * 60 * 1000,
974
+ onToken: async (data) => {
975
+ if (!data.access_token) return false;
727
976
  upsertEnvValue('GITHUB_COPILOT_ACCESS_TOKEN', data.access_token);
728
- logOk('Successfully authenticated and saved GitHub Copilot access token to .env');
729
- return;
730
- } else if (data.error === 'authorization_pending') {
731
- // Continue polling
732
- } else if (data.error === 'slow_down') {
733
- currentPollInterval += 5000;
734
- } else if (data.error) {
735
- throw new Error(`Authentication failed: ${data.error_description || data.error}`);
736
- }
737
- }
738
- throw new Error('GitHub authentication timed out after 15 minutes.');
977
+ logOk('Saved GitHub Copilot access token to .env');
978
+ logInfo('Restarting NeoAgent to apply credentials...');
979
+ cmdRestart();
980
+ return true;
981
+ },
982
+ });
983
+ return;
739
984
  } else if (provider === 'openai-codex') {
740
985
  heading('OpenAI Codex Login');
741
986
  const clientId = 'app_EMoamEEZ73f0CkXaXp7hrann';
@@ -751,47 +996,62 @@ async function cmdLogin(args = []) {
751
996
  throw new Error(`Failed to request device code: HTTP ${reqRes.status}`);
752
997
  }
753
998
 
754
- const { device_code, user_code, verification_uri, interval } = await reqRes.json();
999
+ const data = await reqRes.json();
1000
+ const { device_auth_id, interval } = data;
1001
+ const user_code = data.user_code || data.usercode;
1002
+ const verification_uri = 'https://auth.openai.com/codex/device';
755
1003
 
756
1004
  console.log(`\n ${COLORS.cyan}Please visit:${COLORS.reset} ${verification_uri}`);
757
- console.log(` ${COLORS.cyan}And enter the code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
758
-
1005
+ console.log(` ${COLORS.cyan}Enter code:${COLORS.reset} ${COLORS.bold}${user_code}${COLORS.reset}\n`);
759
1006
  logInfo('Waiting for authorization (timeout in 15m)...');
760
- const startTime = Date.now();
761
- const timeoutMs = 15 * 60 * 1000;
762
- let currentPollInterval = (interval || 5) * 1000;
763
-
764
- while (Date.now() - startTime < timeoutMs) {
765
- await new Promise((r) => setTimeout(r, currentPollInterval));
766
- const tokenRes = await fetch('https://auth.openai.com/api/accounts/deviceauth/token', {
767
- method: 'POST',
768
- headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
769
- body: JSON.stringify({
770
- client_id: clientId,
771
- device_code,
772
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
773
- })
774
- });
775
-
776
- if (!tokenRes.ok) {
777
- const errorText = await tokenRes.text().catch(() => 'Unknown error');
778
- throw new Error(`OpenAI token request failed: HTTP ${tokenRes.status} - ${errorText}`);
779
- }
780
1007
 
781
- const data = await tokenRes.json();
782
- if (data.access_token) {
783
- upsertEnvValue('OPENAI_CODEX_ACCESS_TOKEN', data.access_token);
784
- logOk('Successfully authenticated and saved OpenAI Codex access token to .env');
785
- return;
786
- } else if (data.error === 'authorization_pending') {
787
- // Continue polling
788
- } else if (data.error === 'slow_down') {
789
- currentPollInterval += 5000;
790
- } else if (data.error) {
791
- throw new Error(`Authentication failed: ${data.error_description || data.error}`);
792
- }
1008
+ let authorizationCode = null;
1009
+ let codeVerifier = null;
1010
+
1011
+ await pollDeviceCode({
1012
+ pollUrl: 'https://auth.openai.com/api/accounts/deviceauth/token',
1013
+ pollBody: () => ({ device_auth_id, user_code }),
1014
+ intervalMs: (interval || 5) * 1000,
1015
+ timeoutMs: 15 * 60 * 1000,
1016
+ onToken: async (data) => {
1017
+ if (!data.authorization_code || !data.code_verifier) return false;
1018
+ authorizationCode = data.authorization_code;
1019
+ codeVerifier = data.code_verifier;
1020
+ return true;
1021
+ },
1022
+ });
1023
+
1024
+ logInfo('Exchanging authorization code for access token...');
1025
+ const exchangeRes = await fetch('https://auth.openai.com/oauth/token', {
1026
+ method: 'POST',
1027
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1028
+ body: new URLSearchParams({
1029
+ grant_type: 'authorization_code',
1030
+ code: authorizationCode,
1031
+ redirect_uri: 'https://auth.openai.com/deviceauth/callback',
1032
+ client_id: clientId,
1033
+ code_verifier: codeVerifier,
1034
+ }),
1035
+ });
1036
+
1037
+ if (!exchangeRes.ok) {
1038
+ const errorText = await exchangeRes.text().catch(() => 'Unknown error');
1039
+ throw new Error(`OpenAI token exchange failed: HTTP ${exchangeRes.status} — ${errorText}`);
1040
+ }
1041
+
1042
+ const exchangeData = await exchangeRes.json();
1043
+ if (!exchangeData.access_token) {
1044
+ throw new Error('OpenAI token exchange succeeded but did not return an access token.');
1045
+ }
1046
+ upsertEnvValue('OPENAI_CODEX_ACCESS_TOKEN', exchangeData.access_token);
1047
+ if (exchangeData.refresh_token) {
1048
+ upsertEnvValue('OPENAI_CODEX_REFRESH_TOKEN', exchangeData.refresh_token);
793
1049
  }
794
- throw new Error('OpenAI authentication timed out after 15 minutes.');
1050
+ logOk('Saved OpenAI Codex tokens to .env');
1051
+ logInfo('Restarting NeoAgent to apply credentials...');
1052
+ cmdRestart();
1053
+ } else if (provider === 'claude-code') {
1054
+ await cmdLoginClaudeCode();
795
1055
  }
796
1056
  }
797
1057
 
@@ -891,6 +1151,109 @@ function startFallback() {
891
1151
  logOk(`Started detached process (pid ${child.pid})`);
892
1152
  }
893
1153
 
1154
+ async function ensureQemuInstalled() {
1155
+ heading('Ensure QEMU Installed');
1156
+ const platform = detectPlatform();
1157
+
1158
+ const hasSystem = commandExists('qemu-system-x86_64') || commandExists('qemu-system-aarch64');
1159
+ const hasImg = commandExists('qemu-img');
1160
+
1161
+ if (hasSystem && hasImg) {
1162
+ logOk('QEMU components are already installed');
1163
+ return;
1164
+ }
1165
+
1166
+ logInfo('QEMU components not found. Attempting to install...');
1167
+
1168
+ if (platform === 'macos') {
1169
+ if (!commandExists('brew')) {
1170
+ throw new Error('Homebrew is required to install QEMU on macOS. Please install it first: https://brew.sh/');
1171
+ }
1172
+ logInfo('Running "brew install qemu"...');
1173
+ runOrThrow('brew', ['install', 'qemu']);
1174
+ } else if (platform === 'linux') {
1175
+ const isArm = process.arch === 'arm64' || process.arch === 'aarch64';
1176
+ if (commandExists('apt-get')) {
1177
+ const pkgs = ['qemu-utils'];
1178
+ if (isArm) {
1179
+ pkgs.push('qemu-system-arm');
1180
+ } else {
1181
+ pkgs.push('qemu-system-x86');
1182
+ }
1183
+ logInfo(`Running "sudo apt-get update && sudo apt-get install -y ${pkgs.join(' ')}"...`);
1184
+ runOrThrow('sudo', ['apt-get', 'update']);
1185
+ runOrThrow('sudo', ['apt-get', 'install', '-y', ...pkgs]);
1186
+ } else if (commandExists('dnf')) {
1187
+ logInfo('Running "sudo dnf install -y qemu-kvm qemu-img"...');
1188
+ runOrThrow('sudo', ['dnf', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1189
+ } else if (commandExists('yum')) {
1190
+ logInfo('Running "sudo yum install -y qemu-kvm qemu-img"...');
1191
+ runOrThrow('sudo', ['yum', 'install', '-y', 'qemu-kvm', 'qemu-img']);
1192
+ } else {
1193
+ throw new Error('Unsupported Linux distribution. Please install qemu-system and qemu-utils manually.');
1194
+ }
1195
+ } else {
1196
+ throw new Error('Unsupported platform for automatic QEMU installation. Please install QEMU manually.');
1197
+ }
1198
+
1199
+ const verifiedSystem = commandExists('qemu-system-x86_64') || commandExists('qemu-system-aarch64');
1200
+ const verifiedImg = commandExists('qemu-img');
1201
+
1202
+ if (verifiedSystem && verifiedImg) {
1203
+ logOk('QEMU installed successfully');
1204
+ } else {
1205
+ throw new Error('QEMU installation failed or components not found in PATH after install.');
1206
+ }
1207
+ }
1208
+
1209
+ function ensureYtDlpInstalled() {
1210
+ heading('Ensure yt-dlp Installed');
1211
+ if (commandExists('yt-dlp')) {
1212
+ const ver = runQuiet('yt-dlp', ['--version']);
1213
+ logOk(`yt-dlp ${ver.status === 0 ? ver.stdout.trim() : '(version unknown)'}`);
1214
+ return;
1215
+ }
1216
+
1217
+ logInfo('yt-dlp not found. Attempting to install...');
1218
+ const platform = detectPlatform();
1219
+
1220
+ if (platform === 'macos') {
1221
+ if (!commandExists('brew')) {
1222
+ logWarn('Homebrew not found — skipping yt-dlp install. Install manually: brew install yt-dlp');
1223
+ return;
1224
+ }
1225
+ try {
1226
+ runOrThrow('brew', ['install', 'yt-dlp']);
1227
+ logOk('yt-dlp installed via Homebrew');
1228
+ } catch {
1229
+ logWarn('yt-dlp install failed. Install manually: brew install yt-dlp');
1230
+ }
1231
+ return;
1232
+ }
1233
+
1234
+ if (platform === 'linux') {
1235
+ if (commandExists('pipx')) {
1236
+ try {
1237
+ runOrThrow('pipx', ['install', 'yt-dlp']);
1238
+ logOk('yt-dlp installed via pipx');
1239
+ return;
1240
+ } catch {
1241
+ // fall through to pip3
1242
+ }
1243
+ }
1244
+ if (commandExists('pip3')) {
1245
+ try {
1246
+ runOrThrow('pip3', ['install', '--user', 'yt-dlp']);
1247
+ logOk('yt-dlp installed via pip3');
1248
+ return;
1249
+ } catch {
1250
+ // fall through to warn
1251
+ }
1252
+ }
1253
+ logWarn('Could not install yt-dlp automatically. Install manually: pipx install yt-dlp');
1254
+ }
1255
+ }
1256
+
894
1257
  async function cmdInstall() {
895
1258
  heading(`Install ${APP_NAME}`);
896
1259
  if (!fs.existsSync(ENV_FILE)) {
@@ -899,6 +1262,8 @@ async function cmdInstall() {
899
1262
  }
900
1263
 
901
1264
  installDependencies();
1265
+ await ensureQemuInstalled();
1266
+ ensureYtDlpInstalled();
902
1267
  buildBundledWebClientIfPossible({ required: true });
903
1268
 
904
1269
  const platform = detectPlatform();
@@ -960,7 +1325,7 @@ function cmdStop() {
960
1325
  logOk(`Stopped pid ${pid}`);
961
1326
  stopped = true;
962
1327
  } catch {
963
- logWarn(`pid ${pid} not running`);
1328
+ logWarn(`pid ${pid} was not running (stale PID file)`);
964
1329
  }
965
1330
  }
966
1331
  fs.rmSync(pidPath, { force: true });
@@ -988,6 +1353,11 @@ function cmdRestart() {
988
1353
  cmdStart();
989
1354
  }
990
1355
 
1356
+ async function cmdRebuildWeb() {
1357
+ heading(`Rebuild Flutter Web Client`);
1358
+ buildBundledWebClientIfPossible();
1359
+ }
1360
+
991
1361
  function cmdUninstall() {
992
1362
  heading(`Uninstall ${APP_NAME}`);
993
1363
  const platform = detectPlatform();
@@ -1016,22 +1386,52 @@ async function cmdStatus() {
1016
1386
  const port = loadEnvPort();
1017
1387
  const running = await isPortOpen(port);
1018
1388
  const releaseChannel = currentReleaseChannel();
1389
+ const platform = detectPlatform();
1019
1390
 
1020
1391
  if (running) {
1021
- logOk(`running on http://localhost:${port}`);
1392
+ logOk(`server http://localhost:${port}`);
1022
1393
  } else {
1023
- logWarn(`not reachable on port ${port}`);
1394
+ logWarn(`server not reachable on port ${port}`);
1024
1395
  }
1025
1396
 
1026
- console.log(` install root ${APP_DIR}`);
1027
- console.log(` version ${currentInstalledVersionLabel()}`);
1028
- console.log(` release channel ${releaseChannelSummary(releaseChannel)}`);
1397
+ if (platform === 'macos' && fs.existsSync(PLIST_DST)) {
1398
+ const svcRes = runQuiet('launchctl', ['list', SERVICE_LABEL]);
1399
+ if (svcRes.status === 0 && svcRes.stdout.trim()) {
1400
+ logOk(`service launchd (${SERVICE_LABEL})`);
1401
+ } else {
1402
+ logWarn(`service launchd unit not loaded — run: neoagent install`);
1403
+ }
1404
+ } else if (platform === 'linux' && fs.existsSync(SYSTEMD_UNIT)) {
1405
+ const svcRes = runQuiet('systemctl', ['--user', 'is-active', 'neoagent']);
1406
+ if (svcRes.status === 0 && svcRes.stdout.trim() === 'active') {
1407
+ logOk('service systemd (neoagent)');
1408
+ } else {
1409
+ logWarn('service systemd unit not active — run: neoagent install');
1410
+ }
1411
+ }
1412
+
1413
+ if (fs.existsSync(ENV_FILE)) {
1414
+ logOk(`config ${ENV_FILE}`);
1415
+ } else {
1416
+ logWarn(`config .env not found — run: neoagent setup`);
1417
+ }
1418
+
1419
+ if (hasBundledWebClient(WEB_CLIENT_DIR)) {
1420
+ logOk('web bundled Flutter client present');
1421
+ } else {
1422
+ logWarn('web no bundled client — run: neoagent rebuild-web');
1423
+ }
1424
+
1425
+ console.log('');
1426
+ console.log(` install ${APP_DIR}`);
1427
+ console.log(` version ${currentInstalledVersionLabel()}`);
1428
+ console.log(` channel ${releaseChannelSummary(releaseChannel)}`);
1029
1429
 
1030
1430
  const processes = listNeoAgentServerProcesses();
1031
1431
  if (processes.length > 0) {
1032
- console.log(` neoagent pids ${processes.map((proc) => proc.pid).join(', ')}`);
1432
+ console.log(` pids ${processes.map((proc) => proc.pid).join(', ')}`);
1033
1433
  if (processes.length > 1) {
1034
- logWarn(`multiple NeoAgent server processes detected (${processes.length})`);
1434
+ logWarn(`multiple NeoAgent processes detected (${processes.length})`);
1035
1435
  }
1036
1436
  }
1037
1437
  }
@@ -1083,6 +1483,8 @@ function cmdUpdate(args = []) {
1083
1483
  }
1084
1484
  const versionBefore = currentInstalledVersionLabel();
1085
1485
  let versionAfter = versionBefore;
1486
+ const githubInstallRef = releaseChannel === 'beta' ? '#beta' : '';
1487
+ const githubInstallSpec = `git+https://github.com/NeoLabs-Systems/NeoAgent.git${githubInstallRef}`;
1086
1488
 
1087
1489
  if (fs.existsSync(path.join(APP_DIR, '.git')) && commandExists('git')) {
1088
1490
  const current = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
@@ -1091,6 +1493,7 @@ function cmdUpdate(args = []) {
1091
1493
  const targetBranch = resolvePreferredGitBranch(releaseChannel);
1092
1494
  logInfo(`Using git branch ${targetBranch} for the ${releaseChannel} channel.`);
1093
1495
  ensureGitBranchForReleaseChannel(targetBranch);
1496
+ backupRuntimeData();
1094
1497
  runOrThrow('git', ['pull', '--rebase', '--autostash', 'origin', targetBranch]);
1095
1498
 
1096
1499
  const next = runQuiet('git', ['rev-parse', '--short', 'HEAD']);
@@ -1103,17 +1506,16 @@ function cmdUpdate(args = []) {
1103
1506
  buildBundledWebClientIfPossible();
1104
1507
  }
1105
1508
  } else {
1106
- const npmTag = resolvePreferredNpmTag(releaseChannel);
1107
- logWarn(`No git repo detected; attempting npm global update from ${npmTag}.`);
1509
+ logWarn(`No git repo detected; attempting npm global update from ${githubInstallSpec}.`);
1108
1510
  if (commandExists('npm')) {
1109
1511
  try {
1110
1512
  backupRuntimeData();
1111
- runOrThrow('npm', ['install', '-g', `neoagent@${npmTag}`, '--force'], {
1513
+ runOrThrow('npm', ['install', '-g', githubInstallSpec, '--force'], {
1112
1514
  env: withInstallEnv()
1113
1515
  });
1114
- logOk('npm global update completed (forced reinstall)');
1516
+ logOk('npm global update completed (forced reinstall from GitHub)');
1115
1517
  } catch {
1116
- logWarn(`npm global update failed. Run: npm install -g neoagent@${npmTag} --force`);
1518
+ logWarn(`npm global update failed. Run: npm install -g ${githubInstallSpec} --force`);
1117
1519
  }
1118
1520
  } else {
1119
1521
  logWarn('npm not found. Cannot perform global update.');
@@ -1121,6 +1523,7 @@ function cmdUpdate(args = []) {
1121
1523
  }
1122
1524
 
1123
1525
  versionAfter = currentInstalledVersionLabel();
1526
+ ensureYtDlpInstalled();
1124
1527
 
1125
1528
  if (!hasBundledWebClient(WEB_CLIENT_DIR)) {
1126
1529
  throw new Error('No bundled Flutter web client found after update.');
@@ -1132,11 +1535,16 @@ function cmdUpdate(args = []) {
1132
1535
 
1133
1536
  async function cmdEnv(args = []) {
1134
1537
  heading('Environment Variables');
1135
- let action = args[0];
1538
+ const action = (args[0] || '').trim().toLowerCase();
1136
1539
 
1137
1540
  if (!action) {
1138
- const picked = await ask('Action (list/get/set/unset)', 'list');
1139
- action = (picked || 'list').trim().toLowerCase();
1541
+ console.log('Usage: neoagent env <subcommand>');
1542
+ console.log('');
1543
+ console.log(' neoagent env list List all variables (secrets masked)');
1544
+ console.log(' neoagent env get KEY Print a single variable');
1545
+ console.log(' neoagent env set KEY VALUE Set a variable');
1546
+ console.log(' neoagent env unset KEY Remove a variable');
1547
+ return;
1140
1548
  }
1141
1549
 
1142
1550
  if (action === 'list') {
@@ -1161,16 +1569,17 @@ async function cmdEnv(args = []) {
1161
1569
  }
1162
1570
 
1163
1571
  if (action === 'set') {
1164
- const key = args[1] || await ask('Key', 'PORT');
1165
- const value = args.slice(2).join(' ') || await ask('Value', key === 'PORT' ? '3333' : '');
1572
+ const key = args[1];
1573
+ const value = args.slice(2).join(' ');
1166
1574
  if (!key || !value) throw new Error('Usage: neoagent env set <KEY> <VALUE>');
1575
+ validateEnvKey(key);
1167
1576
  upsertEnvValue(key, value);
1168
1577
  logOk(`Set ${key} in ${ENV_FILE}`);
1169
1578
  return;
1170
1579
  }
1171
1580
 
1172
1581
  if (action === 'unset') {
1173
- const key = args[1] || await ask('Key', 'PORT');
1582
+ const key = args[1];
1174
1583
  if (!key) throw new Error('Usage: neoagent env unset <KEY>');
1175
1584
  removeEnvValue(key);
1176
1585
  logOk(`Removed ${key} from ${ENV_FILE}`);
@@ -1180,16 +1589,56 @@ async function cmdEnv(args = []) {
1180
1589
  throw new Error('Usage: neoagent env [list|get|set|unset] ...');
1181
1590
  }
1182
1591
 
1592
+ function cmdVersion() {
1593
+ console.log(currentInstalledVersionLabel());
1594
+ }
1595
+
1183
1596
  function printHelp() {
1184
- console.log(`${APP_NAME} manager`);
1185
- console.log('Usage: neoagent <command>');
1186
- console.log('Commands: install | setup | env | channel | update | restart | start | stop | status | logs | uninstall | migrate | login');
1187
- console.log('Login usage: neoagent login github-copilot | neoagent login openai-codex');
1188
- console.log('Channel usage: neoagent channel | neoagent channel stable | neoagent channel beta');
1189
- console.log('Update usage: neoagent update | neoagent update stable | neoagent update beta');
1190
- console.log('Env usage: neoagent env list | neoagent env get PORT | neoagent env set PORT 3333 | neoagent env unset PORT');
1191
- console.log('Migrate usage: neoagent migrate | neoagent migrate dry-run | neoagent migrate status');
1192
- console.log(' neoagent migrate openclaw-only | neoagent migrate hermes-only');
1597
+ const c = COLORS;
1598
+ const W = 38;
1599
+
1600
+ function row(cmd, desc) {
1601
+ const padded = ` neoagent ${cmd}`.padEnd(W);
1602
+ console.log(`${padded}${c.dim}${desc}${c.reset}`);
1603
+ }
1604
+
1605
+ console.log(`\n${c.bold}neoagent${c.reset} — manage your NeoAgent server\n`);
1606
+ console.log(`${c.bold}Usage${c.reset} neoagent <command> [args]\n`);
1607
+
1608
+ console.log(`${c.bold}Lifecycle${c.reset}`);
1609
+ row('install', 'First-time install and service setup');
1610
+ row('start', 'Start the server');
1611
+ row('stop', 'Stop the server');
1612
+ row('restart', 'Stop, then start');
1613
+ row('status', 'Health overview (server, service, config)');
1614
+ row('logs', 'Tail server logs');
1615
+ row('uninstall', 'Remove the system service');
1616
+ console.log('');
1617
+
1618
+ console.log(`${c.bold}Configuration${c.reset}`);
1619
+ row('setup', 'Interactive configuration wizard');
1620
+ row('env list', 'List all variables (secrets masked)');
1621
+ row('env get KEY', 'Print a single variable');
1622
+ row('env set KEY VALUE', 'Set a variable');
1623
+ row('env unset KEY', 'Remove a variable');
1624
+ row('channel', 'Show current release channel');
1625
+ row('channel stable|beta', 'Switch release channel');
1626
+ console.log('');
1627
+
1628
+ console.log(`${c.bold}Updates & Auth${c.reset}`);
1629
+ row('update', 'Update to latest on current channel');
1630
+ row('update stable|beta', 'Update and switch channel');
1631
+ row('login github-copilot','Authenticate GitHub Copilot');
1632
+ row('login openai-codex', 'Authenticate OpenAI Codex');
1633
+ row('login claude-code', 'Authenticate Claude Code');
1634
+ console.log('');
1635
+
1636
+ console.log(`${c.bold}Maintenance${c.reset}`);
1637
+ row('migrate', 'Migrate from another agent installation');
1638
+ row('migrate dry-run', 'Preview what would be migrated');
1639
+ row('rebuild-web', 'Rebuild the bundled Flutter web client');
1640
+ row('version', 'Print installed version');
1641
+ console.log('');
1193
1642
  }
1194
1643
 
1195
1644
  async function runCLI(argv) {
@@ -1216,6 +1665,9 @@ async function runCLI(argv) {
1216
1665
  case 'restart':
1217
1666
  cmdRestart();
1218
1667
  break;
1668
+ case 'rebuild-web':
1669
+ await cmdRebuildWeb();
1670
+ break;
1219
1671
  case 'start':
1220
1672
  cmdStart();
1221
1673
  break;
@@ -1237,13 +1689,18 @@ async function runCLI(argv) {
1237
1689
  case 'login':
1238
1690
  await cmdLogin(argv.slice(1));
1239
1691
  break;
1692
+ case 'version':
1693
+ case '--version':
1694
+ case '-V':
1695
+ cmdVersion();
1696
+ break;
1240
1697
  case 'help':
1241
1698
  case '--help':
1242
1699
  case '-h':
1243
1700
  printHelp();
1244
1701
  break;
1245
1702
  default:
1246
- throw new Error(`Unknown command: ${command}`);
1703
+ throw new Error(`Unknown command: ${command}. Run "neoagent --help" for usage.`);
1247
1704
  }
1248
1705
  }
1249
1706