{
  "version": 3,
  "sources": ["../../../src/hooks/ts/session-end/capture-session-summary.ts", "../../../src/hooks/ts/lib/pai-paths.ts", "../../../src/config/pai-home.ts"],
  "sourcesContent": ["#!/usr/bin/env node\n\n/**\n * SessionEnd Hook - Captures session summary for UOCS\n *\n * Generates a session summary document when a Claude Code session ends,\n * documenting what was accomplished during the session.\n */\n\nimport { writeFileSync, mkdirSync, existsSync, readFileSync, readdirSync } from 'fs';\nimport { join } from 'path';\nimport { historyDir } from '../lib/pai-paths';\n\ninterface SessionData {\n  conversation_id: string;\n  timestamp: string;\n  [key: string]: any;\n}\n\nasync function main() {\n  try {\n    // Read input from stdin FIRST \u2014 this must complete before CC's abort signal fires.\n    // Then fork the heavy work into a detached child so CC can't kill it.\n    const chunks: Buffer[] = [];\n    for await (const chunk of process.stdin) {\n      chunks.push(chunk);\n    }\n    const input = Buffer.concat(chunks).toString('utf-8');\n    if (!input || input.trim() === '') {\n      process.exit(0);\n    }\n\n    // Fork: re-exec ourselves with --background flag and pipe the stdin data via env.\n    // This detaches the heavy work (JSONL scan, IPC) from CC's abort signal.\n    if (!process.env.__PAI_HOOK_BG) {\n      const { spawn } = await import('child_process');\n      const child = spawn(process.execPath, [process.argv[1], '--background'], {\n        detached: true,\n        stdio: 'ignore',\n        env: { ...process.env, __PAI_HOOK_BG: '1', __PAI_HOOK_INPUT: input },\n      });\n      child.unref();\n      process.exit(0); // Return immediately \u2014 CC sees success, abort signal is harmless\n    }\n\n    // Background mode: we're detached, safe from abort signals\n    const data: SessionData = JSON.parse(process.env.__PAI_HOOK_INPUT || input);\n\n    // Generate timestamp for filename\n    const now = new Date();\n    const timestamp = now.toISOString()\n      .replace(/:/g, '')\n      .replace(/\\..+/, '')\n      .replace('T', '-'); // YYYY-MM-DD-HHMMSS\n\n    const yearMonth = timestamp.substring(0, 7); // YYYY-MM\n\n    // Try to extract session info from raw outputs\n    const sessionInfo = await analyzeSession(data.conversation_id, yearMonth);\n\n    // Generate filename\n    const filename = `${timestamp}_SESSION_${sessionInfo.focus}.md`;\n\n    // Ensure directory exists\n    const sessionDir = join(historyDir(), 'sessions', yearMonth);\n    if (!existsSync(sessionDir)) {\n      mkdirSync(sessionDir, { recursive: true });\n    }\n\n    // Generate session document\n    const sessionDoc = formatSessionDocument(timestamp, data, sessionInfo);\n\n    // Write session file\n    writeFileSync(join(sessionDir, filename), sessionDoc);\n\n    // Also store structured summary via daemon IPC for the observations system\n    await storeStructuredSummary(data.conversation_id, sessionInfo);\n\n    // Exit successfully\n    process.exit(0);\n  } catch (error) {\n    // Silent failure - don't disrupt workflow\n    console.error(`[UOCS] SessionEnd hook error: ${error}`);\n    process.exit(0);\n  }\n}\n\nasync function analyzeSession(conversationId: string, yearMonth: string): Promise<any> {\n  // Try to read raw outputs for this session\n  const rawOutputsDir = join(historyDir(), 'raw-outputs', yearMonth);\n\n  let filesChanged: string[] = [];\n  let commandsExecuted: string[] = [];\n  let toolsUsed: Set<string> = new Set();\n\n  try {\n    if (existsSync(rawOutputsDir)) {\n      // Only scan today's file \u2014 not the entire month (which can be 400MB+).\n      // JSONL filenames are prefixed with YYYY-MM-DD.\n      const todayPrefix = new Date().toISOString().substring(0, 10);\n      const files = readdirSync(rawOutputsDir).filter(\n        f => f.endsWith('.jsonl') && f.startsWith(todayPrefix)\n      );\n\n      for (const file of files) {\n        const filePath = join(rawOutputsDir, file);\n        const content = readFileSync(filePath, 'utf-8');\n        const lines = content.split('\\n').filter(l => l.trim());\n\n        for (const line of lines) {\n          try {\n            const entry = JSON.parse(line);\n            if (entry.session === conversationId) {\n              toolsUsed.add(entry.tool);\n\n              // Extract file changes\n              if (entry.tool === 'Edit' || entry.tool === 'Write') {\n                if (entry.input?.file_path) {\n                  filesChanged.push(entry.input.file_path);\n                }\n              }\n\n              // Extract bash commands\n              if (entry.tool === 'Bash' && entry.input?.command) {\n                commandsExecuted.push(entry.input.command);\n              }\n            }\n          } catch (e) {\n            // Skip invalid JSON lines\n          }\n        }\n      }\n    }\n  } catch (error) {\n    // Silent failure\n  }\n\n  return {\n    focus: 'general-work',\n    filesChanged: [...new Set(filesChanged)].slice(0, 10), // Unique, max 10\n    commandsExecuted: commandsExecuted.slice(0, 10), // Max 10\n    toolsUsed: Array.from(toolsUsed),\n    duration: 0 // Unknown\n  };\n}\n\nfunction formatSessionDocument(timestamp: string, data: SessionData, info: any): string {\n  const date = timestamp.substring(0, 10); // YYYY-MM-DD\n  const time = timestamp.substring(11).replace(/-/g, ':'); // HH:MM:SS\n  const da = process.env.DA || 'PAI';\n\n  return `---\ncapture_type: SESSION\ntimestamp: ${new Date().toISOString()}\nsession_id: ${data.conversation_id}\nduration_minutes: ${info.duration}\nexecutor: ${da}\n---\n\n# Session: ${info.focus}\n\n**Date:** ${date}\n**Time:** ${time}\n**Session ID:** ${data.conversation_id}\n\n---\n\n## Session Overview\n\n**Focus:** General development work\n**Duration:** ${info.duration > 0 ? `${info.duration} minutes` : 'Unknown'}\n\n---\n\n## Tools Used\n\n${info.toolsUsed.length > 0 ? info.toolsUsed.map((t: string) => `- ${t}`).join('\\n') : '- None recorded'}\n\n---\n\n## Files Modified\n\n${info.filesChanged.length > 0 ? info.filesChanged.map((f: string) => `- \\`${f}\\``).join('\\n') : '- None recorded'}\n\n**Total Files Changed:** ${info.filesChanged.length}\n\n---\n\n## Commands Executed\n\n${info.commandsExecuted.length > 0 ? '```bash\\n' + info.commandsExecuted.join('\\n') + '\\n```' : 'None recorded'}\n\n---\n\n## Notes\n\nThis session summary was automatically generated by the UOCS SessionEnd hook.\n\nFor detailed tool outputs, see: \\`${join(historyDir(), 'raw-outputs', timestamp.substring(0, 7))}/\\`\n\n---\n\n**Session Outcome:** Completed\n**Generated:** ${new Date().toISOString()}\n`;\n}\n\nasync function storeStructuredSummary(\n  sessionId: string,\n  info: { focus: string; filesChanged: string[]; commandsExecuted: string[]; toolsUsed: string[]; duration: number }\n): Promise<void> {\n  try {\n    const cwd = process.cwd();\n    const net = await import('net');\n\n    await new Promise<void>((resolve, _reject) => {\n      const client = net.createConnection('/tmp/pai.sock', () => {\n        const msg = JSON.stringify({\n          id: 1,\n          method: 'session_summary_store',\n          params: {\n            session_id: sessionId,\n            cwd,\n            request: null,      // We don't have the original request\n            investigated: null,\n            learned: null,\n            completed: info.filesChanged.length > 0\n              ? `Modified ${info.filesChanged.length} file(s): ${info.filesChanged.slice(0, 5).join(', ')}`\n              : null,\n            next_steps: null,\n            observation_count: 0,   // Will be filled by daemon from actual count\n          }\n        }) + '\\n';\n        client.write(msg);\n      });\n\n      client.on('data', () => { client.end(); resolve(); });\n      client.on('error', () => resolve());  // Silent failure\n      setTimeout(() => { client.destroy(); resolve(); }, 3000);\n    });\n  } catch {\n    // Silent failure \u2014 don't disrupt session end\n  }\n}\n\nmain();\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": ";;;;;;AASA,SAAS,eAAe,aAAAA,YAAW,cAAAC,aAAY,gBAAAC,eAAc,eAAAC,oBAAmB;AAChF,SAAS,QAAAC,aAAY;;;ACerB,SAAS,WAAAC,gBAAe;AACxB,SAAS,SAAS,QAAAC,aAAY;AAC9B,SAAS,cAAAC,aAAY,gBAAAC,qBAAoB;;;ACjBzC,SAAS,YAAY,WAAW,cAAc,cAAc,YAAY,aAAa,gBAAgB;AACrG,SAAS,eAAe;AACxB,SAAS,MAAM,eAAe;AAuBvB,SAAS,aAAqB;AACnC,SAAO,QAAQ,IAAI,YAAY,KAAK,QAAQ,GAAG,WAAW,KAAK;AACjE;AAGO,SAAS,eAAe,UAA4B;AACzD,SAAO,KAAK,WAAW,GAAG,GAAG,QAAQ;AACvC;AAEA,IAAM,iBAAiB,oBAAI,IAAY;AAShC,SAAS,eAAe,SAAiB,eAAyB,aAA6B;AACpG,MAAI,WAAW,OAAO,EAAG,QAAO;AAChC,aAAW,OAAO,eAAe;AAC/B,QAAI,WAAW,GAAG,GAAG;AACnB,UAAI,CAAC,eAAe,IAAI,OAAO,GAAG;AAChC,uBAAe,IAAI,OAAO;AAC1B,gBAAQ,OAAO;AAAA,UACb,QAAQ,GAAG,uCAAkC,WAAW,oBAAoB,OAAO;AAAA;AAAA,QACrF;AAAA,MACF;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;;;AD1BA,SAAS,cAAoB;AAE3B,QAAM,gBAAgB;AAAA,IACpB,QAAQ,QAAQ,IAAI,eAAe,QAAQ,IAAI,WAAW,IAAI,MAAM;AAAA,IACpE,QAAQC,SAAQ,GAAG,WAAW,MAAM;AAAA,EACtC;AAEA,aAAW,WAAW,eAAe;AACnC,QAAIC,YAAW,OAAO,GAAG;AACvB,UAAI;AACF,cAAM,UAAUC,cAAa,SAAS,OAAO;AAC7C,mBAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,gBAAM,UAAU,KAAK,KAAK;AAE1B,cAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AAEzC,gBAAM,UAAU,QAAQ,QAAQ,GAAG;AACnC,cAAI,UAAU,GAAG;AACf,kBAAM,MAAM,QAAQ,UAAU,GAAG,OAAO,EAAE,KAAK;AAC/C,gBAAI,QAAQ,QAAQ,UAAU,UAAU,CAAC,EAAE,KAAK;AAGhD,gBAAK,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAC3C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,GAAI;AAClD,sBAAQ,MAAM,MAAM,GAAG,EAAE;AAAA,YAC3B;AAGA,oBAAQ,MAAM,QAAQ,WAAWF,SAAQ,CAAC;AAC1C,oBAAQ,MAAM,QAAQ,cAAcA,SAAQ,CAAC;AAG7C,gBAAI,QAAQ,IAAI,GAAG,MAAM,QAAW;AAClC,sBAAQ,IAAI,GAAG,IAAI;AAAA,YACrB;AAAA,UACF;AAAA,QACF;AAEA;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;AAGA,YAAY;AAEZ,SAAS,oBAA4B;AACnC,SAAO,QAAQA,SAAQ,GAAG,SAAS;AACrC;AASA,SAAS,6BAAsC;AAC7C,MAAI,QAAQ,IAAI,sBAAsB,IAAK,QAAO;AAClD,QAAM,MAAM,QAAQ,KAAK,QAAQ,iBAAiB;AAClD,SAAO,QAAQ,MAAM,QAAQ,KAAK,MAAM,CAAC,MAAM;AACjD;AAQA,SAAS,yBAAyB,SAAuB;AACvD,QAAM,IAAI;AACV,MAAI,EAAE,yBAAyB,2BAA2B,EAAG;AAC7D,IAAE,wBAAwB;AAC1B,UAAQ,OAAO,MAAM,QAAQ,OAAO;AAAA,CAAI;AAC1C;AAaA,SAAS,oBAA4B;AACnC,MAAI,QAAQ,IAAI,aAAa;AAC3B,UAAM,aAAa,QAAQ,QAAQ,IAAI,WAAW;AAClD,QAAI,QAAQ,IAAI,SAAS;AACvB,YAAM,SAAS,QAAQ,QAAQ,IAAI,OAAO;AAC1C,UAAI,WAAW,YAAY;AACzB;AAAA,UACE,gBAAgB,UAAU,kBAAkB,MAAM;AAAA,QACpD;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,MAAI,QAAQ,IAAI,SAAS;AACvB,UAAM,SAAS,QAAQ,QAAQ,IAAI,OAAO;AAC1C,QAAI,WAAW,kBAAkB,GAAG;AAClC;AAAA,QACE;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO,kBAAkB;AAC3B;AAGO,IAAM,cAAc,kBAAkB;AAStC,IAAM,YAAYG,MAAK,aAAa,OAAO;AAC3C,IAAM,aAAaA,MAAK,aAAa,QAAQ;AAC7C,IAAM,aAAaA,MAAK,aAAa,QAAQ;AAC7C,IAAM,eAAeA,MAAK,aAAa,UAAU;AAMxD,SAAS,uBAA6B;AACpC,MAAI,CAACC,YAAW,WAAW,GAAG;AAC5B,YAAQ,MAAM,+BAA+B,WAAW,EAAE;AAC1D,YAAQ,MAAM,+DAA+D;AAC7E,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,CAACA,YAAW,SAAS,GAAG;AAC1B,YAAQ,MAAM,kCAAkC,SAAS,EAAE;AAC3D,YAAQ,MAAM,0CAA0C;AACxD,YAAQ,MAAM,2BAA2B,WAAW,EAAE;AACtD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAIA,qBAAqB;AAUrB,SAAS,gBAAwB;AAC/B,SAAOD,MAAK,aAAa,SAAS;AACpC;AAIO,SAAS,aAAqB;AACnC,SAAO,eAAe,YAAY,SAAS,GAAG,CAAC,cAAc,CAAC,GAAG,8BAA8B;AACjG;;;AD/LA,eAAe,OAAO;AACpB,MAAI;AAGF,UAAM,SAAmB,CAAC;AAC1B,qBAAiB,SAAS,QAAQ,OAAO;AACvC,aAAO,KAAK,KAAK;AAAA,IACnB;AACA,UAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO;AACpD,QAAI,CAAC,SAAS,MAAM,KAAK,MAAM,IAAI;AACjC,cAAQ,KAAK,CAAC;AAAA,IAChB;AAIA,QAAI,CAAC,QAAQ,IAAI,eAAe;AAC9B,YAAM,EAAE,MAAM,IAAI,MAAM,OAAO,eAAe;AAC9C,YAAM,QAAQ,MAAM,QAAQ,UAAU,CAAC,QAAQ,KAAK,CAAC,GAAG,cAAc,GAAG;AAAA,QACvE,UAAU;AAAA,QACV,OAAO;AAAA,QACP,KAAK,EAAE,GAAG,QAAQ,KAAK,eAAe,KAAK,kBAAkB,MAAM;AAAA,MACrE,CAAC;AACD,YAAM,MAAM;AACZ,cAAQ,KAAK,CAAC;AAAA,IAChB;AAGA,UAAM,OAAoB,KAAK,MAAM,QAAQ,IAAI,oBAAoB,KAAK;AAG1E,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,YAAY,IAAI,YAAY,EAC/B,QAAQ,MAAM,EAAE,EAChB,QAAQ,QAAQ,EAAE,EAClB,QAAQ,KAAK,GAAG;AAEnB,UAAM,YAAY,UAAU,UAAU,GAAG,CAAC;AAG1C,UAAM,cAAc,MAAM,eAAe,KAAK,iBAAiB,SAAS;AAGxE,UAAM,WAAW,GAAG,SAAS,YAAY,YAAY,KAAK;AAG1D,UAAM,aAAaE,MAAK,WAAW,GAAG,YAAY,SAAS;AAC3D,QAAI,CAACC,YAAW,UAAU,GAAG;AAC3B,MAAAC,WAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AAAA,IAC3C;AAGA,UAAM,aAAa,sBAAsB,WAAW,MAAM,WAAW;AAGrE,kBAAcF,MAAK,YAAY,QAAQ,GAAG,UAAU;AAGpD,UAAM,uBAAuB,KAAK,iBAAiB,WAAW;AAG9D,YAAQ,KAAK,CAAC;AAAA,EAChB,SAAS,OAAO;AAEd,YAAQ,MAAM,iCAAiC,KAAK,EAAE;AACtD,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,eAAe,eAAe,gBAAwB,WAAiC;AAErF,QAAM,gBAAgBA,MAAK,WAAW,GAAG,eAAe,SAAS;AAEjE,MAAI,eAAyB,CAAC;AAC9B,MAAI,mBAA6B,CAAC;AAClC,MAAI,YAAyB,oBAAI,IAAI;AAErC,MAAI;AACF,QAAIC,YAAW,aAAa,GAAG;AAG7B,YAAM,eAAc,oBAAI,KAAK,GAAE,YAAY,EAAE,UAAU,GAAG,EAAE;AAC5D,YAAM,QAAQE,aAAY,aAAa,EAAE;AAAA,QACvC,OAAK,EAAE,SAAS,QAAQ,KAAK,EAAE,WAAW,WAAW;AAAA,MACvD;AAEA,iBAAW,QAAQ,OAAO;AACxB,cAAM,WAAWH,MAAK,eAAe,IAAI;AACzC,cAAM,UAAUI,cAAa,UAAU,OAAO;AAC9C,cAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE,OAAO,OAAK,EAAE,KAAK,CAAC;AAEtD,mBAAW,QAAQ,OAAO;AACxB,cAAI;AACF,kBAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,gBAAI,MAAM,YAAY,gBAAgB;AACpC,wBAAU,IAAI,MAAM,IAAI;AAGxB,kBAAI,MAAM,SAAS,UAAU,MAAM,SAAS,SAAS;AACnD,oBAAI,MAAM,OAAO,WAAW;AAC1B,+BAAa,KAAK,MAAM,MAAM,SAAS;AAAA,gBACzC;AAAA,cACF;AAGA,kBAAI,MAAM,SAAS,UAAU,MAAM,OAAO,SAAS;AACjD,iCAAiB,KAAK,MAAM,MAAM,OAAO;AAAA,cAC3C;AAAA,YACF;AAAA,UACF,SAAS,GAAG;AAAA,UAEZ;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AAAA,EAEhB;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,IACP,cAAc,CAAC,GAAG,IAAI,IAAI,YAAY,CAAC,EAAE,MAAM,GAAG,EAAE;AAAA;AAAA,IACpD,kBAAkB,iBAAiB,MAAM,GAAG,EAAE;AAAA;AAAA,IAC9C,WAAW,MAAM,KAAK,SAAS;AAAA,IAC/B,UAAU;AAAA;AAAA,EACZ;AACF;AAEA,SAAS,sBAAsB,WAAmB,MAAmB,MAAmB;AACtF,QAAM,OAAO,UAAU,UAAU,GAAG,EAAE;AACtC,QAAM,OAAO,UAAU,UAAU,EAAE,EAAE,QAAQ,MAAM,GAAG;AACtD,QAAM,KAAK,QAAQ,IAAI,MAAM;AAE7B,SAAO;AAAA;AAAA,cAEI,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,cACvB,KAAK,eAAe;AAAA,oBACd,KAAK,QAAQ;AAAA,YACrB,EAAE;AAAA;AAAA;AAAA,aAGD,KAAK,KAAK;AAAA;AAAA,YAEX,IAAI;AAAA,YACJ,IAAI;AAAA,kBACE,KAAK,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAOtB,KAAK,WAAW,IAAI,GAAG,KAAK,QAAQ,aAAa,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxE,KAAK,UAAU,SAAS,IAAI,KAAK,UAAU,IAAI,CAAC,MAAc,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI,IAAI,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtG,KAAK,aAAa,SAAS,IAAI,KAAK,aAAa,IAAI,CAAC,MAAc,OAAO,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI,iBAAiB;AAAA;AAAA,2BAEvF,KAAK,aAAa,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjD,KAAK,iBAAiB,SAAS,IAAI,cAAc,KAAK,iBAAiB,KAAK,IAAI,IAAI,UAAU,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oCAQ3EJ,MAAK,WAAW,GAAG,eAAe,UAAU,UAAU,GAAG,CAAC,CAAC,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,kBAK/E,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA;AAEzC;AAEA,eAAe,uBACb,WACA,MACe;AACf,MAAI;AACF,UAAM,MAAM,QAAQ,IAAI;AACxB,UAAM,MAAM,MAAM,OAAO,KAAK;AAE9B,UAAM,IAAI,QAAc,CAACK,UAAS,YAAY;AAC5C,YAAM,SAAS,IAAI,iBAAiB,iBAAiB,MAAM;AACzD,cAAM,MAAM,KAAK,UAAU;AAAA,UACzB,IAAI;AAAA,UACJ,QAAQ;AAAA,UACR,QAAQ;AAAA,YACN,YAAY;AAAA,YACZ;AAAA,YACA,SAAS;AAAA;AAAA,YACT,cAAc;AAAA,YACd,SAAS;AAAA,YACT,WAAW,KAAK,aAAa,SAAS,IAClC,YAAY,KAAK,aAAa,MAAM,aAAa,KAAK,aAAa,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,KACzF;AAAA,YACJ,YAAY;AAAA,YACZ,mBAAmB;AAAA;AAAA,UACrB;AAAA,QACF,CAAC,IAAI;AACL,eAAO,MAAM,GAAG;AAAA,MAClB,CAAC;AAED,aAAO,GAAG,QAAQ,MAAM;AAAE,eAAO,IAAI;AAAG,QAAAA,SAAQ;AAAA,MAAG,CAAC;AACpD,aAAO,GAAG,SAAS,MAAMA,SAAQ,CAAC;AAClC,iBAAW,MAAM;AAAE,eAAO,QAAQ;AAAG,QAAAA,SAAQ;AAAA,MAAG,GAAG,GAAI;AAAA,IACzD,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAEA,KAAK;",
  "names": ["mkdirSync", "existsSync", "readFileSync", "readdirSync", "join", "homedir", "join", "existsSync", "readFileSync", "homedir", "existsSync", "readFileSync", "join", "existsSync", "join", "existsSync", "mkdirSync", "readdirSync", "readFileSync", "resolve"]
}
