ms365-mcp-server 1.1.16 → 1.1.18
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/dist/index.js +1122 -9
- package/dist/utils/batch-performance-monitor.js +106 -0
- package/dist/utils/batch-test-scenarios.js +277 -0
- package/dist/utils/context-aware-search.js +499 -0
- package/dist/utils/cross-reference-detector.js +352 -0
- package/dist/utils/document-workflow.js +433 -0
- package/dist/utils/enhanced-fuzzy-search.js +514 -0
- package/dist/utils/error-handler.js +337 -0
- package/dist/utils/intelligence-engine.js +71 -0
- package/dist/utils/intelligent-cache.js +379 -0
- package/dist/utils/large-mailbox-search.js +599 -0
- package/dist/utils/ms365-operations.js +730 -208
- package/dist/utils/performance-monitor.js +395 -0
- package/dist/utils/proactive-intelligence.js +390 -0
- package/dist/utils/rate-limiter.js +284 -0
- package/dist/utils/search-batch-pipeline.js +222 -0
- package/dist/utils/thread-reconstruction.js +700 -0
- package/package.json +1 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { logger } from './api.js';
|
|
2
|
+
export class IntelligentCache {
|
|
3
|
+
constructor(maxSize = 50 * 1024 * 1024, // 50MB default
|
|
4
|
+
maxEntries = 10000, defaultTTL = 5 * 60 * 1000 // 5 minutes default
|
|
5
|
+
) {
|
|
6
|
+
this.cache = new Map();
|
|
7
|
+
this.stats = {
|
|
8
|
+
hits: 0,
|
|
9
|
+
misses: 0,
|
|
10
|
+
entries: 0,
|
|
11
|
+
size: 0,
|
|
12
|
+
hitRate: 0,
|
|
13
|
+
evictions: 0
|
|
14
|
+
};
|
|
15
|
+
this.maxSize = maxSize;
|
|
16
|
+
this.maxEntries = maxEntries;
|
|
17
|
+
this.defaultTTL = defaultTTL;
|
|
18
|
+
// Cleanup expired entries every minute
|
|
19
|
+
setInterval(() => this.cleanup(), 60000);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get value from cache
|
|
23
|
+
*/
|
|
24
|
+
get(key) {
|
|
25
|
+
const entry = this.cache.get(key);
|
|
26
|
+
if (!entry) {
|
|
27
|
+
this.stats.misses++;
|
|
28
|
+
this.updateHitRate();
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
// Check if entry is expired
|
|
32
|
+
if (this.isExpired(entry)) {
|
|
33
|
+
this.cache.delete(key);
|
|
34
|
+
this.stats.misses++;
|
|
35
|
+
this.stats.entries--;
|
|
36
|
+
this.stats.size -= entry.size;
|
|
37
|
+
this.updateHitRate();
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// Update access statistics
|
|
41
|
+
entry.accessCount++;
|
|
42
|
+
entry.lastAccessed = Date.now();
|
|
43
|
+
this.stats.hits++;
|
|
44
|
+
this.updateHitRate();
|
|
45
|
+
return entry.value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set value in cache
|
|
49
|
+
*/
|
|
50
|
+
set(key, value, options = {}) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const size = this.estimateSize(value);
|
|
53
|
+
const ttl = options.ttl || this.defaultTTL;
|
|
54
|
+
// Check if we need to evict entries
|
|
55
|
+
if (this.shouldEvict(size)) {
|
|
56
|
+
this.evictEntries(size);
|
|
57
|
+
}
|
|
58
|
+
// Create cache entry
|
|
59
|
+
const entry = {
|
|
60
|
+
key,
|
|
61
|
+
value,
|
|
62
|
+
timestamp: now,
|
|
63
|
+
accessCount: 1,
|
|
64
|
+
lastAccessed: now,
|
|
65
|
+
ttl,
|
|
66
|
+
size,
|
|
67
|
+
tags: options.tags || []
|
|
68
|
+
};
|
|
69
|
+
// Remove existing entry if it exists
|
|
70
|
+
if (this.cache.has(key)) {
|
|
71
|
+
const existingEntry = this.cache.get(key);
|
|
72
|
+
this.stats.size -= existingEntry.size;
|
|
73
|
+
this.stats.entries--;
|
|
74
|
+
}
|
|
75
|
+
// Add new entry
|
|
76
|
+
this.cache.set(key, entry);
|
|
77
|
+
this.stats.size += size;
|
|
78
|
+
this.stats.entries++;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Delete entry from cache
|
|
83
|
+
*/
|
|
84
|
+
delete(key) {
|
|
85
|
+
const entry = this.cache.get(key);
|
|
86
|
+
if (entry) {
|
|
87
|
+
this.cache.delete(key);
|
|
88
|
+
this.stats.size -= entry.size;
|
|
89
|
+
this.stats.entries--;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Clear cache by tags
|
|
96
|
+
*/
|
|
97
|
+
clearByTags(tags) {
|
|
98
|
+
let cleared = 0;
|
|
99
|
+
for (const [key, entry] of this.cache) {
|
|
100
|
+
if (entry.tags.some(tag => tags.includes(tag))) {
|
|
101
|
+
this.cache.delete(key);
|
|
102
|
+
this.stats.size -= entry.size;
|
|
103
|
+
this.stats.entries--;
|
|
104
|
+
cleared++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (cleared > 0) {
|
|
108
|
+
logger.log(`🗑️ Cleared ${cleared} cache entries by tags: ${tags.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
return cleared;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Clear all cache
|
|
114
|
+
*/
|
|
115
|
+
clear() {
|
|
116
|
+
this.cache.clear();
|
|
117
|
+
this.stats.entries = 0;
|
|
118
|
+
this.stats.size = 0;
|
|
119
|
+
logger.log('🗑️ Cache cleared');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get cache statistics
|
|
123
|
+
*/
|
|
124
|
+
getStats() {
|
|
125
|
+
return { ...this.stats };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get cache entries for debugging
|
|
129
|
+
*/
|
|
130
|
+
getEntries() {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
return Array.from(this.cache.entries()).map(([key, entry]) => ({
|
|
133
|
+
key,
|
|
134
|
+
size: entry.size,
|
|
135
|
+
age: now - entry.timestamp,
|
|
136
|
+
accessCount: entry.accessCount,
|
|
137
|
+
tags: entry.tags
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Check if entry is expired
|
|
142
|
+
*/
|
|
143
|
+
isExpired(entry) {
|
|
144
|
+
return Date.now() - entry.timestamp > entry.ttl;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Estimate size of object
|
|
148
|
+
*/
|
|
149
|
+
estimateSize(obj) {
|
|
150
|
+
try {
|
|
151
|
+
return JSON.stringify(obj).length * 2; // Rough estimate (UTF-16)
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return 1024; // Default size if can't stringify
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Check if we should evict entries
|
|
159
|
+
*/
|
|
160
|
+
shouldEvict(newEntrySize) {
|
|
161
|
+
return (this.stats.size + newEntrySize > this.maxSize ||
|
|
162
|
+
this.stats.entries >= this.maxEntries);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Evict entries using LRU strategy
|
|
166
|
+
*/
|
|
167
|
+
evictEntries(spaceNeeded) {
|
|
168
|
+
const entries = Array.from(this.cache.entries());
|
|
169
|
+
// Sort by last accessed time (LRU)
|
|
170
|
+
entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
|
|
171
|
+
let freedSpace = 0;
|
|
172
|
+
let evicted = 0;
|
|
173
|
+
for (const [key, entry] of entries) {
|
|
174
|
+
if (freedSpace >= spaceNeeded && this.stats.entries < this.maxEntries) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
this.cache.delete(key);
|
|
178
|
+
freedSpace += entry.size;
|
|
179
|
+
this.stats.size -= entry.size;
|
|
180
|
+
this.stats.entries--;
|
|
181
|
+
evicted++;
|
|
182
|
+
}
|
|
183
|
+
this.stats.evictions += evicted;
|
|
184
|
+
if (evicted > 0) {
|
|
185
|
+
logger.log(`🗑️ Evicted ${evicted} cache entries (freed ${freedSpace} bytes)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Cleanup expired entries
|
|
190
|
+
*/
|
|
191
|
+
cleanup() {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
let cleaned = 0;
|
|
194
|
+
for (const [key, entry] of this.cache) {
|
|
195
|
+
if (this.isExpired(entry)) {
|
|
196
|
+
this.cache.delete(key);
|
|
197
|
+
this.stats.size -= entry.size;
|
|
198
|
+
this.stats.entries--;
|
|
199
|
+
cleaned++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (cleaned > 0) {
|
|
203
|
+
logger.log(`🧹 Cleaned up ${cleaned} expired cache entries`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Update hit rate
|
|
208
|
+
*/
|
|
209
|
+
updateHitRate() {
|
|
210
|
+
const total = this.stats.hits + this.stats.misses;
|
|
211
|
+
this.stats.hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export class MS365CacheManager {
|
|
215
|
+
constructor() {
|
|
216
|
+
// Different cache configurations for different data types
|
|
217
|
+
this.searchCache = new IntelligentCache(20 * 1024 * 1024, // 20MB for search results
|
|
218
|
+
5000, // 5000 entries max
|
|
219
|
+
10 * 60 * 1000 // 10 minutes TTL
|
|
220
|
+
);
|
|
221
|
+
this.emailCache = new IntelligentCache(30 * 1024 * 1024, // 30MB for email content
|
|
222
|
+
3000, // 3000 entries max
|
|
223
|
+
15 * 60 * 1000 // 15 minutes TTL
|
|
224
|
+
);
|
|
225
|
+
this.metadataCache = new IntelligentCache(10 * 1024 * 1024, // 10MB for metadata
|
|
226
|
+
10000, // 10000 entries max
|
|
227
|
+
30 * 60 * 1000 // 30 minutes TTL
|
|
228
|
+
);
|
|
229
|
+
this.attachmentCache = new IntelligentCache(100 * 1024 * 1024, // 100MB for attachments
|
|
230
|
+
1000, // 1000 entries max
|
|
231
|
+
60 * 60 * 1000 // 1 hour TTL
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
static getInstance() {
|
|
235
|
+
if (!this.instance) {
|
|
236
|
+
this.instance = new MS365CacheManager();
|
|
237
|
+
}
|
|
238
|
+
return this.instance;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Cache search results
|
|
242
|
+
*/
|
|
243
|
+
cacheSearchResults(query, results, folder = 'inbox') {
|
|
244
|
+
const cacheKey = this.generateSearchKey(query, folder);
|
|
245
|
+
this.searchCache.set(cacheKey, results, {
|
|
246
|
+
ttl: 5 * 60 * 1000, // 5 minutes for search results
|
|
247
|
+
tags: ['search', folder],
|
|
248
|
+
priority: 'high'
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get cached search results
|
|
253
|
+
*/
|
|
254
|
+
getCachedSearchResults(query, folder = 'inbox') {
|
|
255
|
+
const cacheKey = this.generateSearchKey(query, folder);
|
|
256
|
+
return this.searchCache.get(cacheKey);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Cache email content
|
|
260
|
+
*/
|
|
261
|
+
cacheEmail(email) {
|
|
262
|
+
this.emailCache.set(email.id, email, {
|
|
263
|
+
ttl: 15 * 60 * 1000, // 15 minutes for email content
|
|
264
|
+
tags: ['email', email.from.address],
|
|
265
|
+
priority: 'medium'
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get cached email
|
|
270
|
+
*/
|
|
271
|
+
getCachedEmail(emailId) {
|
|
272
|
+
return this.emailCache.get(emailId);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Cache email metadata
|
|
276
|
+
*/
|
|
277
|
+
cacheEmailMetadata(emailId, metadata) {
|
|
278
|
+
this.metadataCache.set(`metadata:${emailId}`, metadata, {
|
|
279
|
+
ttl: 30 * 60 * 1000, // 30 minutes for metadata
|
|
280
|
+
tags: ['metadata'],
|
|
281
|
+
priority: 'low'
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Get cached email metadata
|
|
286
|
+
*/
|
|
287
|
+
getCachedEmailMetadata(emailId) {
|
|
288
|
+
return this.metadataCache.get(`metadata:${emailId}`);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Cache attachment data
|
|
292
|
+
*/
|
|
293
|
+
cacheAttachment(messageId, attachmentId, attachmentData) {
|
|
294
|
+
const cacheKey = `attachment:${messageId}:${attachmentId}`;
|
|
295
|
+
this.attachmentCache.set(cacheKey, attachmentData, {
|
|
296
|
+
ttl: 60 * 60 * 1000, // 1 hour for attachments
|
|
297
|
+
tags: ['attachment', messageId],
|
|
298
|
+
priority: 'medium'
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get cached attachment
|
|
303
|
+
*/
|
|
304
|
+
getCachedAttachment(messageId, attachmentId) {
|
|
305
|
+
const cacheKey = `attachment:${messageId}:${attachmentId}`;
|
|
306
|
+
return this.attachmentCache.get(cacheKey);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Invalidate caches when emails are modified
|
|
310
|
+
*/
|
|
311
|
+
invalidateEmailCaches(emailId) {
|
|
312
|
+
this.emailCache.delete(emailId);
|
|
313
|
+
this.metadataCache.delete(`metadata:${emailId}`);
|
|
314
|
+
this.attachmentCache.clearByTags([emailId]);
|
|
315
|
+
this.searchCache.clearByTags(['search']); // Clear search cache as it might be outdated
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Invalidate folder-specific caches
|
|
319
|
+
*/
|
|
320
|
+
invalidateFolderCaches(folder) {
|
|
321
|
+
this.searchCache.clearByTags([folder]);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get comprehensive cache statistics
|
|
325
|
+
*/
|
|
326
|
+
getStats() {
|
|
327
|
+
const searchStats = this.searchCache.getStats();
|
|
328
|
+
const emailStats = this.emailCache.getStats();
|
|
329
|
+
const metadataStats = this.metadataCache.getStats();
|
|
330
|
+
const attachmentStats = this.attachmentCache.getStats();
|
|
331
|
+
const totalSize = searchStats.size + emailStats.size + metadataStats.size + attachmentStats.size;
|
|
332
|
+
const totalEntries = searchStats.entries + emailStats.entries + metadataStats.entries + attachmentStats.entries;
|
|
333
|
+
const totalHits = searchStats.hits + emailStats.hits + metadataStats.hits + attachmentStats.hits;
|
|
334
|
+
const totalMisses = searchStats.misses + emailStats.misses + metadataStats.misses + attachmentStats.misses;
|
|
335
|
+
const totalHitRate = totalHits + totalMisses > 0 ? (totalHits / (totalHits + totalMisses)) * 100 : 0;
|
|
336
|
+
return {
|
|
337
|
+
search: searchStats,
|
|
338
|
+
email: emailStats,
|
|
339
|
+
metadata: metadataStats,
|
|
340
|
+
attachment: attachmentStats,
|
|
341
|
+
total: {
|
|
342
|
+
size: totalSize,
|
|
343
|
+
entries: totalEntries,
|
|
344
|
+
hitRate: totalHitRate
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Clear all caches
|
|
350
|
+
*/
|
|
351
|
+
clearAll() {
|
|
352
|
+
this.searchCache.clear();
|
|
353
|
+
this.emailCache.clear();
|
|
354
|
+
this.metadataCache.clear();
|
|
355
|
+
this.attachmentCache.clear();
|
|
356
|
+
logger.log('🗑️ All caches cleared');
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Generate cache key for search
|
|
360
|
+
*/
|
|
361
|
+
generateSearchKey(query, folder) {
|
|
362
|
+
// Create a normalized cache key that's case-insensitive and handles variations
|
|
363
|
+
const normalizedQuery = query.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
364
|
+
return `search:${folder}:${normalizedQuery}`;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Optimize cache performance
|
|
368
|
+
*/
|
|
369
|
+
optimize() {
|
|
370
|
+
// Force cleanup of expired entries
|
|
371
|
+
this.searchCache['cleanup']();
|
|
372
|
+
this.emailCache['cleanup']();
|
|
373
|
+
this.metadataCache['cleanup']();
|
|
374
|
+
this.attachmentCache['cleanup']();
|
|
375
|
+
logger.log('🔧 Cache optimization completed');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Export singleton instance
|
|
379
|
+
export const ms365Cache = MS365CacheManager.getInstance();
|