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.
Files changed (62) hide show
  1. package/.env.example +0 -9
  2. package/README.md +48 -18
  3. package/docs/configuration.md +0 -10
  4. package/flutter_app/lib/main.dart +0 -1
  5. package/flutter_app/lib/main_controller.dart +0 -160
  6. package/flutter_app/lib/main_models.dart +28 -0
  7. package/flutter_app/lib/main_operations.dart +17 -1
  8. package/flutter_app/lib/main_runtime.dart +0 -22
  9. package/flutter_app/lib/main_settings.dart +9 -0
  10. package/flutter_app/lib/main_shared.dart +0 -161
  11. package/flutter_app/lib/src/backend_client.dart +8 -0
  12. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  13. package/flutter_app/pubspec.lock +0 -8
  14. package/flutter_app/pubspec.yaml +0 -1
  15. package/package.json +1 -1
  16. package/server/admin/admin.css +23 -0
  17. package/server/admin/admin.js +17 -19
  18. package/server/admin/analytics.js +254 -48
  19. package/server/admin/index.html +9 -2
  20. package/server/db/database.js +112 -0
  21. package/server/http/routes.js +1 -0
  22. package/server/public/.last_build_id +1 -1
  23. package/server/public/assets/AssetManifest.bin +1 -1
  24. package/server/public/assets/AssetManifest.bin.json +1 -1
  25. package/server/public/assets/NOTICES +0 -183
  26. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  27. package/server/public/flutter_bootstrap.js +2 -2
  28. package/server/public/main.dart.js +77723 -78469
  29. package/server/routes/admin.js +73 -24
  30. package/server/routes/mcp.js +9 -0
  31. package/server/routes/memory.js +35 -2
  32. package/server/routes/runtime.js +5 -2
  33. package/server/routes/settings.js +34 -1
  34. package/server/routes/skills.js +42 -0
  35. package/server/routes/task_webhooks.js +65 -0
  36. package/server/services/ai/engine.js +525 -24
  37. package/server/services/ai/learning.js +56 -8
  38. package/server/services/ai/models.js +4 -4
  39. package/server/services/ai/providers/anthropic.js +40 -8
  40. package/server/services/ai/providers/google.js +10 -0
  41. package/server/services/ai/providers/openai.js +3 -15
  42. package/server/services/ai/providers/openaiCompatible.js +11 -0
  43. package/server/services/ai/repetitionGuard.js +60 -0
  44. package/server/services/ai/systemPrompt.js +52 -16
  45. package/server/services/ai/taskAnalysis.js +15 -6
  46. package/server/services/ai/toolEvidence.js +3 -1
  47. package/server/services/ai/toolRunner.js +43 -1
  48. package/server/services/ai/toolSelector.js +92 -46
  49. package/server/services/ai/tools.js +89 -9
  50. package/server/services/ai/usage.js +114 -0
  51. package/server/services/manager.js +34 -0
  52. package/server/services/memory/manager.js +170 -4
  53. package/server/services/security/capability_audit.js +107 -0
  54. package/server/services/tasks/adapters/index.js +1 -0
  55. package/server/services/tasks/adapters/webhook.js +14 -0
  56. package/server/services/tasks/runtime.js +1 -1
  57. package/server/services/tasks/webhooks.js +146 -0
  58. package/server/services/workspace/code_navigation.js +194 -0
  59. package/server/services/workspace/structured_data.js +93 -0
  60. package/server/utils/cloud-security.js +24 -2
  61. package/flutter_app/lib/src/analytics_service.dart +0 -294
  62. 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
- - **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
27
- - **15+ messaging platforms** — Telegram, WhatsApp, Discord, Signal, Slack, Matrix, iMessage, IRC, LINE, Mattermost, Telnyx Voice
28
- - **Integrations** Google Workspace, Microsoft 365, Notion, Home Assistant, Trello, Spotify, Figma
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
- ## Install
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` runs a preflight, writes a secure default config when setup
40
- cannot prompt, installs Node dependencies, builds or uses the bundled web
41
- client, bootstraps supported system tools where possible, and starts the
42
- service. Available at **http://localhost:3333** when complete.
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
- ## Manage
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
- ## Links
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) | [Why NeoAgent](docs/why-neoagent.md) | [Issues](https://github.com/NeoLabs-Systems/NeoAgent/issues)
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
 
@@ -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 providerKey = _taskTriggerOptionForType(triggerType).providerKey;
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
  ],