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