doc2vec 1.2.0 → 2.0.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/README.md +200 -8
- package/dist/content-processor copy.js +819 -0
- package/dist/content-processor.js +306 -60
- package/dist/database.js +13 -17
- package/dist/doc2vec.js +401 -7
- package/dist/electron-app/src/database.js +218 -0
- package/dist/electron-app/src/embeddings.js +80 -0
- package/dist/electron-app/src/main.js +627 -0
- package/dist/electron-app/src/mcp-server.js +496 -0
- package/dist/electron-app/src/preload.js +24 -0
- package/dist/electron-app/src/processor.js +248 -0
- package/dist/electron-app/src/syncer.js +146 -0
- package/dist/macos-app/migrate-to-vec0.js +156 -0
- package/package.json +4 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EmbeddingService = exports.InvalidApiKeyError = void 0;
|
|
7
|
+
const openai_1 = __importDefault(require("openai"));
|
|
8
|
+
class InvalidApiKeyError extends Error {
|
|
9
|
+
constructor(message = 'Invalid OpenAI API key') {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'InvalidApiKeyError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.InvalidApiKeyError = InvalidApiKeyError;
|
|
15
|
+
class EmbeddingService {
|
|
16
|
+
constructor(apiKey) {
|
|
17
|
+
this.model = 'text-embedding-3-large';
|
|
18
|
+
this._isValid = true;
|
|
19
|
+
this.client = new openai_1.default({ apiKey });
|
|
20
|
+
}
|
|
21
|
+
get isValid() {
|
|
22
|
+
return this._isValid;
|
|
23
|
+
}
|
|
24
|
+
async validateApiKey() {
|
|
25
|
+
try {
|
|
26
|
+
// Make a minimal API call to validate the key
|
|
27
|
+
await this.client.embeddings.create({
|
|
28
|
+
model: this.model,
|
|
29
|
+
input: 'test'
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error?.status === 401 || error?.code === 'invalid_api_key' ||
|
|
35
|
+
error?.message?.includes('Incorrect API key') ||
|
|
36
|
+
error?.message?.includes('invalid_api_key')) {
|
|
37
|
+
this._isValid = false;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// Other errors (rate limit, etc.) - key might still be valid
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async generateEmbedding(text) {
|
|
45
|
+
if (!this._isValid) {
|
|
46
|
+
throw new InvalidApiKeyError();
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const response = await this.client.embeddings.create({
|
|
50
|
+
model: this.model,
|
|
51
|
+
input: text
|
|
52
|
+
});
|
|
53
|
+
if (!response.data?.[0]?.embedding) {
|
|
54
|
+
throw new Error('Failed to get embedding from OpenAI');
|
|
55
|
+
}
|
|
56
|
+
return response.data[0].embedding;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Check for authentication errors (401)
|
|
60
|
+
if (error?.status === 401 || error?.code === 'invalid_api_key' ||
|
|
61
|
+
error?.message?.includes('Incorrect API key') ||
|
|
62
|
+
error?.message?.includes('invalid_api_key')) {
|
|
63
|
+
this._isValid = false;
|
|
64
|
+
throw new InvalidApiKeyError(error.message || 'Invalid OpenAI API key');
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async generateEmbeddings(texts) {
|
|
70
|
+
const embeddings = [];
|
|
71
|
+
for (const text of texts) {
|
|
72
|
+
const embedding = await this.generateEmbedding(text);
|
|
73
|
+
embeddings.push(embedding);
|
|
74
|
+
// Small delay to avoid rate limits
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
76
|
+
}
|
|
77
|
+
return embeddings;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.EmbeddingService = EmbeddingService;
|
|
@@ -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
|
+
});
|