neoagent 3.1.1-beta.9 → 3.2.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 (150) 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 +322 -364
  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 +1 -1
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +88081 -90036
  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/whatsapp.js +18 -0
  80. package/server/services/security/tool_categories.js +0 -5
  81. package/server/services/social_video/service.js +2 -20
  82. package/server/services/tasks/adapters/index.js +1 -1
  83. package/server/services/tasks/integration_runtime.js +3 -67
  84. package/server/services/tasks/runtime.js +1 -6
  85. package/server/services/tasks/task_repository.js +6 -13
  86. package/server/services/timeline/service.js +0 -224
  87. package/server/services/voice/message.js +0 -24
  88. package/server/services/voice/providers.js +42 -1
  89. package/server/services/voice/runtimeManager.js +9 -5
  90. package/server/services/voice/turnRunner.js +0 -22
  91. package/server/services/wearable/service.js +0 -5
  92. package/docs/recordings-and-health.md +0 -41
  93. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  97. package/flutter_app/lib/main_recordings.dart +0 -920
  98. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  99. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  100. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  101. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  102. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  103. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  104. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  105. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  106. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  107. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  108. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  109. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  110. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  111. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  112. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  114. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  115. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  118. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  119. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  120. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  121. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  126. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  130. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  131. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  132. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  133. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  136. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  138. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  140. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  142. package/server/routes/recordings.js +0 -335
  143. package/server/routes/screenHistory.js +0 -190
  144. package/server/services/ai/recordingInsights.js +0 -105
  145. package/server/services/integrations/neomail/provider.js +0 -993
  146. package/server/services/recordings/deepgram.js +0 -58
  147. package/server/services/recordings/manager.js +0 -1273
  148. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  149. package/server/services/voice/screenshotContext.js +0 -73
  150. package/server/services/voice/turnCoordinator.js +0 -51
