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.
- package/.env.example +4 -0
- package/README.md +16 -7
- package/flutter_app/lib/features/location/location_service.dart +2 -4
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_app_shell.dart +17 -15
- package/flutter_app/lib/main_chat.dart +46 -42
- package/flutter_app/lib/main_controller.dart +6 -1
- package/flutter_app/lib/main_devices.dart +86 -742
- package/flutter_app/lib/main_integrations.dart +3 -3
- package/flutter_app/lib/main_settings.dart +50 -0
- package/flutter_app/lib/main_spacing.dart +18 -0
- package/flutter_app/lib/main_theme.dart +9 -0
- package/flutter_app/lib/main_unified.dart +3 -3
- package/lib/manager.js +33 -0
- package/package.json +1 -1
- package/server/db/database.js +74 -16
- package/server/guest_agent.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +50396 -50271
- package/server/services/ai/capabilityHealth.js +2 -3
- package/server/services/android/android_bootstrap_worker.js +18 -3
- package/server/services/android/controller.js +460 -2753
- package/server/services/runtime/backends/local-vm.js +33 -145
- package/server/services/runtime/docker-vm-manager.js +392 -0
- package/server/services/runtime/manager.js +53 -38
- package/server/services/runtime/settings.js +12 -10
- package/server/services/runtime/validation.js +4 -1
- package/server/utils/deployment.js +8 -2
- 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">
|
|
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
|
-
|
|
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/) | [
|
|
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
|
-
|
|
82
|
-
|
|
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:
|
|
363
|
+
color: _qrDarkColor,
|
|
364
364
|
),
|
|
365
365
|
dataModuleStyle: const QrDataModuleStyle(
|
|
366
366
|
dataModuleShape: QrDataModuleShape.square,
|
|
367
|
-
color:
|
|
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:
|
|
821
|
+
color: _qrDarkColor,
|
|
822
822
|
),
|
|
823
823
|
dataModuleStyle:
|
|
824
824
|
const QrDataModuleStyle(
|
|
825
825
|
dataModuleShape:
|
|
826
826
|
QrDataModuleShape.square,
|
|
827
|
-
color:
|
|
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:
|
|
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:
|
|
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 <
|
|
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 <
|
|
992
|
-
viewportConstraints.maxWidth <
|
|
993
|
-
viewportConstraints.maxWidth <
|
|
994
|
-
viewportConstraints.maxWidth <
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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:
|
|
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:
|
|
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']
|