doc2vec 1.3.0 → 2.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.
@@ -0,0 +1,627 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const electron_1 = require("electron");
40
+ const path = __importStar(require("path"));
41
+ const electron_store_1 = __importDefault(require("electron-store"));
42
+ const syncer_1 = require("./syncer");
43
+ const database_1 = require("./database");
44
+ const processor_1 = require("./processor");
45
+ const embeddings_1 = require("./embeddings");
46
+ const mcp_server_1 = require("./mcp-server");
47
+ // Store for persistent settings
48
+ const store = new electron_store_1.default({
49
+ defaults: {
50
+ watchedFolder: '',
51
+ databasePath: '',
52
+ openAIApiKey: '',
53
+ version: '1.0.0',
54
+ fileExtensions: '.md,.txt,.html,.pdf,.doc,.docx',
55
+ recursive: true,
56
+ mcpServerEnabled: false,
57
+ mcpServerPort: 3333
58
+ }
59
+ });
60
+ let mainWindow = null;
61
+ let tray = null;
62
+ let syncer = null;
63
+ let database = null;
64
+ let processor = null;
65
+ let embeddingService = null;
66
+ let mcpServer = null;
67
+ let currentStatus = 'idle'; // 'idle', 'watching', 'processing'
68
+ let processedCount = 0;
69
+ let totalChunks = 0;
70
+ let isQuitting = false;
71
+ function createWindow() {
72
+ mainWindow = new electron_1.BrowserWindow({
73
+ width: 600,
74
+ height: 750,
75
+ webPreferences: {
76
+ nodeIntegration: false,
77
+ contextIsolation: true,
78
+ preload: path.join(__dirname, 'preload.js')
79
+ },
80
+ title: 'Docs4ai',
81
+ show: false
82
+ });
83
+ // Load HTML from src directory (not dist)
84
+ mainWindow.loadFile(path.join(__dirname, '..', 'src', 'index.html'));
85
+ mainWindow.once('ready-to-show', () => {
86
+ mainWindow?.show();
87
+ });
88
+ mainWindow.on('close', (event) => {
89
+ if (process.platform === 'darwin' && !isQuitting) {
90
+ event.preventDefault();
91
+ mainWindow?.hide();
92
+ }
93
+ });
94
+ }
95
+ // Create tray icons for different states
96
+ function createTrayIcon(state) {
97
+ const size = 22; // macOS menu bar size
98
+ const canvas = Buffer.alloc(size * size * 4);
99
+ // Status colors
100
+ let statusColor = { r: 128, g: 128, b: 128 }; // Gray for idle
101
+ if (state === 'syncing') {
102
+ statusColor = { r: 52, g: 199, b: 89 }; // Green
103
+ }
104
+ else if (state === 'processing') {
105
+ statusColor = { r: 255, g: 149, b: 0 }; // Orange
106
+ }
107
+ // Draw a document shape
108
+ for (let y = 2; y < 20; y++) {
109
+ for (let x = 4; x < 18; x++) {
110
+ const idx = (y * size + x) * 4;
111
+ // Folded corner area (top right)
112
+ if (x >= 13 && y < 7 && (x - 13) + (7 - y) < 5) {
113
+ // Folded part - lighter
114
+ if ((x - 13) + (7 - y) === 4) {
115
+ canvas[idx] = 80;
116
+ canvas[idx + 1] = 80;
117
+ canvas[idx + 2] = 80;
118
+ canvas[idx + 3] = 255;
119
+ }
120
+ }
121
+ // Document outline
122
+ else if (y === 2 || y === 19 || x === 4 || x === 17 || (x === 13 && y < 7) || (y === 7 && x > 13)) {
123
+ canvas[idx] = 60;
124
+ canvas[idx + 1] = 60;
125
+ canvas[idx + 2] = 60;
126
+ canvas[idx + 3] = 255;
127
+ }
128
+ // Document fill
129
+ else if (y > 2 && y < 19 && x > 4 && x < 17) {
130
+ canvas[idx] = 40;
131
+ canvas[idx + 1] = 40;
132
+ canvas[idx + 2] = 40;
133
+ canvas[idx + 3] = 180;
134
+ }
135
+ }
136
+ }
137
+ // Draw status indicator dot (bottom right)
138
+ const dotX = 15;
139
+ const dotY = 17;
140
+ const dotRadius = 3;
141
+ for (let dy = -dotRadius; dy <= dotRadius; dy++) {
142
+ for (let dx = -dotRadius; dx <= dotRadius; dx++) {
143
+ if (dx * dx + dy * dy <= dotRadius * dotRadius) {
144
+ const px = dotX + dx;
145
+ const py = dotY + dy;
146
+ if (px >= 0 && px < size && py >= 0 && py < size) {
147
+ const idx = (py * size + px) * 4;
148
+ canvas[idx] = statusColor.r;
149
+ canvas[idx + 1] = statusColor.g;
150
+ canvas[idx + 2] = statusColor.b;
151
+ canvas[idx + 3] = 255;
152
+ }
153
+ }
154
+ }
155
+ }
156
+ const icon = electron_1.nativeImage.createFromBuffer(canvas, { width: size, height: size });
157
+ if (process.platform === 'darwin') {
158
+ icon.setTemplateImage(true);
159
+ }
160
+ return icon;
161
+ }
162
+ function updateTray() {
163
+ if (!tray)
164
+ return;
165
+ const isSyncing = syncer?.isSyncing ?? false;
166
+ const state = currentStatus === 'processing' ? 'processing' : (isSyncing ? 'syncing' : 'idle');
167
+ tray.setImage(createTrayIcon(state));
168
+ let tooltip = 'Docs4ai';
169
+ if (currentStatus === 'processing') {
170
+ tooltip = `Docs4ai - Processing...`;
171
+ }
172
+ else if (isSyncing) {
173
+ tooltip = `Docs4ai - Syncing (${processedCount} files, ${totalChunks} chunks)`;
174
+ }
175
+ else {
176
+ tooltip = 'Docs4ai - Not syncing';
177
+ }
178
+ tray.setToolTip(tooltip);
179
+ // Update context menu
180
+ const contextMenu = electron_1.Menu.buildFromTemplate([
181
+ {
182
+ label: isSyncing ? '● Syncing' : '○ Not syncing',
183
+ enabled: false
184
+ },
185
+ {
186
+ label: `Files: ${processedCount} | Chunks: ${totalChunks}`,
187
+ enabled: false
188
+ },
189
+ { type: 'separator' },
190
+ { label: 'Show Docs4ai', click: () => mainWindow?.show() },
191
+ { type: 'separator' },
192
+ {
193
+ label: isSyncing ? 'Stop Sync' : 'Start Sync',
194
+ click: async () => {
195
+ if (isSyncing) {
196
+ syncCancelled = true;
197
+ syncer?.stop();
198
+ syncer = null;
199
+ sendStats();
200
+ updateTray();
201
+ }
202
+ else {
203
+ // Start watching using the same logic as IPC handler
204
+ await startWatchingInternal();
205
+ }
206
+ }
207
+ },
208
+ { type: 'separator' },
209
+ { label: 'Quit', click: () => electron_1.app.quit() }
210
+ ]);
211
+ tray.setContextMenu(contextMenu);
212
+ }
213
+ function createTray() {
214
+ tray = new electron_1.Tray(createTrayIcon('idle'));
215
+ updateTray();
216
+ tray.on('click', () => {
217
+ mainWindow?.show();
218
+ });
219
+ }
220
+ // IPC Handlers
221
+ function setupIpcHandlers() {
222
+ // Get settings
223
+ electron_1.ipcMain.handle('get-settings', () => {
224
+ return {
225
+ watchedFolder: store.get('watchedFolder'),
226
+ databasePath: store.get('databasePath'),
227
+ openAIApiKey: store.get('openAIApiKey'),
228
+ version: store.get('version'),
229
+ fileExtensions: store.get('fileExtensions'),
230
+ recursive: store.get('recursive'),
231
+ mcpServerEnabled: store.get('mcpServerEnabled'),
232
+ mcpServerPort: store.get('mcpServerPort')
233
+ };
234
+ });
235
+ // Save settings
236
+ electron_1.ipcMain.handle('save-settings', (_event, settings) => {
237
+ Object.entries(settings).forEach(([key, value]) => {
238
+ store.set(key, value);
239
+ });
240
+ // Update embedding service if API key changed
241
+ if (settings.openAIApiKey) {
242
+ embeddingService = new embeddings_1.EmbeddingService(settings.openAIApiKey);
243
+ // Also update MCP server
244
+ if (mcpServer) {
245
+ mcpServer.setApiKey(settings.openAIApiKey);
246
+ }
247
+ }
248
+ return true;
249
+ });
250
+ // Start MCP server
251
+ electron_1.ipcMain.handle('start-mcp-server', async () => {
252
+ const port = store.get('mcpServerPort');
253
+ const dbPath = store.get('databasePath');
254
+ const apiKey = store.get('openAIApiKey');
255
+ if (!dbPath) {
256
+ return { success: false, error: 'Database path not configured' };
257
+ }
258
+ if (!apiKey) {
259
+ return { success: false, error: 'OpenAI API key required for MCP server' };
260
+ }
261
+ try {
262
+ if (mcpServer?.isRunning()) {
263
+ await mcpServer.stop();
264
+ }
265
+ mcpServer = new mcp_server_1.McpServer(port);
266
+ mcpServer.setDatabase(dbPath);
267
+ mcpServer.setApiKey(apiKey);
268
+ await mcpServer.start();
269
+ store.set('mcpServerEnabled', true);
270
+ return { success: true, port };
271
+ }
272
+ catch (error) {
273
+ return { success: false, error: error.message };
274
+ }
275
+ });
276
+ // Stop MCP server
277
+ electron_1.ipcMain.handle('stop-mcp-server', async () => {
278
+ try {
279
+ if (mcpServer) {
280
+ await mcpServer.stop();
281
+ mcpServer = null;
282
+ }
283
+ store.set('mcpServerEnabled', false);
284
+ return { success: true };
285
+ }
286
+ catch (error) {
287
+ return { success: false, error: error.message };
288
+ }
289
+ });
290
+ // Get MCP server status
291
+ electron_1.ipcMain.handle('get-mcp-status', () => {
292
+ return {
293
+ running: mcpServer?.isRunning() ?? false,
294
+ port: mcpServer?.getPort() ?? store.get('mcpServerPort')
295
+ };
296
+ });
297
+ // Select folder
298
+ electron_1.ipcMain.handle('select-folder', async () => {
299
+ const result = await electron_1.dialog.showOpenDialog(mainWindow, {
300
+ properties: ['openDirectory']
301
+ });
302
+ if (!result.canceled && result.filePaths.length > 0) {
303
+ return result.filePaths[0];
304
+ }
305
+ return null;
306
+ });
307
+ // Select database file
308
+ electron_1.ipcMain.handle('select-database', async () => {
309
+ const result = await electron_1.dialog.showSaveDialog(mainWindow, {
310
+ defaultPath: 'docsync.db',
311
+ filters: [{ name: 'SQLite Database', extensions: ['db'] }]
312
+ });
313
+ if (!result.canceled && result.filePath) {
314
+ return result.filePath;
315
+ }
316
+ return null;
317
+ });
318
+ // Get stats
319
+ electron_1.ipcMain.handle('get-stats', () => {
320
+ return {
321
+ isSyncing: syncer?.isSyncing ?? false,
322
+ trackedFiles: database?.getTrackedFilesCount() ?? 0,
323
+ totalChunks: database?.getTotalChunksCount() ?? 0
324
+ };
325
+ });
326
+ // Get all tracked files with info
327
+ electron_1.ipcMain.handle('get-files', () => {
328
+ if (!database)
329
+ return [];
330
+ return database.getAllTrackedFilesWithInfo();
331
+ });
332
+ // Get chunks for a specific file
333
+ electron_1.ipcMain.handle('get-chunks', (_event, filePath) => {
334
+ if (!database)
335
+ return [];
336
+ return database.getChunksForFile(filePath);
337
+ });
338
+ // Start watching
339
+ electron_1.ipcMain.handle('start-watching', async () => {
340
+ return await startWatchingInternal();
341
+ });
342
+ // Stop watching
343
+ electron_1.ipcMain.handle('stop-watching', () => {
344
+ syncCancelled = true; // Cancel any running sync
345
+ syncer?.stop();
346
+ syncer = null;
347
+ sendStats();
348
+ return { success: true };
349
+ });
350
+ // Force full sync
351
+ electron_1.ipcMain.handle('force-sync', async () => {
352
+ const version = store.get('version');
353
+ if (!database) {
354
+ return { success: false, error: 'Database not initialized' };
355
+ }
356
+ database.clearAllData();
357
+ await performInitialSync(version, true); // Force reprocess all
358
+ sendStats();
359
+ return { success: true };
360
+ });
361
+ }
362
+ // Internal function to start watching - used by both IPC handler and tray menu
363
+ async function startWatchingInternal() {
364
+ const watchedFolder = store.get('watchedFolder');
365
+ const databasePath = store.get('databasePath');
366
+ const openAIApiKey = store.get('openAIApiKey');
367
+ const version = store.get('version');
368
+ const fileExtensions = store.get('fileExtensions');
369
+ const recursive = store.get('recursive');
370
+ if (!watchedFolder) {
371
+ return { success: false, error: 'No folder selected' };
372
+ }
373
+ if (!databasePath) {
374
+ return { success: false, error: 'No database path selected' };
375
+ }
376
+ if (!openAIApiKey) {
377
+ return { success: false, error: 'OpenAI API key is required for generating embeddings' };
378
+ }
379
+ try {
380
+ // Initialize database
381
+ database = new database_1.DatabaseManager(databasePath);
382
+ // Initialize processor
383
+ processor = new processor_1.ContentProcessor();
384
+ // Initialize embedding service (required for semantic search)
385
+ embeddingService = new embeddings_1.EmbeddingService(openAIApiKey);
386
+ // Validate API key before starting sync
387
+ console.log('Validating OpenAI API key...');
388
+ try {
389
+ const isValid = await embeddingService.validateApiKey();
390
+ if (!isValid) {
391
+ embeddingService = null;
392
+ return { success: false, error: 'Invalid OpenAI API key' };
393
+ }
394
+ console.log('API key validated successfully');
395
+ }
396
+ catch (error) {
397
+ embeddingService = null;
398
+ return { success: false, error: `Failed to validate API key: ${error.message}` };
399
+ }
400
+ // Parse extensions
401
+ const extensions = fileExtensions.split(',').map(e => e.trim()).filter(e => e);
402
+ // Create syncer
403
+ syncer = new syncer_1.FolderSyncer(watchedFolder, {
404
+ recursive,
405
+ extensions,
406
+ onFileAdd: async (filePath) => {
407
+ await processFile(filePath, version);
408
+ sendStats();
409
+ },
410
+ onFileChange: async (filePath) => {
411
+ await processFile(filePath, version);
412
+ sendStats();
413
+ },
414
+ onFileDelete: async (filePath) => {
415
+ database?.removeChunksForFile(filePath);
416
+ database?.removeFileInfo(filePath);
417
+ sendStats();
418
+ }
419
+ });
420
+ syncer.start();
421
+ // Reset sync cancelled flag
422
+ syncCancelled = false;
423
+ // Initial sync
424
+ await performInitialSync(version);
425
+ sendStats();
426
+ updateTray();
427
+ return { success: true };
428
+ }
429
+ catch (error) {
430
+ console.error('Error starting syncer:', error);
431
+ return { success: false, error: error.message };
432
+ }
433
+ }
434
+ // Flag to track if sync is in progress and should continue
435
+ let syncCancelled = false;
436
+ async function processFile(filePath, version, forceReprocess = false) {
437
+ if (!database || !processor)
438
+ return;
439
+ // Check if sync was cancelled
440
+ if (syncCancelled)
441
+ return;
442
+ try {
443
+ // Check if file needs processing (incremental sync)
444
+ if (!forceReprocess) {
445
+ const fileInfo = database.getFileInfo(filePath);
446
+ if (fileInfo) {
447
+ // Get file's current modification time
448
+ const fs = require('fs');
449
+ try {
450
+ const stats = fs.statSync(filePath);
451
+ const currentModTime = stats.mtime;
452
+ // Skip if file hasn't been modified since last sync
453
+ if (currentModTime <= fileInfo.modifiedAt) {
454
+ console.log(`Skipping (unchanged): ${filePath}`);
455
+ return;
456
+ }
457
+ }
458
+ catch {
459
+ // File might not exist, continue processing
460
+ }
461
+ }
462
+ }
463
+ currentStatus = 'processing';
464
+ updateTray();
465
+ const content = await processor.readFile(filePath);
466
+ if (!content) {
467
+ currentStatus = syncer?.isSyncing ? 'syncing' : 'idle';
468
+ updateTray();
469
+ return;
470
+ }
471
+ // Check content hash - skip if content is identical
472
+ const hash = processor.generateHash(content);
473
+ if (!forceReprocess) {
474
+ const fileInfo = database.getFileInfo(filePath);
475
+ if (fileInfo && fileInfo.hash === hash) {
476
+ console.log(`Skipping (same hash): ${filePath}`);
477
+ // Update modification time even if content unchanged
478
+ database.upsertFileInfo(filePath, hash, new Date(), fileInfo.chunkCount);
479
+ currentStatus = syncer?.isSyncing ? 'syncing' : 'idle';
480
+ updateTray();
481
+ return;
482
+ }
483
+ }
484
+ const chunks = processor.chunkContent(content, filePath, version);
485
+ // Remove old chunks
486
+ database.removeChunksForFile(filePath);
487
+ // Generate embeddings and insert chunks
488
+ for (const chunk of chunks) {
489
+ let embedding = null;
490
+ if (embeddingService) {
491
+ try {
492
+ embedding = await embeddingService.generateEmbedding(chunk.content);
493
+ }
494
+ catch (error) {
495
+ if (error instanceof embeddings_1.InvalidApiKeyError) {
496
+ console.error('Invalid API key detected - stopping sync');
497
+ handleInvalidApiKey();
498
+ return; // Stop processing this file
499
+ }
500
+ console.error('Error generating embedding:', error);
501
+ }
502
+ }
503
+ database.insertChunk(chunk, embedding);
504
+ }
505
+ // Update file info with current timestamp
506
+ database.upsertFileInfo(filePath, hash, new Date(), chunks.length);
507
+ console.log(`Processed: ${filePath} (${chunks.length} chunks)`);
508
+ }
509
+ catch (error) {
510
+ console.error(`Error processing ${filePath}:`, error);
511
+ }
512
+ finally {
513
+ currentStatus = syncer?.isSyncing ? 'syncing' : 'idle';
514
+ updateTray();
515
+ }
516
+ }
517
+ async function performInitialSync(version, forceReprocess = false) {
518
+ if (!syncer || !database)
519
+ return;
520
+ const files = syncer.getSyncedFiles();
521
+ console.log(`Initial sync: ${files.length} files (force=${forceReprocess})`);
522
+ let processed = 0;
523
+ let skipped = 0;
524
+ for (let i = 0; i < files.length; i++) {
525
+ // Check if sync was cancelled
526
+ if (syncCancelled) {
527
+ console.log('Sync cancelled');
528
+ break;
529
+ }
530
+ const filePath = files[i];
531
+ // Check if file needs processing before logging
532
+ const fileInfo = database.getFileInfo(filePath);
533
+ const needsProcessing = forceReprocess || !fileInfo;
534
+ if (!needsProcessing && fileInfo) {
535
+ const fs = require('fs');
536
+ try {
537
+ const stats = fs.statSync(filePath);
538
+ if (stats.mtime <= fileInfo.modifiedAt) {
539
+ skipped++;
540
+ continue; // Skip unchanged files silently
541
+ }
542
+ }
543
+ catch {
544
+ // Process if we can't stat
545
+ }
546
+ }
547
+ console.log(`Processing ${i + 1}/${files.length}: ${filePath}`);
548
+ await processFile(filePath, version, forceReprocess);
549
+ processed++;
550
+ // Update stats after each file during initial sync
551
+ sendStats();
552
+ }
553
+ if (!syncCancelled) {
554
+ console.log(`Sync complete: ${processed} processed, ${skipped} skipped (unchanged)`);
555
+ }
556
+ }
557
+ function sendStats() {
558
+ processedCount = database?.getTrackedFilesCount() ?? 0;
559
+ totalChunks = database?.getTotalChunksCount() ?? 0;
560
+ const stats = {
561
+ isSyncing: syncer?.isSyncing ?? false,
562
+ trackedFiles: processedCount,
563
+ totalChunks: totalChunks
564
+ };
565
+ console.log('Sending stats:', stats);
566
+ if (mainWindow && !mainWindow.isDestroyed()) {
567
+ try {
568
+ mainWindow.webContents.send('stats-update', stats);
569
+ }
570
+ catch (err) {
571
+ console.error('Error sending stats:', err);
572
+ }
573
+ }
574
+ updateTray();
575
+ }
576
+ function handleInvalidApiKey() {
577
+ // Cancel sync and stop watching
578
+ syncCancelled = true;
579
+ if (syncer) {
580
+ syncer.stop();
581
+ syncer = null;
582
+ }
583
+ // Clear the embedding service
584
+ embeddingService = null;
585
+ // Notify the UI
586
+ if (mainWindow && !mainWindow.isDestroyed()) {
587
+ try {
588
+ mainWindow.webContents.send('api-key-error', 'Invalid OpenAI API key. Please check your API key and try again.');
589
+ mainWindow.show(); // Bring window to front
590
+ }
591
+ catch (err) {
592
+ console.error('Error sending API key error:', err);
593
+ }
594
+ }
595
+ sendStats();
596
+ updateTray();
597
+ }
598
+ electron_1.app.whenReady().then(() => {
599
+ createWindow();
600
+ createTray();
601
+ setupIpcHandlers();
602
+ electron_1.app.on('activate', () => {
603
+ if (electron_1.BrowserWindow.getAllWindows().length === 0) {
604
+ createWindow();
605
+ }
606
+ else {
607
+ mainWindow?.show();
608
+ }
609
+ });
610
+ });
611
+ electron_1.app.on('window-all-closed', () => {
612
+ if (process.platform !== 'darwin') {
613
+ electron_1.app.quit();
614
+ }
615
+ });
616
+ electron_1.app.on('before-quit', async () => {
617
+ isQuitting = true;
618
+ syncCancelled = true;
619
+ console.log('Quitting app...');
620
+ syncer?.stop();
621
+ if (mcpServer?.isRunning()) {
622
+ await mcpServer.stop();
623
+ }
624
+ database?.close();
625
+ tray?.destroy();
626
+ console.log('Cleanup complete');
627
+ });