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.
- package/.env.example +2 -2
- package/README.md +5 -1
- package/docs/architecture.md +1 -1
- package/docs/benchmarking.md +68 -87
- package/docs/capabilities.md +1 -1
- package/docs/clients-and-devices.md +3 -4
- package/docs/configuration.md +1 -1
- package/docs/devices.md +1 -2
- package/docs/health.md +24 -0
- package/docs/index.md +2 -2
- package/docs/integrations-architecture.md +6 -0
- package/docs/integrations.md +15 -1
- package/docs/models.md +1 -1
- package/docs/persistence.md +1 -1
- package/docs/why-neoagent.md +1 -1
- package/flutter_app/README.md +0 -5
- package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
- package/flutter_app/lib/main.dart +0 -6
- package/flutter_app/lib/main_app_shell.dart +0 -2
- package/flutter_app/lib/main_controller.dart +2 -867
- package/flutter_app/lib/main_devices.dart +0 -73
- package/flutter_app/lib/main_integrations.dart +339 -187
- package/flutter_app/lib/main_launcher.dart +2 -212
- package/flutter_app/lib/main_models.dart +0 -259
- package/flutter_app/lib/main_navigation.dart +2 -14
- package/flutter_app/lib/main_operations.dart +875 -383
- package/flutter_app/lib/main_runtime.dart +9 -208
- package/flutter_app/lib/main_settings.dart +17 -199
- package/flutter_app/lib/main_shared.dart +20 -591
- package/flutter_app/lib/main_timeline.dart +26 -122
- package/flutter_app/lib/main_voice_assistant.dart +0 -5
- package/flutter_app/lib/src/backend_client.dart +0 -102
- package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
- package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
- package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/macos/Podfile.lock +56 -5
- package/flutter_app/pubspec.lock +0 -7
- package/flutter_app/pubspec.yaml +0 -2
- package/flutter_app/web/index.html +174 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
- package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
- package/landing/index.html +0 -2
- package/lib/manager.js +1 -1
- package/lib/schema_migrations.js +75 -0
- package/package.json +1 -4
- package/server/db/database.js +1 -133
- package/server/http/middleware.js +2 -16
- package/server/http/routes.js +0 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +0 -25
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/index.html +174 -0
- package/server/public/main.dart.js +92036 -91145
- package/server/routes/auth.js +11 -2
- package/server/routes/settings.js +0 -5
- package/server/routes/voice_assistant.js +1 -128
- package/server/services/account/erasure.js +0 -3
- package/server/services/ai/deliverables/artifact_helpers.js +1 -1
- package/server/services/ai/loop/conversation_loop.js +6 -2
- package/server/services/ai/loop/tool_dispatch.js +0 -3
- package/server/services/ai/messagingFallback.js +1 -1
- package/server/services/ai/settings.js +0 -6
- package/server/services/ai/toolEvidence.js +1 -4
- package/server/services/ai/tools.js +0 -234
- package/server/services/desktop/auth.js +0 -3
- package/server/services/desktop/registry.js +2 -50
- package/server/services/integrations/neoarchive/provider.js +616 -0
- package/server/services/integrations/neorecall/client.js +140 -0
- package/server/services/integrations/neorecall/constants.js +87 -0
- package/server/services/integrations/neorecall/provider.js +163 -0
- package/server/services/integrations/registry.js +4 -2
- package/server/services/manager.js +0 -22
- package/server/services/memory/manager.js +44 -19
- package/server/services/memory/retrieval_reasoning.js +36 -14
- package/server/services/messaging/formatting_guides.js +1 -0
- package/server/services/messaging/whatsapp.js +18 -0
- package/server/services/security/tool_categories.js +0 -5
- package/server/services/social_video/service.js +2 -20
- package/server/services/tasks/adapters/index.js +1 -1
- package/server/services/tasks/integration_runtime.js +3 -67
- package/server/services/tasks/runtime.js +1 -6
- package/server/services/tasks/task_repository.js +6 -13
- package/server/services/timeline/service.js +0 -224
- package/server/services/voice/message.js +0 -24
- package/server/services/voice/providers.js +42 -1
- package/server/services/voice/runtimeManager.js +9 -5
- package/server/services/voice/turnRunner.js +0 -22
- package/server/services/wearable/service.js +0 -5
- package/docs/recordings-and-health.md +0 -41
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
- package/flutter_app/lib/main_recordings.dart +0 -920
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
- package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
- package/flutter_app/lib/src/recording_bridge.dart +0 -232
- package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
- package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
- package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
- package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
- package/flutter_app/lib/src/recording_payloads.dart +0 -95
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
- package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
- package/server/routes/recordings.js +0 -335
- package/server/routes/screenHistory.js +0 -190
- package/server/services/ai/recordingInsights.js +0 -105
- package/server/services/integrations/neomail/provider.js +0 -993
- package/server/services/recordings/deepgram.js +0 -58
- package/server/services/recordings/manager.js +0 -1273
- package/server/services/tasks/adapters/neomail_email_received.js +0 -46
- package/server/services/voice/screenshotContext.js +0 -73
- 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;
|