@todesktop/plugin-recall 1.0.1

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/src/main.ts ADDED
@@ -0,0 +1,474 @@
1
+ /**
2
+ * ToDesktop Recall Desktop SDK Plugin - Main Process
3
+ *
4
+ * This file runs in Electron's main process and handles:
5
+ * - Recall Desktop SDK integration and lifecycle management
6
+ * - IPC communication with renderer processes
7
+ * - Meeting detection and recording management
8
+ * - Event forwarding from SDK to frontend
9
+ */
10
+
11
+ import { ipcMain, BrowserWindow } from 'electron';
12
+ import {
13
+ IPC_CHANNELS,
14
+ ApiResponse,
15
+ PluginStatus,
16
+ RecallSdkError,
17
+ StartRecordingRequest,
18
+ StopRecordingRequest,
19
+ PauseRecordingRequest,
20
+ ResumeRecordingRequest,
21
+ UploadRecordingRequest,
22
+ PermissionType,
23
+ SdkInitOptions,
24
+ PrepareDesktopAudioResponse,
25
+ RecallSdkConfig,
26
+ RecallSdkEventType
27
+ } from './shared';
28
+ import { recallSdkStore } from './store';
29
+
30
+ // Import Recall SDK, with a dev-friendly fallback mock if the package isn't installed
31
+ type RecallAiSdk = {
32
+ init(options: any): Promise<null> | void;
33
+ shutdown(): Promise<null> | void;
34
+ startRecording(config: { windowId: string; uploadToken: string }): Promise<null> | void;
35
+ stopRecording(config: { windowId: string }): Promise<null> | void;
36
+ pauseRecording(config: { windowId: string }): Promise<null> | void;
37
+ resumeRecording(config: { windowId: string }): Promise<null> | void;
38
+ uploadRecording(config: { windowId: string }): Promise<null> | void;
39
+ prepareDesktopAudioRecording(): Promise<string>;
40
+ requestPermission(permission: PermissionType): Promise<null> | void;
41
+ addEventListener<T extends RecallSdkEventType>(type: T, callback: (event: any) => void): void;
42
+ };
43
+
44
+ let RecallAiSdk: RecallAiSdk;
45
+ try {
46
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
47
+ const mod = require('@recallai/desktop-sdk');
48
+ RecallAiSdk = (mod && mod.default) ? mod.default : mod;
49
+ } catch {
50
+ RecallAiSdk = {
51
+ init: async (config: any) => { console.log('RecallAiSdk.init called with:', config); return null; },
52
+ shutdown: async () => { console.log('RecallAiSdk.shutdown called'); return null; },
53
+ startRecording: async (params: any) => { console.log('RecallAiSdk.startRecording called with:', params); return null; },
54
+ stopRecording: async (params: any) => { console.log('RecallAiSdk.stopRecording called with:', params); return null; },
55
+ pauseRecording: async (params: any) => { console.log('RecallAiSdk.pauseRecording called with:', params); return null; },
56
+ resumeRecording: async (params: any) => { console.log('RecallAiSdk.resumeRecording called with:', params); return null; },
57
+ uploadRecording: async (params: any) => { console.log('RecallAiSdk.uploadRecording called with:', params); return null; },
58
+ prepareDesktopAudioRecording: async () => { console.log('RecallAiSdk.prepareDesktopAudioRecording called'); return 'mock-window-id'; },
59
+ requestPermission: async (permission: PermissionType) => { console.log(`RecallAiSdk.requestPermission called for: ${permission}`); return null; },
60
+ addEventListener: (_eventType: string) => { /* no-op in mock */ },
61
+ } as RecallAiSdk;
62
+ }
63
+
64
+ class RecallDesktopMain {
65
+ private version = '1.0.0';
66
+ private isInitialized = false;
67
+ private subscriptions: Map<RecallSdkEventType, Map<number, number>> = new Map();
68
+ private trackedWebContents = new Map<number, Electron.WebContents>();
69
+
70
+ async initialize(): Promise<void> {
71
+ try {
72
+ // Initialize plugin store
73
+ await recallSdkStore.initialize();
74
+
75
+ // Load configuration from ToDesktop preferences
76
+ // In a real implementation, this would integrate with ToDesktop's preference system
77
+ this.loadPreferences();
78
+
79
+ // Register IPC handlers
80
+ this.registerIpcHandlers();
81
+
82
+ // Initialize SDK if plugin is enabled
83
+ if (recallSdkStore.isEnabled()) {
84
+ await this.initializeSdk();
85
+ }
86
+
87
+ this.isInitialized = true;
88
+ console.log('RecallDesktopMain: Main process initialized');
89
+ } catch (error) {
90
+ console.error('RecallDesktopMain: Failed to initialize main process:', error);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ private loadPreferences(): void {
96
+ // In a real implementation, this would load from ToDesktop's preference system
97
+ // For now, we'll use defaults
98
+ const preferences = {
99
+ enabled: true,
100
+ apiUrl: 'https://us-east-1.recall.ai',
101
+ requestPermissionsOnStartup: true,
102
+ };
103
+
104
+ recallSdkStore.loadFromPreferences(preferences);
105
+ }
106
+
107
+ private async initializeSdk(): Promise<void> {
108
+ try {
109
+ const config = recallSdkStore.getConfig();
110
+
111
+ const sdkOptions: SdkInitOptions = {
112
+ apiUrl: config.apiUrl,
113
+ acquirePermissionsOnStartup: config.requestPermissionsOnStartup
114
+ ? ['accessibility', 'screen-capture', 'microphone', 'system-audio']
115
+ : undefined,
116
+ restartOnError: true,
117
+ };
118
+
119
+ // Initialize the Recall SDK
120
+ await (RecallAiSdk.init as any)(sdkOptions);
121
+
122
+ // Set up event listeners
123
+ this.setupSdkEventListeners();
124
+
125
+ recallSdkStore.setSdkInitialized(true);
126
+ console.log('RecallDesktopMain: SDK initialized successfully');
127
+ } catch (error) {
128
+ console.error('RecallDesktopMain: Failed to initialize SDK:', error);
129
+ throw new RecallSdkError('SDK initialization failed', 'SDK_INIT_ERROR');
130
+ }
131
+ }
132
+
133
+ private setupSdkEventListeners(): void {
134
+ const broadcast = (type: RecallSdkEventType, data: any) => {
135
+ const channel = `recall-desktop:event:${type}`;
136
+ const subs = this.subscriptions.get(type);
137
+ if (!subs) return;
138
+ subs.forEach((_count, wcId) => {
139
+ const wc = this.trackedWebContents.get(wcId);
140
+ if (!wc) return;
141
+ try {
142
+ wc.send(channel, data);
143
+ } catch (err) {
144
+ console.error('RecallDesktopMain: Failed to send event to webContents', wcId, err);
145
+ }
146
+ });
147
+ };
148
+
149
+ // Permissions granted
150
+ RecallAiSdk.addEventListener('permissions-granted', (evt) => {
151
+ console.log('RecallDesktopMain: Permissions granted');
152
+ const type: RecallSdkEventType = 'permissions-granted';
153
+ recallSdkStore.emitEvent({ type, data: evt });
154
+ broadcast(type, evt);
155
+ });
156
+
157
+ // Meeting detected
158
+ RecallAiSdk.addEventListener('meeting-detected', (evt) => {
159
+ console.log('RecallDesktopMain: Meeting detected:', evt);
160
+ const meeting = evt.window;
161
+ recallSdkStore.addMeeting(meeting);
162
+ const type: RecallSdkEventType = 'meeting-detected';
163
+ recallSdkStore.emitEvent({ type, data: evt });
164
+ broadcast(type, evt);
165
+ });
166
+
167
+ // SDK state change
168
+ RecallAiSdk.addEventListener('sdk-state-change', (evt) => {
169
+ console.log('RecallDesktopMain: SDK state change:', evt);
170
+ const state = evt.sdk.state.code;
171
+ recallSdkStore.setSdkState(state);
172
+ const type: RecallSdkEventType = 'sdk-state-change';
173
+ recallSdkStore.emitEvent({ type, data: evt });
174
+ broadcast(type, evt);
175
+ });
176
+
177
+ // Recording started
178
+ RecallAiSdk.addEventListener('recording-started', (evt) => {
179
+ console.log('RecallDesktopMain: Recording started:', evt);
180
+ const type: RecallSdkEventType = 'recording-started';
181
+ recallSdkStore.emitEvent({ type, data: evt });
182
+ broadcast(type, evt);
183
+ });
184
+
185
+ // Recording ended
186
+ RecallAiSdk.addEventListener('recording-ended', (evt) => {
187
+ console.log('RecallDesktopMain: Recording ended:', evt);
188
+ recallSdkStore.stopRecording(evt.window.id);
189
+ const type: RecallSdkEventType = 'recording-ended';
190
+ recallSdkStore.emitEvent({ type, data: evt });
191
+ broadcast(type, evt);
192
+ });
193
+
194
+ // Meeting closed
195
+ RecallAiSdk.addEventListener('meeting-closed', (evt) => {
196
+ console.log('RecallDesktopMain: Meeting closed:', evt);
197
+ recallSdkStore.removeMeeting(evt.window.id);
198
+ const type: RecallSdkEventType = 'meeting-closed';
199
+ recallSdkStore.emitEvent({ type, data: evt });
200
+ broadcast(type, evt);
201
+ });
202
+
203
+ // Upload progress
204
+ RecallAiSdk.addEventListener('upload-progress', (evt) => {
205
+ console.log(`RecallDesktopMain: Upload progress: ${evt.progress}%`);
206
+ const type: RecallSdkEventType = 'upload-progress';
207
+ recallSdkStore.emitEvent({ type, data: evt });
208
+ broadcast(type, evt);
209
+ });
210
+
211
+ // Other events
212
+ (['realtime-event', 'meeting-updated', 'media-capture-status', 'participant-capture-status'] as RecallSdkEventType[])
213
+ .forEach(eventType => {
214
+ RecallAiSdk.addEventListener(eventType, (evt) => {
215
+ console.log(`RecallDesktopMain: ${eventType}:`, evt);
216
+ recallSdkStore.emitEvent({ type: eventType, data: evt });
217
+ broadcast(eventType, evt);
218
+ });
219
+ });
220
+
221
+ // Permission status
222
+ RecallAiSdk.addEventListener('permission-status', (evt) => {
223
+ console.log('RecallDesktopMain: Permission status:', evt);
224
+ recallSdkStore.setPermissionStatus(evt.permission, evt.status);
225
+ const type: RecallSdkEventType = 'permission-status';
226
+ recallSdkStore.emitEvent({ type, data: evt });
227
+ broadcast(type, evt);
228
+ });
229
+
230
+ // Error handling
231
+ RecallAiSdk.addEventListener('error', (evt) => {
232
+ console.error('RecallDesktopMain: SDK error:', evt);
233
+ const type: RecallSdkEventType = 'error';
234
+ recallSdkStore.emitEvent({ type, data: evt });
235
+ broadcast(type, evt);
236
+ });
237
+
238
+ // Shutdown
239
+ RecallAiSdk.addEventListener('shutdown', (evt) => {
240
+ console.log('RecallDesktopMain: SDK shutdown:', evt);
241
+ recallSdkStore.setSdkInitialized(false);
242
+ const type: RecallSdkEventType = 'shutdown';
243
+ recallSdkStore.emitEvent({ type, data: evt });
244
+ broadcast(type, evt);
245
+ });
246
+ }
247
+
248
+
249
+ private registerIpcHandlers(): void {
250
+ const addDestroyedCleanup = (wc: Electron.WebContents) => {
251
+ const id = wc.id;
252
+ if (this.trackedWebContents.has(id)) return;
253
+ this.trackedWebContents.set(id, wc);
254
+ wc.once('destroyed', () => {
255
+ // Remove this wc from all subscriptions
256
+ this.subscriptions.forEach((map) => {
257
+ map.delete(id);
258
+ });
259
+ this.trackedWebContents.delete(id);
260
+ });
261
+ };
262
+
263
+ // Subscribe to events
264
+ ipcMain.handle(IPC_CHANNELS.SUBSCRIBE_EVENTS, async (event, eventType: RecallSdkEventType): Promise<ApiResponse> => {
265
+ try {
266
+ const wc = event.sender;
267
+ addDestroyedCleanup(wc);
268
+ let map = this.subscriptions.get(eventType);
269
+ if (!map) {
270
+ map = new Map();
271
+ this.subscriptions.set(eventType, map);
272
+ }
273
+ const id = wc.id;
274
+ const prev = map.get(id) || 0;
275
+ map.set(id, prev + 1);
276
+ return { success: true, message: `Subscribed to ${eventType}` };
277
+ } catch (error) {
278
+ console.error('RecallDesktopMain: subscribe-events failed', error);
279
+ return { success: false, message: 'Failed to subscribe to events' };
280
+ }
281
+ });
282
+
283
+ // Unsubscribe from events
284
+ ipcMain.handle(IPC_CHANNELS.UNSUBSCRIBE_EVENTS, async (event, eventType: RecallSdkEventType): Promise<ApiResponse> => {
285
+ try {
286
+ const wc = event.sender;
287
+ const map = this.subscriptions.get(eventType);
288
+ if (map) {
289
+ const id = wc.id;
290
+ const prev = map.get(id) || 0;
291
+ if (prev <= 1) map.delete(id);
292
+ else map.set(id, prev - 1);
293
+ }
294
+ return { success: true, message: `Unsubscribed from ${eventType}` };
295
+ } catch (error) {
296
+ console.error('RecallDesktopMain: unsubscribe-events failed', error);
297
+ return { success: false, message: 'Failed to unsubscribe from events' };
298
+ }
299
+ });
300
+ // Initialize SDK
301
+ ipcMain.handle(IPC_CHANNELS.INIT_SDK, async (): Promise<ApiResponse> => {
302
+ try {
303
+ if (!recallSdkStore.isEnabled()) {
304
+ throw new RecallSdkError('Plugin is disabled', 'PLUGIN_DISABLED');
305
+ }
306
+
307
+ if (recallSdkStore.isSdkInitialized()) {
308
+ return { success: true, message: 'SDK already initialized' };
309
+ }
310
+
311
+ await this.initializeSdk();
312
+ return { success: true, message: 'SDK initialized successfully' };
313
+ } catch (error) {
314
+ console.error('RecallDesktopMain: SDK initialization failed:', error);
315
+ return {
316
+ success: false,
317
+ message: error instanceof RecallSdkError ? error.message : 'SDK initialization failed'
318
+ };
319
+ }
320
+ });
321
+
322
+ // Shutdown SDK
323
+ ipcMain.handle(IPC_CHANNELS.SHUTDOWN_SDK, async (): Promise<ApiResponse> => {
324
+ try {
325
+ await (RecallAiSdk.shutdown as any)();
326
+ recallSdkStore.clearState();
327
+ return { success: true, message: 'SDK shutdown successfully' };
328
+ } catch (error) {
329
+ console.error('RecallDesktopMain: SDK shutdown failed:', error);
330
+ return { success: false, message: 'SDK shutdown failed' };
331
+ }
332
+ });
333
+
334
+ // Get plugin status
335
+ ipcMain.handle(IPC_CHANNELS.GET_STATUS, async (): Promise<PluginStatus> => {
336
+ return {
337
+ initialized: this.isInitialized,
338
+ sdkInitialized: recallSdkStore.isSdkInitialized(),
339
+ version: this.version,
340
+ config: recallSdkStore.getConfig(),
341
+ sdkState: recallSdkStore.getSdkState(),
342
+ permissions: recallSdkStore.getPermissions()
343
+ };
344
+ });
345
+
346
+ // Start recording
347
+ ipcMain.handle(IPC_CHANNELS.START_RECORDING, async (event, request: StartRecordingRequest): Promise<ApiResponse> => {
348
+ try {
349
+ if (!recallSdkStore.isSdkInitialized()) {
350
+ throw new RecallSdkError('SDK not initialized', 'SDK_NOT_INITIALIZED');
351
+ }
352
+
353
+ await (RecallAiSdk.startRecording as any)({
354
+ windowId: request.windowId,
355
+ uploadToken: request.uploadToken
356
+ });
357
+
358
+ recallSdkStore.startRecording(request.windowId, request.uploadToken);
359
+ return { success: true, message: 'Recording started successfully' };
360
+ } catch (error) {
361
+ console.error('RecallDesktopMain: Start recording failed:', error);
362
+ return {
363
+ success: false,
364
+ message: error instanceof RecallSdkError ? error.message : 'Failed to start recording'
365
+ };
366
+ }
367
+ });
368
+
369
+ // Stop recording
370
+ ipcMain.handle(IPC_CHANNELS.STOP_RECORDING, async (event, request: StopRecordingRequest): Promise<ApiResponse> => {
371
+ try {
372
+ await (RecallAiSdk.stopRecording as any)({ windowId: request.windowId });
373
+ recallSdkStore.stopRecording(request.windowId);
374
+ return { success: true, message: 'Recording stopped successfully' };
375
+ } catch (error) {
376
+ console.error('RecallDesktopMain: Stop recording failed:', error);
377
+ return { success: false, message: 'Failed to stop recording' };
378
+ }
379
+ });
380
+
381
+ // Pause recording
382
+ ipcMain.handle(IPC_CHANNELS.PAUSE_RECORDING, async (event, request: PauseRecordingRequest): Promise<ApiResponse> => {
383
+ try {
384
+ await (RecallAiSdk.pauseRecording as any)({ windowId: request.windowId });
385
+ return { success: true, message: 'Recording paused successfully' };
386
+ } catch (error) {
387
+ console.error('RecallDesktopMain: Pause recording failed:', error);
388
+ return { success: false, message: 'Failed to pause recording' };
389
+ }
390
+ });
391
+
392
+ // Resume recording
393
+ ipcMain.handle(IPC_CHANNELS.RESUME_RECORDING, async (event, request: ResumeRecordingRequest): Promise<ApiResponse> => {
394
+ try {
395
+ await (RecallAiSdk.resumeRecording as any)({ windowId: request.windowId });
396
+ return { success: true, message: 'Recording resumed successfully' };
397
+ } catch (error) {
398
+ console.error('RecallDesktopMain: Resume recording failed:', error);
399
+ return { success: false, message: 'Failed to resume recording' };
400
+ }
401
+ });
402
+
403
+ // Upload recording
404
+ ipcMain.handle(IPC_CHANNELS.UPLOAD_RECORDING, async (event, request: UploadRecordingRequest): Promise<ApiResponse> => {
405
+ try {
406
+ await (RecallAiSdk.uploadRecording as any)({ windowId: request.windowId });
407
+ return { success: true, message: 'Recording upload started successfully' };
408
+ } catch (error) {
409
+ console.error('RecallDesktopMain: Upload recording failed:', error);
410
+ return { success: false, message: 'Failed to upload recording' };
411
+ }
412
+ });
413
+
414
+ // Prepare desktop audio recording
415
+ ipcMain.handle(IPC_CHANNELS.PREPARE_DESKTOP_AUDIO, async (): Promise<ApiResponse<PrepareDesktopAudioResponse>> => {
416
+ try {
417
+ const windowId = await (RecallAiSdk.prepareDesktopAudioRecording as any)();
418
+ return {
419
+ success: true,
420
+ message: 'Desktop audio recording prepared successfully',
421
+ data: { windowId }
422
+ };
423
+ } catch (error) {
424
+ console.error('RecallDesktopMain: Prepare desktop audio failed:', error);
425
+ return { success: false, message: 'Failed to prepare desktop audio recording' };
426
+ }
427
+ });
428
+
429
+ // Request permission
430
+ ipcMain.handle(IPC_CHANNELS.REQUEST_PERMISSION, async (event, permission: PermissionType): Promise<ApiResponse> => {
431
+ try {
432
+ await (RecallAiSdk.requestPermission as any)(permission);
433
+ return { success: true, message: `Permission request sent for ${permission}` };
434
+ } catch (error) {
435
+ console.error('RecallDesktopMain: Request permission failed:', error);
436
+ return { success: false, message: 'Failed to request permission' };
437
+ }
438
+ });
439
+
440
+ // Set configuration
441
+ ipcMain.handle(IPC_CHANNELS.SET_CONFIG, async (event, config: Partial<RecallSdkConfig>): Promise<ApiResponse> => {
442
+ try {
443
+ recallSdkStore.setConfig(config);
444
+
445
+ // If SDK settings changed and SDK is initialized, reinitialize
446
+ if (recallSdkStore.isSdkInitialized() && (config.apiUrl || config.requestPermissionsOnStartup !== undefined)) {
447
+ console.log('RecallDesktopMain: Reinitializing SDK due to configuration change');
448
+ await (RecallAiSdk.shutdown as any)();
449
+ await this.initializeSdk();
450
+ }
451
+
452
+ return { success: true, message: 'Configuration updated successfully' };
453
+ } catch (error) {
454
+ console.error('RecallDesktopMain: Failed to set configuration:', error);
455
+ return { success: false, message: 'Failed to update configuration' };
456
+ }
457
+ });
458
+
459
+ // Get configuration
460
+ ipcMain.handle(IPC_CHANNELS.GET_CONFIG, async (): Promise<ApiResponse<RecallSdkConfig>> => {
461
+ return {
462
+ success: true,
463
+ message: 'Configuration retrieved successfully',
464
+ data: recallSdkStore.getConfig()
465
+ };
466
+ });
467
+ }
468
+ }
469
+
470
+ // Initialize plugin
471
+ const recallDesktopMain = new RecallDesktopMain();
472
+ recallDesktopMain.initialize().catch(console.error);
473
+
474
+ export default recallDesktopMain;