Skip to main content
Glama
by xxczaki
history-parser.ts6.58 kB
import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import logger from './logger.ts'; export interface HistoryEntry { timestamp: number; content: string; filePath: string; relativePath: string; } export interface FileHistory { originalFilePath: string; entries: HistoryEntry[]; } /** * Convert a file URI to a file system path */ export function uriToPath(uri: string): string { if (uri.startsWith('file://')) { // Remove file:// prefix and decode URI components let path = uri.slice(7); // Decode URI-encoded characters path = decodeURIComponent(path); return path; } return uri; // Return as-is if not a URI } /** * Convert a file system path to a file URI */ export function pathToUri(filePath: string): string { if (!filePath.startsWith('file://')) { return `file://${encodeURI(filePath)}`; } return filePath; // Return as-is if already a URI } export class VSCodeHistoryParser { private historyDir: string; constructor() { this.historyDir = this.getHistoryDirectory(); } /** * Get the VS Code/Cursor Local History directory based on the current OS * Tries Cursor first, then falls back to VS Code */ private getHistoryDirectory(): string { const homeDir = os.homedir(); let cursorPath: string; let vsCodePath: string; switch (process.platform) { case 'win32': cursorPath = path.join( process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'Cursor', 'User', 'History', ); vsCodePath = path.join( process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), 'Code', 'User', 'History', ); break; case 'darwin': cursorPath = path.join( homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'History', ); vsCodePath = path.join( homeDir, 'Library', 'Application Support', 'Code', 'User', 'History', ); break; case 'linux': cursorPath = path.join(homeDir, '.config', 'Cursor', 'User', 'History'); vsCodePath = path.join(homeDir, '.config', 'Code', 'User', 'History'); break; default: throw new Error(`Unsupported operating system: ${process.platform}`); } // Prefer Cursor if it exists, otherwise fall back to VS Code if (fs.existsSync(cursorPath)) { return cursorPath; } if (fs.existsSync(vsCodePath)) { return vsCodePath; } return cursorPath; // Return Cursor path as default for new installations } /** * Check if the VS Code history directory exists */ public historyDirectoryExists(): boolean { return fs.existsSync(this.historyDir); } /** * Get all file history directories */ public getHistoryDirectories(): string[] { if (!this.historyDirectoryExists()) { return []; } try { return fs .readdirSync(this.historyDir, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) .map((dirent) => dirent.name); } catch (error) { logger.warn({ error }, 'Failed to read history directory'); return []; } } /** * Parse the entries.json file to get metadata about a file's history */ private parseEntriesJson( historyDirPath: string, ): { resource?: string } | null { const entriesJsonPath = path.join(historyDirPath, 'entries.json'); if (!fs.existsSync(entriesJsonPath)) { return null; } try { const content = fs.readFileSync(entriesJsonPath, 'utf-8'); return JSON.parse(content); } catch (error) { logger.warn({ error, entriesJsonPath }, 'Failed to parse entries.json'); return null; } } /** * Get history for a specific file by its hash directory */ public getFileHistory(hashDir: string): FileHistory | null { const historyDirPath = path.join(this.historyDir, hashDir); if (!fs.existsSync(historyDirPath)) { return null; } const entriesJson = this.parseEntriesJson(historyDirPath); if (!entriesJson || !entriesJson.resource) { return null; } const originalFilePath = entriesJson.resource; const entries: HistoryEntry[] = []; try { // Read all files in the history directory (excluding entries.json) const historyFiles = fs .readdirSync(historyDirPath) .filter((file) => file !== 'entries.json') .sort(); // Sort to get chronological order for (const file of historyFiles) { const filePath = path.join(historyDirPath, file); const stats = fs.statSync(filePath); try { const content = fs.readFileSync(filePath, 'utf-8'); entries.push({ timestamp: stats.mtime.getTime(), content, filePath: filePath, relativePath: file, }); } catch (error) { logger.warn({ error, file }, 'Failed to read history file'); } } return { originalFilePath, entries: entries.sort((a, b) => b.timestamp - a.timestamp), // Most recent first }; } catch (error) { logger.warn({ error, hashDir }, 'Failed to read history directory'); return null; } } /** * Get all file histories */ public getAllFileHistories(): FileHistory[] { const histories: FileHistory[] = []; const hashDirs = this.getHistoryDirectories(); for (const hashDir of hashDirs) { const history = this.getFileHistory(hashDir); if (history) { histories.push(history); } } return histories; } /** * Find history for a specific file path */ public findHistoryByFilePath(targetFilePath: string): FileHistory | null { const histories = this.getAllFileHistories(); // Convert URI to path if needed const cleanTargetPath = uriToPath(targetFilePath); const normalizedTarget = path.resolve(cleanTargetPath); for (const history of histories) { const cleanHistoryPath = uriToPath(history.originalFilePath); const normalizedHistory = path.resolve(cleanHistoryPath); // Exact match if (normalizedTarget === normalizedHistory) { return history; } // Case-insensitive match (for case-insensitive filesystems) if (normalizedTarget.toLowerCase() === normalizedHistory.toLowerCase()) { return history; } } return null; } /** * Get basic statistics about the history */ public getHistoryStats(): { totalFiles: number; totalEntries: number; historyDirExists: boolean; historyDirPath: string; } { const histories = this.getAllFileHistories(); const totalEntries = histories.reduce( (sum, history) => sum + history.entries.length, 0, ); return { totalFiles: histories.length, totalEntries, historyDirExists: this.historyDirectoryExists(), historyDirPath: this.historyDir, }; } }

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/xxczaki/local-history-mcp'

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