import { spawnSync } from 'node:child_process';
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readdirSync, readlinkSync, readSync, realpathSync, statSync } from 'node:fs';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import * as nodePty from 'node-pty';
import path from 'node:path';
import { resolveCodexTranscriptFromProcessLogs } from './codexTranscriptSource.js';
// Dedicated socket name so NTerminal's tmux server is completely isolated from
// any tmux the operator runs themselves (different `-L` = different daemon).
// Their `tmux ls` won't show our sessions, and our sessions don't pick up
// their ~/.tmux.conf either.
const TMUX_SOCKET = 'nterminal';
const TRANSCRIPT_FILE_CACHE_TTL_MS = 30_000;
const TRANSCRIPT_FILE_NEGATIVE_CACHE_TTL_MS = 5_000;
const CLAUDE_TRANSCRIPT_START_SKEW_MS = 10_000;
const CLAUDE_TRANSCRIPT_SAMPLE_BYTES = 512 * 1024;
const transcriptFileCache = new Map();
// Wraps each PTY in a tmux session so shells survive `nterminalctl restart`
// and NTerminal version updates without losing state. The operator never
// interacts with tmux directly — the bundled `nterminal-tmux.conf` strips the
// status line, disables mouse capture, and unbinds every key so the
// experience matches a raw shell.
export class TmuxPtyAdapter {
    configPath;
    socket;
    constructor(options = {}) {
        this.configPath = options.configPath ?? defaultConfigPath();
        this.socket = options.socket ?? TMUX_SOCKET;
    }
    // Returns an instance only when `tmux` is on PATH and the bundled config
    // file actually exists. The config-existence check prevents a confusing
    // mid-flight failure mode where detect() returned an adapter but every
    // `spawn` then errored with "no such file" on `tmux -f <conf>`. The
    // caller wires up `NodePtyAdapter` as the fallback so a host without
    // tmux (or with the config missing) still works — it just loses the
    // across-restart persistence.
    static detect(options = {}) {
        const probe = spawnSync('sh', ['-c', 'command -v tmux'], { stdio: ['ignore', 'pipe', 'ignore'] });
        if (probe.status !== 0) {
            return null;
        }
        const configPath = options.configPath ?? defaultConfigPath();
        if (!existsSync(configPath)) {
            return null;
        }
        return new TmuxPtyAdapter({ ...options, configPath });
    }
    spawn(options) {
        if (!options.sessionId) {
            throw new Error('TmuxPtyAdapter.spawn requires a sessionId');
        }
        // `new-session -A` = attach to the named session if it already exists,
        // otherwise create it. On the create path tmux runs `shell` inside the
        // session in `-c cwd`; on the attach path both are ignored, which is
        // exactly the behavior we want for the "NTerminal restarted, reattach to
        // live shells" flow.
        //
        // -n <title> sets the window name only on the create path. On attach it
        // is ignored — we don't want a re-attach to overwrite a name the
        // operator already chose via the rename UI in a previous session.
        const args = [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'new-session',
            '-A',
            '-s',
            options.sessionId,
            '-c',
            options.cwd
        ];
        if (options.title) {
            args.push('-n', sanitizeWindowName(options.title));
        }
        args.push(options.shell);
        const pty = nodePty.spawn('tmux', args, {
            name: 'xterm-256color',
            cols: options.cols,
            rows: options.rows,
            cwd: options.cwd,
            env: options.env
        });
        return wrapTmuxPty(pty, this.socket, this.configPath, options.sessionId);
    }
    listExisting() {
        // Pull both session name and the (single) window's name in one call so
        // restoreSessions in TerminalManager can repopulate the title without a
        // per-session round-trip. The custom delimiter `\t` avoids ambiguity if a
        // window name happens to contain spaces or pipes.
        const result = spawnSync('tmux', [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'list-sessions',
            '-F',
            '#{session_name}\t#{=128:window_name}'
        ], { stdio: ['ignore', 'pipe', 'pipe'] });
        if (result.status !== 0) {
            // Common when no server is running yet (no sessions exist). tmux exits
            // non-zero with stderr "no server running"; treat as empty list.
            return [];
        }
        return result.stdout
            .toString()
            .split('\n')
            .map((line) => line.trim())
            .filter(Boolean)
            .map((line) => {
            const tab = line.indexOf('\t');
            if (tab === -1) {
                return { sessionId: line };
            }
            const sessionId = line.slice(0, tab);
            const title = line.slice(tab + 1);
            return title ? { sessionId, title } : { sessionId };
        });
    }
    destroySession(sessionId) {
        const result = spawnSync('tmux', ['-L', this.socket, '-f', this.configPath, 'kill-session', '-t', sessionId], { stdio: 'ignore' });
        return result.status === 0;
    }
    async pollSessions() {
        // list-panes -a returns every pane across every session in one call.
        // We use one pane per session, so the result is one row per terminal.
        // `pane_current_command` is whichever process tmux sees as the
        // foreground in that pane (the shell when at a prompt, otherwise vim
        // / node / etc.). `pane_pid` is the root of that pane's process
        // subtree — we walk its descendants below to decide busy vs idle.
        const result = spawnSync('tmux', [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'list-panes',
            '-a',
            '-F',
            '#{session_name}\t#{pane_pid}\t#{pane_current_command}'
        ], { stdio: ['ignore', 'pipe', 'pipe'] });
        const out = new Map();
        if (result.status !== 0)
            return out;
        const tree = readProcessTree();
        for (const line of result.stdout.toString().split('\n')) {
            const trimmed = line.trim();
            if (!trimmed)
                continue;
            const parts = trimmed.split('\t');
            if (parts.length < 3)
                continue;
            const sessionId = parts[0];
            const panePid = Number(parts[1]);
            const currentCommand = parts[2].trim();
            if (!sessionId || !currentCommand || !Number.isFinite(panePid))
                continue;
            const reportedCommand = resolveTranscriptForegroundCommand(tree, panePid, currentCommand);
            const busy = tree ? hasRunningDescendant(tree, panePid) : false;
            out.set(sessionId, { currentCommand: reportedCommand, busy });
        }
        return out;
    }
    setSessionTitle(sessionId, title) {
        // Single-window-per-session in our setup, so renaming the window is the
        // visible "session title" the operator perceives via the tab/pill label.
        // tmux's rename-session is also possible but session names are how we
        // address sessions in spawn/kill — those must stay stable, so we change
        // window name instead.
        spawnSync('tmux', [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'rename-window',
            '-t',
            `${sessionId}:0`,
            sanitizeWindowName(title)
        ], { stdio: 'ignore' });
    }
    captureHistory(sessionId, options) {
        const limit = normalizeHistoryLimit(options.limit);
        const metrics = this.readPaneMetrics(sessionId);
        if (!metrics) {
            return null;
        }
        const oldestLine = -metrics.historySize;
        const newestLine = Math.max(0, metrics.paneHeight - 1);
        const requestedEnd = options.beforeLine === undefined ? newestLine : options.beforeLine - 1;
        const endLine = Math.min(newestLine, requestedEnd);
        if (endLine < oldestLine) {
            return { startLine: oldestLine, endLine: oldestLine - 1, hasMore: false, output: [] };
        }
        const startLine = Math.max(oldestLine, endLine - limit + 1);
        const result = spawnSync('tmux', [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'capture-pane',
            '-p',
            '-e',
            '-J',
            '-S',
            String(startLine),
            '-E',
            String(endLine),
            '-t',
            sessionId
        ], { stdio: ['ignore', 'pipe', 'pipe'] });
        if (result.status !== 0) {
            return null;
        }
        const text = normalizeCapturedPane(result.stdout.toString());
        return {
            startLine,
            endLine,
            hasMore: startLine > oldestLine,
            output: text ? [text] : []
        };
    }
    resolveTranscriptSource(sessionId) {
        const panePid = this.readPanePid(sessionId);
        if (panePid === null) {
            return null;
        }
        const tree = readProcessTree();
        if (!tree) {
            return null;
        }
        const candidates = new Map();
        for (const pid of listDescendantPids(tree, panePid)) {
            const args = readProcessArgs(pid);
            if (args.length === 0) {
                continue;
            }
            const candidate = extractTranscriptCandidate(args) ??
                resolveTranscriptCandidateFromProcessLogs(pid, args) ??
                resolveClaudeTranscriptCandidateFromProjectFiles(pid, args);
            if (candidate) {
                candidates.set(`${candidate.provider}:${candidate.sessionId}`, candidate);
            }
        }
        if (candidates.size !== 1) {
            return null;
        }
        const [candidate] = candidates.values();
        if (!candidate) {
            return null;
        }
        const matches = candidate.filePath ? [candidate.filePath] : findTranscriptFiles(candidate.provider, candidate.sessionId);
        if (matches.length !== 1) {
            return null;
        }
        return { provider: candidate.provider, sessionId: candidate.sessionId, filePath: matches[0] };
    }
    readPaneMetrics(sessionId) {
        const result = spawnSync('tmux', [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'display-message',
            '-p',
            '-t',
            sessionId,
            '#{history_size}\t#{pane_height}'
        ], { stdio: ['ignore', 'pipe', 'pipe'] });
        if (result.status !== 0) {
            return null;
        }
        const [historyRaw, heightRaw] = result.stdout.toString().trim().split('\t');
        const historySize = Number(historyRaw);
        const paneHeight = Number(heightRaw);
        if (!Number.isInteger(historySize) || historySize < 0 || !Number.isInteger(paneHeight) || paneHeight <= 0) {
            return null;
        }
        return { historySize, paneHeight };
    }
    readPanePid(sessionId) {
        const result = spawnSync('tmux', [
            '-L',
            this.socket,
            '-f',
            this.configPath,
            'display-message',
            '-p',
            '-t',
            sessionId,
            '#{pane_pid}'
        ], { stdio: ['ignore', 'pipe', 'pipe'] });
        if (result.status !== 0) {
            return null;
        }
        const panePid = Number(result.stdout.toString().trim());
        return Number.isInteger(panePid) && panePid > 0 ? panePid : null;
    }
}
// tmux window names are a single line of text without ESC sequences. Cap the
// length and strip anything that could break terminal output if a future read
// path embedded the name in a status line. 80 chars is plenty for a tab
// label and matches the length we ask for in list-sessions's =128 cap.
function sanitizeWindowName(raw) {
    const collapsed = raw.replace(/[\u0000-\u001f\u007f]+/g, ' ').trim();
    return collapsed.slice(0, 80) || 'Terminal';
}
function normalizeHistoryLimit(value) {
    if (!Number.isInteger(value) || value <= 0) {
        return 1000;
    }
    return Math.min(value, 5000);
}
function normalizeCapturedPane(text) {
    const stripped = text.replace(/\r?\n$/, '');
    return stripped ? `${stripped.replace(/\r?\n/g, '\r\n')}\r\n` : '';
}
// Wrap node-pty's IPty so PtyProcess.kill() destroys the tmux SESSION (the
// goal of TerminalManager.closeTerminal — actually free the shell), not just
// the local tmux client. Without this, "close pane" would leave a zombie
// session behind and the operator's `tmux ls -L nterminal` would slowly grow.
function wrapTmuxPty(pty, socket, configPath, sessionId) {
    return {
        pid: pty.pid,
        write(data) {
            pty.write(data);
        },
        resize(cols, rows) {
            pty.resize(cols, rows);
        },
        pause() {
            pty.pause();
        },
        resume() {
            pty.resume();
        },
        kill() {
            // kill-session tears down the tmux session for this PTY. The local
            // tmux client process (this pty) exits as a side effect once the
            // server reports the session is gone. Done synchronously so the
            // caller's "the shell is gone now" assumption holds.
            spawnSync('tmux', ['-L', socket, '-f', configPath, 'kill-session', '-t', sessionId], { stdio: 'ignore' });
            // Belt-and-suspenders: if kill-session failed (e.g., session already
            // exited), make sure the client process is gone too.
            try {
                pty.kill();
            }
            catch {
                // tmux client may have already exited
            }
        },
        onData(listener) {
            const disposable = pty.onData(listener);
            return { dispose: () => disposable.dispose() };
        },
        onExit(listener) {
            const disposable = pty.onExit((event) => {
                listener({ exitCode: event.exitCode ?? null });
            });
            return { dispose: () => disposable.dispose() };
        }
    };
}
function defaultConfigPath() {
    // import.meta.url points to the compiled file under dist/server; from
    // there `../../../scripts/nterminal-tmux.conf` resolves back to the source
    // tree's config. Same layout in dev (running directly under src/).
    return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../scripts/nterminal-tmux.conf');
}
const SESSION_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
// One `ps` dump covers every pane in the poll cycle — much cheaper than
// per-pane shell-outs. The output is `pid ppid state` with leading
// whitespace trimmed; state is the kernel-reported run state where
// 'R' = runnable / on-cpu, 'D' = uninterruptible I/O wait, anything
// else (S, T, t, Z, I) we treat as "not actively working right now".
function readProcessTree() {
    const result = spawnSync('ps', ['-eo', 'pid=,ppid=,state='], {
        stdio: ['ignore', 'pipe', 'pipe']
    });
    if (result.status !== 0)
        return null;
    const childrenByPpid = new Map();
    for (const line of result.stdout.toString().split('\n')) {
        const parts = line.trim().split(/\s+/);
        if (parts.length < 3)
            continue;
        const pid = Number(parts[0]);
        const ppid = Number(parts[1]);
        const state = parts[2].charAt(0);
        if (!Number.isFinite(pid) || !Number.isFinite(ppid) || !state)
            continue;
        const bucket = childrenByPpid.get(ppid);
        if (bucket)
            bucket.push({ pid, state });
        else
            childrenByPpid.set(ppid, [{ pid, state }]);
    }
    return { childrenByPpid };
}
function hasRunningDescendant(tree, root) {
    // BFS from the shell PID (root). Only descendants count — the shell
    // itself is virtually always sleeping in read(), so including it
    // would never flip the result anyway, but skipping it documents the
    // intent: "is something the operator launched currently doing work".
    const stack = [root];
    const seen = new Set([root]);
    while (stack.length > 0) {
        const pid = stack.pop();
        const kids = tree.childrenByPpid.get(pid);
        if (!kids)
            continue;
        for (const child of kids) {
            if (child.state === 'R' || child.state === 'D')
                return true;
            if (seen.has(child.pid))
                continue;
            seen.add(child.pid);
            stack.push(child.pid);
        }
    }
    return false;
}
function listDescendantPids(tree, root) {
    const out = [];
    const stack = [root];
    const seen = new Set([root]);
    while (stack.length > 0) {
        const pid = stack.pop();
        const kids = tree.childrenByPpid.get(pid);
        if (!kids)
            continue;
        for (const child of kids) {
            if (seen.has(child.pid))
                continue;
            seen.add(child.pid);
            out.push(child.pid);
            stack.push(child.pid);
        }
    }
    return out;
}
function resolveTranscriptForegroundCommand(tree, root, currentCommand) {
    if (!tree || !isNodeCommand(currentCommand)) {
        return currentCommand;
    }
    const descendantArgs = listDescendantPids(tree, root).map((pid) => readProcessArgs(pid));
    return normalizeTmuxCurrentCommand(currentCommand, descendantArgs);
}
export function normalizeTmuxCurrentCommand(currentCommand, descendantArgs) {
    if (!isNodeCommand(currentCommand)) {
        return currentCommand;
    }
    for (const args of descendantArgs) {
        if (findCommandArg(args, 'codex') !== -1) {
            return 'codex';
        }
        if (findCommandArg(args, 'claude') !== -1) {
            return 'claude';
        }
    }
    return currentCommand;
}
function isNodeCommand(command) {
    const base = path.basename(command).toLowerCase();
    return base === 'node' || base === 'nodejs';
}
function readProcessArgs(pid) {
    try {
        const raw = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
        return raw.split('\0').filter(Boolean);
    }
    catch {
        return [];
    }
}
function extractTranscriptCandidate(args) {
    const codexIndex = findCommandArg(args, 'codex');
    if (codexIndex !== -1) {
        const sessionId = findCodexSessionId(args, codexIndex);
        return sessionId ? { provider: 'codex', sessionId } : null;
    }
    const claudeIndex = findCommandArg(args, 'claude');
    if (claudeIndex !== -1) {
        const sessionId = findClaudeSessionId(args, claudeIndex);
        return sessionId ? { provider: 'claude', sessionId } : null;
    }
    return null;
}
function resolveTranscriptCandidateFromProcessLogs(pid, args) {
    if (findCommandArg(args, 'codex') === -1) {
        return null;
    }
    return resolveCodexTranscriptFromProcessLogs(pid);
}
function resolveClaudeTranscriptCandidateFromProjectFiles(pid, args) {
    if (findCommandArg(args, 'claude') === -1) {
        return null;
    }
    const cwdCandidates = [readProcessCwd(pid), readProcessEnvValue(pid, 'PWD')].filter((value) => Boolean(value));
    if (cwdCandidates.length === 0) {
        return null;
    }
    return resolveClaudeTranscriptFromProjectFiles({
        cwdCandidates,
        processStartedAtMs: readProcessStartedAtMilliseconds(pid)
    });
}
export function resolveClaudeTranscriptFromProjectFiles(options) {
    const cwdCandidates = normalizeClaudeCwdCandidates(options.cwdCandidates);
    if (cwdCandidates.length === 0) {
        return null;
    }
    const projectRoot = path.join(options.homeDir ?? os.homedir(), '.claude', 'projects');
    const projectDirs = new Set(cwdCandidates.map((cwd) => path.join(projectRoot, encodeClaudeProjectPath(cwd))));
    const candidates = new Map();
    for (const projectDir of projectDirs) {
        let entries;
        try {
            entries = readdirSync(projectDir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            if (!entry.isFile() || !entry.name.endsWith('.jsonl')) {
                continue;
            }
            const sessionId = entry.name.slice(0, -'.jsonl'.length);
            if (!isSessionId(sessionId)) {
                continue;
            }
            const filePath = path.join(projectDir, entry.name);
            if (!isPlausibleClaudeTranscriptFile(filePath, sessionId, cwdCandidates, options.processStartedAtMs)) {
                continue;
            }
            candidates.set(sessionId, { provider: 'claude', sessionId, filePath });
        }
    }
    if (candidates.size !== 1) {
        return null;
    }
    return [...candidates.values()][0] ?? null;
}
function findCommandArg(args, command) {
    return args.findIndex((arg) => {
        const base = path.basename(arg).toLowerCase();
        if (base === command || base === `${command}.js` || base.startsWith(`${command}-`)) {
            return true;
        }
        if (command === 'claude') {
            const normalized = arg.replace(/\\/g, '/').toLowerCase();
            return normalized.includes('/@anthropic-ai/claude-code/') && base === 'cli.js';
        }
        return false;
    });
}
function readProcessCwd(pid) {
    try {
        return readlinkSync(`/proc/${pid}/cwd`);
    }
    catch {
        return null;
    }
}
function readProcessEnvValue(pid, key) {
    try {
        const raw = readFileSync(`/proc/${pid}/environ`, 'utf8');
        const prefix = `${key}=`;
        for (const entry of raw.split('\0')) {
            if (entry.startsWith(prefix)) {
                return entry.slice(prefix.length) || null;
            }
        }
        return null;
    }
    catch {
        return null;
    }
}
function readProcessStartedAtMilliseconds(pid) {
    const bootTime = readBootTimeSeconds();
    const clockTicks = readClockTicksPerSecond();
    if (bootTime === null || clockTicks === null) {
        return null;
    }
    let raw;
    try {
        raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
    }
    catch {
        return null;
    }
    const closeParen = raw.lastIndexOf(')');
    if (closeParen === -1) {
        return null;
    }
    const fieldsAfterCommand = raw.slice(closeParen + 2).trim().split(/\s+/);
    const startTicks = Number(fieldsAfterCommand[19]);
    if (!Number.isFinite(startTicks) || startTicks < 0) {
        return null;
    }
    return (bootTime + startTicks / clockTicks) * 1000;
}
function readBootTimeSeconds() {
    let raw;
    try {
        raw = readFileSync('/proc/stat', 'utf8');
    }
    catch {
        return null;
    }
    const line = raw.split('\n').find((entry) => entry.startsWith('btime '));
    if (!line) {
        return null;
    }
    const bootTime = Number(line.slice('btime '.length).trim());
    return Number.isFinite(bootTime) && bootTime > 0 ? bootTime : null;
}
function readClockTicksPerSecond() {
    const result = spawnSync('getconf', ['CLK_TCK'], {
        stdio: ['ignore', 'pipe', 'ignore'],
        encoding: 'utf8',
        timeout: 1000
    });
    const ticks = Number(result.stdout.trim());
    return Number.isFinite(ticks) && ticks > 0 ? ticks : null;
}
function normalizeClaudeCwdCandidates(values) {
    const out = new Set();
    for (const value of values) {
        const trimmed = value.trim();
        if (!trimmed) {
            continue;
        }
        const resolved = path.resolve(trimmed);
        out.add(resolved);
        try {
            out.add(realpathSync(resolved));
        }
        catch {
            // The process cwd is enough when the path no longer resolves.
        }
    }
    return [...out];
}
function encodeClaudeProjectPath(cwd) {
    return cwd.replace(/[^A-Za-z0-9_-]/g, '-');
}
function isPlausibleClaudeTranscriptFile(filePath, sessionId, cwdCandidates, processStartedAtMs) {
    let stat;
    try {
        stat = statSync(filePath);
    }
    catch {
        return false;
    }
    if (!stat.isFile()) {
        return false;
    }
    if (processStartedAtMs !== undefined &&
        processStartedAtMs !== null &&
        stat.mtimeMs + CLAUDE_TRANSCRIPT_START_SKEW_MS < processStartedAtMs) {
        return false;
    }
    const sample = readFileHeadTail(filePath, CLAUDE_TRANSCRIPT_SAMPLE_BYTES);
    if (!sample) {
        return false;
    }
    const sessionNeedle = `"sessionId":${JSON.stringify(sessionId)}`;
    if (!sample.includes(sessionNeedle)) {
        return false;
    }
    return cwdCandidates.some((cwd) => sample.includes(`"cwd":${JSON.stringify(cwd)}`));
}
function readFileHeadTail(filePath, bytes) {
    let fd = null;
    try {
        fd = openSync(filePath, 'r');
        const stat = fstatSync(fd);
        const headBytes = Math.min(bytes, stat.size);
        const head = Buffer.alloc(headBytes);
        readSync(fd, head, 0, headBytes, 0);
        if (stat.size <= bytes) {
            return head.toString('utf8');
        }
        const tailBytes = Math.min(bytes, stat.size - headBytes);
        const tail = Buffer.alloc(tailBytes);
        readSync(fd, tail, 0, tailBytes, stat.size - tailBytes);
        return `${head.toString('utf8')}\n${tail.toString('utf8')}`;
    }
    catch {
        return null;
    }
    finally {
        if (fd !== null) {
            closeSync(fd);
        }
    }
}
function findCodexSessionId(args, commandIndex) {
    for (let index = commandIndex + 1; index < args.length - 1; index += 1) {
        if (args[index] === 'resume' && isSessionId(args[index + 1])) {
            return args[index + 1];
        }
    }
    return null;
}
function findClaudeSessionId(args, commandIndex) {
    for (let index = commandIndex + 1; index < args.length; index += 1) {
        const arg = args[index];
        if ((arg === '--session-id' || arg === '--resume' || arg === '-r') && isSessionId(args[index + 1])) {
            return args[index + 1];
        }
        const sessionId = readLongOptionValue(arg, '--session-id') ?? readLongOptionValue(arg, '--resume');
        if (sessionId && isSessionId(sessionId)) {
            return sessionId;
        }
    }
    return null;
}
function readLongOptionValue(arg, option) {
    const prefix = `${option}=`;
    return arg.startsWith(prefix) ? arg.slice(prefix.length) : null;
}
function isSessionId(value) {
    return typeof value === 'string' && SESSION_ID_PATTERN.test(value);
}
function findTranscriptFiles(provider, sessionId) {
    const cacheKey = `${provider}:${sessionId}`;
    const cached = transcriptFileCache.get(cacheKey);
    const now = Date.now();
    if (cached && cached.expiresAt > now) {
        return cached.matches.slice();
    }
    const root = provider === 'codex'
        ? path.join(os.homedir(), '.codex', 'sessions')
        : path.join(os.homedir(), '.claude', 'projects');
    const matches = [];
    findTranscriptFilesUnder(root, provider, sessionId, matches);
    transcriptFileCache.set(cacheKey, {
        expiresAt: now + (matches.length === 1 ? TRANSCRIPT_FILE_CACHE_TTL_MS : TRANSCRIPT_FILE_NEGATIVE_CACHE_TTL_MS),
        matches: matches.slice()
    });
    return matches;
}
function findTranscriptFilesUnder(root, provider, sessionId, matches) {
    if (matches.length > 1) {
        return;
    }
    let entries;
    try {
        entries = readdirSync(root, { withFileTypes: true });
    }
    catch {
        return;
    }
    for (const entry of entries) {
        if (matches.length > 1) {
            return;
        }
        const fullPath = path.join(root, entry.name);
        if (entry.isDirectory()) {
            findTranscriptFilesUnder(fullPath, provider, sessionId, matches);
            continue;
        }
        if (!entry.isFile()) {
            continue;
        }
        if (provider === 'codex' && entry.name.startsWith('rollout-') && entry.name.endsWith(`${sessionId}.jsonl`)) {
            matches.push(fullPath);
            continue;
        }
        if (provider === 'claude' && entry.name === `${sessionId}.jsonl`) {
            matches.push(fullPath);
        }
    }
}
//# sourceMappingURL=TmuxPtyAdapter.js.map