@@ -1,87 +0,0 @@
1
- #ifndef FLUTTER_PLUGIN_SYSTEM_AUDIO_CAPTURE_PLUGIN_H_
2
- #define FLUTTER_PLUGIN_SYSTEM_AUDIO_CAPTURE_PLUGIN_H_
3
-
4
- #include <flutter/event_channel.h>
5
- #include <flutter/method_channel.h>
6
- #include <flutter/plugin_registrar_windows.h>
7
- #include <flutter/standard_method_codec.h>
8
-
9
- #include <memory>
10
- #include <mutex>
11
- #include <thread>
12
- #include <atomic>
13
- #include <vector>
14
-
15
- // Include Windows headers for WAVEFORMATEX
16
- #include <mmsystem.h>
17
-
18
- // Forward declarations for WASAPI interfaces
19
- struct IAudioClient;
20
- struct IAudioCaptureClient;
21
- struct IMMDevice;
22
-
23
- namespace audio_capture {
24
-
25
- class SystemAudioCapturePlugin : public flutter::Plugin {
26
- public:
27
- static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
28
-
29
- SystemAudioCapturePlugin(flutter::PluginRegistrarWindows *registrar);
30
- ~SystemAudioCapturePlugin();
31
-
32
- // Disallow copy and assign.
33
- SystemAudioCapturePlugin(const SystemAudioCapturePlugin&) = delete;
34
- SystemAudioCapturePlugin& operator=(const SystemAudioCapturePlugin&) = delete;
35
-
36
- void HandleMethodCall(
37
- const flutter::MethodCall<flutter::EncodableValue> &method_call,
38
- std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
39
-
40
- private:
41
- bool StartCapture(const flutter::EncodableMap* args);
42
- bool StopCapture();
43
- void CaptureThread();
44
- void SetThreadPriority();
45
- double CalculateDecibel(const int16_t* samples, size_t sample_count);
46
- void ApplyGainBoostAndConvertToMono(const int16_t* input, int16_t* output,
47
- size_t frame_count, int input_channels,
48
- float gain_boost);
49
- void SendStatusUpdate(bool is_active);
50
- void SendDecibelUpdate(double decibel);
51
-
52
- flutter::PluginRegistrarWindows* registrar_;
53
- std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> method_channel_;
54
- std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> event_channel_;
55
- std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> status_event_channel_;
56
- std::unique_ptr<flutter::EventChannel<flutter::EncodableValue>> decibel_event_channel_;
57
-
58
- std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> event_sink_;
59
- std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> status_event_sink_;
60
- std::unique_ptr<flutter::EventSink<flutter::EncodableValue>> decibel_event_sink_;
61
-
62
- std::mutex mutex_;
63
- std::atomic<bool> is_capturing_;
64
- std::atomic<bool> should_stop_;
65
- std::thread capture_thread_;
66
-
67
- // Audio configuration
68
- int sample_rate_;
69
- int channels_;
70
- int bits_per_sample_;
71
- int chunk_duration_ms_;
72
- float gain_boost_;
73
- float input_volume_;
74
-
75
- // WASAPI interfaces
76
- IAudioClient* audio_client_;
77
- IAudioCaptureClient* capture_client_;
78
- IMMDevice* device_;
79
- WAVEFORMATEX* mix_format_;
80
- UINT32 buffer_frame_count_;
81
- bool com_initialized_; // Track if we initialized COM
82
- };
83
-
84
- } // namespace audio_capture
85
-
86
- #endif // FLUTTER_PLUGIN_SYSTEM_AUDIO_CAPTURE_PLUGIN_H_
87
-
@@ -1,335 +0,0 @@
1
- const express = require('express');
2
- const fs = require('fs');
3
- const { finished } = require('stream/promises');
4
-
5
- const { requireAuth } = require('../middleware/auth');
6
- const { sanitizeError } = require('../utils/security');
7
- const db = require('../db/database');
8
- const { readChunkBody } = require('./_helpers/readChunkBody');
9
-
10
- const router = express.Router();
11
-
12
- router.use(requireAuth);
13
-
14
- function getRecordingManager(req) {
15
- return req.app.locals.recordingManager;
16
- }
17
-
18
- function parsePositiveInt(value, fallback) {
19
- const parsed = Number.parseInt(value, 10);
20
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
21
- }
22
-
23
- function getChunkMetadata(req) {
24
- const parseNonNegativeNumber = (value, fieldName) => {
25
- const parsed = Number(value);
26
- if (!Number.isFinite(parsed) || parsed < 0) {
27
- throw new Error(`${fieldName} must be a non-negative number`);
28
- }
29
- return parsed;
30
- };
31
-
32
- const sequenceIndexRaw = req.get('x-recording-sequence') || req.query.sequenceIndex;
33
- const startMsRaw = req.get('x-recording-start-ms') || req.query.startMs;
34
- const endMsRaw = req.get('x-recording-end-ms') || req.query.endMs;
35
- const mimeRaw = req.get('content-type') || req.query.mimeType || '';
36
-
37
- return {
38
- sourceKey: req.get('x-recording-source-key') || req.query.sourceKey,
39
- sequenceIndex: parseNonNegativeNumber(sequenceIndexRaw, 'sequenceIndex'),
40
- startMs: parseNonNegativeNumber(startMsRaw, 'startMs'),
41
- endMs: parseNonNegativeNumber(endMsRaw, 'endMs'),
42
- mimeType: String(mimeRaw).split(';')[0].trim(),
43
- };
44
- }
45
-
46
- const WAV_HEADER_BYTES = 44;
47
-
48
- // Reads the canonical 44-byte WAV header our recorders emit. Returns the audio
49
- // format fields, or null when the file is not a canonical WAV (in which case
50
- // the caller streams the chunks unmodified instead of rebuilding a WAV).
51
- async function readCanonicalWavHeader(filePath) {
52
- let handle;
53
- try {
54
- handle = await fs.promises.open(filePath, 'r');
55
- const { bytesRead, buffer } = await handle.read(
56
- Buffer.alloc(WAV_HEADER_BYTES),
57
- 0,
58
- WAV_HEADER_BYTES,
59
- 0,
60
- );
61
- if (
62
- bytesRead < WAV_HEADER_BYTES
63
- || buffer.toString('ascii', 0, 4) !== 'RIFF'
64
- || buffer.toString('ascii', 8, 12) !== 'WAVE'
65
- || buffer.toString('ascii', 36, 40) !== 'data'
66
- ) {
67
- return null;
68
- }
69
- return {
70
- audioFormat: buffer.readUInt16LE(20),
71
- channelCount: buffer.readUInt16LE(22),
72
- sampleRate: buffer.readUInt32LE(24),
73
- bitsPerSample: buffer.readUInt16LE(34),
74
- };
75
- } catch {
76
- return null;
77
- } finally {
78
- await handle?.close();
79
- }
80
- }
81
-
82
- function buildWavHeader({ audioFormat, channelCount, sampleRate, bitsPerSample }, dataLength) {
83
- const blockAlign = channelCount * (bitsPerSample / 8);
84
- const byteRate = sampleRate * blockAlign;
85
- const header = Buffer.alloc(WAV_HEADER_BYTES);
86
- header.write('RIFF', 0, 4, 'ascii');
87
- header.writeUInt32LE(36 + dataLength, 4);
88
- header.write('WAVE', 8, 4, 'ascii');
89
- header.write('fmt ', 12, 4, 'ascii');
90
- header.writeUInt32LE(16, 16);
91
- header.writeUInt16LE(audioFormat, 20);
92
- header.writeUInt16LE(channelCount, 22);
93
- header.writeUInt32LE(sampleRate, 24);
94
- header.writeUInt32LE(byteRate, 28);
95
- header.writeUInt16LE(blockAlign, 32);
96
- header.writeUInt16LE(bitsPerSample, 34);
97
- header.write('data', 36, 4, 'ascii');
98
- header.writeUInt32LE(dataLength, 40);
99
- return header;
100
- }
101
-
102
- function buildInlineFilename(sourceKey) {
103
- const original = `${String(sourceKey || 'recording').normalize('NFKC')}.audio`
104
- .replace(/[\r\n]+/g, ' ')
105
- .trim() || 'recording.audio';
106
- const safeAscii = original
107
- .replace(/["]/g, "'")
108
- .replace(/[^\x20-\x7E]+/g, '_')
109
- .trim() || 'recording.audio';
110
- return `inline; filename="${safeAscii}"; filename*=UTF-8''${encodeURIComponent(original)}`;
111
- }
112
-
113
- function statusFromMessage(message, rules, fallbackStatus = 500) {
114
- for (const rule of rules) {
115
- if (rule.pattern.test(message)) {
116
- return rule.status;
117
- }
118
- }
119
- return fallbackStatus;
120
- }
121
-
122
- function respondWithMappedError(res, err, rules, fallbackStatus = 500) {
123
- const message = sanitizeError(err);
124
- res.status(statusFromMessage(message, rules, fallbackStatus)).json({ error: message });
125
- }
126
-
127
- router.get('/', (req, res) => {
128
- try {
129
- const manager = getRecordingManager(req);
130
- const sessions = manager.listSessions(req.session.userId, {
131
- limit: parsePositiveInt(req.query.limit, 24),
132
- });
133
- res.json(sessions);
134
- } catch (err) {
135
- res.status(500).json({ error: sanitizeError(err) });
136
- }
137
- });
138
-
139
- router.get('/:sessionId', (req, res) => {
140
- try {
141
- const manager = getRecordingManager(req);
142
- const session = manager.getSession(req.session.userId, req.params.sessionId);
143
- res.json({ session });
144
- } catch (err) {
145
- respondWithMappedError(res, err, [
146
- { pattern: /not found/i, status: 404 },
147
- ]);
148
- }
149
- });
150
-
151
- router.get('/:sessionId/audio/:sourceKey', async (req, res) => {
152
- try {
153
- const sessionId = req.params.sessionId;
154
- const sourceKey = String(req.params.sourceKey || '').trim();
155
- if (!sourceKey) {
156
- return res.status(400).json({ error: 'sourceKey is required.' });
157
- }
158
-
159
- const session = db.prepare(`
160
- SELECT id
161
- FROM recording_sessions
162
- WHERE id = ? AND user_id = ?
163
- `).get(sessionId, req.session.userId);
164
- if (!session) {
165
- return res.status(404).json({ error: 'Recording session not found.' });
166
- }
167
-
168
- const source = db.prepare(`
169
- SELECT id, source_key, mime_type
170
- FROM recording_sources
171
- WHERE session_id = ? AND LOWER(source_key) = LOWER(?)
172
- LIMIT 1
173
- `).get(sessionId, sourceKey);
174
- if (!source) {
175
- return res.status(404).json({ error: 'Recording source not found.' });
176
- }
177
-
178
- const chunks = db.prepare(`
179
- SELECT file_path, mime_type
180
- FROM recording_chunks
181
- WHERE source_id = ?
182
- ORDER BY sequence_index ASC
183
- `).all(source.id);
184
- if (!Array.isArray(chunks) || chunks.length == 0) {
185
- return res.status(404).json({ error: 'No audio chunks available.' });
186
- }
187
-
188
- const mimeType = String(source.mime_type || chunks[0]?.mime_type || 'application/octet-stream');
189
- if (!mimeType.startsWith('audio/')) {
190
- return res.status(415).json({
191
- error: `Playback unsupported for mime type: ${mimeType}`,
192
- });
193
- }
194
-
195
- const readableChunks = [];
196
- for (const chunk of chunks) {
197
- const filePath = chunk.file_path;
198
- if (!filePath) {
199
- continue;
200
- }
201
- try {
202
- await fs.promises.access(filePath, fs.constants.R_OK);
203
- readableChunks.push(chunk);
204
- } catch {
205
- // Skip chunk files that are missing or unreadable on disk.
206
- }
207
- }
208
- if (readableChunks.length === 0) {
209
- return res.status(404).json({ error: 'No audio chunks available.' });
210
- }
211
-
212
- res.setHeader('Content-Type', mimeType);
213
- res.setHeader('Cache-Control', 'no-store');
214
- res.setHeader('Content-Disposition', buildInlineFilename(source.source_key));
215
-
216
- // Each WAV chunk is a self-contained file with its own 44-byte RIFF header
217
- // whose declared length only covers that chunk. Streaming them back-to-back
218
- // produces a file most players stop reading after the first chunk. Rebuild
219
- // one continuous WAV: a single header sized for the combined PCM, followed
220
- // by the raw sample data from every chunk. Non-WAV containers (e.g. WebM)
221
- // concatenate into a valid stream as-is.
222
- const wavFormat = /wav/i.test(mimeType)
223
- ? await readCanonicalWavHeader(readableChunks[0].file_path)
224
- : null;
225
-
226
- if (wavFormat) {
227
- let totalPcmBytes = 0;
228
- for (const chunk of readableChunks) {
229
- const { size } = await fs.promises.stat(chunk.file_path);
230
- totalPcmBytes += Math.max(0, size - WAV_HEADER_BYTES);
231
- }
232
- res.write(buildWavHeader(wavFormat, totalPcmBytes));
233
- for (const chunk of readableChunks) {
234
- const stream = fs.createReadStream(chunk.file_path, { start: WAV_HEADER_BYTES });
235
- stream.pipe(res, { end: false });
236
- await finished(stream);
237
- }
238
- res.end();
239
- return;
240
- }
241
-
242
- for (const chunk of readableChunks) {
243
- const stream = fs.createReadStream(chunk.file_path);
244
- stream.pipe(res, { end: false });
245
- await finished(stream);
246
- }
247
-
248
- res.end();
249
- } catch (err) {
250
- res.status(500).json({ error: sanitizeError(err) });
251
- }
252
- });
253
-
254
- router.post('/', (req, res) => {
255
- try {
256
- const manager = getRecordingManager(req);
257
- const session = manager.createSession(req.session.userId, req.body || {});
258
- res.status(201).json({ session });
259
- } catch (err) {
260
- respondWithMappedError(res, err, [
261
- { pattern: /source|title|required|duplicate/i, status: 400 },
262
- ]);
263
- }
264
- });
265
-
266
- router.post('/:sessionId/chunks', async (req, res) => {
267
- try {
268
- const manager = getRecordingManager(req);
269
- const body = await readChunkBody(req);
270
- const result = manager.appendChunk(req.session.userId, req.params.sessionId, getChunkMetadata(req), body);
271
- res.status(result.duplicate ? 200 : 201).json(result);
272
- } catch (err) {
273
- console.error('[Recordings] Chunk upload failed:', err);
274
- respondWithMappedError(res, err, [
275
- { pattern: /not found/i, status: 404 },
276
- { pattern: /empty|required|unknown|non-negative|accepting|sequence|contiguous/i, status: 400 },
277
- ]);
278
- }
279
- });
280
-
281
- router.post('/:sessionId/finalize', (req, res) => {
282
- try {
283
- const manager = getRecordingManager(req);
284
- const session = manager.finalizeSession(req.session.userId, req.params.sessionId, req.body || {});
285
- res.json({ session });
286
- } catch (err) {
287
- respondWithMappedError(res, err, [
288
- { pattern: /not found/i, status: 404 },
289
- ]);
290
- }
291
- });
292
-
293
- router.post('/:sessionId/retry', async (req, res) => {
294
- try {
295
- const manager = getRecordingManager(req);
296
- const session = await manager.retrySession(req.session.userId, req.params.sessionId);
297
- res.json({ session });
298
- } catch (err) {
299
- respondWithMappedError(res, err, [
300
- { pattern: /not found/i, status: 404 },
301
- { pattern: /configured/i, status: 400 },
302
- ]);
303
- }
304
- });
305
-
306
- router.delete('/:sessionId/segments/:segmentId', (req, res) => {
307
- try {
308
- const manager = getRecordingManager(req);
309
- const session = manager.deleteTranscriptSegment(
310
- req.session.userId,
311
- req.params.sessionId,
312
- req.params.segmentId,
313
- );
314
- res.json({ session });
315
- } catch (err) {
316
- respondWithMappedError(res, err, [
317
- { pattern: /not found/i, status: 404 },
318
- { pattern: /positive integer/i, status: 400 },
319
- ]);
320
- }
321
- });
322
-
323
- router.delete('/:sessionId', (req, res) => {
324
- try {
325
- const manager = getRecordingManager(req);
326
- manager.deleteSession(req.session.userId, req.params.sessionId);
327
- res.status(204).send();
328
- } catch (err) {
329
- respondWithMappedError(res, err, [
330
- { pattern: /not found/i, status: 404 },
331
- ]);
332
- }
333
- });
334
-
335
- module.exports = router;
@@ -1,190 +0,0 @@
1
- 'use strict';
2
-
3
- const express = require('express');
4
- const db = require('../db/database');
5
- const { buildFtsQuery } = require('../db/ftsQuery');
6
- const { requireAuth } = require('../middleware/auth');
7
- const { getErrorMessage } = require('../services/bootstrap_helpers');
8
-
9
- const router = express.Router();
10
-
11
- router.use(requireAuth);
12
-
13
- function getTimelineService(req) {
14
- return req.app?.locals?.timelineService || null;
15
- }
16
-
17
- function normalizeEntries(rawEntries) {
18
- if (!Array.isArray(rawEntries)) {
19
- throw new Error('entries must be an array.');
20
- }
21
- return rawEntries.map((entry) => {
22
- if (!entry || typeof entry !== 'object') {
23
- throw new Error('Each entry must be an object.');
24
- }
25
- const text = String(entry.text || '').replace(/\s+/g, ' ').trim();
26
- if (!text) {
27
- throw new Error('Entry text is required.');
28
- }
29
- return {
30
- capturedAt: entry.capturedAt,
31
- frontmostApp: entry.frontmostApp,
32
- windowTitle: entry.windowTitle,
33
- text,
34
- ocrConfidence: entry.ocrConfidence,
35
- };
36
- });
37
- }
38
-
39
- router.get('/search', (req, res) => {
40
- const { q, limit = 50, offset = 0 } = req.query;
41
- const userId = req.session.userId;
42
-
43
- try {
44
- let results = [];
45
- const ftsQuery = q ? buildFtsQuery(q) : null;
46
- if (ftsQuery) {
47
- // Full text search. buildFtsQuery sanitizes user input so FTS5 operator
48
- // characters (hyphens, AND/OR/NOT) don't throw and 500 the request.
49
- results = db.prepare(`
50
- SELECT
51
- s.id,
52
- s.timestamp,
53
- s.captured_at,
54
- s.captured_at AS capturedAt,
55
- s.device_id,
56
- s.device_id AS deviceId,
57
- s.device_label,
58
- s.device_label AS deviceLabel,
59
- s.app_name,
60
- s.app_name AS appName,
61
- s.window_title,
62
- s.window_title AS windowTitle,
63
- s.text_content,
64
- s.ocr_engine,
65
- s.ocr_engine AS ocrEngine,
66
- s.ocr_confidence,
67
- s.ocr_confidence AS ocrConfidence
68
- FROM screen_history_fts fts
69
- JOIN screen_history s ON fts.rowid = s.id
70
- WHERE screen_history_fts MATCH ? AND s.user_id = ?
71
- ORDER BY s.timestamp DESC
72
- LIMIT ? OFFSET ?
73
- `).all(ftsQuery, userId, Number(limit), Number(offset));
74
- } else if (q) {
75
- // Query had no usable search tokens — return no matches rather than error.
76
- results = [];
77
- } else {
78
- // Recent history
79
- results = db.prepare(`
80
- SELECT
81
- id,
82
- timestamp,
83
- captured_at,
84
- captured_at AS capturedAt,
85
- device_id,
86
- device_id AS deviceId,
87
- device_label,
88
- device_label AS deviceLabel,
89
- app_name,
90
- app_name AS appName,
91
- window_title,
92
- window_title AS windowTitle,
93
- text_content,
94
- ocr_engine,
95
- ocr_engine AS ocrEngine,
96
- ocr_confidence,
97
- ocr_confidence AS ocrConfidence
98
- FROM screen_history
99
- WHERE user_id = ?
100
- ORDER BY timestamp DESC
101
- LIMIT ? OFFSET ?
102
- `).all(userId, Number(limit), Number(offset));
103
- }
104
-
105
- res.json({ results });
106
- } catch (err) {
107
- console.error('[ScreenHistory] Search error:', getErrorMessage(err));
108
- res.status(500).json({ error: 'Failed to search screen history' });
109
- }
110
- });
111
-
112
- router.post('/entries', express.json(), (req, res) => {
113
- try {
114
- const userId = req.session.userId;
115
- const deviceId = String(req.body?.deviceId || '').trim();
116
- const activationId = String(req.body?.activationId || '').trim();
117
- if (!deviceId) {
118
- return res.status(400).json({ error: 'deviceId is required.' });
119
- }
120
- if (!activationId) {
121
- return res.status(400).json({ error: 'activationId is required.' });
122
- }
123
-
124
- const device = db.prepare(
125
- `SELECT device_id, label, activation_id, revoked_at, passive_history_enabled
126
- FROM desktop_companion_devices
127
- WHERE user_id = ? AND device_id = ?
128
- LIMIT 1`
129
- ).get(userId, deviceId);
130
- if (!device) {
131
- return res.status(404).json({ error: 'Desktop companion device not found.' });
132
- }
133
- if (device.revoked_at) {
134
- return res.status(403).json({ error: 'Desktop companion device has been revoked.' });
135
- }
136
- if (String(device.activation_id || '') !== activationId) {
137
- return res.status(403).json({ error: 'activationId does not match the registered desktop companion device.' });
138
- }
139
- if (device.passive_history_enabled !== 1) {
140
- return res.status(403).json({ error: 'Passive screen history is disabled for this desktop companion device.' });
141
- }
142
-
143
- const entries = normalizeEntries(req.body?.entries);
144
- const timelineService = getTimelineService(req);
145
- if (!timelineService || typeof timelineService.storeScreenEntries !== 'function') {
146
- return res.status(503).json({ error: 'Timeline service is unavailable.' });
147
- }
148
-
149
- const result = timelineService.storeScreenEntries({
150
- userId,
151
- deviceId,
152
- deviceLabel: device.label || deviceId,
153
- entries,
154
- });
155
- req.app?.locals?.desktopCompanionRegistry?.updatePassiveHistoryState?.(
156
- userId,
157
- deviceId,
158
- {
159
- enabled: true,
160
- lastUploadedAt: new Date().toISOString(),
161
- lastError: null,
162
- },
163
- );
164
- res.status(201).json({
165
- ok: true,
166
- insertedCount: result.insertedCount,
167
- });
168
- } catch (err) {
169
- const message = getErrorMessage(err);
170
- const userId = req.session?.userId;
171
- const deviceId = String(req.body?.deviceId || '').trim();
172
- if (userId && deviceId) {
173
- try {
174
- req.app?.locals?.desktopCompanionRegistry?.updatePassiveHistoryState?.(
175
- userId,
176
- deviceId,
177
- {
178
- enabled: true,
179
- lastError: message,
180
- },
181
- );
182
- } catch {
183
- // Best-effort; do not mask the original ingest failure.
184
- }
185
- }
186
- res.status(400).json({ error: message });
187
- }
188
- });
189
-
190
- module.exports = router;