@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.
- package/README.md +159 -3
- package/package.json +4 -2
- package/src/index.js +544 -200
- package/src/intelligence/index.js +12 -0
- package/src/intelligence/pattern-detector.js +338 -0
- package/src/intelligence/strategy-selector.js +362 -0
- package/src/intelligence/suggestion-engine.js +489 -0
- package/src/memory/index.js +17 -0
- package/src/memory/memory-engine.js +676 -0
- package/src/memory/stores/database.js +104 -0
- package/src/memory/utils/chunker.js +97 -0
- package/src/memory/utils/daily-logs.js +263 -0
- package/src/memory/utils/dashboard-client.js +141 -0
- package/src/memory/utils/embedder.js +217 -0
- package/src/memory/utils/enhanced-embedder.js +717 -0
- package/src/memory/utils/indexer.js +118 -0
- package/src/memory/utils/simple-embedder.js +234 -0
- package/src/memory/utils/smart-router.js +344 -0
- package/src/memory/utils/sync-engine.js +388 -0
- package/src/memory/utils/ultra-embedder.js +1448 -0
- package/src/memory/watchers/file-watcher.js +61 -0
- package/src/tools/memory_get.js +342 -0
- package/src/tools/memory_search.js +215 -0
|
@@ -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;
|