"use strict";
/**
 * MCP server stderr tee — dual-destination log capture.
 *
 * Claude Code spawns MCP servers as child processes and consumes their stderr
 * internally. The platform cannot intercept at the spawn level. Each server
 * must tee its own stderr so diagnostic output is retrievable.
 *
 * ┌─────────────────────────┐
 * │ MCP server process      │
 * │                         │
 * │ console.error("[x] ..") │
 * │           │             │
 * │           ▼             │
 * │ patched stderr.write()  │
 * │     │                   │
 * │     ├──► original stderr (consumed by Claude Code — opaque)
 * │     │
 * │     ├──► mcp-{name}-stderr-{YYYY-MM-DD}.log    (raw chunks, per-plugin)
 * │     │
 * │     └──► $STREAM_LOG_PATH                      (per-line, prefixed)
 * │           "[<iso>] [mcp:{name}] <line>"
 * └─────────────────────────┘
 *
 * $STREAM_LOG_PATH is set by the spawner (`getMcpServers` in `claude-agent.ts`)
 * to the per-conversation stream log file. The MCP server code itself knows
 * nothing about conversations — it just trusts the spawner's path. This is
 * the scope boundary introduced: the tee is attached per spawn,
 * not per MCP-server process lifetime. Servers spawned for conversation A
 * write to conversation A's file; servers for B write to B's file.
 *
 * Every decision is logged via `[mcp-tee-*]` markers on both the target file
 * and the original stderr so an investigator can confirm from the stream log
 * which tees were wired up, which were skipped, and why.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.initStderrTee = initStderrTee;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_string_decoder_1 = require("node:string_decoder");
// Marker on process.stderr.write so repeat calls to initStderrTee() are
// detected at runtime, not just warned about in the docstring. A second
// call would stack patches and double-log every chunk.
const INIT_MARKER = Symbol.for("maxy.mcpStderrTee.installed");
/**
 * Patch process.stderr.write to tee to:
 *   1. Per-server raw log (`mcp-<serverName>-stderr-<date>.log` under LOG_DIR).
 *   2. The per-conversation stream log at `STREAM_LOG_PATH` — per-line, prefixed.
 *   3. The original stderr (consumed by Claude Code) — always preserved.
 *
 * LOG_DIR absent → skip per-server raw file (dev mode / tool discovery).
 * STREAM_LOG_PATH absent → skip stream-log destination (older spawner or
 *   standalone invocation). In both absent cases, stderr works unchanged —
 *   a `[mcp-tee-skip]` marker is written to original stderr so the skip is
 *   visible to journalctl-level readers.
 *
 * Safe to call once at MCP server module load. Refused on second call to
 * prevent stacking patches.
 */
