neoagent 2.3.1-beta.89 → 2.3.1-beta.91

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 (31) hide show
  1. package/.env.example +4 -0
  2. package/README.md +16 -7
  3. package/flutter_app/lib/features/location/location_service.dart +2 -4
  4. package/flutter_app/lib/main.dart +1 -0
  5. package/flutter_app/lib/main_app_shell.dart +17 -15
  6. package/flutter_app/lib/main_chat.dart +46 -42
  7. package/flutter_app/lib/main_controller.dart +6 -1
  8. package/flutter_app/lib/main_devices.dart +86 -742
  9. package/flutter_app/lib/main_integrations.dart +3 -3
  10. package/flutter_app/lib/main_settings.dart +50 -0
  11. package/flutter_app/lib/main_spacing.dart +18 -0
  12. package/flutter_app/lib/main_theme.dart +9 -0
  13. package/flutter_app/lib/main_unified.dart +3 -3
  14. package/lib/manager.js +33 -0
  15. package/package.json +1 -1
  16. package/server/db/database.js +74 -16
  17. package/server/guest_agent.js +1 -0
  18. package/server/public/.last_build_id +1 -1
  19. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  20. package/server/public/flutter_bootstrap.js +1 -1
  21. package/server/public/main.dart.js +50396 -50271
  22. package/server/services/ai/capabilityHealth.js +2 -3
  23. package/server/services/android/android_bootstrap_worker.js +18 -3
  24. package/server/services/android/controller.js +460 -2753
  25. package/server/services/runtime/backends/local-vm.js +33 -145
  26. package/server/services/runtime/docker-vm-manager.js +392 -0
  27. package/server/services/runtime/manager.js +53 -38
  28. package/server/services/runtime/settings.js +12 -10
  29. package/server/services/runtime/validation.js +4 -1
  30. package/server/utils/deployment.js +8 -2
  31. package/server/services/runtime/qemu.js +0 -1118
package/.env.example CHANGED
@@ -26,6 +26,10 @@ NEOAGENT_DEPLOYMENT_MODE=self_hosted
26
26
  # prod -> multi-user / isolated per-user VM runtime
27
27
  NEOAGENT_PROFILE=prod
28
28
 
29
+ # Allow new user sign-ups. Set to false to disable registration (the first user
30
+ # can always register regardless of this setting).
31
+ # NEOAGENT_ALLOW_SIGNUP=false
32
+
29
33
  # VM runtime settings used by `prod`.
30
34
  # Set these before switching NEOAGENT_PROFILE=prod.
31
35
  # The app can cache a shared base image automatically from a URL, then create
package/README.md CHANGED
@@ -9,28 +9,37 @@
9
9
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-a855f7?style=flat-square" alt="License"></a>
10
10
  </p>
11
11
 
12
- <p align="center">A self-hosted, proactive AI agent with a Flutter client for web and Android.</p>
12
+ <p align="center">Self-hosted AI agent runs as a system service, controls Android over ADB, connects to 15+ messaging platforms, all credentials on your server.</p>
13
+
14
+ <p align="center">
15
+ <img src="demo.gif" alt="NeoAgent demo" width="100%">
16
+ </p>
13
17
 
14
18
  | | | | |
15
19
  | --- | --- | --- | --- |
16
20
  | <img alt="WebUI" src="https://github.com/user-attachments/assets/3c76d59a-b6e3-4698-929b-9c94741ccf1e" height="420"> | <img height="494" alt="Android" src="https://github.com/user-attachments/assets/e8a0af7a-6881-485d-ad52-f3bc6f2023ca"> | <img alt="Mobile Telegram" src="https://github.com/user-attachments/assets/1fd41a9b-5452-4aa4-9478-888c8ad7363a" height="420"> | <img height="494" alt="image" src="https://github.com/user-attachments/assets/d5a57282-0851-4902-9588-d8de4b82d45c"> |
17
21
 
18
-
22
+ - **Android control** — screenshot, observe UI, tap, swipe, type, launch apps, install APKs, `adb shell` — the agent operates Android, not just an app running on it
23
+ - **15+ messaging platforms** — Telegram, WhatsApp, Discord, Signal, Slack, Matrix, iMessage, IRC, LINE, Mattermost, Telnyx Voice
24
+ - **Integrations** — Google Workspace, Microsoft 365, Notion, Home Assistant, Trello, Spotify, Figma
25
+ - **Browser + shell** — VM-isolated server-side browser automation, full PTY terminal
26
+ - **Runs locally** — Ollama support, no API key required; credentials stay in `~/.neoagent/.env`, never in the client
19
27
 
20
28
  ## Install
21
29
 
