crawlforge-mcp-server 3.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/CLAUDE.md +315 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/package.json +115 -0
- package/server.js +1963 -0
- package/setup.js +112 -0
- package/src/constants/config.js +615 -0
- package/src/core/ActionExecutor.js +1104 -0
- package/src/core/AlertNotificationSystem.js +601 -0
- package/src/core/AuthManager.js +315 -0
- package/src/core/ChangeTracker.js +2306 -0
- package/src/core/JobManager.js +687 -0
- package/src/core/LLMsTxtAnalyzer.js +753 -0
- package/src/core/LocalizationManager.js +1615 -0
- package/src/core/PerformanceManager.js +828 -0
- package/src/core/ResearchOrchestrator.js +1327 -0
- package/src/core/SnapshotManager.js +1037 -0
- package/src/core/StealthBrowserManager.js +1795 -0
- package/src/core/WebhookDispatcher.js +745 -0
- package/src/core/analysis/ContentAnalyzer.js +749 -0
- package/src/core/analysis/LinkAnalyzer.js +972 -0
- package/src/core/cache/CacheManager.js +821 -0
- package/src/core/connections/ConnectionPool.js +553 -0
- package/src/core/crawlers/BFSCrawler.js +845 -0
- package/src/core/integrations/PerformanceIntegration.js +377 -0
- package/src/core/llm/AnthropicProvider.js +135 -0
- package/src/core/llm/LLMManager.js +415 -0
- package/src/core/llm/LLMProvider.js +97 -0
- package/src/core/llm/OpenAIProvider.js +127 -0
- package/src/core/processing/BrowserProcessor.js +986 -0
- package/src/core/processing/ContentProcessor.js +505 -0
- package/src/core/processing/PDFProcessor.js +448 -0
- package/src/core/processing/StreamProcessor.js +673 -0
- package/src/core/queue/QueueManager.js +98 -0
- package/src/core/workers/WorkerPool.js +585 -0
- package/src/core/workers/worker.js +743 -0
- package/src/monitoring/healthCheck.js +600 -0
- package/src/monitoring/metrics.js +761 -0
- package/src/optimization/wave3-optimizations.js +932 -0
- package/src/security/security-patches.js +120 -0
- package/src/security/security-tests.js +355 -0
- package/src/security/wave3-security.js +652 -0
- package/src/tools/advanced/BatchScrapeTool.js +1089 -0
- package/src/tools/advanced/ScrapeWithActionsTool.js +669 -0
- package/src/tools/crawl/crawlDeep.js +449 -0
- package/src/tools/crawl/mapSite.js +400 -0
- package/src/tools/extract/analyzeContent.js +624 -0
- package/src/tools/extract/extractContent.js +329 -0
- package/src/tools/extract/processDocument.js +503 -0
- package/src/tools/extract/summarizeContent.js +376 -0
- package/src/tools/llmstxt/generateLLMsTxt.js +570 -0
- package/src/tools/research/deepResearch.js +706 -0
- package/src/tools/search/adapters/duckduckgoSearch.js +398 -0
- package/src/tools/search/adapters/googleSearch.js +236 -0
- package/src/tools/search/adapters/searchProviderFactory.js +96 -0
- package/src/tools/search/queryExpander.js +543 -0
- package/src/tools/search/ranking/ResultDeduplicator.js +676 -0
- package/src/tools/search/ranking/ResultRanker.js +497 -0
- package/src/tools/search/searchWeb.js +482 -0
- package/src/tools/tracking/trackChanges.js +1355 -0
- package/src/utils/CircuitBreaker.js +515 -0
- package/src/utils/ErrorHandlingConfig.js +342 -0
- package/src/utils/HumanBehaviorSimulator.js +569 -0
- package/src/utils/Logger.js +568 -0
- package/src/utils/MemoryMonitor.js +173 -0
- package/src/utils/RetryManager.js +386 -0
- package/src/utils/contentUtils.js +588 -0
- package/src/utils/domainFilter.js +612 -0
- package/src/utils/inputValidation.js +766 -0
- package/src/utils/rateLimiter.js +196 -0
- package/src/utils/robotsChecker.js +91 -0
- package/src/utils/securityMiddleware.js +416 -0
- package/src/utils/sitemapParser.js +678 -0
- package/src/utils/ssrfProtection.js +640 -0
- package/src/utils/urlNormalizer.js +168 -0
|
@@ -0,0 +1,1355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrackChanges Tool - Change Tracking MCP Tool
|
|
3
|
+
* Provides baseline capture, comparison, scheduled monitoring,
|
|
4
|
+
* and change notification capabilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import ChangeTracker from '../../core/ChangeTracker.js';
|
|
9
|
+
import SnapshotManager from '../../core/SnapshotManager.js';
|
|
10
|
+
import CacheManager from '../../core/cache/CacheManager.js';
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
|
|
13
|
+
// Input validation schemas
|
|
14
|
+
const TrackChangesSchema = z.object({
|
|
15
|
+
url: z.string().url(),
|
|
16
|
+
operation: z.enum([
|
|
17
|
+
'create_baseline',
|
|
18
|
+
'compare',
|
|
19
|
+
'monitor',
|
|
20
|
+
'get_history',
|
|
21
|
+
'get_stats',
|
|
22
|
+
// Enhanced Phase 2.4 operations
|
|
23
|
+
'create_scheduled_monitor',
|
|
24
|
+
'stop_scheduled_monitor',
|
|
25
|
+
'get_dashboard',
|
|
26
|
+
'export_history',
|
|
27
|
+
'create_alert_rule',
|
|
28
|
+
'generate_trend_report',
|
|
29
|
+
'get_monitoring_templates'
|
|
30
|
+
]).default('compare'),
|
|
31
|
+
|
|
32
|
+
// Content options
|
|
33
|
+
content: z.string().optional(),
|
|
34
|
+
html: z.string().optional(),
|
|
35
|
+
|
|
36
|
+
// Tracking options
|
|
37
|
+
trackingOptions: z.object({
|
|
38
|
+
granularity: z.enum(['page', 'section', 'element', 'text']).default('section'),
|
|
39
|
+
trackText: z.boolean().default(true),
|
|
40
|
+
trackStructure: z.boolean().default(true),
|
|
41
|
+
trackAttributes: z.boolean().default(false),
|
|
42
|
+
trackImages: z.boolean().default(false),
|
|
43
|
+
trackLinks: z.boolean().default(true),
|
|
44
|
+
ignoreWhitespace: z.boolean().default(true),
|
|
45
|
+
ignoreCase: z.boolean().default(false),
|
|
46
|
+
customSelectors: z.array(z.string()).optional(),
|
|
47
|
+
excludeSelectors: z.array(z.string()).optional().default([
|
|
48
|
+
'script', 'style', 'noscript', '.advertisement', '.ad', '#comments'
|
|
49
|
+
]),
|
|
50
|
+
significanceThresholds: z.object({
|
|
51
|
+
minor: z.number().min(0).max(1).default(0.1),
|
|
52
|
+
moderate: z.number().min(0).max(1).default(0.3),
|
|
53
|
+
major: z.number().min(0).max(1).default(0.7)
|
|
54
|
+
}).optional()
|
|
55
|
+
}).optional().default({}),
|
|
56
|
+
|
|
57
|
+
// Monitoring options
|
|
58
|
+
monitoringOptions: z.object({
|
|
59
|
+
enabled: z.boolean().default(false),
|
|
60
|
+
interval: z.number().min(60000).max(24 * 60 * 60 * 1000).default(300000), // 5 minutes to 24 hours
|
|
61
|
+
maxRetries: z.number().min(0).max(5).default(3),
|
|
62
|
+
retryDelay: z.number().min(1000).max(60000).default(5000),
|
|
63
|
+
notificationThreshold: z.enum(['minor', 'moderate', 'major', 'critical']).default('moderate'),
|
|
64
|
+
enableWebhook: z.boolean().default(false),
|
|
65
|
+
webhookUrl: z.string().url().optional(),
|
|
66
|
+
webhookSecret: z.string().optional()
|
|
67
|
+
}).optional(),
|
|
68
|
+
|
|
69
|
+
// Storage options
|
|
70
|
+
storageOptions: z.object({
|
|
71
|
+
enableSnapshots: z.boolean().default(true),
|
|
72
|
+
retainHistory: z.boolean().default(true),
|
|
73
|
+
maxHistoryEntries: z.number().min(1).max(1000).default(100),
|
|
74
|
+
compressionEnabled: z.boolean().default(true),
|
|
75
|
+
deltaStorageEnabled: z.boolean().default(true)
|
|
76
|
+
}).optional().default({}),
|
|
77
|
+
|
|
78
|
+
// Query options for history retrieval
|
|
79
|
+
queryOptions: z.object({
|
|
80
|
+
limit: z.number().min(1).max(500).default(50),
|
|
81
|
+
offset: z.number().min(0).default(0),
|
|
82
|
+
startTime: z.number().optional(),
|
|
83
|
+
endTime: z.number().optional(),
|
|
84
|
+
includeContent: z.boolean().default(false),
|
|
85
|
+
significanceFilter: z.enum(['all', 'minor', 'moderate', 'major', 'critical']).optional()
|
|
86
|
+
}).optional(),
|
|
87
|
+
|
|
88
|
+
// Notification options
|
|
89
|
+
notificationOptions: z.object({
|
|
90
|
+
email: z.object({
|
|
91
|
+
enabled: z.boolean().default(false),
|
|
92
|
+
recipients: z.array(z.string().email()).optional(),
|
|
93
|
+
subject: z.string().optional(),
|
|
94
|
+
includeDetails: z.boolean().default(true)
|
|
95
|
+
}).optional(),
|
|
96
|
+
webhook: z.object({
|
|
97
|
+
enabled: z.boolean().default(false),
|
|
98
|
+
url: z.string().url().optional(),
|
|
99
|
+
method: z.enum(['POST', 'PUT']).default('POST'),
|
|
100
|
+
headers: z.record(z.string()).optional(),
|
|
101
|
+
signingSecret: z.string().optional(),
|
|
102
|
+
includeContent: z.boolean().default(false)
|
|
103
|
+
}).optional(),
|
|
104
|
+
slack: z.object({
|
|
105
|
+
enabled: z.boolean().default(false),
|
|
106
|
+
webhookUrl: z.string().url().optional(),
|
|
107
|
+
channel: z.string().optional(),
|
|
108
|
+
username: z.string().optional()
|
|
109
|
+
}).optional()
|
|
110
|
+
}).optional(),
|
|
111
|
+
|
|
112
|
+
// Enhanced Phase 2.4 options
|
|
113
|
+
scheduledMonitorOptions: z.object({
|
|
114
|
+
schedule: z.string().optional(), // Cron expression
|
|
115
|
+
templateId: z.string().optional(), // Monitoring template ID
|
|
116
|
+
enabled: z.boolean().default(true)
|
|
117
|
+
}).optional(),
|
|
118
|
+
|
|
119
|
+
alertRuleOptions: z.object({
|
|
120
|
+
ruleId: z.string().optional(),
|
|
121
|
+
condition: z.string().optional(), // Condition description
|
|
122
|
+
actions: z.array(z.enum(['webhook', 'email', 'slack'])).optional(),
|
|
123
|
+
throttle: z.number().min(0).optional(),
|
|
124
|
+
priority: z.enum(['low', 'medium', 'high']).optional()
|
|
125
|
+
}).optional(),
|
|
126
|
+
|
|
127
|
+
exportOptions: z.object({
|
|
128
|
+
format: z.enum(['json', 'csv']).default('json'),
|
|
129
|
+
startTime: z.number().optional(),
|
|
130
|
+
endTime: z.number().optional(),
|
|
131
|
+
includeContent: z.boolean().default(false),
|
|
132
|
+
includeSnapshots: z.boolean().default(false)
|
|
133
|
+
}).optional(),
|
|
134
|
+
|
|
135
|
+
dashboardOptions: z.object({
|
|
136
|
+
includeRecentAlerts: z.boolean().default(true),
|
|
137
|
+
includeTrends: z.boolean().default(true),
|
|
138
|
+
includeMonitorStatus: z.boolean().default(true)
|
|
139
|
+
}).optional()
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export class TrackChangesTool extends EventEmitter {
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
super();
|
|
145
|
+
|
|
146
|
+
this.options = {
|
|
147
|
+
cacheEnabled: true,
|
|
148
|
+
cacheTTL: 3600000, // 1 hour
|
|
149
|
+
snapshotStorageDir: './snapshots',
|
|
150
|
+
enableRealTimeMonitoring: true,
|
|
151
|
+
maxConcurrentMonitors: 50,
|
|
152
|
+
defaultPollingInterval: 300000, // 5 minutes
|
|
153
|
+
...options
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Initialize components
|
|
157
|
+
this.changeTracker = new ChangeTracker({
|
|
158
|
+
enableRealTimeTracking: this.options.enableRealTimeMonitoring,
|
|
159
|
+
enableSemanticAnalysis: false, // Can be enabled if needed
|
|
160
|
+
contentSimilarityThreshold: 0.8
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.snapshotManager = new SnapshotManager({
|
|
164
|
+
storageDir: this.options.snapshotStorageDir,
|
|
165
|
+
enableCompression: true,
|
|
166
|
+
enableDeltaStorage: true,
|
|
167
|
+
cacheEnabled: this.options.cacheEnabled
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
this.cache = this.options.cacheEnabled ?
|
|
171
|
+
new CacheManager({ ttl: this.options.cacheTTL }) : null;
|
|
172
|
+
|
|
173
|
+
// Active monitors
|
|
174
|
+
this.activeMonitors = new Map();
|
|
175
|
+
this.monitorStats = new Map();
|
|
176
|
+
|
|
177
|
+
// Notification handlers
|
|
178
|
+
this.notificationHandlers = {
|
|
179
|
+
webhook: this.sendWebhookNotification.bind(this),
|
|
180
|
+
email: this.sendEmailNotification.bind(this),
|
|
181
|
+
slack: this.sendSlackNotification.bind(this)
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
this.initialize();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async initialize() {
|
|
188
|
+
try {
|
|
189
|
+
await this.snapshotManager.initialize();
|
|
190
|
+
|
|
191
|
+
// Set up event handlers
|
|
192
|
+
this.setupEventHandlers();
|
|
193
|
+
|
|
194
|
+
this.emit('initialized');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.emit('error', { operation: 'initialize', error: error.message });
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setupEventHandlers() {
|
|
202
|
+
// Handle change tracker events
|
|
203
|
+
this.changeTracker.on('changeDetected', (changeRecord) => {
|
|
204
|
+
this.handleChangeDetected(changeRecord);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.changeTracker.on('baselineCreated', (baseline) => {
|
|
208
|
+
this.emit('baselineCreated', baseline);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Handle snapshot manager events
|
|
212
|
+
this.snapshotManager.on('snapshotStored', (snapshot) => {
|
|
213
|
+
this.emit('snapshotStored', snapshot);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
this.snapshotManager.on('error', (error) => {
|
|
217
|
+
this.emit('error', error);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Execute the track changes tool
|
|
223
|
+
* @param {Object} params - Tool parameters
|
|
224
|
+
* @returns {Object} - Execution results
|
|
225
|
+
*/
|
|
226
|
+
async execute(params) {
|
|
227
|
+
try {
|
|
228
|
+
const validated = TrackChangesSchema.parse(params);
|
|
229
|
+
const { url, operation } = validated;
|
|
230
|
+
|
|
231
|
+
switch (operation) {
|
|
232
|
+
case 'create_baseline':
|
|
233
|
+
return await this.createBaseline(validated);
|
|
234
|
+
|
|
235
|
+
case 'compare':
|
|
236
|
+
return await this.compareWithBaseline(validated);
|
|
237
|
+
|
|
238
|
+
case 'monitor':
|
|
239
|
+
return await this.setupMonitoring(validated);
|
|
240
|
+
|
|
241
|
+
case 'get_history':
|
|
242
|
+
return await this.getChangeHistory(validated);
|
|
243
|
+
|
|
244
|
+
case 'get_stats':
|
|
245
|
+
return await this.getStatistics(validated);
|
|
246
|
+
|
|
247
|
+
// Enhanced Phase 2.4 operations
|
|
248
|
+
case 'create_scheduled_monitor':
|
|
249
|
+
return await this.createScheduledMonitor(validated);
|
|
250
|
+
|
|
251
|
+
case 'stop_scheduled_monitor':
|
|
252
|
+
return await this.stopScheduledMonitor(validated);
|
|
253
|
+
|
|
254
|
+
case 'get_dashboard':
|
|
255
|
+
return await this.getMonitoringDashboard(validated);
|
|
256
|
+
|
|
257
|
+
case 'export_history':
|
|
258
|
+
return await this.exportHistoricalData(validated);
|
|
259
|
+
|
|
260
|
+
case 'create_alert_rule':
|
|
261
|
+
return await this.createAlertRule(validated);
|
|
262
|
+
|
|
263
|
+
case 'generate_trend_report':
|
|
264
|
+
return await this.generateTrendReport(validated);
|
|
265
|
+
|
|
266
|
+
case 'get_monitoring_templates':
|
|
267
|
+
return await this.getMonitoringTemplates(validated);
|
|
268
|
+
|
|
269
|
+
default:
|
|
270
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
success: false,
|
|
276
|
+
error: error.message,
|
|
277
|
+
timestamp: Date.now()
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Create a baseline for change tracking
|
|
284
|
+
* @param {Object} params - Parameters
|
|
285
|
+
* @returns {Object} - Baseline creation results
|
|
286
|
+
*/
|
|
287
|
+
async createBaseline(params) {
|
|
288
|
+
const { url, content, html, trackingOptions, storageOptions } = params;
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Fetch content if not provided
|
|
292
|
+
let sourceContent = content || html;
|
|
293
|
+
let fetchMetadata = {};
|
|
294
|
+
|
|
295
|
+
if (!sourceContent) {
|
|
296
|
+
const fetchResult = await this.fetchContent(url);
|
|
297
|
+
sourceContent = fetchResult.content;
|
|
298
|
+
fetchMetadata = fetchResult.metadata;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Create baseline with change tracker
|
|
302
|
+
const baseline = await this.changeTracker.createBaseline(
|
|
303
|
+
url,
|
|
304
|
+
sourceContent,
|
|
305
|
+
trackingOptions
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Store snapshot if enabled
|
|
309
|
+
let snapshotInfo = null;
|
|
310
|
+
if (storageOptions.enableSnapshots) {
|
|
311
|
+
const snapshotResult = await this.snapshotManager.storeSnapshot(
|
|
312
|
+
url,
|
|
313
|
+
sourceContent,
|
|
314
|
+
{
|
|
315
|
+
...fetchMetadata,
|
|
316
|
+
baseline: true,
|
|
317
|
+
trackingOptions
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
snapshotInfo = snapshotResult;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
operation: 'create_baseline',
|
|
326
|
+
url,
|
|
327
|
+
baseline: {
|
|
328
|
+
version: baseline.version,
|
|
329
|
+
contentHash: baseline.analysis?.hashes?.page,
|
|
330
|
+
sections: Object.keys(baseline.analysis?.hashes?.sections || {}).length,
|
|
331
|
+
elements: Object.keys(baseline.analysis?.hashes?.elements || {}).length,
|
|
332
|
+
createdAt: baseline.timestamp,
|
|
333
|
+
options: trackingOptions
|
|
334
|
+
},
|
|
335
|
+
snapshot: snapshotInfo,
|
|
336
|
+
timestamp: Date.now()
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw new Error(`Failed to create baseline: ${error.message}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Compare current content with baseline
|
|
346
|
+
* @param {Object} params - Parameters
|
|
347
|
+
* @returns {Object} - Comparison results
|
|
348
|
+
*/
|
|
349
|
+
async compareWithBaseline(params) {
|
|
350
|
+
const { url, content, html, trackingOptions, storageOptions, notificationOptions } = params;
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Fetch current content if not provided
|
|
354
|
+
let currentContent = content || html;
|
|
355
|
+
let fetchMetadata = {};
|
|
356
|
+
|
|
357
|
+
if (!currentContent) {
|
|
358
|
+
const fetchResult = await this.fetchContent(url);
|
|
359
|
+
currentContent = fetchResult.content;
|
|
360
|
+
fetchMetadata = fetchResult.metadata;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Perform comparison
|
|
364
|
+
const comparisonResult = await this.changeTracker.compareWithBaseline(
|
|
365
|
+
url,
|
|
366
|
+
currentContent,
|
|
367
|
+
trackingOptions
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Store snapshot if changes detected and storage enabled
|
|
371
|
+
let snapshotInfo = null;
|
|
372
|
+
if (comparisonResult.hasChanges && storageOptions.enableSnapshots) {
|
|
373
|
+
const snapshotResult = await this.snapshotManager.storeSnapshot(
|
|
374
|
+
url,
|
|
375
|
+
currentContent,
|
|
376
|
+
{
|
|
377
|
+
...fetchMetadata,
|
|
378
|
+
changes: comparisonResult.summary,
|
|
379
|
+
significance: comparisonResult.significance
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
snapshotInfo = snapshotResult;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Send notifications if significant changes detected
|
|
386
|
+
if (comparisonResult.hasChanges && notificationOptions) {
|
|
387
|
+
await this.sendNotifications(url, comparisonResult, notificationOptions);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
operation: 'compare',
|
|
393
|
+
url,
|
|
394
|
+
hasChanges: comparisonResult.hasChanges,
|
|
395
|
+
significance: comparisonResult.significance,
|
|
396
|
+
changeType: comparisonResult.changeType,
|
|
397
|
+
summary: comparisonResult.summary,
|
|
398
|
+
details: comparisonResult.details,
|
|
399
|
+
metrics: comparisonResult.metrics,
|
|
400
|
+
recommendations: comparisonResult.recommendations,
|
|
401
|
+
snapshot: snapshotInfo,
|
|
402
|
+
timestamp: Date.now()
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
} catch (error) {
|
|
406
|
+
throw new Error(`Failed to compare with baseline: ${error.message}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Set up continuous monitoring for a URL
|
|
412
|
+
* @param {Object} params - Parameters
|
|
413
|
+
* @returns {Object} - Monitoring setup results
|
|
414
|
+
*/
|
|
415
|
+
async setupMonitoring(params) {
|
|
416
|
+
const { url, monitoringOptions, trackingOptions, storageOptions, notificationOptions } = params;
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
// Check if already monitoring this URL
|
|
420
|
+
if (this.activeMonitors.has(url)) {
|
|
421
|
+
const existing = this.activeMonitors.get(url);
|
|
422
|
+
clearInterval(existing.timer);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Create monitoring configuration
|
|
426
|
+
const monitorConfig = {
|
|
427
|
+
url,
|
|
428
|
+
options: {
|
|
429
|
+
...monitoringOptions,
|
|
430
|
+
trackingOptions,
|
|
431
|
+
storageOptions,
|
|
432
|
+
notificationOptions
|
|
433
|
+
},
|
|
434
|
+
stats: {
|
|
435
|
+
started: Date.now(),
|
|
436
|
+
checks: 0,
|
|
437
|
+
changesDetected: 0,
|
|
438
|
+
errors: 0,
|
|
439
|
+
lastCheck: null,
|
|
440
|
+
lastChange: null,
|
|
441
|
+
averageResponseTime: 0
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Set up polling timer
|
|
446
|
+
const timer = setInterval(async () => {
|
|
447
|
+
await this.performMonitoringCheck(url, monitorConfig);
|
|
448
|
+
}, monitoringOptions.interval);
|
|
449
|
+
|
|
450
|
+
monitorConfig.timer = timer;
|
|
451
|
+
|
|
452
|
+
// Store active monitor
|
|
453
|
+
this.activeMonitors.set(url, monitorConfig);
|
|
454
|
+
this.monitorStats.set(url, monitorConfig.stats);
|
|
455
|
+
|
|
456
|
+
// Perform initial check
|
|
457
|
+
await this.performMonitoringCheck(url, monitorConfig);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
success: true,
|
|
461
|
+
operation: 'monitor',
|
|
462
|
+
url,
|
|
463
|
+
monitoring: {
|
|
464
|
+
enabled: true,
|
|
465
|
+
interval: monitoringOptions.interval,
|
|
466
|
+
notificationThreshold: monitoringOptions.notificationThreshold,
|
|
467
|
+
startedAt: monitorConfig.stats.started
|
|
468
|
+
},
|
|
469
|
+
timestamp: Date.now()
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
} catch (error) {
|
|
473
|
+
throw new Error(`Failed to setup monitoring: ${error.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get change history for a URL
|
|
479
|
+
* @param {Object} params - Parameters
|
|
480
|
+
* @returns {Object} - Change history
|
|
481
|
+
*/
|
|
482
|
+
async getChangeHistory(params) {
|
|
483
|
+
const { url, queryOptions } = params;
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
// Get change history from change tracker
|
|
487
|
+
const changeHistory = this.changeTracker.getChangeHistory(url, queryOptions.limit);
|
|
488
|
+
|
|
489
|
+
// Get snapshot history from snapshot manager
|
|
490
|
+
const snapshotHistory = await this.snapshotManager.getChangeHistory(url, queryOptions);
|
|
491
|
+
|
|
492
|
+
// Merge and enrich history data
|
|
493
|
+
const combinedHistory = this.mergeHistoryData(changeHistory, snapshotHistory.history);
|
|
494
|
+
|
|
495
|
+
// Apply filters
|
|
496
|
+
let filteredHistory = combinedHistory;
|
|
497
|
+
if (queryOptions.significanceFilter && queryOptions.significanceFilter !== 'all') {
|
|
498
|
+
filteredHistory = combinedHistory.filter(entry =>
|
|
499
|
+
this.matchesSignificanceFilter(entry, queryOptions.significanceFilter)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Apply pagination
|
|
504
|
+
const start = queryOptions.offset || 0;
|
|
505
|
+
const end = start + (queryOptions.limit || 50);
|
|
506
|
+
const paginatedHistory = filteredHistory.slice(start, end);
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
success: true,
|
|
510
|
+
operation: 'get_history',
|
|
511
|
+
url,
|
|
512
|
+
history: paginatedHistory,
|
|
513
|
+
pagination: {
|
|
514
|
+
total: filteredHistory.length,
|
|
515
|
+
limit: queryOptions.limit,
|
|
516
|
+
offset: queryOptions.offset,
|
|
517
|
+
hasMore: end < filteredHistory.length
|
|
518
|
+
},
|
|
519
|
+
timespan: {
|
|
520
|
+
earliest: combinedHistory.length > 0 ?
|
|
521
|
+
combinedHistory[combinedHistory.length - 1].timestamp : null,
|
|
522
|
+
latest: combinedHistory.length > 0 ?
|
|
523
|
+
combinedHistory[0].timestamp : null,
|
|
524
|
+
totalEntries: combinedHistory.length
|
|
525
|
+
},
|
|
526
|
+
timestamp: Date.now()
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
} catch (error) {
|
|
530
|
+
throw new Error(`Failed to get change history: ${error.message}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Get statistics for change tracking
|
|
536
|
+
* @param {Object} params - Parameters
|
|
537
|
+
* @returns {Object} - Statistics
|
|
538
|
+
*/
|
|
539
|
+
async getStatistics(params) {
|
|
540
|
+
const { url } = params;
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
// Get change tracker stats
|
|
544
|
+
const changeTrackerStats = this.changeTracker.getStats();
|
|
545
|
+
|
|
546
|
+
// Get snapshot manager stats
|
|
547
|
+
const snapshotManagerStats = this.snapshotManager.getStats();
|
|
548
|
+
|
|
549
|
+
// Get monitoring stats
|
|
550
|
+
const monitoringStats = url ?
|
|
551
|
+
this.monitorStats.get(url) :
|
|
552
|
+
this.getAggregatedMonitoringStats();
|
|
553
|
+
|
|
554
|
+
// Get URL-specific stats if URL provided
|
|
555
|
+
let urlStats = null;
|
|
556
|
+
if (url) {
|
|
557
|
+
urlStats = await this.getUrlSpecificStats(url);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
success: true,
|
|
562
|
+
operation: 'get_stats',
|
|
563
|
+
url: url || 'global',
|
|
564
|
+
stats: {
|
|
565
|
+
changeTracking: changeTrackerStats,
|
|
566
|
+
snapshotStorage: snapshotManagerStats,
|
|
567
|
+
monitoring: monitoringStats,
|
|
568
|
+
urlSpecific: urlStats,
|
|
569
|
+
system: {
|
|
570
|
+
activeMonitors: this.activeMonitors.size,
|
|
571
|
+
cacheEnabled: !!this.cache,
|
|
572
|
+
cacheStats: this.cache ? this.cache.getStats() : null
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
timestamp: Date.now()
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
} catch (error) {
|
|
579
|
+
throw new Error(`Failed to get statistics: ${error.message}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Helper methods
|
|
584
|
+
|
|
585
|
+
async fetchContent(url) {
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetch(url, {
|
|
588
|
+
headers: {
|
|
589
|
+
'User-Agent': 'MCP-WebScraper-ChangeTracker/3.0',
|
|
590
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
591
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
592
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
593
|
+
'Cache-Control': 'no-cache'
|
|
594
|
+
},
|
|
595
|
+
timeout: 30000
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (!response.ok) {
|
|
599
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const content = await response.text();
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
content,
|
|
606
|
+
metadata: {
|
|
607
|
+
statusCode: response.status,
|
|
608
|
+
contentType: response.headers.get('content-type'),
|
|
609
|
+
contentLength: content.length,
|
|
610
|
+
lastModified: response.headers.get('last-modified'),
|
|
611
|
+
etag: response.headers.get('etag'),
|
|
612
|
+
fetchedAt: Date.now()
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
} catch (error) {
|
|
617
|
+
throw new Error(`Failed to fetch content: ${error.message}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async performMonitoringCheck(url, monitorConfig) {
|
|
622
|
+
const startTime = Date.now();
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
monitorConfig.stats.checks++;
|
|
626
|
+
|
|
627
|
+
// Fetch current content
|
|
628
|
+
const fetchResult = await this.fetchContent(url);
|
|
629
|
+
|
|
630
|
+
// Perform comparison
|
|
631
|
+
const comparisonResult = await this.changeTracker.compareWithBaseline(
|
|
632
|
+
url,
|
|
633
|
+
fetchResult.content,
|
|
634
|
+
monitorConfig.options.trackingOptions
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// Update stats
|
|
638
|
+
const responseTime = Date.now() - startTime;
|
|
639
|
+
monitorConfig.stats.averageResponseTime =
|
|
640
|
+
(monitorConfig.stats.averageResponseTime * (monitorConfig.stats.checks - 1) + responseTime) /
|
|
641
|
+
monitorConfig.stats.checks;
|
|
642
|
+
|
|
643
|
+
monitorConfig.stats.lastCheck = Date.now();
|
|
644
|
+
|
|
645
|
+
// Handle changes if detected
|
|
646
|
+
if (comparisonResult.hasChanges) {
|
|
647
|
+
monitorConfig.stats.changesDetected++;
|
|
648
|
+
monitorConfig.stats.lastChange = Date.now();
|
|
649
|
+
|
|
650
|
+
// Check if change meets notification threshold
|
|
651
|
+
if (this.meetsNotificationThreshold(
|
|
652
|
+
comparisonResult.significance,
|
|
653
|
+
monitorConfig.options.notificationThreshold
|
|
654
|
+
)) {
|
|
655
|
+
// Store snapshot if enabled
|
|
656
|
+
if (monitorConfig.options.storageOptions?.enableSnapshots) {
|
|
657
|
+
await this.snapshotManager.storeSnapshot(
|
|
658
|
+
url,
|
|
659
|
+
fetchResult.content,
|
|
660
|
+
{
|
|
661
|
+
...fetchResult.metadata,
|
|
662
|
+
changes: comparisonResult.summary,
|
|
663
|
+
significance: comparisonResult.significance,
|
|
664
|
+
monitoring: true
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Send notifications
|
|
670
|
+
if (monitorConfig.options.notificationOptions) {
|
|
671
|
+
await this.sendNotifications(
|
|
672
|
+
url,
|
|
673
|
+
comparisonResult,
|
|
674
|
+
monitorConfig.options.notificationOptions
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
this.emit('monitoringCheck', {
|
|
681
|
+
url,
|
|
682
|
+
hasChanges: comparisonResult.hasChanges,
|
|
683
|
+
significance: comparisonResult.significance,
|
|
684
|
+
responseTime,
|
|
685
|
+
timestamp: Date.now()
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
} catch (error) {
|
|
689
|
+
monitorConfig.stats.errors++;
|
|
690
|
+
|
|
691
|
+
this.emit('monitoringError', {
|
|
692
|
+
url,
|
|
693
|
+
error: error.message,
|
|
694
|
+
timestamp: Date.now()
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// If too many errors, disable monitoring
|
|
698
|
+
if (monitorConfig.stats.errors > monitorConfig.options.maxRetries) {
|
|
699
|
+
this.stopMonitoring(url);
|
|
700
|
+
|
|
701
|
+
this.emit('monitoringDisabled', {
|
|
702
|
+
url,
|
|
703
|
+
reason: 'Too many errors',
|
|
704
|
+
totalErrors: monitorConfig.stats.errors
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async sendNotifications(url, changeResult, notificationOptions) {
|
|
711
|
+
const notifications = [];
|
|
712
|
+
|
|
713
|
+
if (notificationOptions.webhook?.enabled) {
|
|
714
|
+
notifications.push(this.sendWebhookNotification(url, changeResult, notificationOptions.webhook));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (notificationOptions.email?.enabled) {
|
|
718
|
+
notifications.push(this.sendEmailNotification(url, changeResult, notificationOptions.email));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (notificationOptions.slack?.enabled) {
|
|
722
|
+
notifications.push(this.sendSlackNotification(url, changeResult, notificationOptions.slack));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
await Promise.allSettled(notifications);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async sendWebhookNotification(url, changeResult, webhookConfig) {
|
|
729
|
+
try {
|
|
730
|
+
const payload = {
|
|
731
|
+
event: 'change_detected',
|
|
732
|
+
url,
|
|
733
|
+
timestamp: Date.now(),
|
|
734
|
+
significance: changeResult.significance,
|
|
735
|
+
changeType: changeResult.changeType,
|
|
736
|
+
summary: changeResult.summary,
|
|
737
|
+
details: webhookConfig.includeContent ? changeResult.details : undefined
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const response = await fetch(webhookConfig.url, {
|
|
741
|
+
method: webhookConfig.method || 'POST',
|
|
742
|
+
headers: {
|
|
743
|
+
'Content-Type': 'application/json',
|
|
744
|
+
'User-Agent': 'MCP-WebScraper-ChangeTracker/3.0',
|
|
745
|
+
...webhookConfig.headers
|
|
746
|
+
},
|
|
747
|
+
body: JSON.stringify(payload)
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (!response.ok) {
|
|
751
|
+
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
this.emit('notificationSent', {
|
|
755
|
+
type: 'webhook',
|
|
756
|
+
url,
|
|
757
|
+
success: true
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
} catch (error) {
|
|
761
|
+
this.emit('notificationError', {
|
|
762
|
+
type: 'webhook',
|
|
763
|
+
url,
|
|
764
|
+
error: error.message
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async sendEmailNotification(url, changeResult, emailConfig) {
|
|
770
|
+
// Email notification implementation would go here
|
|
771
|
+
// This would integrate with email service providers
|
|
772
|
+
this.emit('notificationSent', {
|
|
773
|
+
type: 'email',
|
|
774
|
+
url,
|
|
775
|
+
success: true,
|
|
776
|
+
note: 'Email notifications require external service integration'
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async sendSlackNotification(url, changeResult, slackConfig) {
|
|
781
|
+
try {
|
|
782
|
+
const payload = {
|
|
783
|
+
text: `🔄 Content Change Detected`,
|
|
784
|
+
attachments: [{
|
|
785
|
+
color: this.getSlackColor(changeResult.significance),
|
|
786
|
+
fields: [
|
|
787
|
+
{
|
|
788
|
+
title: 'URL',
|
|
789
|
+
value: url,
|
|
790
|
+
short: false
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
title: 'Significance',
|
|
794
|
+
value: changeResult.significance.toUpperCase(),
|
|
795
|
+
short: true
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
title: 'Change Type',
|
|
799
|
+
value: changeResult.changeType.replace('_', ' '),
|
|
800
|
+
short: true
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
title: 'Summary',
|
|
804
|
+
value: changeResult.summary.changeDescription,
|
|
805
|
+
short: false
|
|
806
|
+
}
|
|
807
|
+
],
|
|
808
|
+
timestamp: Math.floor(Date.now() / 1000)
|
|
809
|
+
}],
|
|
810
|
+
channel: slackConfig.channel,
|
|
811
|
+
username: slackConfig.username || 'Change Tracker'
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const response = await fetch(slackConfig.webhookUrl, {
|
|
815
|
+
method: 'POST',
|
|
816
|
+
headers: {
|
|
817
|
+
'Content-Type': 'application/json'
|
|
818
|
+
},
|
|
819
|
+
body: JSON.stringify(payload)
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
if (!response.ok) {
|
|
823
|
+
throw new Error(`Slack notification failed: ${response.status}`);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
this.emit('notificationSent', {
|
|
827
|
+
type: 'slack',
|
|
828
|
+
url,
|
|
829
|
+
success: true
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
} catch (error) {
|
|
833
|
+
this.emit('notificationError', {
|
|
834
|
+
type: 'slack',
|
|
835
|
+
url,
|
|
836
|
+
error: error.message
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
mergeHistoryData(changeHistory, snapshotHistory) {
|
|
842
|
+
// Merge change tracker history with snapshot history
|
|
843
|
+
const merged = [];
|
|
844
|
+
|
|
845
|
+
// Add change history entries
|
|
846
|
+
changeHistory.forEach(entry => {
|
|
847
|
+
merged.push({
|
|
848
|
+
...entry,
|
|
849
|
+
source: 'change_tracker',
|
|
850
|
+
hasSnapshot: false
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// Add snapshot history entries
|
|
855
|
+
snapshotHistory.forEach(entry => {
|
|
856
|
+
const existing = merged.find(m =>
|
|
857
|
+
Math.abs(m.timestamp - entry.timestamp) < 60000 // Within 1 minute
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
if (existing) {
|
|
861
|
+
existing.hasSnapshot = true;
|
|
862
|
+
existing.snapshotId = entry.snapshotId;
|
|
863
|
+
} else {
|
|
864
|
+
merged.push({
|
|
865
|
+
...entry,
|
|
866
|
+
source: 'snapshot',
|
|
867
|
+
hasSnapshot: true
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Sort by timestamp (newest first)
|
|
873
|
+
return merged.sort((a, b) => b.timestamp - a.timestamp);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
matchesSignificanceFilter(entry, filter) {
|
|
877
|
+
const significanceLevels = ['none', 'minor', 'moderate', 'major', 'critical'];
|
|
878
|
+
const entryLevel = significanceLevels.indexOf(entry.significance || 'none');
|
|
879
|
+
const filterLevel = significanceLevels.indexOf(filter);
|
|
880
|
+
|
|
881
|
+
return entryLevel >= filterLevel;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
meetsNotificationThreshold(significance, threshold) {
|
|
885
|
+
const levels = ['none', 'minor', 'moderate', 'major', 'critical'];
|
|
886
|
+
const significanceLevel = levels.indexOf(significance);
|
|
887
|
+
const thresholdLevel = levels.indexOf(threshold);
|
|
888
|
+
|
|
889
|
+
return significanceLevel >= thresholdLevel;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
getSlackColor(significance) {
|
|
893
|
+
const colors = {
|
|
894
|
+
'none': '#36a64f',
|
|
895
|
+
'minor': '#ffeb3b',
|
|
896
|
+
'moderate': '#ff9800',
|
|
897
|
+
'major': '#f44336',
|
|
898
|
+
'critical': '#9c27b0'
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
return colors[significance] || '#36a64f';
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
async getUrlSpecificStats(url) {
|
|
905
|
+
try {
|
|
906
|
+
const changeHistory = this.changeTracker.getChangeHistory(url, 100);
|
|
907
|
+
const snapshotHistory = await this.snapshotManager.querySnapshots({
|
|
908
|
+
url,
|
|
909
|
+
limit: 100,
|
|
910
|
+
includeContent: false
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
return {
|
|
914
|
+
totalChanges: changeHistory.length,
|
|
915
|
+
totalSnapshots: snapshotHistory.snapshots.length,
|
|
916
|
+
lastChange: changeHistory.length > 0 ? changeHistory[0].timestamp : null,
|
|
917
|
+
averageChangeInterval: this.calculateAverageInterval(changeHistory),
|
|
918
|
+
significanceDistribution: this.calculateSignificanceDistribution(changeHistory),
|
|
919
|
+
isBeingMonitored: this.activeMonitors.has(url)
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
} catch (error) {
|
|
923
|
+
return {
|
|
924
|
+
error: error.message
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
getAggregatedMonitoringStats() {
|
|
930
|
+
const stats = {
|
|
931
|
+
totalMonitors: this.activeMonitors.size,
|
|
932
|
+
totalChecks: 0,
|
|
933
|
+
totalChanges: 0,
|
|
934
|
+
totalErrors: 0,
|
|
935
|
+
averageResponseTime: 0,
|
|
936
|
+
oldestMonitor: null,
|
|
937
|
+
newestMonitor: null
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const allStats = Array.from(this.monitorStats.values());
|
|
941
|
+
|
|
942
|
+
if (allStats.length === 0) return stats;
|
|
943
|
+
|
|
944
|
+
stats.totalChecks = allStats.reduce((sum, s) => sum + s.checks, 0);
|
|
945
|
+
stats.totalChanges = allStats.reduce((sum, s) => sum + s.changesDetected, 0);
|
|
946
|
+
stats.totalErrors = allStats.reduce((sum, s) => sum + s.errors, 0);
|
|
947
|
+
stats.averageResponseTime = allStats.reduce((sum, s) => sum + s.averageResponseTime, 0) / allStats.length;
|
|
948
|
+
stats.oldestMonitor = Math.min(...allStats.map(s => s.started));
|
|
949
|
+
stats.newestMonitor = Math.max(...allStats.map(s => s.started));
|
|
950
|
+
|
|
951
|
+
return stats;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
calculateAverageInterval(changeHistory) {
|
|
955
|
+
if (changeHistory.length < 2) return null;
|
|
956
|
+
|
|
957
|
+
let totalInterval = 0;
|
|
958
|
+
for (let i = 1; i < changeHistory.length; i++) {
|
|
959
|
+
totalInterval += changeHistory[i - 1].timestamp - changeHistory[i].timestamp;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return totalInterval / (changeHistory.length - 1);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
calculateSignificanceDistribution(changeHistory) {
|
|
966
|
+
const distribution = {
|
|
967
|
+
none: 0,
|
|
968
|
+
minor: 0,
|
|
969
|
+
moderate: 0,
|
|
970
|
+
major: 0,
|
|
971
|
+
critical: 0
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
changeHistory.forEach(entry => {
|
|
975
|
+
const significance = entry.significance || 'none';
|
|
976
|
+
if (distribution.hasOwnProperty(significance)) {
|
|
977
|
+
distribution[significance]++;
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
return distribution;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async handleChangeDetected(changeRecord) {
|
|
985
|
+
// Store snapshot if significant change
|
|
986
|
+
if (changeRecord.significance !== 'none') {
|
|
987
|
+
try {
|
|
988
|
+
await this.snapshotManager.storeSnapshot(
|
|
989
|
+
changeRecord.url,
|
|
990
|
+
changeRecord.details.current || '',
|
|
991
|
+
{
|
|
992
|
+
changes: changeRecord.details,
|
|
993
|
+
significance: changeRecord.significance,
|
|
994
|
+
changeType: changeRecord.changeType
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
} catch (error) {
|
|
998
|
+
this.emit('error', {
|
|
999
|
+
operation: 'storeChangeSnapshot',
|
|
1000
|
+
url: changeRecord.url,
|
|
1001
|
+
error: error.message
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Public API methods
|
|
1008
|
+
|
|
1009
|
+
stopMonitoring(url) {
|
|
1010
|
+
if (this.activeMonitors.has(url)) {
|
|
1011
|
+
const monitor = this.activeMonitors.get(url);
|
|
1012
|
+
clearInterval(monitor.timer);
|
|
1013
|
+
this.activeMonitors.delete(url);
|
|
1014
|
+
|
|
1015
|
+
this.emit('monitoringStopped', { url });
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
stopAllMonitoring() {
|
|
1022
|
+
const urls = Array.from(this.activeMonitors.keys());
|
|
1023
|
+
urls.forEach(url => this.stopMonitoring(url));
|
|
1024
|
+
|
|
1025
|
+
this.emit('allMonitoringStopped', { count: urls.length });
|
|
1026
|
+
return urls.length;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
getActiveMonitors() {
|
|
1030
|
+
return Array.from(this.activeMonitors.keys()).map(url => ({
|
|
1031
|
+
url,
|
|
1032
|
+
config: this.activeMonitors.get(url).options,
|
|
1033
|
+
stats: this.monitorStats.get(url)
|
|
1034
|
+
}));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Create scheduled monitor using enhanced features
|
|
1039
|
+
* @param {Object} params - Parameters
|
|
1040
|
+
* @returns {Object} - Scheduled monitor results
|
|
1041
|
+
*/
|
|
1042
|
+
async createScheduledMonitor(params) {
|
|
1043
|
+
const { url, scheduledMonitorOptions, trackingOptions, notificationOptions } = params;
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
const schedule = scheduledMonitorOptions?.schedule || '0 */1 * * *'; // Hourly default
|
|
1047
|
+
const templateId = scheduledMonitorOptions?.templateId;
|
|
1048
|
+
|
|
1049
|
+
// Apply template if specified
|
|
1050
|
+
let monitorOptions = { ...trackingOptions };
|
|
1051
|
+
if (templateId && this.changeTracker.monitoringTemplates.has(templateId)) {
|
|
1052
|
+
const template = this.changeTracker.monitoringTemplates.get(templateId);
|
|
1053
|
+
monitorOptions = { ...template.options, ...monitorOptions };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Create scheduled monitor
|
|
1057
|
+
const result = await this.changeTracker.createScheduledMonitor(
|
|
1058
|
+
url,
|
|
1059
|
+
schedule,
|
|
1060
|
+
{
|
|
1061
|
+
...monitorOptions,
|
|
1062
|
+
alertRules: {
|
|
1063
|
+
threshold: 'moderate',
|
|
1064
|
+
methods: ['webhook'],
|
|
1065
|
+
throttle: 600000,
|
|
1066
|
+
...notificationOptions
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
);
|
|
1070
|
+
|
|
1071
|
+
return {
|
|
1072
|
+
success: true,
|
|
1073
|
+
operation: 'create_scheduled_monitor',
|
|
1074
|
+
url,
|
|
1075
|
+
monitor: result,
|
|
1076
|
+
template: templateId ? this.changeTracker.monitoringTemplates.get(templateId)?.name : null,
|
|
1077
|
+
timestamp: Date.now()
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
throw new Error(`Failed to create scheduled monitor: ${error.message}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Stop scheduled monitor
|
|
1087
|
+
* @param {Object} params - Parameters
|
|
1088
|
+
* @returns {Object} - Stop results
|
|
1089
|
+
*/
|
|
1090
|
+
async stopScheduledMonitor(params) {
|
|
1091
|
+
const { url } = params;
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
// Find and stop the scheduled monitor for this URL
|
|
1095
|
+
let stoppedMonitors = 0;
|
|
1096
|
+
|
|
1097
|
+
for (const [id, monitor] of this.changeTracker.scheduledMonitors.entries()) {
|
|
1098
|
+
if (monitor.url === url) {
|
|
1099
|
+
if (monitor.cronJob) {
|
|
1100
|
+
monitor.cronJob.destroy();
|
|
1101
|
+
}
|
|
1102
|
+
monitor.status = 'stopped';
|
|
1103
|
+
this.changeTracker.scheduledMonitors.delete(id);
|
|
1104
|
+
stoppedMonitors++;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return {
|
|
1109
|
+
success: true,
|
|
1110
|
+
operation: 'stop_scheduled_monitor',
|
|
1111
|
+
url,
|
|
1112
|
+
stoppedMonitors,
|
|
1113
|
+
timestamp: Date.now()
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
throw new Error(`Failed to stop scheduled monitor: ${error.message}`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Get monitoring dashboard
|
|
1123
|
+
* @param {Object} params - Parameters
|
|
1124
|
+
* @returns {Object} - Dashboard data
|
|
1125
|
+
*/
|
|
1126
|
+
async getMonitoringDashboard(params) {
|
|
1127
|
+
const { dashboardOptions } = params;
|
|
1128
|
+
|
|
1129
|
+
try {
|
|
1130
|
+
const dashboard = this.changeTracker.getMonitoringDashboard();
|
|
1131
|
+
|
|
1132
|
+
// Filter based on options
|
|
1133
|
+
if (!dashboardOptions?.includeRecentAlerts) {
|
|
1134
|
+
delete dashboard.recentAlerts;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (!dashboardOptions?.includeTrends) {
|
|
1138
|
+
delete dashboard.trends;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (!dashboardOptions?.includeMonitorStatus) {
|
|
1142
|
+
dashboard.monitors = dashboard.monitors.map(m => ({
|
|
1143
|
+
id: m.id,
|
|
1144
|
+
url: m.url,
|
|
1145
|
+
status: m.status
|
|
1146
|
+
}));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
success: true,
|
|
1151
|
+
operation: 'get_dashboard',
|
|
1152
|
+
dashboard,
|
|
1153
|
+
timestamp: Date.now()
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
throw new Error(`Failed to get monitoring dashboard: ${error.message}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Export historical data
|
|
1163
|
+
* @param {Object} params - Parameters
|
|
1164
|
+
* @returns {Object} - Exported data
|
|
1165
|
+
*/
|
|
1166
|
+
async exportHistoricalData(params) {
|
|
1167
|
+
const { url, exportOptions } = params;
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
const exportData = await this.changeTracker.exportHistoricalData({
|
|
1171
|
+
...exportOptions,
|
|
1172
|
+
url
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
return {
|
|
1176
|
+
success: true,
|
|
1177
|
+
operation: 'export_history',
|
|
1178
|
+
url: url || 'global',
|
|
1179
|
+
export: exportData,
|
|
1180
|
+
timestamp: Date.now()
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
throw new Error(`Failed to export historical data: ${error.message}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Create custom alert rule
|
|
1190
|
+
* @param {Object} params - Parameters
|
|
1191
|
+
* @returns {Object} - Alert rule results
|
|
1192
|
+
*/
|
|
1193
|
+
async createAlertRule(params) {
|
|
1194
|
+
const { alertRuleOptions } = params;
|
|
1195
|
+
|
|
1196
|
+
try {
|
|
1197
|
+
const ruleId = alertRuleOptions?.ruleId ||
|
|
1198
|
+
`custom_rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1199
|
+
|
|
1200
|
+
const rule = {
|
|
1201
|
+
condition: this.parseCondition(alertRuleOptions?.condition || 'significance === "major"'),
|
|
1202
|
+
actions: alertRuleOptions?.actions || ['webhook'],
|
|
1203
|
+
throttle: alertRuleOptions?.throttle || 600000,
|
|
1204
|
+
priority: alertRuleOptions?.priority || 'medium'
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// Store the alert rule
|
|
1208
|
+
this.changeTracker.alertRules.set(ruleId, rule);
|
|
1209
|
+
|
|
1210
|
+
return {
|
|
1211
|
+
success: true,
|
|
1212
|
+
operation: 'create_alert_rule',
|
|
1213
|
+
ruleId,
|
|
1214
|
+
rule,
|
|
1215
|
+
timestamp: Date.now()
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
throw new Error(`Failed to create alert rule: ${error.message}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Generate trend analysis report
|
|
1225
|
+
* @param {Object} params - Parameters
|
|
1226
|
+
* @returns {Object} - Trend report
|
|
1227
|
+
*/
|
|
1228
|
+
async generateTrendReport(params) {
|
|
1229
|
+
const { url } = params;
|
|
1230
|
+
|
|
1231
|
+
try {
|
|
1232
|
+
const report = await this.changeTracker.generateTrendAnalysisReport(url);
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
success: true,
|
|
1236
|
+
operation: 'generate_trend_report',
|
|
1237
|
+
report,
|
|
1238
|
+
timestamp: Date.now()
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
throw new Error(`Failed to generate trend report: ${error.message}`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Get available monitoring templates
|
|
1248
|
+
* @param {Object} params - Parameters
|
|
1249
|
+
* @returns {Object} - Templates list
|
|
1250
|
+
*/
|
|
1251
|
+
async getMonitoringTemplates(params) {
|
|
1252
|
+
try {
|
|
1253
|
+
const templates = {};
|
|
1254
|
+
|
|
1255
|
+
for (const [id, template] of this.changeTracker.monitoringTemplates.entries()) {
|
|
1256
|
+
templates[id] = {
|
|
1257
|
+
name: template.name,
|
|
1258
|
+
frequency: template.frequency,
|
|
1259
|
+
description: this.generateTemplateDescription(template),
|
|
1260
|
+
options: template.options,
|
|
1261
|
+
alertRules: template.alertRules
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return {
|
|
1266
|
+
success: true,
|
|
1267
|
+
operation: 'get_monitoring_templates',
|
|
1268
|
+
templates,
|
|
1269
|
+
count: Object.keys(templates).length,
|
|
1270
|
+
timestamp: Date.now()
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
throw new Error(`Failed to get monitoring templates: ${error.message}`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Helper methods for enhanced features
|
|
1279
|
+
|
|
1280
|
+
parseCondition(conditionString) {
|
|
1281
|
+
// Simple condition parser - in production would use a proper parser
|
|
1282
|
+
return (changeResult, history) => {
|
|
1283
|
+
try {
|
|
1284
|
+
// Basic condition evaluation
|
|
1285
|
+
if (conditionString.includes('significance')) {
|
|
1286
|
+
const match = conditionString.match(/significance\s*===\s*["'](\w+)["']/);
|
|
1287
|
+
if (match) {
|
|
1288
|
+
return changeResult.significance === match[1];
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
if (conditionString.includes('frequent')) {
|
|
1293
|
+
const recent = history.filter(h => Date.now() - h.timestamp < 3600000);
|
|
1294
|
+
return recent.length > 3;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
return false;
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
generateTemplateDescription(template) {
|
|
1305
|
+
const descriptions = {
|
|
1306
|
+
'news-site': 'Optimized for news websites with frequent content updates',
|
|
1307
|
+
'e-commerce': 'Tracks product pages, prices, and inventory changes',
|
|
1308
|
+
'documentation': 'Monitors documentation sites with less frequent but important changes'
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
return descriptions[template.name] || 'Custom monitoring template';
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async shutdown() {
|
|
1315
|
+
this.stopAllMonitoring();
|
|
1316
|
+
await this.snapshotManager.shutdown();
|
|
1317
|
+
await this.changeTracker.cleanup();
|
|
1318
|
+
this.emit('shutdown');
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export default TrackChangesTool;
|
|
1323
|
+
// Create and export tool instance for MCP compatibility
|
|
1324
|
+
export const trackChangesTool = new TrackChangesTool();
|
|
1325
|
+
|
|
1326
|
+
// Add name property for MCP protocol compliance
|
|
1327
|
+
trackChangesTool.name = 'track_changes';
|
|
1328
|
+
|
|
1329
|
+
// Add validateParameters method for MCP protocol compliance
|
|
1330
|
+
trackChangesTool.validateParameters = function(params) {
|
|
1331
|
+
return TrackChangesSchema.parse(params);
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
// Add description property for MCP protocol compliance
|
|
1335
|
+
trackChangesTool.description = 'Track and analyze content changes with baseline capture, comparison, and monitoring capabilities';
|
|
1336
|
+
|
|
1337
|
+
// Add inputSchema property for MCP protocol compliance
|
|
1338
|
+
trackChangesTool.inputSchema = {
|
|
1339
|
+
type: 'object',
|
|
1340
|
+
properties: {
|
|
1341
|
+
url: {
|
|
1342
|
+
type: 'string',
|
|
1343
|
+
description: 'URL to track for changes'
|
|
1344
|
+
},
|
|
1345
|
+
operation: {
|
|
1346
|
+
type: 'string',
|
|
1347
|
+
description: 'Operation to perform: create_baseline, compare, monitor, get_history, get_stats'
|
|
1348
|
+
},
|
|
1349
|
+
content: {
|
|
1350
|
+
type: 'string',
|
|
1351
|
+
description: 'Content to analyze or compare'
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
required: ['url']
|
|
1355
|
+
};
|