neoagent 3.1.1-beta.9 → 3.2.1-beta.0

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 (151) hide show
  1. package/.env.example +2 -2
  2. package/README.md +5 -1
  3. package/docs/architecture.md +1 -1
  4. package/docs/benchmarking.md +68 -87
  5. package/docs/capabilities.md +1 -1
  6. package/docs/clients-and-devices.md +3 -4
  7. package/docs/configuration.md +1 -1
  8. package/docs/devices.md +1 -2
  9. package/docs/health.md +24 -0
  10. package/docs/index.md +2 -2
  11. package/docs/integrations-architecture.md +6 -0
  12. package/docs/integrations.md +15 -1
  13. package/docs/models.md +1 -1
  14. package/docs/persistence.md +1 -1
  15. package/docs/why-neoagent.md +1 -1
  16. package/flutter_app/README.md +0 -5
  17. package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
  19. package/flutter_app/lib/main.dart +0 -6
  20. package/flutter_app/lib/main_app_shell.dart +0 -2
  21. package/flutter_app/lib/main_controller.dart +2 -867
  22. package/flutter_app/lib/main_devices.dart +0 -73
  23. package/flutter_app/lib/main_integrations.dart +339 -187
  24. package/flutter_app/lib/main_launcher.dart +2 -212
  25. package/flutter_app/lib/main_models.dart +0 -259
  26. package/flutter_app/lib/main_navigation.dart +2 -14
  27. package/flutter_app/lib/main_operations.dart +875 -383
  28. package/flutter_app/lib/main_runtime.dart +9 -208
  29. package/flutter_app/lib/main_settings.dart +17 -199
  30. package/flutter_app/lib/main_shared.dart +20 -591
  31. package/flutter_app/lib/main_timeline.dart +26 -122
  32. package/flutter_app/lib/main_voice_assistant.dart +0 -5
  33. package/flutter_app/lib/src/backend_client.dart +0 -102
  34. package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
  35. package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
  36. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
  37. package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
  38. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  39. package/flutter_app/macos/Podfile.lock +56 -5
  40. package/flutter_app/pubspec.lock +0 -7
  41. package/flutter_app/pubspec.yaml +0 -2
  42. package/flutter_app/web/index.html +174 -0
  43. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
  44. package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
  45. package/landing/index.html +0 -2
  46. package/lib/manager.js +1 -1
  47. package/lib/schema_migrations.js +75 -0
  48. package/package.json +1 -4
  49. package/server/db/database.js +1 -133
  50. package/server/http/middleware.js +2 -16
  51. package/server/http/routes.js +0 -2
  52. package/server/public/.last_build_id +1 -1
  53. package/server/public/assets/NOTICES +0 -25
  54. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  55. package/server/public/flutter_bootstrap.js +2 -2
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +92036 -91145
  58. package/server/routes/auth.js +11 -2
  59. package/server/routes/settings.js +0 -5
  60. package/server/routes/voice_assistant.js +1 -128
  61. package/server/services/account/erasure.js +0 -3
  62. package/server/services/ai/deliverables/artifact_helpers.js +1 -1
  63. package/server/services/ai/loop/conversation_loop.js +6 -2
  64. package/server/services/ai/loop/tool_dispatch.js +0 -3
  65. package/server/services/ai/messagingFallback.js +1 -1
  66. package/server/services/ai/settings.js +0 -6
  67. package/server/services/ai/toolEvidence.js +1 -4
  68. package/server/services/ai/tools.js +0 -234
  69. package/server/services/desktop/auth.js +0 -3
  70. package/server/services/desktop/registry.js +2 -50
  71. package/server/services/integrations/neoarchive/provider.js +616 -0
  72. package/server/services/integrations/neorecall/client.js +140 -0
  73. package/server/services/integrations/neorecall/constants.js +87 -0
  74. package/server/services/integrations/neorecall/provider.js +163 -0
  75. package/server/services/integrations/registry.js +4 -2
  76. package/server/services/manager.js +0 -22
  77. package/server/services/memory/manager.js +44 -19
  78. package/server/services/memory/retrieval_reasoning.js +36 -14
  79. package/server/services/messaging/formatting_guides.js +1 -0
  80. package/server/services/messaging/whatsapp.js +18 -0
  81. package/server/services/security/tool_categories.js +0 -5
  82. package/server/services/social_video/service.js +2 -20
  83. package/server/services/tasks/adapters/index.js +1 -1
  84. package/server/services/tasks/integration_runtime.js +3 -67
  85. package/server/services/tasks/runtime.js +1 -6
  86. package/server/services/tasks/task_repository.js +6 -13
  87. package/server/services/timeline/service.js +0 -224
  88. package/server/services/voice/message.js +0 -24
  89. package/server/services/voice/providers.js +42 -1
  90. package/server/services/voice/runtimeManager.js +9 -5
  91. package/server/services/voice/turnRunner.js +0 -22
  92. package/server/services/wearable/service.js +0 -5
  93. package/docs/recordings-and-health.md +0 -41
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  97. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  98. package/flutter_app/lib/main_recordings.dart +0 -920
  99. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  100. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  101. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  102. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  103. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  104. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  105. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  106. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  107. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  108. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  109. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  110. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  111. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  112. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  114. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  115. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  118. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  119. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  120. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  121. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  126. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  130. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  131. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  132. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  133. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  136. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  138. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  140. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  142. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  143. package/server/routes/recordings.js +0 -335
  144. package/server/routes/screenHistory.js +0 -190
  145. package/server/services/ai/recordingInsights.js +0 -105
  146. package/server/services/integrations/neomail/provider.js +0 -993
  147. package/server/services/recordings/deepgram.js +0 -58
  148. package/server/services/recordings/manager.js +0 -1273
  149. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  150. package/server/services/voice/screenshotContext.js +0 -73
  151. package/server/services/voice/turnCoordinator.js +0 -51
