@mrxkun/mcfast-mcp 4.0.0 → 4.0.3

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,388 @@
1
+ /**
2
+ * Sync Engine
3
+ * Local ↔ Cloud synchronization for memory
4
+ * Uses MCFAST_TOKEN for authentication (same as Dashboard)
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import crypto from 'crypto';
11
+
12
+ export class SyncEngine {
13
+ constructor(options = {}) {
14
+ this.memoryPath = options.memoryPath || path.join(os.homedir(), '.mcfast', 'memory');
15
+
16
+ // Use Dashboard URL for sync (same as DashboardClient)
17
+ this.baseUrl = options.baseUrl || process.env.MCFAST_DASHBOARD_URL || 'https://mcfast.vercel.app';
18
+ this.apiKey = options.apiKey || process.env.MCFAST_TOKEN;
19
+
20
+ this.syncInterval = options.syncInterval || 5 * 60 * 1000; // 5 minutes
21
+ this.lastSyncTime = null;
22
+ this.syncQueue = [];
23
+ this.isOnline = true;
24
+ this.isSyncing = false;
25
+ this.listeners = new Set();
26
+
27
+ // Check online status
28
+ if (typeof window !== 'undefined') {
29
+ this.isOnline = navigator.onLine;
30
+ window.addEventListener('online', () => this.handleOnline());
31
+ window.addEventListener('offline', () => this.handleOffline());
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get headers for API calls
37
+ */
38
+ getHeaders() {
39
+ return {
40
+ 'Content-Type': 'application/json',
41
+ 'Authorization': `Bearer ${this.apiKey}`,
42
+ 'X-MCP-Client': 'mcfast-mcp-v4'
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Initialize sync engine
48
+ */
49
+ async initialize() {
50
+ // Load last sync time
51
+ const syncMetaPath = path.join(this.memoryPath, '.sync-meta.json');
52
+ if (fs.existsSync(syncMetaPath)) {
53
+ try {
54
+ const meta = JSON.parse(fs.readFileSync(syncMetaPath, 'utf-8'));
55
+ this.lastSyncTime = meta.lastSyncTime || null;
56
+ } catch (e) {
57
+ console.warn('[SyncEngine] Failed to load sync metadata');
58
+ }
59
+ }
60
+
61
+ // Start periodic sync
62
+ this.startPeriodicSync();
63
+
64
+ console.log(`[SyncEngine] Initialized. Last sync: ${this.lastSyncTime ? new Date(this.lastSyncTime).toISOString() : 'Never'}`);
65
+ }
66
+
67
+ /**
68
+ * Start periodic sync
69
+ */
70
+ startPeriodicSync() {
71
+ if (this.syncTimer) {
72
+ clearInterval(this.syncTimer);
73
+ }
74
+
75
+ this.syncTimer = setInterval(() => {
76
+ this.sync().catch(err => {
77
+ console.warn('[SyncEngine] Periodic sync failed:', err.message);
78
+ });
79
+ }, this.syncInterval);
80
+ }
81
+
82
+ /**
83
+ * Stop periodic sync
84
+ */
85
+ stopPeriodicSync() {
86
+ if (this.syncTimer) {
87
+ clearInterval(this.syncTimer);
88
+ this.syncTimer = null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Handle online event
94
+ */
95
+ handleOnline() {
96
+ console.log('[SyncEngine] Back online, triggering sync...');
97
+ this.isOnline = true;
98
+ this.sync().catch(err => console.warn('[SyncEngine] Sync after reconnect failed:', err.message));
99
+ this.emit('online');
100
+ }
101
+
102
+ /**
103
+ * Handle offline event
104
+ */
105
+ handleOffline() {
106
+ console.log('[SyncEngine] Went offline');
107
+ this.isOnline = false;
108
+ this.emit('offline');
109
+ }
110
+
111
+ /**
112
+ * Queue a change for sync
113
+ */
114
+ queueChange(change) {
115
+ const changeItem = {
116
+ ...change,
117
+ timestamp: Date.now(),
118
+ id: crypto.randomUUID()
119
+ };
120
+
121
+ this.syncQueue.push(changeItem);
122
+
123
+ // Persist queue
124
+ this.persistQueue();
125
+
126
+ // Trigger immediate sync if online
127
+ if (this.isOnline) {
128
+ this.sync().catch(err => console.warn('[SyncEngine] Immediate sync failed:', err.message));
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Persist sync queue to disk
134
+ */
135
+ persistQueue() {
136
+ const queuePath = path.join(this.memoryPath, '.sync-queue.json');
137
+ try {
138
+ fs.writeFileSync(queuePath, JSON.stringify(this.syncQueue));
139
+ } catch (e) {
140
+ console.warn('[SyncEngine] Failed to persist queue:', e.message);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Load persisted queue
146
+ */
147
+ loadQueue() {
148
+ const queuePath = path.join(this.memoryPath, '.sync-queue.json');
149
+ if (fs.existsSync(queuePath)) {
150
+ try {
151
+ this.syncQueue = JSON.parse(fs.readFileSync(queuePath, 'utf-8'));
152
+ } catch (e) {
153
+ this.syncQueue = [];
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Perform sync
160
+ */
161
+ async sync() {
162
+ if (!this.isOnline || this.isSyncing || !this.apiKey) {
163
+ return { status: !this.apiKey ? 'no_token' : this.isOnline ? 'idle' : 'offline', queued: this.syncQueue.length };
164
+ }
165
+
166
+ this.isSyncing = true;
167
+ const startTime = Date.now();
168
+
169
+ try {
170
+ // 1. Get local changes since last sync
171
+ const localChanges = await this.getLocalChanges();
172
+
173
+ // 2. Push local changes to cloud (via Dashboard API)
174
+ if (localChanges.length > 0) {
175
+ await this.pushToCloud(localChanges);
176
+ }
177
+
178
+ // 3. Pull cloud changes
179
+ const cloudChanges = await this.pullFromCloud();
180
+
181
+ // 4. Apply cloud changes to local
182
+ if (cloudChanges.length > 0) {
183
+ await this.applyCloudChanges(cloudChanges);
184
+ }
185
+
186
+ // 5. Update last sync time
187
+ this.lastSyncTime = Date.now();
188
+ this.saveSyncMeta();
189
+
190
+ // 6. Clear queue after successful sync
191
+ this.syncQueue = [];
192
+ this.persistQueue();
193
+
194
+ const duration = Date.now() - startTime;
195
+ const result = {
196
+ status: 'success',
197
+ pushed: localChanges.length,
198
+ pulled: cloudChanges.length,
199
+ duration: `${duration}ms`
200
+ };
201
+
202
+ console.log(`[SyncEngine] ✅ Sync complete:`, result);
203
+ this.emit('sync', result);
204
+
205
+ return result;
206
+
207
+ } catch (error) {
208
+ console.error('[SyncEngine] ❌ Sync failed:', error.message);
209
+ this.emit('error', error);
210
+ throw error;
211
+ } finally {
212
+ this.isSyncing = false;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Get local changes since last sync
218
+ */
219
+ async getLocalChanges() {
220
+ this.loadQueue();
221
+
222
+ const changes = [...this.syncQueue];
223
+
224
+ // Also check for new/modified files
225
+ if (fs.existsSync(this.memoryPath)) {
226
+ const files = fs.readdirSync(this.memoryPath)
227
+ .filter(f => f.endsWith('.md') && !f.startsWith('.'));
228
+
229
+ for (const file of files) {
230
+ const filePath = path.join(this.memoryPath, file);
231
+ const stats = fs.statSync(filePath);
232
+
233
+ // If file modified after last sync
234
+ if (!this.lastSyncTime || stats.mtimeMs > this.lastSyncTime) {
235
+ const content = fs.readFileSync(filePath, 'utf-8');
236
+ changes.push({
237
+ type: 'file',
238
+ action: 'upsert',
239
+ path: file,
240
+ content,
241
+ timestamp: stats.mtimeMs
242
+ });
243
+ }
244
+ }
245
+ }
246
+
247
+ return changes;
248
+ }
249
+
250
+ /**
251
+ * Push changes to cloud via Dashboard API
252
+ */
253
+ async pushToCloud(changes) {
254
+ const url = `${this.baseUrl}/api/v1/memory/sync/push`;
255
+
256
+ const response = await fetch(url, {
257
+ method: 'POST',
258
+ headers: this.getHeaders(),
259
+ body: JSON.stringify({
260
+ changes,
261
+ lastSyncTime: this.lastSyncTime
262
+ })
263
+ });
264
+
265
+ if (!response.ok) {
266
+ throw new Error(`Cloud push failed: ${response.status}`);
267
+ }
268
+
269
+ return response.json();
270
+ }
271
+
272
+ /**
273
+ * Pull changes from cloud via Dashboard API
274
+ */
275
+ async pullFromCloud() {
276
+ const url = `${this.baseUrl}/api/v1/memory/sync/pull?since=${this.lastSyncTime || 0}`;
277
+
278
+ const response = await fetch(url, {
279
+ method: 'GET',
280
+ headers: this.getHeaders()
281
+ });
282
+
283
+ if (!response.ok) {
284
+ throw new Error(`Cloud pull failed: ${response.status}`);
285
+ }
286
+
287
+ const data = await response.json();
288
+ return data.changes || [];
289
+ }
290
+
291
+ /**
292
+ * Apply cloud changes to local
293
+ */
294
+ async applyCloudChanges(changes) {
295
+ for (const change of changes) {
296
+ if (change.type === 'file') {
297
+ const filePath = path.join(this.memoryPath, change.path);
298
+
299
+ if (change.action === 'delete') {
300
+ if (fs.existsSync(filePath)) {
301
+ fs.unlinkSync(filePath);
302
+ }
303
+ } else {
304
+ // Ensure directory exists
305
+ const dir = path.dirname(filePath);
306
+ if (!fs.existsSync(dir)) {
307
+ fs.mkdirSync(dir, { recursive: true });
308
+ }
309
+ fs.writeFileSync(filePath, change.content);
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Save sync metadata
317
+ */
318
+ saveSyncMeta() {
319
+ const syncMetaPath = path.join(this.memoryPath, '.sync-meta.json');
320
+ fs.writeFileSync(syncMetaPath, JSON.stringify({
321
+ lastSyncTime: this.lastSyncTime,
322
+ updatedAt: new Date().toISOString()
323
+ }));
324
+ }
325
+
326
+ /**
327
+ * Force full resync
328
+ */
329
+ async fullResync() {
330
+ this.lastSyncTime = 0;
331
+ this.saveSyncMeta();
332
+ return this.sync();
333
+ }
334
+
335
+ /**
336
+ * Get sync status
337
+ */
338
+ getStatus() {
339
+ return {
340
+ isOnline: this.isOnline,
341
+ isSyncing: this.isSyncing,
342
+ lastSyncTime: this.lastSyncTime,
343
+ queueLength: this.syncQueue.length,
344
+ syncInterval: this.syncInterval,
345
+ hasToken: !!this.apiKey
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Add event listener
351
+ */
352
+ on(event, callback) {
353
+ if (!this.listeners.has(event)) {
354
+ this.listeners.set(event, new Set());
355
+ }
356
+ this.listeners.get(event).add(callback);
357
+ }
358
+
359
+ /**
360
+ * Remove event listener
361
+ */
362
+ off(event, callback) {
363
+ if (this.listeners.has(event)) {
364
+ this.listeners.get(event).delete(callback);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Emit event
370
+ */
371
+ emit(event, data) {
372
+ if (this.listeners.has(event)) {
373
+ for (const callback of this.listeners.get(event)) {
374
+ callback(data);
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Shutdown sync engine
381
+ */
382
+ shutdown() {
383
+ this.stopPeriodicSync();
384
+ this.persistQueue();
385
+ }
386
+ }
387
+
388
+ export default SyncEngine;