function initStderrTee(serverName) {
    const logDir = process.env.LOG_DIR;
    const streamLogPath = process.env.STREAM_LOG_PATH;
    // Refuse repeat patches — stacking them would double-log every chunk
    // and corrupt the re-entrancy guard (originalWrite would capture the
    // already-patched function on the second call).
    if (process.stderr.write[INIT_MARKER])
        return;
    // Keep a direct reference to the unpatched writer. All diagnostic output
    // from inside this module MUST go through originalWrite to avoid
    // re-entering the patched writer (which would recurse on a tee failure).
    const originalWrite = process.stderr.write.bind(process.stderr);
    const tsPrefix = () => `[${new Date().toISOString()}]`;
    const skipTee = (reason, destination) => {
        originalWrite(`${tsPrefix()} [platform] [mcp-tee-skip] server=${serverName} destination=${destination} reason=${JSON.stringify(reason)}\n`);
    };
    // --- Destination 1: per-server raw file (optional, existing behaviour)
    let perServerStream;
    if (logDir) {
        try {
            (0, node_fs_1.mkdirSync)(logDir, { recursive: true });
            const date = new Date().toISOString().slice(0, 10);
            perServerStream = (0, node_fs_1.createWriteStream)((0, node_path_1.resolve)(logDir, `mcp-${serverName}-stderr-${date}.log`), { flags: "a" });
            perServerStream.on("error", (err) => {
                originalWrite(`${tsPrefix()} [platform] [mcp-tee-error] server=${serverName} destination=per-server reason=${JSON.stringify(err.message)}\n`);
            });
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            skipTee(msg, "per-server");
            perServerStream = undefined;
        }
    }
    else {
        skipTee("LOG_DIR not set", "per-server");
    }
    // --- Destination 2: per-conversation stream log
    let streamLogStream;
    if (streamLogPath) {
        try {
            (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(streamLogPath), { recursive: true });
            const s = (0, node_fs_1.createWriteStream)(streamLogPath, { flags: "a" });
            s.on("error", (err) => {
                originalWrite(`${tsPrefix()} [platform] [mcp-tee-error] server=${serverName} destination=stream-log reason=${JSON.stringify(err.message)}\n`);
            });
            streamLogStream = s;
            // Attach marker lands in the stream log itself so investigators can
            // confirm per-conversation the tee was wired up.
            s.write(`${tsPrefix()} [platform] [mcp-tee-attach] server=${serverName} streamLogPath=${streamLogPath}\n`);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            skipTee(msg, "stream-log");
            streamLogStream = undefined;
        }
    }
    else {
        skipTee("STREAM_LOG_PATH not set", "stream-log");
    }
    // If neither tee target is available, leave stderr untouched — patching
    // the writer to a no-op tee would still consume event-loop cycles on
    // every write for zero observability gain.
    if (!perServerStream && !streamLogStream) {
        return;
    }
    // Line buffer — accumulates characters written to stderr so complete
    // newline-terminated lines can be prefixed and emitted to the stream log.
    // Partial chunks are held until a newline arrives (or beforeExit flushes).
    let lineBuffer = "";
    // StringDecoder buffers trailing UTF-8 continuation bytes across chunks.
    // Without it, a multi-byte codepoint that straddles a write boundary would
    // render as two U+FFFD replacement characters in the stream log. Only
    // matters when a caller writes Uint8Array chunks directly — console.error
    // produces strings — but plugins piping child-process stderr can hit this.
    const utf8 = new node_string_decoder_1.StringDecoder("utf8");
    const emitCompleteLinesToStreamLog = (chunk) => {
        if (!streamLogStream)
            return;
        lineBuffer += chunk;
        let newlineIndex;
        // eslint-disable-next-line no-cond-assign
        while ((newlineIndex = lineBuffer.indexOf("\n")) !== -1) {
            const line = lineBuffer.slice(0, newlineIndex);
            lineBuffer = lineBuffer.slice(newlineIndex + 1);
            if (line.length === 0)
                continue; // skip blank lines
            if (streamLogStream.destroyed || streamLogStream.writableEnded)
                continue;
            streamLogStream.write(`${tsPrefix()} [mcp:${serverName}] ${line}\n`);
        }
    };
    process.stderr.write = (chunk, ...args) => {
        // 1. Per-server raw file — behaviour (no prefix).
        if (perServerStream && !perServerStream.destroyed) {
            perServerStream.write(chunk);
        }
        // 2. Stream log — per-line, prefixed.
        if (streamLogStream) {
            try {
                const text = typeof chunk === "string"
                    ? chunk
                    : utf8.write(Buffer.from(chunk));
                emitCompleteLinesToStreamLog(text);
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                originalWrite(`${tsPrefix()} [platform] [mcp-tee-emit-error] server=${serverName} reason=${JSON.stringify(msg)}\n`);
            }
        }
        // 3. Original stderr — Claude Code still gets what it's always got.
        return originalWrite(chunk, ...args);
    };
    // Mark the patch so a second initStderrTee() call is refused at the top.
    process.stderr.write[INIT_MARKER] = true;
    // Flush any trailing unterminated segment on graceful event-loop drain.
    // `beforeExit` can fire repeatedly: each async write to `streamLogStream`
    // schedules I/O that keeps the event loop alive, which drains, which fires
    // `beforeExit` again. With N MCP servers attached, the handler can emit
    // thousands of detach lines per second. The `detached` flag gates the
    // drain+detach emission to a single invocation; subsequent firings are
    // no-ops. Buffer flushing also runs only once — partial-line state at true
    // process exit doesn't change across re-fires.
    let detached = false;
    process.on("beforeExit", () => {
        if (detached)
            return;
        detached = true;
        if (streamLogStream && !streamLogStream.destroyed && !streamLogStream.writableEnded) {
            if (lineBuffer.length > 0) {
                streamLogStream.write(`${tsPrefix()} [mcp:${serverName}] ${lineBuffer}\n`);
            }
            streamLogStream.write(`${tsPrefix()} [platform] [mcp-tee-detach] server=${serverName} reason=process-before-exit\n`);
        }
        lineBuffer = "";
    });
}
//# sourceMappingURL=index.js.map