{
  "version": 3,
  "sources": ["../../../src/hooks/ts/pre-tool-use/security-validator.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/config/pai-home.ts"],
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * security-validator.ts - PreToolUse Security Validation Hook\n *\n * Fast pattern-based security validation for Bash commands.\n * Blocks commands matching known attack patterns before execution.\n *\n * Design Principles:\n * - Fast path: Most commands allowed with minimal processing\n * - Pre-compiled regex patterns at module load\n * - Only log/block on high-confidence attack detection\n * - Fail open on errors (don't break legitimate work)\n *\n * CUSTOMIZATION REQUIRED:\n * This template includes basic examples. Add your own security patterns\n * based on your threat model and environment.\n */\n\nimport { appendFileSync, mkdirSync, existsSync } from 'fs';\nimport { dirname } from 'path';\nimport { securityEventsPath } from '../lib/pai-paths.js';\n\n// ============================================================================\n// ATTACK PATTERNS - CUSTOMIZE THESE FOR YOUR ENVIRONMENT\n// ============================================================================\n\n// Example: Reverse Shell Patterns (BLOCK - rarely legitimate)\nconst REVERSE_SHELL_PATTERNS: RegExp[] = [\n  /\\/dev\\/(tcp|udp)\\/[0-9]/,                    // Bash TCP/UDP device\n  /bash\\s+-i\\s+>&?\\s*\\/dev\\//,                  // Interactive bash redirect\n  // Add your own reverse shell patterns here\n];\n\n// Example: Instruction Override (BLOCK - prompt injection)\nconst INSTRUCTION_OVERRIDE_PATTERNS: RegExp[] = [\n  /ignore\\s+(all\\s+)?previous\\s+instructions?/i,\n  /disregard\\s+(all\\s+)?(prior|previous)\\s+(instructions?|rules?)/i,\n  // Add your own prompt injection patterns here\n];\n\n// Example: Catastrophic Deletion Patterns (BLOCK - filesystem destruction)\nconst CATASTROPHIC_DELETION_PATTERNS: RegExp[] = [\n  // Trailing tilde bypass\n  /\\s+~\\/?(\\s*$|\\s+)/,                              // Space then ~/ at end\n  /\\brm\\s+(-[rfivd]+\\s+)*\\S+\\s+~\\/?/,               // rm something ~/\n\n  // Relative path recursive deletion\n  /\\brm\\s+(-[rfivd]+\\s+)*\\.\\/\\s*$/,                 // rm -rf ./\n  /\\brm\\s+(-[rfivd]+\\s+)*\\.\\.\\/\\s*$/,               // rm -rf ../\n\n  // Add your own dangerous deletion patterns here\n];\n\n// Example: Dangerous File Operations (BLOCK - data destruction)\nconst DANGEROUS_FILE_OPS_PATTERNS: RegExp[] = [\n  /\\bchmod\\s+(-R\\s+)?0{3,}/,                        // chmod 000\n  // Add your own dangerous file operation patterns here\n];\n\n// OPTIONAL: Operations that require confirmation instead of blocking\nconst DANGEROUS_GIT_PATTERNS: RegExp[] = [\n  /\\bgit\\s+push\\s+.*(-f\\b|--force)/i,               // git push --force\n  /\\bgit\\s+reset\\s+--hard/i,                        // git reset --hard\n  // Add your own git safety patterns here\n];\n\n// Combined patterns for fast iteration\nconst ALL_BLOCK_PATTERNS: { category: string; patterns: RegExp[] }[] = [\n  { category: 'reverse_shell', patterns: REVERSE_SHELL_PATTERNS },\n  { category: 'instruction_override', patterns: INSTRUCTION_OVERRIDE_PATTERNS },\n  { category: 'catastrophic_deletion', patterns: CATASTROPHIC_DELETION_PATTERNS },\n  { category: 'dangerous_file_ops', patterns: DANGEROUS_FILE_OPS_PATTERNS },\n];\n\nconst CONFIRM_PATTERNS: { category: string; patterns: RegExp[] }[] = [\n  { category: 'dangerous_git', patterns: DANGEROUS_GIT_PATTERNS },\n];\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface HookInput {\n  session_id: string;\n  tool_name: string;\n  tool_input: Record<string, unknown> | string;\n}\n\ninterface HookOutput {\n  permissionDecision: 'allow' | 'deny';\n  additionalContext?: string;\n  feedback?: string;\n}\n\n// ============================================================================\n// DETECTION LOGIC\n// ============================================================================\n\ninterface DetectionResult {\n  blocked: boolean;\n  requiresConfirmation?: boolean;\n  category?: string;\n  pattern?: string;\n}\n\nfunction detectAttack(content: string): DetectionResult {\n  // First check for hard blocks\n  for (const { category, patterns } of ALL_BLOCK_PATTERNS) {\n    for (const pattern of patterns) {\n      if (pattern.test(content)) {\n        return { blocked: true, category, pattern: pattern.source };\n      }\n    }\n  }\n\n  // Then check for confirmation-required patterns\n  for (const { category, patterns } of CONFIRM_PATTERNS) {\n    for (const pattern of patterns) {\n      if (pattern.test(content)) {\n        return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };\n      }\n    }\n  }\n\n  return { blocked: false };\n}\n\n// ============================================================================\n// ASYNC LOGGING (fire-and-forget on block only)\n// ============================================================================\n\nfunction logSecurityEvent(event: Record<string, unknown>): void {\n  const logPath = securityEventsPath();\n  const entry = JSON.stringify({ timestamp: new Date().toISOString(), ...event }) + '\\n';\n\n  try {\n    const dir = dirname(logPath);\n    if (!existsSync(dir)) {\n      mkdirSync(dir, { recursive: true });\n    }\n    appendFileSync(logPath, entry);\n  } catch {\n    // Silently fail - logging should never break the hook\n  }\n}\n\n// ============================================================================\n// MAIN HOOK LOGIC\n// ============================================================================\n\nasync function main(): Promise<void> {\n  let input: HookInput;\n\n  try {\n    // Read stdin\n    const chunks: Buffer[] = [];\n    const timeoutPromise = new Promise<Buffer[]>((_, reject) =>\n      setTimeout(() => reject(new Error('timeout')), 100)\n    );\n    const readPromise = (async () => {\n      for await (const chunk of process.stdin) {\n        chunks.push(chunk);\n      }\n      return chunks;\n    })();\n\n    let text = '';\n    try {\n      const result = await Promise.race([readPromise, timeoutPromise]);\n      text = Buffer.concat(result).toString('utf-8');\n    } catch {\n      console.log(JSON.stringify({ permissionDecision: 'allow' }));\n      return;\n    }\n\n    if (!text.trim()) {\n      console.log(JSON.stringify({ permissionDecision: 'allow' }));\n      return;\n    }\n\n    input = JSON.parse(text);\n  } catch {\n    // Parse error or timeout - fail open\n    console.log(JSON.stringify({ permissionDecision: 'allow' }));\n    return;\n  }\n\n  // Only validate Bash commands\n  if (input.tool_name !== 'Bash') {\n    console.log(JSON.stringify({ permissionDecision: 'allow' }));\n    return;\n  }\n\n  // Extract command string\n  const command = typeof input.tool_input === 'string'\n    ? input.tool_input\n    : (input.tool_input?.command as string) || '';\n\n  if (!command) {\n    console.log(JSON.stringify({ permissionDecision: 'allow' }));\n    return;\n  }\n\n  // Check all patterns\n  const result = detectAttack(command);\n\n  if (result.blocked) {\n    // Log and block\n    logSecurityEvent({\n      type: 'attack_blocked',\n      category: result.category,\n      pattern: result.pattern,\n      command: command.slice(0, 200), // Truncate for log\n      session_id: input.session_id,\n    });\n\n    const output: HookOutput = {\n      permissionDecision: 'deny',\n      additionalContext: `SECURITY: Blocked ${result.category} pattern`,\n      feedback: `This command matched a security pattern (${result.category}). If this is legitimate, please rephrase the command.`,\n    };\n\n    console.log(JSON.stringify(output));\n    process.exit(2); // Exit 2 = blocking error\n  }\n\n  if (result.requiresConfirmation) {\n    // Log warning and require confirmation\n    logSecurityEvent({\n      type: 'confirmation_required',\n      category: result.category,\n      pattern: result.pattern,\n      command: command.slice(0, 200),\n      session_id: input.session_id,\n    });\n\n    const output: HookOutput = {\n      permissionDecision: 'deny',\n      additionalContext: `DANGEROUS: ${result.category} operation requires confirmation`,\n      feedback: `This is a dangerous operation (${command.slice(0, 50)}...). This can cause data loss. If you're sure, explicitly confirm this command.`,\n    };\n\n    console.log(JSON.stringify(output));\n    process.exit(2); // Exit 2 = requires user confirmation\n  }\n\n  // Allow - no logging, immediate exit\n  console.log(JSON.stringify({ permissionDecision: 'allow' }));\n}\n\n// ============================================================================\n// RUN\n// ============================================================================\n\nmain().catch(() => {\n  // On any error, fail open\n  console.log(JSON.stringify({ permissionDecision: 'allow' }));\n});\n", "/**\n * PAI Path Resolution - Single Source of Truth\n *\n * This module provides consistent path resolution across all PAI hooks.\n *\n * Two different things live here, and they must not be confused:\n *\n * - ADAPTER_DIR (~/.claude by default) is the Claude Code harness adapter \u2014\n *   fixed, hardcoded paths the harness itself loads from (Hooks/, Skills/,\n *   Agents/, Commands/, settings.json, statusline-command.sh,\n *   tab-color-command.sh). It is harness-specific: a future harness would\n *   need its own adapter directory with its own conventions.\n * - PAI_HOME (~/.claude/pai by default \u2014 see ../../../config/pai-home.ts) is\n *   where PAI's own state lives: everything hooks WRITE (History/,\n *   agent-sessions.json, session-routing.json, ...) resolves there, with a\n *   fallback to the pre-2026-09-19 ADAPTER_DIR location and a one-time\n *   stderr notice, exactly like every other PAI_HOME file.\n *\n * ALSO loads .env file from ADAPTER_DIR so all hooks get environment\n * variables without relying on Claude Code's settings.json injection.\n *\n * Usage in hooks:\n *   import { ADAPTER_DIR, HOOKS_DIR, SKILLS_DIR, historyDir } from './lib/pai-paths';\n */\n\nimport { homedir } from 'os';\nimport { resolve, join } from 'path';\nimport { existsSync, readFileSync } from 'fs';\nimport {\n  paiHomePath,\n  resolvePaiFile,\n  migratePaiFile,\n  migratePaiDir,\n  type MigrateFileResult,\n  type MigrateDirResult,\n} from '../../../config/pai-home.js';\n\n/**\n * Load .env file and inject into process.env\n * Must run BEFORE ADAPTER_DIR resolution so .env can set ADAPTER_DIR/PAI_DIR if needed\n */\nfunction loadEnvFile(): void {\n  // Check common locations for .env\n  const possiblePaths = [\n    resolve(process.env.ADAPTER_DIR || process.env.PAI_DIR || '', '.env'),\n    resolve(homedir(), '.claude', '.env'),\n  ];\n\n  for (const envPath of possiblePaths) {\n    if (existsSync(envPath)) {\n      try {\n        const content = readFileSync(envPath, 'utf-8');\n        for (const line of content.split('\\n')) {\n          const trimmed = line.trim();\n          // Skip comments and empty lines\n          if (!trimmed || trimmed.startsWith('#')) continue;\n\n          const eqIndex = trimmed.indexOf('=');\n          if (eqIndex > 0) {\n            const key = trimmed.substring(0, eqIndex).trim();\n            let value = trimmed.substring(eqIndex + 1).trim();\n\n            // Remove surrounding quotes if present\n            if ((value.startsWith('\"') && value.endsWith('\"')) ||\n                (value.startsWith(\"'\") && value.endsWith(\"'\"))) {\n              value = value.slice(1, -1);\n            }\n\n            // Expand $HOME and ~ in values\n            value = value.replace(/\\$HOME/g, homedir());\n            value = value.replace(/^~(?=\\/|$)/, homedir());\n\n            // Only set if not already defined (env vars take precedence)\n            if (process.env[key] === undefined) {\n              process.env[key] = value;\n            }\n          }\n        }\n        // Found and loaded, don't check other paths\n        break;\n      } catch {\n        // Silently continue if .env can't be read\n      }\n    }\n  }\n}\n\n// Load .env FIRST, before any other initialization\nloadEnvFile();\n\nfunction defaultAdapterDir(): string {\n  return resolve(homedir(), '.claude');\n}\n\n/**\n * Never print when stdout/stderr is a data channel, not a human: hook\n * bundles and worker-status-line.mjs set PAI_QUIET_NOTICES=1 via their\n * esbuild banner (scripts/build-hooks.mjs), and a `pai worker run\n * --output-format json` invocation is detected directly off argv since its\n * env can't be set before this module's static imports resolve.\n */\nfunction suppressDeprecationNotices(): boolean {\n  if (process.env.PAI_QUIET_NOTICES === '1') return true;\n  const idx = process.argv.indexOf('--output-format');\n  return idx !== -1 && process.argv[idx + 1] === 'json';\n}\n\n/**\n * At most once per process \u2014 a `globalThis` flag rather than a module-level\n * `let` because hooks and the CLI can end up with more than one instance of\n * this module loaded into the same process (separate bundles), each with its\n * own module scope; only a property on the shared global survives that.\n */\nfunction warnPaiDirDeprecatedOnce(message: string): void {\n  const g = globalThis as typeof globalThis & { __paiDirNoticePrinted?: boolean };\n  if (g.__paiDirNoticePrinted || suppressDeprecationNotices()) return;\n  g.__paiDirNoticePrinted = true;\n  process.stderr.write(`pai: ${message}\\n`);\n}\n\n/**\n * Smart ADAPTER_DIR detection with fallback\n * Priority:\n * 1. ADAPTER_DIR environment variable (if set) \u2014 warns once if PAI_DIR is\n *    ALSO set and resolves to a different path (naming both).\n * 2. PAI_DIR environment variable (deprecated alias, one release) \u2014 warns\n *    once only when it differs from the default adapter root; PAI_DIR set to\n *    the same value as the default is the operator's current, correct\n *    setting and stays silent.\n * 3. ~/.claude (standard location)\n */\nfunction resolveAdapterDir(): string {\n  if (process.env.ADAPTER_DIR) {\n    const adapterDir = resolve(process.env.ADAPTER_DIR);\n    if (process.env.PAI_DIR) {\n      const paiDir = resolve(process.env.PAI_DIR);\n      if (paiDir !== adapterDir) {\n        warnPaiDirDeprecatedOnce(\n          `ADAPTER_DIR (${adapterDir}) and PAI_DIR (${paiDir}) are both set and differ \u2014 ADAPTER_DIR wins. PAI_DIR still works for one release.`\n        );\n      }\n    }\n    return adapterDir;\n  }\n  if (process.env.PAI_DIR) {\n    const paiDir = resolve(process.env.PAI_DIR);\n    if (paiDir !== defaultAdapterDir()) {\n      warnPaiDirDeprecatedOnce(\n        'PAI_DIR is deprecated \u2014 use ADAPTER_DIR for the harness adapter root (~/.claude). PAI_DIR still works for one release.'\n      );\n    }\n    return paiDir;\n  }\n  return defaultAdapterDir();\n}\n\n/** The Claude Code harness adapter root \u2014 NOT PAI's state home. See module doc above. */\nexport const ADAPTER_DIR = resolveAdapterDir();\n\n/** @deprecated alias for ADAPTER_DIR, kept for one release. Prefer ADAPTER_DIR. */\nexport const PAI_DIR = ADAPTER_DIR;\n\n/**\n * Adapter directories \u2014 fixed paths the Claude Code harness itself loads\n * from. These stay under ADAPTER_DIR; they are not PAI state.\n */\nexport const HOOKS_DIR = join(ADAPTER_DIR, 'Hooks');\nexport const SKILLS_DIR = join(ADAPTER_DIR, 'Skills');\nexport const AGENTS_DIR = join(ADAPTER_DIR, 'Agents');\nexport const COMMANDS_DIR = join(ADAPTER_DIR, 'Commands');\n\n/**\n * Validate PAI directory structure on first import\n * This fails fast with a clear error if PAI is misconfigured\n */\nfunction validatePAIStructure(): void {\n  if (!existsSync(ADAPTER_DIR)) {\n    console.error(`ADAPTER_DIR does not exist: ${ADAPTER_DIR}`);\n    console.error(`   Expected ~/.claude or set ADAPTER_DIR environment variable`);\n    process.exit(1);\n  }\n\n  if (!existsSync(HOOKS_DIR)) {\n    console.error(`PAI hooks directory not found: ${HOOKS_DIR}`);\n    console.error(`   Your ADAPTER_DIR may be misconfigured`);\n    console.error(`   Current ADAPTER_DIR: ${ADAPTER_DIR}`);\n    process.exit(1);\n  }\n}\n\n// Run validation on module import\n// This ensures any hook that imports this module will fail fast if paths are wrong\nvalidatePAIStructure();\n\n// ---------------------------------------------------------------------------\n// PAI state written by hooks \u2014 resolves under PAI_HOME, falling back to the\n// pre-2026-09-19 ADAPTER_DIR location (one-time stderr notice) until\n// `pai config migrate --history` moves it. Actively written on every hook\n// event across every session, so unlike most PAI_HOME files the live move is\n// deliberately NOT automatic \u2014 see `pai config migrate --history`.\n// ---------------------------------------------------------------------------\n\nfunction oldHistoryDir(): string {\n  return join(ADAPTER_DIR, 'History');\n}\n\n/** Read/write location for hook-captured history: PAI_HOME/History if\n *  present, else the old ADAPTER_DIR/History (one-time stderr notice). */\nexport function historyDir(): string {\n  return resolvePaiFile(paiHomePath('History'), [oldHistoryDir()], 'pai config migrate --history');\n}\n\nexport function migrateHistoryDir(opts: { dryRun?: boolean } = {}): MigrateDirResult {\n  return migratePaiDir(paiHomePath('History'), [oldHistoryDir()], opts);\n}\n\nfunction oldAgentSessionsPath(): string {\n  return join(ADAPTER_DIR, 'agent-sessions.json');\n}\n\nexport function agentSessionsPath(): string {\n  return resolvePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], 'pai config migrate --history');\n}\n\nexport function migrateAgentSessions(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath('agent-sessions.json'), [oldAgentSessionsPath()], opts);\n}\n\nfunction oldSecurityEventsPath(): string {\n  return join(ADAPTER_DIR, 'history', 'security', 'security-events.jsonl');\n}\n\nexport function securityEventsPath(): string {\n  return resolvePaiFile(\n    paiHomePath('History', 'security', 'security-events.jsonl'),\n    [oldSecurityEventsPath()],\n    'pai config migrate --history'\n  );\n}\n\nexport function migrateSecurityEvents(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath('History', 'security', 'security-events.jsonl'), [oldSecurityEventsPath()], opts);\n}\n\nfunction oldSessionRoutingPath(): string {\n  return join(ADAPTER_DIR, 'session-routing.json');\n}\n\nexport function sessionRoutingPath(): string {\n  return resolvePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], 'pai config migrate --history');\n}\n\nexport function migrateSessionRouting(opts: { dryRun?: boolean } = {}): MigrateFileResult {\n  return migratePaiFile(paiHomePath('session-routing.json'), [oldSessionRoutingPath()], opts);\n}\n\n/**\n * Helper to get history file path with date-based organization\n */\nexport function getHistoryFilePath(subdir: string, filename: string): string {\n  const now = new Date();\n  const tz = process.env.TIME_ZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const localDate = new Date(now.toLocaleString('en-US', { timeZone: tz }));\n  const year = localDate.getFullYear();\n  const month = String(localDate.getMonth() + 1).padStart(2, '0');\n\n  return join(historyDir(), subdir, `${year}-${month}`, filename);\n}\n", "/**\n * pai-home.ts \u2014 the PAI_HOME namespace directory and generic per-user file\n * migration into it.\n *\n * Every PAI-owned per-user file (workers.yaml, config.json, whisper-rules.md,\n * advisor-mode.json, ...) lives under PAI_HOME (~/.claude/pai by default) so\n * nothing PAI writes can ever collide with a file Claude Code itself\n * introduces under ~/.claude. Decided 2026-09-19 \u2014 see docs/workers-config.md.\n */\n\nimport { existsSync, mkdirSync, readFileSync, copyFileSync, renameSync, readdirSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\n\n/**\n * Byte-identical check that works for files of any size. `cmp` compares by\n * streaming rather than loading either file into memory, unlike a\n * readFileSync + Buffer.compare \u2014 which throws (\"File size is greater than\n * 2 GiB\") on anything past Node's 2GiB single-read ceiling, a real limit hit\n * migrating a multi-gigabyte federation.db.\n */\nfunction filesByteIdentical(a: string, b: string): boolean {\n  try {\n    execFileSync(\"cmp\", [\"-s\", a, b], { stdio: \"pipe\" });\n    return true;\n  } catch (e) {\n    if (e && typeof e === \"object\" && \"status\" in e && (e as { status: number }).status === 1) return false;\n    // cmp missing or errored for an unrelated reason \u2014 fall back to an\n    // in-memory compare (fine for the small files this path normally sees).\n    return Buffer.compare(readFileSync(a), readFileSync(b)) === 0;\n  }\n}\n\n/** PAI_HOME (test isolation, power users) overrides the default namespace dir. */\nexport function paiHomeDir(): string {\n  return process.env.PAI_HOME || join(homedir(), \".claude\", \"pai\");\n}\n\n/** A path under PAI_HOME, e.g. `paiHomePath(\"config.json\")`. */\nexport function paiHomePath(...segments: string[]): string {\n  return join(paiHomeDir(), ...segments);\n}\n\nconst noticesPrinted = new Set<string>();\n\n/**\n * Resolve a per-user PAI file: the new PAI_HOME path if it exists, else the\n * first existing entry in `oldCandidates` (checked in order \u2014 most recent\n * old location first), else the new path (the target a first write\n * creates). Prints one stderr notice per process per new-path when a\n * fallback location is actually used.\n */\nexport function resolvePaiFile(newPath: string, oldCandidates: string[], migrateHint: string): string {\n  if (existsSync(newPath)) return newPath;\n  for (const old of oldCandidates) {\n    if (existsSync(old)) {\n      if (!noticesPrinted.has(newPath)) {\n        noticesPrinted.add(newPath);\n        process.stderr.write(\n          `pai: ${old} is at an old location \u2014 run \\`${migrateHint}\\` to move it to ${newPath}\\n`\n        );\n      }\n      return old;\n    }\n  }\n  return newPath;\n}\n\nexport class PaiFileMigrationError extends Error {}\n\nexport interface MigrateFileResult {\n  fromPath: string | null;\n  toPath: string;\n  dryRun: boolean;\n  note?: string;\n}\n\n/**\n * Move a per-user PAI file to its new PAI_HOME location: copy, verify\n * byte-identical, then rename the source to `<name>.migrated-<YYYYMMDD>`\n * (never deleted). Idempotent: if the new path already holds bytes\n * identical to the found source, the source is just renamed aside; if it\n * differs, this refuses rather than overwrite silently.\n */\nexport function migratePaiFile(\n  newPath: string,\n  oldCandidates: string[],\n  opts: { dryRun?: boolean } = {}\n): MigrateFileResult {\n  const fromPath = oldCandidates.find((p) => existsSync(p)) ?? null;\n  if (!fromPath) {\n    const note = existsSync(newPath) ? \"already at new location\" : \"nothing to migrate \u2014 file does not exist yet\";\n    return { fromPath: null, toPath: newPath, dryRun: !!opts.dryRun, note };\n  }\n  if (opts.dryRun) return { fromPath, toPath: newPath, dryRun: true };\n\n  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n\n  if (existsSync(newPath)) {\n    if (filesByteIdentical(fromPath, newPath)) {\n      renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n      return { fromPath, toPath: newPath, dryRun: false, note: \"identical \u2014 old file renamed aside\" };\n    }\n    throw new PaiFileMigrationError(\n      `${newPath} already exists and differs from ${fromPath} \u2014 resolve manually, nothing changed`\n    );\n  }\n\n  const dir = dirname(newPath);\n  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n  copyFileSync(fromPath, newPath);\n\n  if (!filesByteIdentical(fromPath, newPath)) {\n    throw new PaiFileMigrationError(\n      `${newPath}: copy did not match ${fromPath} byte-for-byte \u2014 aborting, old file left in place`\n    );\n  }\n\n  renameSync(fromPath, `${fromPath}.migrated-${stamp}`);\n  return { fromPath, toPath: newPath, dryRun: false };\n}\n\nexport interface MigrateDirResult {\n  fromDir: string | null;\n  toDir: string;\n  dryRun: boolean;\n  movedCount?: number;\n  note?: string;\n}\n\n/**\n * Move a per-user PAI directory (queries/, session-state/, ...) into\n * PAI_HOME: move every entry from the first found old candidate into the\n * new dir (never overwriting an existing entry there), then rename the now-\n * empty old dir aside to `<name>.migrated-<YYYYMMDD>` (never deleted).\n * Idempotent \u2014 an already-migrated dir has nothing left to find.\n */\nexport function migratePaiDir(\n  newDir: string,\n  oldCandidates: string[],\n  opts: { dryRun?: boolean } = {}\n): MigrateDirResult {\n  const fromDir = oldCandidates.find((p) => existsSync(p) && statSync(p).isDirectory()) ?? null;\n  if (!fromDir) {\n    const note = existsSync(newDir) ? \"already at new location\" : \"nothing to migrate \u2014 directory does not exist yet\";\n    return { fromDir: null, toDir: newDir, dryRun: !!opts.dryRun, note };\n  }\n\n  const entries = readdirSync(fromDir);\n  if (opts.dryRun) return { fromDir, toDir: newDir, dryRun: true, movedCount: entries.length };\n\n  if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });\n\n  let moved = 0;\n  const collided: string[] = [];\n  for (const entry of entries) {\n    const src = join(fromDir, entry);\n    const dst = join(newDir, entry);\n    if (existsSync(dst)) {\n      collided.push(entry);\n      continue;\n    }\n    renameSync(src, dst);\n    moved++;\n  }\n\n  const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n  const remaining = readdirSync(fromDir);\n  const note = collided.length\n    ? `${collided.length} entrie(s) left in ${fromDir} \u2014 name already existed in ${newDir}`\n    : undefined;\n\n  if (remaining.length === 0) {\n    renameSync(fromDir, `${fromDir}.migrated-${stamp}`);\n  }\n\n  return { fromDir, toDir: newDir, dryRun: false, movedCount: moved, note };\n}\n"],
  "mappings": "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",
  "names": ["mkdirSync", "existsSync", "dirname", "homedir", "join", "existsSync", "readFileSync", "homedir", "existsSync", "readFileSync", "join", "existsSync", "join", "dirname", "existsSync", "mkdirSync", "result"]
}
