#!/usr/bin/env node import { appendFileSync, existsSync, mkdirSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Log file is in the skill root directory, not in scripts/ const SKILL_ROOT = join(__dirname, '..'); const LOG_FILE = join(SKILL_ROOT, 'action-log.jsonl'); /** * Ensure the log file directory exists */ function ensureLogDirectory() { const logDir = dirname(LOG_FILE); if (!existsSync(logDir)) { mkdirSync(logDir, { recursive: true }); } } /** * Log an action to the log file * @param {string} action - The action type (e.g., 'list_events', 'create_event', 'update_event') * @param {Object} params - The parameters used for the action * @param {Object} result - The result of the action * @param {Object} metadata - Additional metadata (e.g., account, duration) */ export function logAction(action, params, result, metadata = {}) { ensureLogDirectory(); const logEntry = { timestamp: new Date().toISOString(), action, params: sanitizeParams(params), result: sanitizeResult(result), metadata: { ...metadata, skill: 'google-calendar' } }; try { // Append as JSON Lines format (one JSON object per line) appendFileSync(LOG_FILE, JSON.stringify(logEntry) + '\n', 'utf8'); } catch (error) { // Don't fail the main action if logging fails console.error('Warning: Failed to write to action log:', error.message); } } /** * Sanitize parameters to remove sensitive data and limit size */ function sanitizeParams(params) { const sanitized = { ...params }; // Remove sensitive fields const sensitiveFields = ['password', 'token', 'access_token', 'refresh_token']; sensitiveFields.forEach(field => { if (sanitized[field]) { sanitized[field] = '[REDACTED]'; } }); // Truncate very long string values Object.keys(sanitized).forEach(key => { if (typeof sanitized[key] === 'string' && sanitized[key].length > 1000) { sanitized[key] = sanitized[key].substring(0, 1000) + '... [truncated]'; } }); return sanitized; } /** * Sanitize result to limit size while keeping useful information */ function sanitizeResult(result) { const sanitized = { ...result }; // Keep success status and counts if (sanitized.success !== undefined) { // Success is important } if (sanitized.count !== undefined || sanitized.total !== undefined) { // Counts are useful } // For event results, keep summary info but limit details if (sanitized.events && Array.isArray(sanitized.events)) { sanitized.eventCount = sanitized.events.length; // Keep only essential fields from first few events sanitized.eventSummary = sanitized.events.slice(0, 3).map(event => ({ id: event.id, summary: event.summary, start: event.start, end: event.end, status: event.status, attendees: event.attendees ? event.attendees.length : 0 })); delete sanitized.events; } // For single event results if (sanitized.event) { sanitized.eventSummary = { id: sanitized.event.id, summary: sanitized.event.summary, start: sanitized.event.start, end: sanitized.event.end, status: sanitized.event.status }; delete sanitized.event; } // Truncate error messages if (sanitized.error && sanitized.error.length > 500) { sanitized.error = sanitized.error.substring(0, 500) + '... [truncated]'; } return sanitized; } /** * Get the log file path (useful for scripts that want to reference it) */ export function getLogFilePath() { return LOG_FILE; }