/ utils / pasteStore.ts
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  }