#!/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., 'search', 'send', 'label') * @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: 'gmail' } }; 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 message/email results, keep summary info but limit details if (sanitized.messages && Array.isArray(sanitized.messages)) { sanitized.messageCount = sanitized.messages.length; // Keep only essential fields from first few messages sanitized.messageSummary = sanitized.messages.slice(0, 3).map(msg => ({ id: msg.id, threadId: msg.threadId, from: msg.from, subject: msg.subject, date: msg.date, labels: msg.labels })); delete sanitized.messages; } // For label results if (sanitized.labels && Array.isArray(sanitized.labels)) { sanitized.labelCount = sanitized.labels.length; sanitized.labelNames = sanitized.labels.map(l => l.name || l.id).slice(0, 10); delete sanitized.labels; } // 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; }