Skip to main content
Glama

Cut-Copy-Paste Clipboard Server

operation-logger.ts•13 kB
import { randomBytes, createCipheriv, createDecipheriv } from 'crypto'; import { DatabaseManager } from './database.js'; export interface CopyOperation { sourceFile: string; startLine: number; endLine: number; content: string; } export interface PasteTarget { filePath: string; targetLine: number; originalContent: string; } export interface PasteOperation { targets: PasteTarget[]; content: string; cutSourceFile?: string; cutSourceContent?: string; } export interface OperationRecord { operationId: number; operationType: 'copy' | 'cut' | 'paste' | 'undo'; timestamp: number; sourceFile?: string; startLine?: number; endLine?: number; targetFile?: string; targetLine?: number; } export interface PasteRecord { operationId: number; targets: Array<{ filePath: string; targetLine: number; originalContent: string; modifiedContent: string; }>; timestamp: number; cutSourceFile?: string; cutSourceContent?: string; } /** * Logs all clipboard operations for audit trail and undo support */ export class OperationLogger { private dbManager: DatabaseManager; private readonly ENCRYPTION_VERSION = 1; constructor(dbManager: DatabaseManager) { this.dbManager = dbManager; } /** * Log a copy operation * @param sessionId - The session ID * @param operation - Copy operation details * @returns Operation ID */ logCopy(sessionId: string, operation: CopyOperation): number { const db = this.dbManager.getConnection(); const timestamp = Date.now(); // Store only metadata, not actual content (content is encrypted in clipboard_buffer) const contentSummary = JSON.stringify({ lines: operation.endLine - operation.startLine + 1, bytes: Buffer.byteLength(operation.content, 'utf-8'), }); const result = db .prepare( `INSERT INTO operations_log (session_id, operation_type, timestamp, source_file, source_start_line, source_end_line, content_snapshot) VALUES (?, ?, ?, ?, ?, ?, ?)` ) .run( sessionId, 'copy', timestamp, operation.sourceFile, operation.startLine, operation.endLine, contentSummary ); return result.lastInsertRowid as number; } /** * Log a cut operation * @param sessionId - The session ID * @param operation - Cut operation details * @returns Operation ID */ logCut(sessionId: string, operation: CopyOperation): number { const db = this.dbManager.getConnection(); const timestamp = Date.now(); // Store only metadata, not actual content (content is encrypted in clipboard_buffer) const contentSummary = JSON.stringify({ lines: operation.endLine - operation.startLine + 1, bytes: Buffer.byteLength(operation.content, 'utf-8'), }); const result = db .prepare( `INSERT INTO operations_log (session_id, operation_type, timestamp, source_file, source_start_line, source_end_line, content_snapshot, undoable) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` ) .run( sessionId, 'cut', timestamp, operation.sourceFile, operation.startLine, operation.endLine, contentSummary, 1 // Cut operations are undoable ); return result.lastInsertRowid as number; } /** * Log a paste operation with its history for undo support * @param sessionId - The session ID * @param operation - Paste operation details * @returns Operation ID */ logPaste(sessionId: string, operation: PasteOperation): number { const db = this.dbManager.getConnection(); return this.dbManager.transaction(() => { const timestamp = Date.now(); // Store only metadata in operations_log, not actual content // Actual content is stored in paste_history for undo support const contentSummary = JSON.stringify({ targets: operation.targets.length, bytes: Buffer.byteLength(operation.content, 'utf-8'), hasCutSource: !!operation.cutSourceFile, }); // Log the paste operation const result = db .prepare( `INSERT INTO operations_log (session_id, operation_type, timestamp, content_snapshot, undoable) VALUES (?, ?, ?, ?, ?)` ) .run(sessionId, 'paste', timestamp, contentSummary, 1); const operationId = result.lastInsertRowid as number; // Create paste history entries for each target // Note: All sensitive content is now encrypted at rest const historyStmt = db.prepare( `INSERT INTO paste_history (session_id, operation_log_id, file_path, original_content, modified_content, paste_line, timestamp, undone, cut_source_file, cut_source_content, encrypted_payload, encryption_iv, encryption_tag, encryption_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ); for (const target of operation.targets) { // Encrypt the sensitive content const { ciphertext, iv, authTag } = this.encryptPastePayload({ originalContent: target.originalContent, modifiedContent: operation.content, cutSourceContent: operation.cutSourceContent || null, }); historyStmt.run( sessionId, operationId, target.filePath, '[encrypted]', // original_content - redacted '[encrypted]', // modified_content - redacted target.targetLine, timestamp, 0, // Not undone yet operation.cutSourceFile || null, null, // cut_source_content - encrypted in payload ciphertext, iv, authTag, this.ENCRYPTION_VERSION ); } return operationId; }); } /** * Log an undo operation and mark the original paste as undone * @param sessionId - The session ID * @param originalPasteOpId - The operation ID of the paste being undone * @returns Operation ID */ logUndo(sessionId: string, originalPasteOpId: number): number { const db = this.dbManager.getConnection(); return this.dbManager.transaction(() => { const timestamp = Date.now(); // Log the undo operation const result = db .prepare( `INSERT INTO operations_log (session_id, operation_type, timestamp, content_snapshot) VALUES (?, ?, ?, ?)` ) .run( sessionId, 'undo', timestamp, JSON.stringify({ undoneOperationId: originalPasteOpId }) ); // Mark paste history as undone db.prepare( `UPDATE paste_history SET undone = 1 WHERE operation_log_id = ? AND session_id = ?` ).run(originalPasteOpId, sessionId); return result.lastInsertRowid as number; }); } /** * Get operation history for a session * @param sessionId - The session ID * @param limit - Maximum number of operations to return (default: 10) * @returns Array of operation records */ getHistory(sessionId: string, limit: number = 10): OperationRecord[] { const db = this.dbManager.getConnection(); const rows = db .prepare( `SELECT id, operation_type, timestamp, source_file, source_start_line, source_end_line, target_file, target_line FROM operations_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?` ) .all(sessionId, limit) as Array<{ id: number; operation_type: 'copy' | 'cut' | 'paste' | 'undo'; timestamp: number; source_file: string | null; source_start_line: number | null; source_end_line: number | null; target_file: string | null; target_line: number | null; }>; return rows.map((row) => ({ operationId: row.id, operationType: row.operation_type, timestamp: row.timestamp, sourceFile: row.source_file || undefined, startLine: row.source_start_line || undefined, endLine: row.source_end_line || undefined, targetFile: row.target_file || undefined, targetLine: row.target_line || undefined, })); } /** * Get the last paste operation that hasn't been undone * @param sessionId - The session ID * @returns Paste record or null if none found */ getLastPaste(sessionId: string): PasteRecord | null { const db = this.dbManager.getConnection(); // Find the most recent paste operation that hasn't been undone const pasteOp = db .prepare( `SELECT ol.id, ol.timestamp FROM operations_log ol WHERE ol.session_id = ? AND ol.operation_type = 'paste' AND EXISTS ( SELECT 1 FROM paste_history ph WHERE ph.operation_log_id = ol.id AND ph.undone = 0 ) ORDER BY ol.timestamp DESC LIMIT 1` ) .get(sessionId) as { id: number; timestamp: number } | undefined; if (!pasteOp) { return null; } // Get all paste history entries for this operation const historyRows = db .prepare( `SELECT file_path, original_content, modified_content, paste_line, cut_source_file, cut_source_content, encrypted_payload, encryption_iv, encryption_tag, encryption_version FROM paste_history WHERE operation_log_id = ? AND undone = 0` ) .all(pasteOp.id) as Array<{ file_path: string; original_content: string; modified_content: string; paste_line: number; cut_source_file: string | null; cut_source_content: string | null; encrypted_payload: string | null; encryption_iv: string | null; encryption_tag: string | null; encryption_version: number | null; }>; // Decrypt and extract content from rows const targets = historyRows.map((row) => { // Decrypt if encrypted payload exists if (row.encrypted_payload && row.encryption_iv && row.encryption_tag) { const decrypted = this.decryptPastePayload({ ciphertext: row.encrypted_payload, iv: row.encryption_iv, authTag: row.encryption_tag, version: row.encryption_version ?? this.ENCRYPTION_VERSION, }); return { filePath: row.file_path, targetLine: row.paste_line, originalContent: decrypted.originalContent, modifiedContent: decrypted.modifiedContent, cutSourceContent: decrypted.cutSourceContent || undefined, }; } // Fallback for legacy plaintext rows return { filePath: row.file_path, targetLine: row.paste_line, originalContent: row.original_content, modifiedContent: row.modified_content, cutSourceContent: row.cut_source_content || undefined, }; }); // Extract cut source info from first row (all rows should have same cut source) const cutSourceFile = historyRows[0]?.cut_source_file || undefined; const cutSourceContent = targets[0]?.cutSourceContent; return { operationId: pasteOp.id, targets: targets.map((t) => ({ filePath: t.filePath, targetLine: t.targetLine, originalContent: t.originalContent, modifiedContent: t.modifiedContent, })), timestamp: pasteOp.timestamp, cutSourceFile, cutSourceContent, }; } /** * Encrypt paste history payload */ private encryptPastePayload(payload: { originalContent: string; modifiedContent: string; cutSourceContent?: string | null; }): { ciphertext: string; iv: string; authTag: string } { const key = this.dbManager.getEncryptionKey(); const iv = randomBytes(12); const cipher = createCipheriv('aes-256-gcm', key, iv); const plaintext = JSON.stringify(payload); const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); const authTag = cipher.getAuthTag(); return { ciphertext: encrypted.toString('base64'), iv: iv.toString('base64'), authTag: authTag.toString('base64'), }; } /** * Decrypt paste history payload */ private decryptPastePayload(params: { ciphertext: string; iv: string; authTag: string; version: number; }): { originalContent: string; modifiedContent: string; cutSourceContent?: string | null; } { if (params.version !== this.ENCRYPTION_VERSION) { throw new Error(`Unsupported encryption version: ${params.version}`); } const key = this.dbManager.getEncryptionKey(); const iv = Buffer.from(params.iv, 'base64'); const authTag = Buffer.from(params.authTag, 'base64'); const encrypted = Buffer.from(params.ciphertext, 'base64'); const decipher = createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(authTag); const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); return JSON.parse(decrypted.toString('utf8')); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Pr0j3c7t0dd-Ltd/cut-copy-paste-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server