neoagent 2.4.2-beta.4 → 2.4.2-beta.6
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 +0 -9
- package/README.md +48 -18
- package/docs/configuration.md +0 -10
- package/flutter_app/lib/main.dart +0 -1
- package/flutter_app/lib/main_controller.dart +0 -160
- package/flutter_app/lib/main_models.dart +28 -0
- package/flutter_app/lib/main_operations.dart +17 -1
- package/flutter_app/lib/main_runtime.dart +0 -22
- package/flutter_app/lib/main_settings.dart +9 -0
- package/flutter_app/lib/main_shared.dart +0 -161
- package/flutter_app/lib/src/backend_client.dart +8 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/pubspec.lock +0 -8
- package/flutter_app/pubspec.yaml +0 -1
- package/package.json +1 -1
- package/server/admin/admin.css +23 -0
- package/server/admin/admin.js +17 -19
- package/server/admin/analytics.js +254 -48
- package/server/admin/index.html +9 -2
- package/server/db/database.js +112 -0
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +0 -183
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +77723 -78469
- package/server/routes/admin.js +73 -24
- package/server/routes/mcp.js +9 -0
- package/server/routes/memory.js +35 -2
- package/server/routes/runtime.js +5 -2
- package/server/routes/settings.js +34 -1
- package/server/routes/skills.js +42 -0
- package/server/routes/task_webhooks.js +65 -0
- package/server/services/ai/engine.js +525 -24
- package/server/services/ai/learning.js +56 -8
- package/server/services/ai/models.js +4 -4
- package/server/services/ai/providers/anthropic.js +40 -8
- package/server/services/ai/providers/google.js +10 -0
- package/server/services/ai/providers/openai.js +3 -15
- package/server/services/ai/providers/openaiCompatible.js +11 -0
- package/server/services/ai/repetitionGuard.js +60 -0
- package/server/services/ai/systemPrompt.js +52 -16
- package/server/services/ai/taskAnalysis.js +15 -6
- package/server/services/ai/toolEvidence.js +3 -1
- package/server/services/ai/toolRunner.js +43 -1
- package/server/services/ai/toolSelector.js +92 -46
- package/server/services/ai/tools.js +89 -9
- package/server/services/ai/usage.js +114 -0
- package/server/services/manager.js +34 -0
- package/server/services/memory/manager.js +170 -4
- package/server/services/security/capability_audit.js +107 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/webhook.js +14 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/tasks/webhooks.js +146 -0
- package/server/services/workspace/code_navigation.js +194 -0
- package/server/services/workspace/structured_data.js +93 -0
- package/server/utils/cloud-security.js +24 -2
- package/flutter_app/lib/src/analytics_service.dart +0 -294
- package/server/config/analytics.js +0 -30
package/.env.example
CHANGED
|
@@ -67,15 +67,6 @@ NEOAGENT_RELEASE_CHANNEL=stable
|
|
|
67
67
|
# Example: http://localhost:3333,https://yourdomain.com
|
|
68
68
|
ALLOWED_ORIGINS=
|
|
69
69
|
|
|
70
|
-
########################################
|
|
71
|
-
# Analytics
|
|
72
|
-
########################################
|
|
73
|
-
|
|
74
|
-
# Mixpanel project token used by the Flutter app.
|
|
75
|
-
# Leave this blank to disable analytics, or set your own public project token.
|
|
76
|
-
# Keep the token below if you want to support the development of NeoAgent with anonymous usage analytics.
|
|
77
|
-
NEOAGENT_MIXPANEL_TOKEN=4a47ae6a05cf39a8faf0438a1200dde6
|
|
78
|
-
|
|
79
70
|
########################################
|
|
80
71
|
# Service email
|
|
81
72
|
########################################
|
package/README.md
CHANGED
|
@@ -6,53 +6,83 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center"><strong>Your agent. Your server. Your rules.</strong></p>
|
|
8
8
|
|
|
9
|
+
<p align="center">A self-hosted AI agent that runs as a service, operates Android over ADB, and connects to 15+ messaging platforms while keeping credentials on your server.</p>
|
|
10
|
+
|
|
9
11
|
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/neoagent"><img src="https://img.shields.io/npm/v/neoagent?style=flat-square&label=npm" alt="npm version"></a>
|
|
10
13
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-20+-5fa04e?style=flat-square&logo=node.js&logoColor=white" alt="Node.js"></a>
|
|
11
14
|
<a href="https://sqlite.org"><img src="https://img.shields.io/badge/SQLite-WAL-003b57?style=flat-square&logo=sqlite&logoColor=white" alt="SQLite"></a>
|
|
12
15
|
<a href="https://flutter.dev"><img src="https://img.shields.io/badge/Flutter-web%20%2B%20android-02569B?style=flat-square&logo=flutter&logoColor=white" alt="Flutter"></a>
|
|
13
16
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-AGPL--3.0-a855f7?style=flat-square" alt="License"></a>
|
|
14
17
|
</p>
|
|
15
18
|
|
|
16
|
-
<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>
|
|
17
|
-
|
|
18
19
|
<p align="center">
|
|
19
20
|
<img src="demo.gif" alt="NeoAgent demo" width="100%">
|
|
20
21
|
</p>
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
| --- | --- | --- | --- |
|
|
24
|
-
| <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"> |
|
|
23
|
+
## Why NeoAgent
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- **Browser + shell** — VM-isolated server-side browser automation, full PTY terminal
|
|
30
|
-
- **Runs locally** — Ollama support, no API key required; credentials stay in `~/.neoagent/.env`, never in the client
|
|
25
|
+
| Android that the agent can operate | Credentials that stay server-side |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| NeoAgent can inspect the UI, take screenshots, tap, swipe, type, launch apps, install APKs, and run `adb shell` against a connected device or emulator. | API keys, OAuth tokens, messaging credentials, history, and runtime data are stored under `~/.neoagent` on the server, never in the client. |
|
|
31
28
|
|
|
32
|
-
##
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Requires Node.js 20 or newer.
|
|
33
32
|
|
|
34
33
|
```bash
|
|
35
34
|
npm install -g neoagent
|
|
36
35
|
neoagent install
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
`neoagent install`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
`neoagent install` checks the host, creates secure runtime configuration,
|
|
39
|
+
installs dependencies and supported system tools, and starts the service. Open
|
|
40
|
+
**http://localhost:3333** when it finishes.
|
|
41
|
+
|
|
42
|
+
No hosted-model key is required when using local [Ollama](https://ollama.com/).
|
|
43
|
+
See [Getting Started](docs/getting-started.md) for prerequisites and first-run
|
|
44
|
+
setup.
|
|
45
|
+
|
|
46
|
+
## What It Does
|
|
47
|
+
|
|
48
|
+
- **15+ messaging platforms**: Telegram, WhatsApp, Discord, Signal, Slack, Matrix, iMessage, Teams, IRC, LINE, Mattermost, Telnyx Voice, and webhook bridges.
|
|
49
|
+
- **Automation**: cron schedules, integration and weather triggers, reusable skills, MCP tools, and subagents.
|
|
50
|
+
- **Browser and shell**: an isolated browser runtime plus a full PTY terminal on the NeoAgent server.
|
|
51
|
+
- **Integrations**: Google Workspace, Microsoft 365, Notion, Home Assistant, Trello, Spotify, Slack, Figma, GitHub, and more.
|
|
52
|
+
- **Recordings and memory**: audio capture, transcription, transcript search, long-term memory, session history, and health summaries.
|
|
53
|
+
- **Model choice**: Anthropic, OpenAI, Gemini, Grok, MiniMax, NVIDIA NIM, OpenRouter, GitHub Copilot, OpenAI Codex, or local Ollama.
|
|
43
54
|
|
|
44
|
-
##
|
|
55
|
+
## Interfaces
|
|
56
|
+
|
|
57
|
+
| | | | |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| <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"> |
|
|
60
|
+
|
|
61
|
+
## Service Commands
|
|
45
62
|
|
|
46
63
|
```bash
|
|
47
64
|
neoagent status
|
|
65
|
+
neoagent start
|
|
66
|
+
neoagent stop
|
|
67
|
+
neoagent restart
|
|
48
68
|
neoagent update
|
|
49
69
|
neoagent fix
|
|
50
70
|
neoagent logs
|
|
51
71
|
```
|
|
52
72
|
|
|
53
|
-
##
|
|
73
|
+
## Project Status
|
|
74
|
+
|
|
75
|
+
NeoAgent is beta software maintained by one person. Expect rough edges, and
|
|
76
|
+
please report failures with enough detail to reproduce them. Contributions to
|
|
77
|
+
the backend, Flutter clients, integrations, skills, tests, and documentation
|
|
78
|
+
are welcome.
|
|
79
|
+
|
|
80
|
+
Read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. Security
|
|
81
|
+
issues should be reported privately according to [SECURITY.md](SECURITY.md).
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
54
84
|
|
|
55
|
-
[Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Getting Started](docs/getting-started.md) | [Configuration](docs/configuration.md) | [Capabilities](docs/capabilities.md) | [
|
|
85
|
+
[Docs](https://neolabs-systems.github.io/NeoAgent/docs/) | [Getting Started](docs/getting-started.md) | [Configuration](docs/configuration.md) | [Capabilities](docs/capabilities.md) | [Skills and MCP](docs/skills.md) | [Operations](docs/operations.md) | [Discussions](https://github.com/NeoLabs-Systems/NeoAgent/discussions) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
|
|
56
86
|
|
|
57
87
|
---
|
|
58
88
|
|
package/docs/configuration.md
CHANGED
|
@@ -127,16 +127,6 @@ Optional. When configured, NeoAgent uses SMTP for account flows: signup confirma
|
|
|
127
127
|
|
|
128
128
|
Runtime profiles (`trusted-host`, `secure-vm`) are set in user settings, not `.env`. See [Capabilities: Runtime Modes](capabilities.md#runtime-modes).
|
|
129
129
|
|
|
130
|
-
## Analytics
|
|
131
|
-
|
|
132
|
-
NeoAgent can send anonymous usage events to your own Mixpanel project. Events cover navigation and feature usage only — no message content, credentials, or personal data.
|
|
133
|
-
|
|
134
|
-
| Variable | Description |
|
|
135
|
-
|---|---|
|
|
136
|
-
| `NEOAGENT_MIXPANEL_TOKEN` | Public Mixpanel project token — leave blank to disable analytics |
|
|
137
|
-
|
|
138
|
-
When analytics is enabled, the web client shows a cookie-consent banner until accepted or declined.
|
|
139
|
-
|
|
140
130
|
## Runtime Paths
|
|
141
131
|
|
|
142
132
|
| Path | Contents |
|
|
@@ -28,7 +28,6 @@ import 'src/android_apk_drop_zone.dart';
|
|
|
28
28
|
import 'src/android_launcher_bridge.dart';
|
|
29
29
|
import 'src/app_launch_bridge.dart';
|
|
30
30
|
import 'src/app_release_updater.dart' as app_release_updater;
|
|
31
|
-
import 'src/analytics_service.dart';
|
|
32
31
|
import 'src/backend_client.dart';
|
|
33
32
|
import 'src/desktop_companion.dart';
|
|
34
33
|
import 'src/desktop_screen_capture.dart';
|
|
@@ -37,7 +37,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
37
37
|
final OAuthLauncher _oauthLauncher;
|
|
38
38
|
final app_release_updater.AppReleaseUpdater _appReleaseUpdater =
|
|
39
39
|
app_release_updater.AppReleaseUpdater();
|
|
40
|
-
final AppAnalytics _analytics = AppAnalytics();
|
|
41
40
|
final LiveVoiceCapture _liveVoiceCapture = LiveVoiceCapture();
|
|
42
41
|
final DesktopScreenCapture _desktopScreenCapture =
|
|
43
42
|
createDesktopScreenCapture();
|
|
@@ -104,10 +103,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
104
103
|
bool networkStatusKnown = false;
|
|
105
104
|
bool _desktopFloatingToolbarPopupRequested = false;
|
|
106
105
|
bool _voiceAssistantIncludeScreenContext = false;
|
|
107
|
-
bool _didTrackAppOpen = false;
|
|
108
|
-
bool _analyticsConfigured = false;
|
|
109
|
-
bool _analyticsConsentResolved = false;
|
|
110
|
-
bool _analyticsConsentGranted = false;
|
|
111
106
|
|
|
112
107
|
io.Socket? get streamSocket => socketConnected ? _socket : null;
|
|
113
108
|
|
|
@@ -343,7 +338,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
343
338
|
_diagnosticLogSubscription?.cancel();
|
|
344
339
|
_connectivitySubscription?.cancel();
|
|
345
340
|
_appReleaseUpdater.dispose();
|
|
346
|
-
unawaited(_analytics.dispose());
|
|
347
341
|
_desktopCompanion.removeListener(notifyListeners);
|
|
348
342
|
unawaited(_desktopCompanion.disconnect());
|
|
349
343
|
_recordingBridge.removeListener(_handleRecordingBridgeChanged);
|
|
@@ -374,12 +368,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
374
368
|
bool get canCaptureVoiceAssistantScreenContext =>
|
|
375
369
|
_desktopScreenCapture.isSupported;
|
|
376
370
|
|
|
377
|
-
bool get showAnalyticsConsentBanner =>
|
|
378
|
-
kIsWeb && _analyticsConfigured && !_analyticsConsentResolved;
|
|
379
|
-
|
|
380
|
-
bool get analyticsConsentGranted =>
|
|
381
|
-
_analyticsConsentResolved && _analyticsConsentGranted;
|
|
382
|
-
|
|
383
371
|
bool get isLiveVoiceCaptureEngaged =>
|
|
384
372
|
_isStartingLiveVoice || _liveVoiceCaptureActive;
|
|
385
373
|
|
|
@@ -735,7 +723,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
735
723
|
appUpdateAutoCheckEnabled =
|
|
736
724
|
_prefs?.getBool('app.update.autoCheckEnabled') ?? true;
|
|
737
725
|
installedAppVersion = await _safeLoadInstalledAppVersion();
|
|
738
|
-
await _initializeAnalytics();
|
|
739
726
|
await refreshConnectivityStatus();
|
|
740
727
|
if (_connectivityPluginAvailable && _connectivitySubscription == null) {
|
|
741
728
|
try {
|
|
@@ -898,105 +885,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
898
885
|
}
|
|
899
886
|
}
|
|
900
887
|
|
|
901
|
-
Future<void> _initializeAnalytics() async {
|
|
902
|
-
try {
|
|
903
|
-
final runtimeConfig = await _backendClient.fetchRuntimeConfig(backendUrl);
|
|
904
|
-
final analyticsConfig = runtimeConfig['analytics'];
|
|
905
|
-
final mixpanelConfig = analyticsConfig is Map
|
|
906
|
-
? analyticsConfig['mixpanel']
|
|
907
|
-
: null;
|
|
908
|
-
final token = mixpanelConfig is Map
|
|
909
|
-
? mixpanelConfig['token']?.toString()
|
|
910
|
-
: null;
|
|
911
|
-
_analyticsConfigured = token != null && token.trim().isNotEmpty;
|
|
912
|
-
final consentState = _prefs?.getBool('analytics.cookieConsent');
|
|
913
|
-
// On web, require explicit opt-in (GDPR). On native, consent is implicit
|
|
914
|
-
// unless the user has previously declined.
|
|
915
|
-
_analyticsConsentResolved = consentState != null || !kIsWeb;
|
|
916
|
-
_analyticsConsentGranted = kIsWeb
|
|
917
|
-
? consentState == true
|
|
918
|
-
: consentState != false;
|
|
919
|
-
await _analytics.initialize(
|
|
920
|
-
token: token,
|
|
921
|
-
consentGranted: _analyticsConsentGranted,
|
|
922
|
-
);
|
|
923
|
-
_trackAppOpenedIfNeeded();
|
|
924
|
-
} catch (_) {
|
|
925
|
-
_analyticsConfigured = false;
|
|
926
|
-
_analyticsConsentResolved = false;
|
|
927
|
-
_analyticsConsentGranted = false;
|
|
928
|
-
await _analytics.initialize(token: null, consentGranted: false);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
void _trackAppOpenedIfNeeded({String? consentSource}) {
|
|
933
|
-
if (!_analytics.enabled || _didTrackAppOpen) {
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
_didTrackAppOpen = true;
|
|
937
|
-
unawaited(
|
|
938
|
-
_analytics.trackAppOpened(
|
|
939
|
-
appMode: appMode.name,
|
|
940
|
-
platform: _analyticsPlatformLabel(),
|
|
941
|
-
backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
|
|
942
|
-
selectedSection: selectedSection.label,
|
|
943
|
-
deploymentProfile: deploymentProfile,
|
|
944
|
-
authenticated: isAuthenticated,
|
|
945
|
-
),
|
|
946
|
-
);
|
|
947
|
-
if (consentSource != null) {
|
|
948
|
-
unawaited(
|
|
949
|
-
_analytics.track(
|
|
950
|
-
'analytics_consent_granted',
|
|
951
|
-
properties: <String, Object?>{'consent_source': consentSource},
|
|
952
|
-
),
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
String _analyticsPlatformLabel() {
|
|
958
|
-
if (kIsWeb) {
|
|
959
|
-
return 'web';
|
|
960
|
-
}
|
|
961
|
-
switch (defaultTargetPlatform) {
|
|
962
|
-
case TargetPlatform.android:
|
|
963
|
-
return 'android';
|
|
964
|
-
case TargetPlatform.iOS:
|
|
965
|
-
return 'ios';
|
|
966
|
-
case TargetPlatform.macOS:
|
|
967
|
-
return 'macos';
|
|
968
|
-
case TargetPlatform.windows:
|
|
969
|
-
return 'windows';
|
|
970
|
-
case TargetPlatform.linux:
|
|
971
|
-
return 'linux';
|
|
972
|
-
case TargetPlatform.fuchsia:
|
|
973
|
-
return 'fuchsia';
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
Future<void> acceptAnalyticsConsent() async {
|
|
978
|
-
if (!_analyticsConfigured) {
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
_analyticsConsentResolved = true;
|
|
982
|
-
_analyticsConsentGranted = true;
|
|
983
|
-
await _prefs?.setBool('analytics.cookieConsent', true);
|
|
984
|
-
await _analytics.setConsentGranted(true);
|
|
985
|
-
_trackAppOpenedIfNeeded(consentSource: 'banner');
|
|
986
|
-
notifyListeners();
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
Future<void> declineAnalyticsConsent() async {
|
|
990
|
-
if (!_analyticsConfigured) {
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
_analyticsConsentResolved = true;
|
|
994
|
-
_analyticsConsentGranted = false;
|
|
995
|
-
await _prefs?.setBool('analytics.cookieConsent', false);
|
|
996
|
-
await _analytics.setConsentGranted(false);
|
|
997
|
-
notifyListeners();
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
888
|
Future<void> setAppUpdateChannel(String channel) async {
|
|
1001
889
|
final normalized = channel.trim().toLowerCase() == 'beta'
|
|
1002
890
|
? 'beta'
|
|
@@ -1021,7 +909,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1021
909
|
if (isCheckingAppUpdate) {
|
|
1022
910
|
return;
|
|
1023
911
|
}
|
|
1024
|
-
unawaited(_analytics.trackAppUpdateCheck(silent: silent));
|
|
1025
912
|
if (!appUpdaterConfigured) {
|
|
1026
913
|
appUpdateErrorMessage = kIsWeb
|
|
1027
914
|
? null
|
|
@@ -1120,11 +1007,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1120
1007
|
isBooting = true;
|
|
1121
1008
|
notifyListeners();
|
|
1122
1009
|
await bootstrap();
|
|
1123
|
-
unawaited(
|
|
1124
|
-
_analytics.trackBackendUrlSaved(
|
|
1125
|
-
backendMode: backendUrl.trim().isEmpty ? 'same_origin' : 'custom',
|
|
1126
|
-
),
|
|
1127
|
-
);
|
|
1128
1010
|
return true;
|
|
1129
1011
|
} catch (error) {
|
|
1130
1012
|
errorMessage = _friendlyErrorMessage(error);
|
|
@@ -1370,14 +1252,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1370
1252
|
_clearQrLoginChallenge();
|
|
1371
1253
|
await _persistCredentials();
|
|
1372
1254
|
await refresh();
|
|
1373
|
-
if (isAuthenticated) {
|
|
1374
|
-
unawaited(
|
|
1375
|
-
_analytics.trackSignedIn(
|
|
1376
|
-
authMethod: authMethod,
|
|
1377
|
-
isRegistration: isRegistration,
|
|
1378
|
-
),
|
|
1379
|
-
);
|
|
1380
|
-
}
|
|
1381
1255
|
if (!isAuthenticated && retentionErrorMessage != null) {
|
|
1382
1256
|
errorMessage = retentionErrorMessage;
|
|
1383
1257
|
}
|
|
@@ -1596,7 +1470,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1596
1470
|
final logoutFuture = _backendClient.logout(backendUrl);
|
|
1597
1471
|
_authCycle += 1;
|
|
1598
1472
|
_clearAuthenticatedState();
|
|
1599
|
-
unawaited(_analytics.trackSignedOut());
|
|
1600
1473
|
isAuthenticating = true;
|
|
1601
1474
|
notifyListeners();
|
|
1602
1475
|
|
|
@@ -1614,7 +1487,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1614
1487
|
notifyListeners();
|
|
1615
1488
|
try {
|
|
1616
1489
|
await _backendClient.completeOnboarding(backendUrl);
|
|
1617
|
-
unawaited(_analytics.trackOnboardingDismissed());
|
|
1618
1490
|
if (isAuthenticated && user != null) {
|
|
1619
1491
|
user!['hasCompletedOnboarding'] = true;
|
|
1620
1492
|
}
|
|
@@ -1795,14 +1667,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
1795
1667
|
if (section == AppSection.accountSettings) {
|
|
1796
1668
|
unawaited(refreshAccountSettings());
|
|
1797
1669
|
}
|
|
1798
|
-
if (previousSection != section) {
|
|
1799
|
-
unawaited(
|
|
1800
|
-
_analytics.trackSectionChanged(
|
|
1801
|
-
section: section.label,
|
|
1802
|
-
previousSection: previousSection.label,
|
|
1803
|
-
),
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
1670
|
notifyListeners();
|
|
1807
1671
|
}
|
|
1808
1672
|
|
|
@@ -2651,9 +2515,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
2651
2515
|
}
|
|
2652
2516
|
|
|
2653
2517
|
Future<void> refreshWidgets({bool all = false}) async {
|
|
2654
|
-
if (all) {
|
|
2655
|
-
unawaited(_analytics.trackWidgetRefreshRequested(all: all));
|
|
2656
|
-
}
|
|
2657
2518
|
widgets = _decodeModelList(
|
|
2658
2519
|
'widgets',
|
|
2659
2520
|
await _backendClient.fetchWidgets(
|
|
@@ -3661,7 +3522,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3661
3522
|
}
|
|
3662
3523
|
|
|
3663
3524
|
Future<void> startBackgroundRecording() async {
|
|
3664
|
-
unawaited(_analytics.trackRecordingStarted(kind: 'background'));
|
|
3665
3525
|
await _startRecordingCapture(
|
|
3666
3526
|
logKey: 'start_background',
|
|
3667
3527
|
payload: buildAndroidBackgroundRecordingPayload(),
|
|
@@ -3688,7 +3548,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
3688
3548
|
}
|
|
3689
3549
|
return;
|
|
3690
3550
|
}
|
|
3691
|
-
unawaited(_analytics.trackRecordingStarted(kind: 'desktop'));
|
|
3692
3551
|
await _startRecordingCapture(
|
|
3693
3552
|
logKey: 'start_desktop',
|
|
3694
3553
|
payload: buildDesktopRecordingPayload(),
|
|
@@ -4072,16 +3931,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4072
3931
|
notifyListeners();
|
|
4073
3932
|
try {
|
|
4074
3933
|
_desktopFloatingToolbarPopupRequested = false;
|
|
4075
|
-
unawaited(
|
|
4076
|
-
_analytics.trackRecordingStopped(
|
|
4077
|
-
kind:
|
|
4078
|
-
recordingRuntime.supportsBackgroundMic &&
|
|
4079
|
-
!recordingRuntime.supportsScreenAndMic
|
|
4080
|
-
? 'background'
|
|
4081
|
-
: 'desktop',
|
|
4082
|
-
stopReason: stopReason,
|
|
4083
|
-
),
|
|
4084
|
-
);
|
|
4085
3934
|
await _recordingBridge.stopActiveRecording();
|
|
4086
3935
|
if (isAndroidBackgroundStop) {
|
|
4087
3936
|
await Future<void>.delayed(const Duration(milliseconds: 600));
|
|
@@ -4735,13 +4584,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
4735
4584
|
if (outgoingTask.isEmpty || (isSendingMessage && !canSteerLiveRun)) {
|
|
4736
4585
|
return;
|
|
4737
4586
|
}
|
|
4738
|
-
unawaited(
|
|
4739
|
-
_analytics.trackChatMessageSent(
|
|
4740
|
-
length: trimmed.length,
|
|
4741
|
-
steeringLiveRun: canSteerLiveRun,
|
|
4742
|
-
),
|
|
4743
|
-
);
|
|
4744
|
-
|
|
4745
4587
|
final optimistic = ChatEntry(
|
|
4746
4588
|
id: '',
|
|
4747
4589
|
role: 'user',
|
|
@@ -5204,7 +5046,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
5204
5046
|
errorMessage = null;
|
|
5205
5047
|
notifyListeners();
|
|
5206
5048
|
try {
|
|
5207
|
-
unawaited(_analytics.trackAppUpdateTriggered());
|
|
5208
5049
|
await _backendClient.triggerUpdate(backendUrl);
|
|
5209
5050
|
await refreshUpdateStatus();
|
|
5210
5051
|
} catch (error) {
|
|
@@ -6083,7 +5924,6 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
6083
5924
|
return;
|
|
6084
5925
|
}
|
|
6085
5926
|
_startManualRunCooldown('task', '$id');
|
|
6086
|
-
unawaited(_analytics.trackTaskRunRequested(taskId: id));
|
|
6087
5927
|
await _backendClient.runSavedTask(backendUrl, id);
|
|
6088
5928
|
await refreshTasks();
|
|
6089
5929
|
await refreshRunsOnly();
|
|
@@ -2059,6 +2059,7 @@ class ModelMeta {
|
|
|
2059
2059
|
final bool available;
|
|
2060
2060
|
final String providerStatus;
|
|
2061
2061
|
final String providerStatusLabel;
|
|
2062
|
+
|
|
2062
2063
|
/// Pricing tier: 'free' | 'cheap' | 'medium' | 'expensive' | null (unknown)
|
|
2063
2064
|
final String? priceTier;
|
|
2064
2065
|
}
|
|
@@ -2308,18 +2309,33 @@ class TokenUsageSnapshot {
|
|
|
2308
2309
|
required this.avgTokensPerRun,
|
|
2309
2310
|
required this.last7DaysTokens,
|
|
2310
2311
|
required this.last7DaysRuns,
|
|
2312
|
+
required this.cachedReadTokens,
|
|
2313
|
+
required this.cacheWriteTokens,
|
|
2314
|
+
required this.reasoningTokens,
|
|
2315
|
+
required this.modelCallCount,
|
|
2316
|
+
required this.cacheHitRatio,
|
|
2317
|
+
this.estimatedCostUsd,
|
|
2311
2318
|
});
|
|
2312
2319
|
|
|
2313
2320
|
factory TokenUsageSnapshot.fromJson(Map<dynamic, dynamic> json) {
|
|
2314
2321
|
final totals = json['totals'] is Map
|
|
2315
2322
|
? Map<String, dynamic>.from(json['totals'] as Map)
|
|
2316
2323
|
: const <String, dynamic>{};
|
|
2324
|
+
final modelUsage = json['modelUsage'] is Map
|
|
2325
|
+
? Map<String, dynamic>.from(json['modelUsage'] as Map)
|
|
2326
|
+
: const <String, dynamic>{};
|
|
2317
2327
|
return TokenUsageSnapshot(
|
|
2318
2328
|
totalTokens: _asInt(totals['totalTokens']),
|
|
2319
2329
|
totalRuns: _asInt(totals['totalRuns']),
|
|
2320
2330
|
avgTokensPerRun: _asInt(totals['avgTokensPerRun']),
|
|
2321
2331
|
last7DaysTokens: _asInt(totals['last7DaysTokens']),
|
|
2322
2332
|
last7DaysRuns: _asInt(totals['last7DaysRuns']),
|
|
2333
|
+
cachedReadTokens: _asInt(modelUsage['cachedReadTokens']),
|
|
2334
|
+
cacheWriteTokens: _asInt(modelUsage['cacheWriteTokens']),
|
|
2335
|
+
reasoningTokens: _asInt(modelUsage['reasoningTokens']),
|
|
2336
|
+
modelCallCount: _asInt(modelUsage['callCount']),
|
|
2337
|
+
cacheHitRatio: (modelUsage['cacheHitRatio'] as num?)?.toDouble() ?? 0,
|
|
2338
|
+
estimatedCostUsd: (modelUsage['estimatedCostUsd'] as num?)?.toDouble(),
|
|
2323
2339
|
);
|
|
2324
2340
|
}
|
|
2325
2341
|
|
|
@@ -2328,12 +2344,24 @@ class TokenUsageSnapshot {
|
|
|
2328
2344
|
final int avgTokensPerRun;
|
|
2329
2345
|
final int last7DaysTokens;
|
|
2330
2346
|
final int last7DaysRuns;
|
|
2347
|
+
final int cachedReadTokens;
|
|
2348
|
+
final int cacheWriteTokens;
|
|
2349
|
+
final int reasoningTokens;
|
|
2350
|
+
final int modelCallCount;
|
|
2351
|
+
final double cacheHitRatio;
|
|
2352
|
+
final double? estimatedCostUsd;
|
|
2331
2353
|
|
|
2332
2354
|
String get totalTokensLabel => _formatNumber(totalTokens);
|
|
2333
2355
|
String get totalRunsLabel => _formatNumber(totalRuns);
|
|
2334
2356
|
String get avgTokensPerRunLabel => _formatNumber(avgTokensPerRun);
|
|
2335
2357
|
String get last7DaysTokensLabel => _formatNumber(last7DaysTokens);
|
|
2336
2358
|
String get last7DaysRunsLabel => _formatNumber(last7DaysRuns);
|
|
2359
|
+
String get cachedReadTokensLabel => _formatNumber(cachedReadTokens);
|
|
2360
|
+
String get cacheHitRatioLabel =>
|
|
2361
|
+
'${(cacheHitRatio * 100).toStringAsFixed(1)}%';
|
|
2362
|
+
String get estimatedCostLabel => estimatedCostUsd == null
|
|
2363
|
+
? 'Unknown'
|
|
2364
|
+
: '\$${estimatedCostUsd!.toStringAsFixed(4)}';
|
|
2337
2365
|
}
|
|
2338
2366
|
|
|
2339
2367
|
class UpdateStatusSnapshot {
|
|
@@ -4842,6 +4842,7 @@ class _TaskTriggerOption {
|
|
|
4842
4842
|
required this.description,
|
|
4843
4843
|
required this.icon,
|
|
4844
4844
|
this.providerKey,
|
|
4845
|
+
this.appKey,
|
|
4845
4846
|
});
|
|
4846
4847
|
|
|
4847
4848
|
final String type;
|
|
@@ -4854,6 +4855,12 @@ class _TaskTriggerOption {
|
|
|
4854
4855
|
/// When set, the task editor shows a connected-account dropdown instead of
|
|
4855
4856
|
/// a raw connection ID text field.
|
|
4856
4857
|
final String? providerKey;
|
|
4858
|
+
|
|
4859
|
+
/// The app within the provider (matches [OfficialIntegrationAppItem.id]).
|
|
4860
|
+
/// Narrows account lookup to just the relevant app so multi-app providers
|
|
4861
|
+
/// (e.g. Google Workspace with Gmail + Drive + Calendar) don't show
|
|
4862
|
+
/// duplicate accounts.
|
|
4863
|
+
final String? appKey;
|
|
4857
4864
|
}
|
|
4858
4865
|
|
|
4859
4866
|
const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
@@ -4878,6 +4885,7 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
4878
4885
|
description: 'Run when a matching Gmail message arrives.',
|
|
4879
4886
|
icon: Icons.mail_rounded,
|
|
4880
4887
|
providerKey: 'google_workspace',
|
|
4888
|
+
appKey: 'gmail',
|
|
4881
4889
|
),
|
|
4882
4890
|
_TaskTriggerOption(
|
|
4883
4891
|
type: 'outlook_email_received',
|
|
@@ -4886,6 +4894,7 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
4886
4894
|
description: 'Run when a matching Outlook email arrives.',
|
|
4887
4895
|
icon: Icons.markunread_rounded,
|
|
4888
4896
|
providerKey: 'microsoft_365',
|
|
4897
|
+
appKey: 'outlook',
|
|
4889
4898
|
),
|
|
4890
4899
|
_TaskTriggerOption(
|
|
4891
4900
|
type: 'slack_message_received',
|
|
@@ -4894,6 +4903,7 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
4894
4903
|
description: 'Run when a Slack message matches the selected scope.',
|
|
4895
4904
|
icon: Icons.forum_rounded,
|
|
4896
4905
|
providerKey: 'slack',
|
|
4906
|
+
appKey: 'slack',
|
|
4897
4907
|
),
|
|
4898
4908
|
_TaskTriggerOption(
|
|
4899
4909
|
type: 'teams_message_received',
|
|
@@ -4902,6 +4912,7 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
4902
4912
|
description: 'Run when a Teams chat message matches the selected scope.',
|
|
4903
4913
|
icon: Icons.groups_rounded,
|
|
4904
4914
|
providerKey: 'microsoft_365',
|
|
4915
|
+
appKey: 'teams',
|
|
4905
4916
|
),
|
|
4906
4917
|
_TaskTriggerOption(
|
|
4907
4918
|
type: 'weather_event',
|
|
@@ -4911,6 +4922,7 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
4911
4922
|
'Run when configured weather events are forecast for a location.',
|
|
4912
4923
|
icon: Icons.cloudy_snowing,
|
|
4913
4924
|
providerKey: 'weather',
|
|
4925
|
+
appKey: 'forecast',
|
|
4914
4926
|
),
|
|
4915
4927
|
_TaskTriggerOption(
|
|
4916
4928
|
type: 'whatsapp_personal_message_received',
|
|
@@ -4919,6 +4931,7 @@ const List<_TaskTriggerOption> _taskTriggerOptions = <_TaskTriggerOption>[
|
|
|
4919
4931
|
description: 'Run on inbound personal WhatsApp messages.',
|
|
4920
4932
|
icon: Icons.chat_bubble_rounded,
|
|
4921
4933
|
providerKey: 'whatsapp_personal',
|
|
4934
|
+
appKey: 'personal',
|
|
4922
4935
|
),
|
|
4923
4936
|
];
|
|
4924
4937
|
|
|
@@ -5578,13 +5591,16 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5578
5591
|
List<OfficialIntegrationAccountItem> _connectedAccountsForTrigger(
|
|
5579
5592
|
String triggerType,
|
|
5580
5593
|
) {
|
|
5581
|
-
final
|
|
5594
|
+
final option = _taskTriggerOptionForType(triggerType);
|
|
5595
|
+
final providerKey = option.providerKey;
|
|
5582
5596
|
if (providerKey == null) return const <OfficialIntegrationAccountItem>[];
|
|
5597
|
+
final appKey = option.appKey;
|
|
5583
5598
|
final seen = <int>{};
|
|
5584
5599
|
final result = <OfficialIntegrationAccountItem>[];
|
|
5585
5600
|
for (final integration in controller.officialIntegrations) {
|
|
5586
5601
|
if (integration.id != providerKey) continue;
|
|
5587
5602
|
for (final app in integration.apps) {
|
|
5603
|
+
if (appKey != null && app.id != appKey) continue;
|
|
5588
5604
|
for (final account in app.accounts) {
|
|
5589
5605
|
if (account.connected && seen.add(account.id)) {
|
|
5590
5606
|
result.add(account);
|
|
@@ -636,28 +636,6 @@ class _NeoAgentAppState extends State<NeoAgentApp>
|
|
|
636
636
|
),
|
|
637
637
|
),
|
|
638
638
|
),
|
|
639
|
-
if (!_desktopToolbarWindowMode &&
|
|
640
|
-
!_desktopAssistantPopupWindowMode &&
|
|
641
|
-
_controller.showAnalyticsConsentBanner)
|
|
642
|
-
Positioned(
|
|
643
|
-
left: 0,
|
|
644
|
-
right: 0,
|
|
645
|
-
bottom: 0,
|
|
646
|
-
child: SafeArea(
|
|
647
|
-
top: false,
|
|
648
|
-
child: Padding(
|
|
649
|
-
padding: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
|
650
|
-
child: Center(
|
|
651
|
-
child: ConstrainedBox(
|
|
652
|
-
constraints: const BoxConstraints(maxWidth: 980),
|
|
653
|
-
child: _GlobalAnalyticsConsentBanner(
|
|
654
|
-
controller: _controller,
|
|
655
|
-
),
|
|
656
|
-
),
|
|
657
|
-
),
|
|
658
|
-
),
|
|
659
|
-
),
|
|
660
|
-
),
|
|
661
639
|
if (_supportsDesktopShell &&
|
|
662
640
|
!_desktopToolbarWindowMode &&
|
|
663
641
|
!_desktopAssistantPopupWindowMode)
|
|
@@ -1546,6 +1546,15 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1546
1546
|
Text(
|
|
1547
1547
|
'Avg/run: ${controller.tokenUsage!.avgTokensPerRunLabel} tokens',
|
|
1548
1548
|
),
|
|
1549
|
+
const SizedBox(height: 6),
|
|
1550
|
+
Text(
|
|
1551
|
+
'Prompt cache: ${controller.tokenUsage!.cachedReadTokensLabel} cached tokens '
|
|
1552
|
+
'(${controller.tokenUsage!.cacheHitRatioLabel} hit ratio)',
|
|
1553
|
+
),
|
|
1554
|
+
const SizedBox(height: 6),
|
|
1555
|
+
Text(
|
|
1556
|
+
'Measured model cost: ${controller.tokenUsage!.estimatedCostLabel}',
|
|
1557
|
+
),
|
|
1549
1558
|
],
|
|
1550
1559
|
),
|
|
1551
1560
|
],
|