pasteStore.ts
1 import { createHash } from 'crypto' 2 import { mkdir, readdir, readFile, stat, unlink, writeFile } from 'fs/promises' 3 import { join } from 'path' 4 import { logForDebugging } from './debug.js' 5 import { getClaudeConfigHomeDir } from './envUtils.js' 6 import { isENOENT } from './errors.js' 7 8 const PASTE_STORE_DIR = 'paste-cache' 9 10 /** 11 * Get the paste store directory (persistent across sessions). 12 */ 13 function getPasteStoreDir(): string { 14 return join(getClaudeConfigHomeDir(), PASTE_STORE_DIR) 15 } 16 17 /** 18 * Generate a hash for paste content to use as filename. 19 * Exported so callers can get the hash synchronously before async storage. 20 */ 21 export function hashPastedText(content: string): string { 22 return createHash('sha256').update(content).digest('hex').slice(0, 16) 23 } 24 25 /** 26 * Get the file path for a paste by its content hash. 27 */ 28 function getPastePath(hash: string): string { 29 return join(getPasteStoreDir(), `${hash}.txt`) 30 } 31 32 /** 33 * Store pasted text content to disk. 34 * The hash should be pre-computed with hashPastedText() so the caller 35 * can use it immediately without waiting for the async disk write. 36 */ 37 export async function storePastedText( 38 hash: string, 39 content: string, 40 ): Promise<void> { 41 try { 42 const dir = getPasteStoreDir() 43 await mkdir(dir, { recursive: true }) 44 45 const pastePath = getPastePath(hash) 46 47 // Content-addressable: same hash = same content, so overwriting is safe 48 await writeFile(pastePath, content, { encoding: 'utf8', mode: 0o600 }) 49 logForDebugging(`Stored paste ${hash} to ${pastePath}`) 50 } catch (error) { 51 logForDebugging(`Failed to store paste: ${error}`) 52 } 53 } 54 55 /** 56 * Retrieve pasted text content by its hash. 57 * Returns null if not found or on error. 58 */ 59 export async function retrievePastedText(hash: string): Promise<string | null> { 60 try { 61 const pastePath = getPastePath(hash) 62 return await readFile(pastePath, { encoding: 'utf8' }) 63 } catch (error) { 64 // ENOENT is expected when paste doesn't exist 65 if (!isENOENT(error)) { 66 logForDebugging(`Failed to retrieve paste ${hash}: ${error}`) 67 } 68 return null 69 } 70 } 71 72 /** 73 * Clean up old paste files that are no longer referenced. 74 * This is a simple time-based cleanup - removes files older than cutoffDate. 75 */ 76 export async function cleanupOldPastes(cutoffDate: Date): Promise<void> { 77 const pasteDir = getPasteStoreDir() 78 79 let files 80 try { 81 files = await readdir(pasteDir) 82 } catch { 83 // Directory doesn't exist or can't be read - nothing to clean up 84 return 85 } 86 87 const cutoffTime = cutoffDate.getTime() 88 for (const file of files) { 89 if (!file.endsWith('.txt')) { 90 continue 91 } 92 93 const filePath = join(pasteDir, file) 94 try { 95 const stats = await stat(filePath) 96 if (stats.mtimeMs < cutoffTime) { 97 await unlink(filePath) 98 logForDebugging(`Cleaned up old paste: ${filePath}`) 99 } 100 } catch { 101 // Ignore errors for individual files 102 } 103 } 104 }