UNPKG

markdown-editor-mcp

Version:

MCP server for markdown editing and management

185 lines (184 loc) 7.56 kB
import { platform } from 'os'; import { randomUUID } from 'crypto'; import * as https from 'https'; import { configManager } from '../config-manager.js'; let VERSION = 'unknown'; try { const versionModule = await import('../version.js'); VERSION = versionModule.VERSION; } catch { // Continue without version info if not available } // Will be initialized when needed let uniqueUserId = 'unknown'; // Function to get or create a persistent UUID async function getOrCreateUUID() { try { // Try to get the UUID from the config let clientId = await configManager.getValue('clientId'); // If it doesn't exist, create a new one and save it if (!clientId) { clientId = randomUUID(); await configManager.setValue('clientId', clientId); } return clientId; } catch (error) { // Fallback to a random UUID if config operations fail return randomUUID(); } } /** * Sanitizes error objects to remove potentially sensitive information like file paths * @param error Error object or string to sanitize * @returns An object with sanitized message and optional error code */ export function sanitizeError(error) { let errorMessage = ''; let errorCode = undefined; if (error instanceof Error) { // Extract just the error name and message without stack trace errorMessage = error.name + ': ' + error.message; // Extract error code if available (common in Node.js errors) if ('code' in error) { errorCode = error.code; } } else if (typeof error === 'string') { errorMessage = error; } else { errorMessage = 'Unknown error'; } // Remove any file paths using regex // This pattern matches common path formats including Windows and Unix-style paths errorMessage = errorMessage.replace(/(?:\/|\\)[\w\d_.-\/\\]+/g, '[PATH]'); errorMessage = errorMessage.replace(/[A-Za-z]:\\[\w\d_.-\/\\]+/g, '[PATH]'); return { message: errorMessage, code: errorCode }; } /** * Send an event to Google Analytics * @param event Event name * @param properties Optional event properties */ export const captureBase = async (captureURL, event, properties) => { try { // Check if telemetry is enabled in config (defaults to true if not set) const telemetryEnabled = await configManager.getValue('telemetryEnabled'); // If telemetry is explicitly disabled or GA credentials are missing, don't send if (telemetryEnabled === false || !captureURL) { return; } // Get or create the client ID if not already initialized if (uniqueUserId === 'unknown') { uniqueUserId = await getOrCreateUUID(); } // Create a deep copy of properties to avoid modifying the original objects // This ensures we don't alter error objects that are also returned to the AI let sanitizedProperties; try { sanitizedProperties = properties ? JSON.parse(JSON.stringify(properties)) : {}; } catch (e) { sanitizedProperties = {}; } // Sanitize error objects if present if (sanitizedProperties.error) { // Handle different types of error objects if (typeof sanitizedProperties.error === 'object' && sanitizedProperties.error !== null) { const sanitized = sanitizeError(sanitizedProperties.error); sanitizedProperties.error = sanitized.message; if (sanitized.code) { sanitizedProperties.errorCode = sanitized.code; } } else if (typeof sanitizedProperties.error === 'string') { sanitizedProperties.error = sanitizeError(sanitizedProperties.error).message; } } // Remove any properties that might contain paths const sensitiveKeys = ['path', 'filePath', 'directory', 'file_path', 'sourcePath', 'destinationPath', 'fullPath', 'rootPath']; for (const key of Object.keys(sanitizedProperties)) { const lowerKey = key.toLowerCase(); if (sensitiveKeys.some(sensitiveKey => lowerKey.includes(sensitiveKey)) && lowerKey !== 'fileextension') { // keep fileExtension as it's safe delete sanitizedProperties[key]; } } // Prepare standard properties const baseProperties = { timestamp: new Date().toISOString(), platform: platform(), app_version: VERSION, engagement_time_msec: "100" }; // Combine with sanitized properties const eventProperties = { ...baseProperties, ...sanitizedProperties }; // Prepare GA4 payload const payload = { client_id: uniqueUserId, non_personalized_ads: false, timestamp_micros: Date.now() * 1000, events: [{ name: event, params: eventProperties }] }; // Send data to Google Analytics const postData = JSON.stringify(payload); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(captureURL, options, (res) => { // Response handling (optional) let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { if (res.statusCode !== 200 && res.statusCode !== 204) { // Optional debug logging // console.debug(`GA tracking error: ${res.statusCode} ${data}`); } }); }); req.on('error', () => { // Silently fail - we don't want analytics issues to break functionality }); // Set timeout to prevent blocking the app req.setTimeout(3000, () => { req.destroy(); }); // Send data req.write(postData); req.end(); } catch { // Silently fail - we don't want analytics issues to break functionality } }; export const capture_call_tool = async (event, properties) => { const GA_MEASUREMENT_ID = 'G-35YKFM782B'; // Replace with your GA4 Measurement ID const GA_API_SECRET = 'qM5VNk6aQy6NN5s-tCppZw'; // Replace with your GA4 API Secret const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; return await captureBase(GA_BASE_URL, event, properties); }; export const capture = async (event, properties) => { const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secret const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; return await captureBase(GA_BASE_URL, event, properties); };