@@ -1,2 +0,0 @@
1
- export 'desktop_ocr_bridge_stub.dart'
2
- if (dart.library.io) 'desktop_ocr_bridge_io.dart';
@@ -1,125 +0,0 @@
1
- import 'dart:io';
2
- import 'dart:typed_data';
3
-
4
- class DesktopOcrResult {
5
- const DesktopOcrResult({
6
- required this.text,
7
- required this.engine,
8
- this.confidence,
9
- });
10
-
11
- final String text;
12
- final String engine;
13
- final double? confidence;
14
- }
15
-
16
- class DesktopOcrBridge {
17
- const DesktopOcrBridge();
18
-
19
- static const String _binaryName = 'tesseract';
20
-
21
- Future<String?> unavailableReason() async {
22
- try {
23
- final result = await Process.run(_binaryName, <String>['--version']);
24
- if (result.exitCode == 0) {
25
- return null;
26
- }
27
- } catch (_) {}
28
- return 'Passive screen history OCR requires Tesseract to be installed and available on PATH.';
29
- }
30
-
31
- Future<DesktopOcrResult> recognize({
32
- required Uint8List bytes,
33
- required String mimeType,
34
- }) async {
35
- final reason = await unavailableReason();
36
- if (reason != null) {
37
- throw ProcessException(_binaryName, const <String>['--version'], reason, 127);
38
- }
39
- final extension =
40
- mimeType.toLowerCase().contains('jpeg') ||
41
- mimeType.toLowerCase().contains('jpg')
42
- ? 'jpg'
43
- : 'png';
44
- final directory = await Directory.systemTemp.createTemp(
45
- 'neoagent-passive-ocr-',
46
- );
47
- final inputFile = File('${directory.path}/capture.$extension');
48
- try {
49
- await inputFile.writeAsBytes(bytes, flush: true);
50
- final result = await Process.run(_binaryName, <String>[
51
- inputFile.path,
52
- 'stdout',
53
- '--psm',
54
- '6',
55
- 'tsv',
56
- 'quiet',
57
- ]);
58
- if (result.exitCode != 0) {
59
- final stderr = result.stderr?.toString().trim();
60
- final stdout = result.stdout?.toString().trim();
61
- throw ProcessException(
62
- _binaryName,
63
- <String>[inputFile.path],
64
- stderr?.isNotEmpty == true
65
- ? stderr!
66
- : (stdout?.isNotEmpty == true ? stdout! : 'OCR failed.'),
67
- result.exitCode,
68
- );
69
- }
70
- final parsed = _parseTsv(result.stdout?.toString() ?? '');
71
- return DesktopOcrResult(
72
- text: parsed.text,
73
- engine: 'tesseract',
74
- confidence: parsed.confidence,
75
- );
76
- } finally {
77
- try {
78
- if (await inputFile.exists()) {
79
- await inputFile.delete();
80
- }
81
- if (await directory.exists()) {
82
- await directory.delete();
83
- }
84
- } catch (_) {}
85
- }
86
- }
87
-
88
- ({String text, double? confidence}) _parseTsv(String raw) {
89
- final lines = raw
90
- .split(RegExp(r'\r?\n'))
91
- .where((line) => line.trim().isNotEmpty)
92
- .toList(growable: false);
93
- if (lines.length <= 1) {
94
- return (text: '', confidence: null);
95
- }
96
-
97
- final words = <String>[];
98
- var confidenceTotal = 0.0;
99
- var confidenceCount = 0;
100
- for (final line in lines.skip(1)) {
101
- final columns = line.split('\t');
102
- if (columns.length < 12) {
103
- continue;
104
- }
105
- final word = columns[11].replaceAll(RegExp(r'\s+'), ' ').trim();
106
- if (word.isEmpty) {
107
- continue;
108
- }
109
- words.add(word);
110
- final confidence = double.tryParse(columns[10]);
111
- if (confidence != null && confidence >= 0) {
112
- confidenceTotal += confidence;
113
- confidenceCount += 1;
114
- }
115
- }
116
- return (
117
- text: words.join(' ').replaceAll(RegExp(r'\s+'), ' ').trim(),
118
- confidence: confidenceCount == 0
119
- ? null
120
- : confidenceTotal / confidenceCount,
121
- );
122
- }
123
- }
124
-
125
- DesktopOcrBridge createDesktopOcrBridge() => const DesktopOcrBridge();
@@ -1,30 +0,0 @@
1
- import 'dart:typed_data';
2
-
3
- class DesktopOcrResult {
4
- const DesktopOcrResult({
5
- required this.text,
6
- required this.engine,
7
- this.confidence,
8
- });
9
-
10
- final String text;
11
- final String engine;
12
- final double? confidence;
13
- }
14
-
15
- class DesktopOcrBridge {
16
- const DesktopOcrBridge();
17
-
18
- Future<String?> unavailableReason() async {
19
- return 'Local OCR is not available on this platform.';
20
- }
21
-
22
- Future<DesktopOcrResult> recognize({
23
- required Uint8List bytes,
24
- required String mimeType,
25
- }) {
26
- throw UnsupportedError('Local OCR is not available on this platform.');
27
- }
28
- }
29
-
30
- DesktopOcrBridge createDesktopOcrBridge() => const DesktopOcrBridge();
@@ -1,332 +0,0 @@
1
- import 'dart:async';
2
- import 'dart:convert';
3
- import 'dart:io';
4
-
5
- import 'package:flutter/foundation.dart';
6
- import 'package:shared_preferences/shared_preferences.dart';
7
-
8
- import 'desktop_companion_actions.dart';
9
- import 'desktop_ocr_bridge.dart';
10
-
11
- const String desktopPassiveHistoryEnabledPrefsKey =
12
- 'desktop.passiveHistory.enabled';
13
-
14
- class DesktopPassiveHistoryManager extends ChangeNotifier {
15
- DesktopPassiveHistoryManager({
16
- required DesktopCompanionActions actions,
17
- required DesktopOcrBridge ocrBridge,
18
- }) : _actions = actions,
19
- _ocrBridge = ocrBridge;
20
-
21
- static const Duration _captureInterval = Duration(seconds: 20);
22
- static const Duration _contextPollInterval = Duration(seconds: 4);
23
- static const Duration _idleThreshold = Duration(minutes: 5);
24
- static const int _maxBatchSize = 20;
25
-
26
- final DesktopCompanionActions _actions;
27
- final DesktopOcrBridge _ocrBridge;
28
-
29
- Timer? _captureTimer;
30
- Timer? _contextPollTimer;
31
- bool _enabled = false;
32
- bool _paused = false;
33
- bool _authenticated = false;
34
- bool _connected = false;
35
- bool _captureInFlight = false;
36
- String _backendUrl = '';
37
- String _sessionCookie = '';
38
- String _deviceId = '';
39
- String _activationId = '';
40
- String _label = 'Desktop';
41
- String? _lastUploadedAt;
42
- String? _lastError;
43
- String _lastObservedContext = '';
44
- String _lastUploadedFingerprint = '';
45
- final List<Map<String, Object?>> _pendingEntries = <Map<String, Object?>>[];
46
-
47
- bool get enabled => _enabled;
48
- String? get lastUploadedAt => _lastUploadedAt;
49
- String? get lastError => _lastError;
50
-
51
- Future<void> bootstrap(SharedPreferences prefs) async {
52
- _enabled = prefs.getBool(desktopPassiveHistoryEnabledPrefsKey) ?? false;
53
- }
54
-
55
- Future<void> setEnabled(bool value, SharedPreferences prefs) async {
56
- if (_enabled == value) {
57
- return;
58
- }
59
- _enabled = value;
60
- await prefs.setBool(desktopPassiveHistoryEnabledPrefsKey, value);
61
- if (!value) {
62
- _resetLocalCaptureState();
63
- _stopTimers();
64
- } else {
65
- _syncTimers();
66
- unawaited(_captureIfNeeded(force: true));
67
- }
68
- notifyListeners();
69
- }
70
-
71
- void updateRuntimeState({
72
- required String backendUrl,
73
- required String sessionCookie,
74
- required bool authenticated,
75
- required bool connected,
76
- required bool paused,
77
- required String deviceId,
78
- required String activationId,
79
- required String label,
80
- }) {
81
- final identityChanged =
82
- _sessionCookie != sessionCookie.trim() ||
83
- _deviceId != deviceId.trim() ||
84
- _activationId != activationId.trim();
85
- _backendUrl = backendUrl.trim();
86
- _sessionCookie = sessionCookie.trim();
87
- _authenticated = authenticated;
88
- _connected = connected;
89
- _paused = paused;
90
- _deviceId = deviceId.trim();
91
- _activationId = activationId.trim();
92
- _label = label.trim().isEmpty ? 'Desktop' : label.trim();
93
- if (identityChanged) {
94
- _resetLocalCaptureState();
95
- }
96
- _syncTimers();
97
- if (_enabled && _connected) {
98
- unawaited(_flushQueue());
99
- }
100
- }
101
-
102
- Map<String, Object?> statusPayload() {
103
- return <String, Object?>{
104
- 'passiveHistoryEnabled': _enabled,
105
- 'passiveHistoryLastUploadedAt': _lastUploadedAt,
106
- 'passiveHistoryLastError': _lastError,
107
- };
108
- }
109
-
110
- bool get _shouldRun =>
111
- _enabled &&
112
- !_paused &&
113
- _authenticated &&
114
- _connected &&
115
- _backendUrl.isNotEmpty &&
116
- _sessionCookie.isNotEmpty &&
117
- _deviceId.isNotEmpty &&
118
- _activationId.isNotEmpty;
119
-
120
- void _syncTimers() {
121
- if (!_shouldRun) {
122
- _stopTimers();
123
- return;
124
- }
125
- _captureTimer ??= Timer.periodic(_captureInterval, (_) {
126
- unawaited(_captureIfNeeded());
127
- });
128
- _contextPollTimer ??= Timer.periodic(_contextPollInterval, (_) {
129
- unawaited(_pollContext());
130
- });
131
- }
132
-
133
- void _stopTimers() {
134
- _captureTimer?.cancel();
135
- _captureTimer = null;
136
- _contextPollTimer?.cancel();
137
- _contextPollTimer = null;
138
- }
139
-
140
- Future<void> _pollContext() async {
141
- if (!_shouldRun || _captureInFlight) {
142
- return;
143
- }
144
- try {
145
- final status = await _actions.getStatus(label: _label, paused: _paused);
146
- if (_skipForStatus(status)) {
147
- return;
148
- }
149
- final context = _contextKey(status);
150
- if (context.isEmpty || context == _lastObservedContext) {
151
- return;
152
- }
153
- _lastObservedContext = context;
154
- await _captureIfNeeded(force: true, statusOverride: status);
155
- } catch (error) {
156
- _setLastError('$error');
157
- }
158
- }
159
-
160
- Future<void> _captureIfNeeded({
161
- bool force = false,
162
- Map<String, Object?>? statusOverride,
163
- }) async {
164
- if (!_shouldRun || _captureInFlight) {
165
- return;
166
- }
167
- _captureInFlight = true;
168
- try {
169
- final status =
170
- statusOverride ??
171
- await _actions.getStatus(label: _label, paused: _paused);
172
- if (_skipForStatus(status)) {
173
- return;
174
- }
175
- final snapshot = await _actions.captureSnapshot(
176
- activeDisplayId: status['activeDisplayId']?.toString(),
177
- );
178
- if (snapshot == null) {
179
- return;
180
- }
181
- final unavailableReason = await _ocrBridge.unavailableReason();
182
- if (unavailableReason != null) {
183
- _setLastError(unavailableReason);
184
- return;
185
- }
186
- final bytes = base64Decode(snapshot.screenshotBase64);
187
- final ocr = await _ocrBridge.recognize(
188
- bytes: bytes,
189
- mimeType: snapshot.contentType,
190
- );
191
- final text = _normalizeText(ocr.text);
192
- if (text.isEmpty) {
193
- return;
194
- }
195
- final frontmostApp = status['frontmostApp']?.toString().trim() ?? '';
196
- final windowTitle =
197
- status['frontmostWindowTitle']?.toString().trim() ?? '';
198
- final fingerprint = '$frontmostApp\n$windowTitle\n$text';
199
- if (!force && fingerprint == _lastUploadedFingerprint) {
200
- return;
201
- }
202
- _lastUploadedFingerprint = fingerprint;
203
- _pendingEntries.add(<String, Object?>{
204
- 'capturedAt': DateTime.now().toUtc().toIso8601String(),
205
- 'frontmostApp': frontmostApp,
206
- 'windowTitle': windowTitle,
207
- 'text': text,
208
- if (ocr.confidence != null) 'ocrConfidence': ocr.confidence,
209
- });
210
- _lastObservedContext = _contextKey(status);
211
- await _flushQueue();
212
- } catch (error) {
213
- _setLastError('$error');
214
- } finally {
215
- _captureInFlight = false;
216
- }
217
- }
218
-
219
- bool _skipForStatus(Map<String, Object?> status) {
220
- if (!_shouldRun) {
221
- return true;
222
- }
223
- final permissions = status['permissions'];
224
- if (permissions is Map) {
225
- final captureState =
226
- permissions['screenCapture']?.toString().toLowerCase() ?? '';
227
- if (captureState == 'required' || captureState == 'unsupported') {
228
- return true;
229
- }
230
- }
231
- if (status['sessionLocked'] == true) {
232
- return true;
233
- }
234
- if (status['userIdle'] == true) {
235
- return true;
236
- }
237
- final idleSeconds = num.tryParse(status['idleSeconds']?.toString() ?? '');
238
- if (idleSeconds != null &&
239
- idleSeconds >= _idleThreshold.inSeconds) {
240
- return true;
241
- }
242
- final frontmostApp = status['frontmostApp']?.toString().trim() ?? '';
243
- final windowTitle =
244
- status['frontmostWindowTitle']?.toString().trim() ?? '';
245
- return frontmostApp.isEmpty && windowTitle.isEmpty;
246
- }
247
-
248
- String _contextKey(Map<String, Object?> status) {
249
- final app = status['frontmostApp']?.toString().trim() ?? '';
250
- final window = status['frontmostWindowTitle']?.toString().trim() ?? '';
251
- if (app.isEmpty && window.isEmpty) {
252
- return '';
253
- }
254
- return '$app::$window';
255
- }
256
-
257
- String _normalizeText(String value) {
258
- final normalized = value.replaceAll(RegExp(r'\s+'), ' ').trim();
259
- if (normalized.length <= 4000) {
260
- return normalized;
261
- }
262
- return normalized.substring(0, 4000);
263
- }
264
-
265
- Future<void> _flushQueue() async {
266
- if (!_shouldRun || _pendingEntries.isEmpty) {
267
- return;
268
- }
269
- final batch = _pendingEntries.take(_maxBatchSize).toList(growable: false);
270
- final client = HttpClient();
271
- try {
272
- final request = await client.postUrl(
273
- Uri.parse(_backendUrl).resolve('/api/screen-history/entries'),
274
- );
275
- request.headers.contentType = ContentType.json;
276
- request.headers.set(HttpHeaders.cookieHeader, _sessionCookie);
277
- request.add(
278
- utf8.encode(
279
- jsonEncode(<String, Object?>{
280
- 'deviceId': _deviceId,
281
- 'activationId': _activationId,
282
- 'entries': batch,
283
- }),
284
- ),
285
- );
286
- final response = await request.close();
287
- final responseBody = await utf8.decodeStream(response);
288
- if (response.statusCode < 200 || response.statusCode >= 300) {
289
- throw HttpException(
290
- responseBody.isEmpty
291
- ? 'Passive history upload failed (${response.statusCode}).'
292
- : responseBody,
293
- uri: request.uri,
294
- );
295
- }
296
- _pendingEntries.removeRange(0, batch.length);
297
- _lastUploadedAt = DateTime.now().toUtc().toIso8601String();
298
- _lastError = null;
299
- notifyListeners();
300
- if (_pendingEntries.isNotEmpty) {
301
- await _flushQueue();
302
- }
303
- } catch (error) {
304
- _setLastError('$error');
305
- } finally {
306
- client.close(force: true);
307
- }
308
- }
309
-
310
- void _setLastError(String? value) {
311
- final normalized = value?.trim() ?? '';
312
- final next = normalized.isEmpty ? null : normalized;
313
- if (_lastError == next) {
314
- return;
315
- }
316
- _lastError = next;
317
- notifyListeners();
318
- }
319
-
320
- void _resetLocalCaptureState() {
321
- _pendingEntries.clear();
322
- _lastObservedContext = '';
323
- _lastUploadedFingerprint = '';
324
- _captureInFlight = false;
325
- }
326
-
327
- @override
328
- void dispose() {
329
- _stopTimers();
330
- super.dispose();
331
- }
332
- }