30
+ Requires Node.js 20+ and QEMU — see [getting started](docs/getting-started.md) for details.
31
+
22
32
  ```bash
23
33
  npm install -g neoagent
24
34
  neoagent install
25
-
26
- neoagent migrate
27
35
  ```
28
36
 
29
- ## Manage the Service
37
+ Runs as a `launchd` user service on macOS and `systemd --user` on Linux.
38
+
39
+ ## Manage
30
40
 
31
41
  ```bash
32
42
  neoagent status
33
- neoagent channel beta
34
43
  neoagent update
35
44
  neoagent fix
36
45
  neoagent logs
@@ -38,7 +47,7 @@ neoagent logs
38
47
 
39
48
  ## Links
40
49
 
41
- [Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Homepage](https://neolabs-systems.github.io/NeoAgent/) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
50
+ [Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Capabilities](docs/capabilities.md) | [Why NeoAgent](docs/why-neoagent.md) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
42
51
 
43
52
  ---
44
53
 
@@ -2,8 +2,6 @@ import 'dart:convert';
2
2
  import 'package:flutter/material.dart';
3
3
  import 'package:geolocator/geolocator.dart';
4
4
  import 'package:http/http.dart' as http;
5
- import 'package:shared_preferences/shared_preferences.dart';
6
-
7
5
  class LocationService {
8
6
  static final LocationService _instance = LocationService._internal();
9
7
  factory LocationService() => _instance;
@@ -78,8 +76,8 @@ class LocationService {
78
76
  void _trackLoop(String backendUrl, String token) async {
79
77
  while (_isTracking) {
80
78
  try {
81
- Position position = await Geolocator.getCurrentPosition(
82
- desiredAccuracy: LocationAccuracy.low); // low accuracy = approx position
79
+ await Geolocator.getCurrentPosition(
80
+ locationSettings: const LocationSettings(accuracy: LocationAccuracy.low));
83
81
 
84
82
  // Here we would normally fetch active geofences from local DB or backend
85
83
  // and calculate the distance. If inside a geofence, we trigger:
@@ -47,6 +47,7 @@ import 'features/location/location_service.dart';
47
47
  import 'features/notifications/notification_interceptor.dart';
48
48
  import 'features/onboarding/onboarding_shell.dart';
49
49
 
50
+ part 'main_spacing.dart';
50
51
  part 'main_theme.dart';
51
52
  part 'main_app_shell.dart';
52
53
  part 'main_launcher.dart';
@@ -360,11 +360,11 @@ class _AuthViewState extends State<AuthView> {
360
360
  version: QrVersions.auto,
361
361
  eyeStyle: const QrEyeStyle(
362
362
  eyeShape: QrEyeShape.square,
363
- color: Color(0xFF04111D),
363
+ color: _qrDarkColor,
364
364
  ),
365
365
  dataModuleStyle: const QrDataModuleStyle(
366
366
  dataModuleShape: QrDataModuleShape.square,
367
- color: Color(0xFF04111D),
367
+ color: _qrDarkColor,
368
368
  ),
369
369
  )
370
370
  : controller.isPreparingQrLogin
@@ -818,13 +818,13 @@ class _AuthViewState extends State<AuthView> {
818
818
  version: QrVersions.auto,
819
819
  eyeStyle: const QrEyeStyle(
820
820
  eyeShape: QrEyeShape.square,
821
- color: Color(0xFF04111D),
821
+ color: _qrDarkColor,
822
822
  ),
823
823
  dataModuleStyle:
824
824
  const QrDataModuleStyle(
825
825
  dataModuleShape:
826
826
  QrDataModuleShape.square,
827
- color: Color(0xFF04111D),
827
+ color: _qrDarkColor,
828
828
  ),
829
829
  )
830
830
  : controller.isPreparingQrLogin
@@ -852,14 +852,14 @@ class _AuthViewState extends State<AuthView> {
852
852
  style: FilledButton.styleFrom(
853
853
  minimumSize: const Size.fromHeight(56),
854
854
  backgroundColor: Colors.white,
855
- foregroundColor: const Color(0xFF04111D),
855
+ foregroundColor: _qrDarkColor,
856
856
  ),
857
857
  icon: controller.isPreparingQrLogin
858
858
  ? const SizedBox.square(
859
859
  dimension: 16,
860
860
  child: CircularProgressIndicator(
861
861
  strokeWidth: 2,
862
- color: Color(0xFF04111D),
862
+ color: _qrDarkColor,
863
863
  ),
864
864
  )
865
865
  : const Icon(Icons.qr_code_2_rounded),
@@ -972,7 +972,7 @@ class _AuthViewState extends State<AuthView> {
972
972
  ),
973
973
  child: Padding(
974
974
  padding: EdgeInsets.all(
975
- viewportConstraints.maxWidth < 480 ? 14 : 24,
975
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 14 : 24,
976
976
  ),
977
977
  child: Center(
978
978
  child: ConstrainedBox(
@@ -988,10 +988,10 @@ class _AuthViewState extends State<AuthView> {
988
988
  fillColor: _glassFill,
989
989
  child: Padding(
990
990
  padding: EdgeInsets.fromLTRB(
991
- viewportConstraints.maxWidth < 480 ? 18 : 34,
992
- viewportConstraints.maxWidth < 480 ? 20 : 30,
993
- viewportConstraints.maxWidth < 480 ? 18 : 34,
994
- viewportConstraints.maxWidth < 480 ? 20 : 30,
991
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 18 : 34,
992
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 20 : 30,
993
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 18 : 34,
994
+ viewportConstraints.maxWidth < AppBreakpoints.mobile ? 20 : 30,
995
995
  ),
996
996
  child: LayoutBuilder(
997
997
  builder: (context, panelConstraints) {
@@ -1064,6 +1064,7 @@ class _HomeViewState extends State<HomeView> {
1064
1064
  bool _blockedDialogOpen = false;
1065
1065
  SidebarGroup? _expandedSidebarGroup;
1066
1066
  AppSection? _lastSelectedSection;
1067
+ final GlobalKey _devicesPanelKey = GlobalKey();
1067
1068
 
1068
1069
  @override
1069
1070
  void initState() {
@@ -1221,7 +1222,7 @@ class _HomeViewState extends State<HomeView> {
1221
1222
  key: ValueKey<AppSection>(
1222
1223
  controller.selectedSection,
1223
1224
  ),
1224
- child: _SectionBody(controller: controller),
1225
+ child: _SectionBody(controller: controller, devicesPanelKey: _devicesPanelKey),
1225
1226
  ),
1226
1227
  ),
1227
1228
  ),
@@ -1271,7 +1272,7 @@ class _HomeViewState extends State<HomeView> {
1271
1272
  switchOutCurve: Curves.easeInCubic,
1272
1273
  child: KeyedSubtree(
1273
1274
  key: ValueKey<AppSection>(controller.selectedSection),
1274
- child: _SectionBody(controller: controller),
1275
+ child: _SectionBody(controller: controller, devicesPanelKey: _devicesPanelKey),
1275
1276
  ),
1276
1277
  ),
1277
1278
  ),
@@ -2096,9 +2097,10 @@ class _MobileDrawer extends StatelessWidget {
2096
2097
  }
2097
2098
 
2098
2099
  class _SectionBody extends StatelessWidget {
2099
- const _SectionBody({required this.controller});
2100
+ const _SectionBody({required this.controller, this.devicesPanelKey});
2100
2101
 
2101
2102
  final NeoAgentController controller;
2103
+ final GlobalKey? devicesPanelKey;
2102
2104
 
2103
2105
  @override
2104
2106
  Widget build(BuildContext context) {
@@ -2108,7 +2110,7 @@ class _SectionBody extends StatelessWidget {
2108
2110
  case AppSection.voiceAssistant:
2109
2111
  return VoiceAssistantPanel(controller: controller);
2110
2112
  case AppSection.devices:
2111
- return DevicesPanel(controller: controller);
2113
+ return DevicesPanel(key: devicesPanelKey, controller: controller);
2112
2114
  case AppSection.recordings:
2113
2115
  return RecordingsPanel(controller: controller);
2114
2116
  case AppSection.messaging:
@@ -339,49 +339,52 @@ class _ChatPanelState extends State<ChatPanel> {
339
339
  child: Icon(Icons.call_rounded, color: Colors.white),
340
340
  ),
341
341
  const SizedBox(width: 8),
342
- FilledButton(
343
- onPressed: _isSendingChatMessage
344
- ? null
345
- : () async {
346
- final task = _composerController.text;
347
- if ((task.trim().isEmpty &&
348
- _pendingSharedAttachments.isEmpty) ||
349
- _isSendingChatMessage) {
350
- return;
351
- }
352
- setState(() {
353
- _isSendingChatMessage = true;
354
- });
355
- _composerController.clear();
356
- final outgoingAttachments =
357
- _pendingSharedAttachments;
358
- _clearSharedPayload();
359
- try {
360
- await controller.sendMessage(
361
- task,
362
- sharedAttachments: outgoingAttachments,
363
- );
364
- } finally {
365
- if (mounted) {
366
- setState(() {
367
- _isSendingChatMessage = false;
368
- });
342
+ Tooltip(
343
+ message: 'Send (⌘↩)',
344
+ child: FilledButton(
345
+ onPressed: _isSendingChatMessage
346
+ ? null
347
+ : () async {
348
+ final task = _composerController.text;
349
+ if ((task.trim().isEmpty &&
350
+ _pendingSharedAttachments.isEmpty) ||
351
+ _isSendingChatMessage) {
352
+ return;
369
353
  }
370
- }
371
- },
372
- style: FilledButton.styleFrom(
373
- minimumSize: const Size(46, 42),
374
- padding: const EdgeInsets.symmetric(horizontal: 12),
375
- backgroundColor: _accent,
376
- shape: RoundedRectangleBorder(
377
- borderRadius: BorderRadius.circular(10),
354
+ setState(() {
355
+ _isSendingChatMessage = true;
356
+ });
357
+ _composerController.clear();
358
+ final outgoingAttachments =
359
+ _pendingSharedAttachments;
360
+ _clearSharedPayload();
361
+ try {
362
+ await controller.sendMessage(
363
+ task,
364
+ sharedAttachments: outgoingAttachments,
365
+ );
366
+ } finally {
367
+ if (mounted) {
368
+ setState(() {
369
+ _isSendingChatMessage = false;
370
+ });
371
+ }
372
+ }
373
+ },
374
+ style: FilledButton.styleFrom(
375
+ minimumSize: const Size(46, 42),
376
+ padding: const EdgeInsets.symmetric(horizontal: 12),
377
+ backgroundColor: _accent,
378
+ shape: RoundedRectangleBorder(
379
+ borderRadius: BorderRadius.circular(10),
380
+ ),
381
+ ),
382
+ child: Icon(
383
+ controller.hasLiveRun
384
+ ? Icons.alt_route_rounded
385
+ : Icons.north_east_rounded,
386
+ color: Colors.white,
378
387
  ),
379
- ),
380
- child: Icon(
381
- controller.hasLiveRun
382
- ? Icons.alt_route_rounded
383
- : Icons.north_east_rounded,
384
- color: Colors.white,
385
388
  ),
386
389
  ),
387
390
  ],
@@ -1009,7 +1012,7 @@ class _MessagingMetricCard extends StatelessWidget {
1009
1012
  value,
1010
1013
  style: TextStyle(
1011
1014
  color: _textPrimary,
1012
- fontSize: 26,
1015
+ fontSize: 22,
1013
1016
  fontWeight: FontWeight.w800,
1014
1017
  ),
1015
1018
  ),
@@ -2244,6 +2247,7 @@ Future<_MessagingRuleSelection?> _showMessagingAccessRulePicker(
2244
2247
  required MessagingPlatformDescriptor platform,
2245
2248
  required MessagingAccessCatalog catalog,
2246
2249
  }) async {
2250
+ // Use BottomSheet for contextual actions, AlertDialog for confirmations.
2247
2251
  return showModalBottomSheet<_MessagingRuleSelection>(
2248
2252
  context: context,
2249
2253
  isScrollControlled: true,
@@ -2980,7 +2980,7 @@ class NeoAgentController extends ChangeNotifier {
2980
2980
  await _runDeviceAction(
2981
2981
  () => _backendClient.startAndroidEmulator(backendUrl),
2982
2982
  browser: false,
2983
- refreshAppsAfter: true,
2983
+ refreshAppsAfter: false,
2984
2984
  );
2985
2985
  }
2986
2986
 
@@ -4560,6 +4560,7 @@ class NeoAgentController extends ChangeNotifier {
4560
4560
 
4561
4561
  Future<void> saveSettings({
4562
4562
  required String browserBackend,
4563
+ required String cliBackend,
4563
4564
  required bool smarterSelector,
4564
4565
  required List<String> enabledModels,
4565
4566
  required String defaultChatModel,
@@ -4588,6 +4589,7 @@ class NeoAgentController extends ChangeNotifier {
4588
4589
  final payload = <String, dynamic>{
4589
4590
  'headless_browser': true,
4590
4591
  'browser_backend': browserBackend,
4592
+ 'cli_backend': cliBackend,
4591
4593
  'smarter_model_selector': smarterSelector,
4592
4594
  'enabled_models': enabledModels,
4593
4595
  'default_chat_model': defaultChatModel,
@@ -6122,6 +6124,9 @@ class NeoAgentController extends ChangeNotifier {
6122
6124
  String get browserBackend =>
6123
6125
  settings['browser_backend']?.toString().trim().toLowerCase() ?? 'vm';
6124
6126
 
6127
+ String get cliBackend =>
6128
+ settings['cli_backend']?.toString().trim().toLowerCase() ?? 'vm';
6129
+
6125
6130
  String get cloudBrowserBackend {
6126
6131
  final browser = browserBackend;
6127
6132
  final profile = settings['runtime_profile']