"use strict";
/**
 * Agent-mode NDJSON event schema.
 *
 * The wizard's `--agent` mode emits one JSON line per significant moment to
 * stdout. Outer agents (Claude Code, Cursor, Codex, custom orchestrators)
 * parse these to drive their own UX — display login URLs, inspect plans,
 * decide whether to retry, surface choices to the human.
 *
 * This module is the source of truth for that wire format. Every event is a
 * member of `AgentEvent` and shares the `AgentEventEnvelope` shape:
 *
 *   { v, '@timestamp', type, message, session_id, run_id, data, level? }
 *
 * Schema rules:
 *   - `v` is the wire-format version. Bump on any breaking shape change.
 *   - Discriminator is `data.event` for `lifecycle` / `prompt` / `result` / `error` events.
 *   - Never include access tokens, API keys, refresh tokens, or full URLs
 *     containing query-string secrets in any payload.
 *   - Resume hints (`resumeFlags`, `resumeCommand`) are arrays of CLI argv,
 *     not shell strings, so outer agents can spawn directly.
 *
 * The `AgentUI` class (`src/ui/agent-ui.ts`) is the only writer of these
 * events. New events MUST land here first so the schema doc and the emitter
 * stay in sync.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_LOG_MESSAGE_LENGTH = exports.EVENT_DATA_VERSIONS = exports.AGENT_EVENT_WIRE_VERSION = void 0;
exports.summarizeForEvent = summarizeForEvent;
exports.summarizeToolInput = summarizeToolInput;
exports.classifyRunError = classifyRunError;
exports.truncateLogMessage = truncateLogMessage;
exports.classifyWriteOperation = classifyWriteOperation;
/**
 * Envelope (top-level) wire-format version. Bump on any breaking change
 * to the FRAME shape — i.e. the keys directly on the JSON line itself
 * (`v`, `@timestamp`, `type`, `message`, `session_id`, `run_id`,
 * `level`, `data`, `data_version`).
 *
 * Per-event `data` shapes get their own version on the envelope via
 * `data_version` (see below). Bumping `v` is for the framing layer
 * only.
 */
exports.AGENT_EVENT_WIRE_VERSION = 1;
/**
 * Per-event-type data-shape version. The key insight from orchestrator
 * feedback: pinning to envelope `v: 1` doesn't protect orchestrators
 * from breaking changes inside `data`. Adding/renaming a field on
 * (say) `event_plan_proposed` keeps envelope v=1 stable but silently
 * shifts the contract for that one event.
 *
 * Solution: every event whose `data` shape is part of the public API
 * carries a `data_version` integer on the envelope. Orchestrators
 * should branch on `(type, data?.event, data_version)` rather than
 * envelope `v` alone. The default for events without a registered
 * version is 1 — adding `data_version` to an event for the first time
 * is itself the v=1 baseline. Bump to 2 on the first breaking change.
 *
 * Why a flat number per event-type+discriminator instead of one global
 * counter: a global counter forces every orchestrator to upgrade in
 * lockstep when any event changes. Per-event versions let one event's
 * shape evolve without invalidating an orchestrator that only cares
 * about (e.g.) `tool_call` and `dashboard_created`.
 *
 * Registry: see `EVENT_DATA_VERSIONS` below — single source of truth.
 * To bump a version, update that map AND add a regression test pinning
 * the new shape.
 */
exports.EVENT_DATA_VERSIONS = {
    // Lifecycle
    start_run: 1,
    /**
     * Terminal lifecycle event — emitted exactly once per run, immediately
     * before the process exits via `wizardSuccessExit` / `wizardAbort`.
     * Carries the structured outcome (`success` / `error` / `cancelled`),
     * the exit code the process is about to return, and the run duration.
     * Orchestrators should treat absence of `run_completed` as
     * "wizard crashed mid-stream" — distinct from a clean failure exit.
     */
    run_completed: 1,
    intro: 1,
    outro: 1,
    cancel: 1,
    auth_required: 1,
    nested_agent: 1,
    inner_agent_started: 1,
    // Project create. Discriminators must match the actual `data.event`
    // strings emitted by AgentUI — bugbot caught a previous mismatch
    // (`project_created` vs the emitted `project_create_success`) that
    // silently dropped the data_version stamp from those events.
    project_create_start: 1,
    project_create_success: 1,
    project_create_error: 1,
    // Tool / file changes
    tool_call: 1,
    file_change_planned: 1,
    file_change_applied: 1,
    // Event plan
    event_plan_proposed: 1,
    event_plan_confirmed: 1,
    event_plan: 1,
    event_plan_set: 1,
    // Verification
    verification_started: 1,
    verification_result: 1,
    // Other results
    events_detected: 1,
    dashboard_created: 1,
    /**
     * `setup_context` — emitted by `plan` (in the JSON envelope) and at
     * `apply_started`, before any work happens. Carries the resolved
     * Amplitude scope (region, org, project, app, env) so the outer
     * agent can SHOW the user exactly what's about to be modified before
     * asking them to approve. Without this, an outer agent has no
     * authoritative handle on which Amplitude app the wizard will write
     * to and may render data from a stale project for follow-up queries.
     *
     * Each scope field carries a `source` discriminator (`auto` /
     * `flag` / `saved` / `recommended`) so the orchestrator can decide
     * whether a re-confirm is warranted (e.g. `auto` from a single-match
     * still benefits from a "look right?" prompt).
     */
    setup_context: 1,
    /**
     * `setup_complete` — terminal artifact event emitted exactly once
     * per successful `apply` run, immediately before `run_completed`.
     * Single source of truth for the artifacts the outer agent needs
     * for follow-up work: which app to query, which files were
     * written, which env vars were set, which dashboard URL to render.
     *
     * Skill rule: after this event fires, the outer agent MUST replace
     * any cached Amplitude project context with `amplitude.appId` —
     * otherwise follow-up MCP queries (charts, dashboards, events) hit
     * the wrong project.
     */
    setup_complete: 1,
    /**
     * `agent_metrics` — emitted once per agent run at finalize time
     * with aggregated token usage, tool call counts, and run duration.
     * Lets orchestrators bill / cap / monitor cost without re-parsing
     * the full event stream. Token counts come straight from the
     * Claude Agent SDK's terminal `result` message.
     */
    agent_metrics: 1,
    /**
     * `needs_input` — structured prompt asking the orchestrator (or
     * human) for one of N choices. Carries the question, choices,
     * recommended pick, manual-entry hint, and pagination. The most
     * orchestrator-facing event in the wire — without this `data_version`
     * stamp consumers couldn't safely evolve schema for it.
     */
    needs_input: 1,
    /**
     * `decision_auto` — emitted alongside a `needs_input` whenever the
     * wizard auto-resolves the prompt (under `--auto-approve` /
     * `--yes` / `--ci` / `--force`, OR the `--agent`-implies-autoApprove
     * back-compat path). Lets orchestrators distinguish "you should
     * surface this question to a human" from "FYI, I auto-picked the
     * recommended value." Without it, a strict orchestrator subscribing
     * to `needs_input` would race the wizard's auto-resolve.
     *
     * Fires AFTER the corresponding `needs_input` so a single-event
     * subscriber that sees `needs_input` first is guaranteed to see the
     * auto-resolution next on the same stream.
     */
    decision_auto: 1,
    /**
     * `heartbeat` — periodic liveness signal emitted every ~10s while
     * the inner agent is running. Carries elapsed wall-clock time, the
     * current retry attempt count, and the rolling tail of pushStatus
     * messages so an orchestrator can render a "still working…" widget
     * without going dark when a long tool call (Bash, MCP, file edit
     * chain) eats 30+ seconds of silence. Always fires on the cadence,
     * regardless of whether the agent has been chatty — absence of
     * heartbeat events is the canonical signal that the wizard is
     * stalled.
     */
    heartbeat: 1,
    /**
     * `checkpoint_saved` — emitted whenever the wizard writes a session
     * snapshot to `~/.amplitude/wizard/runs/<sha>/checkpoint.json`.
     * Lets orchestrators know there's a recoverable state on disk so
     * a rerun can pass `--resume` to skip already-completed steps
     * (region pick, OAuth, framework detection, etc.).
     */
    checkpoint_saved: 1,
    /**
     * `checkpoint_loaded` — emitted at startup in agent / CI mode when
     * `--resume` finds a fresh, schema-valid checkpoint and restores
     * the session from it. Carries the file age so an orchestrator can
     * decide whether the checkpoint is too stale to trust ("you saved
     * this 22h ago, are you sure you want to keep going?").
     */
    checkpoint_loaded: 1,
    /**
     * `checkpoint_cleared` — emitted when the wizard removes a saved
     * checkpoint. The `reason` discriminator covers the three legitimate
     * triggers (`success` after a clean run, `manual` from a slash
     * command, `logout` after sign-out). Lets orchestrators avoid
     * showing a "resume?" prompt once the underlying state is gone.
     */
    checkpoint_cleared: 1,
};
// ── Tool-input summarizer ───────────────────────────────────────────
//
// PreToolUse hooks receive the raw tool input which can include large
// prompts, full file contents, or shell commands. We surface only a short
// summary string in NDJSON so:
//   - large file contents don't blow up the outer agent's context
//   - commands stay scannable in a transcript
//   - prompts/messages aren't leaked downstream
/** Truncate a string for inclusion in NDJSON event payloads. */
function summarizeForEvent(s, max = 120) {
    if (s.length <= max)
        return s;
    return `${s.slice(0, max - 1)}…`;
}
/**
 * Best-effort summarizer for PreToolUse `input` payloads. Recognizes the
 * common Claude tools (Read/Edit/Write/Bash/Grep/Glob/Task/TodoWrite/MCP)
 * and produces a short human-readable string. Falls back to a JSON head
 * for unknown tool shapes.
 */
function summarizeToolInput(toolName, input) {
    if (input === null || typeof input !== 'object')
        return undefined;
    const obj = input;
    switch (toolName) {
        case 'Read':
        case 'Edit':
        case 'Write':
        case 'MultiEdit':
        case 'NotebookEdit':
            return typeof obj.file_path === 'string'
                ? summarizeForEvent(obj.file_path)
                : typeof obj.path === 'string'
                    ? summarizeForEvent(obj.path)
                    : undefined;
        case 'Bash':
            return typeof obj.command === 'string'
                ? summarizeForEvent(obj.command)
                : undefined;
        case 'Grep':
        case 'Glob':
            return typeof obj.pattern === 'string'
                ? summarizeForEvent(obj.pattern)
                : undefined;
        case 'Task':
            return typeof obj.description === 'string'
                ? summarizeForEvent(obj.description)
                : undefined;
        case 'TodoWrite':
            return Array.isArray(obj.todos)
                ? `${obj.todos.length} todo(s)`
                : undefined;
        default: {
            // Unknown tool: emit a short JSON head, stripped of newlines.
            try {
                return summarizeForEvent(JSON.stringify(input).replace(/\s+/g, ' '), 80);
            }
            catch {
                return undefined;
            }
        }
    }
}
/**
 * Best-effort classifier — maps an Error's name + message to the most
 * specific `recoverable` hint we can derive. Used by `setRunError` so
 * every uncaught run-aborting error carries a hint without the caller
 * having to know which bucket it fell into.
 *
 * Patterns sourced from production Sentry traces and the audit doc.
 * Pure (no side effects, no network) so it's safe to call in any
 * emit path.
 */
function classifyRunError(error) {
    const msg = `${error.name}: ${error.message}`.toLowerCase();
    // Auth / token expired — only the human can re-login. Pre-empt the
    // generic-retry path so orchestrators don't burn budget retrying.
    if (msg.includes('authentication_error') ||
        msg.includes('authentication_failed') ||
        msg.includes('invalid or expired token') ||
        msg.includes('401') ||
        msg.includes('needs-auth')) {
        return {
            recoverable: 'human_required',
            suggestedAction: {
                command: ['amplitude-wizard', 'login'],
                docsUrl: 'https://github.com/amplitude/wizard#login',
            },
        };
    }
    // Quota / forbidden — needs an admin or a billing change.
    if (msg.includes('quota_reached') ||
        msg.includes('quota exceeded') ||
        msg.includes('forbidden') ||
        msg.includes('403')) {
        return { recoverable: 'human_required' };
    }
    // Write refused — the agent needed write permission and didn't have it.
    // Re-running with `--yes` is the canonical remediation.
    if (msg.includes('write_refused') ||
        msg.includes('permission denied') ||
        msg.includes('eacces')) {
        return {
            recoverable: 'reinvoke_with_flag',
            suggestedAction: {
                command: ['amplitude-wizard', '--yes'],
            },
        };
    }
    // Gateway / upstream blips — transient, safe to retry.
    if (msg.includes('gateway_down') ||
        msg.includes('terminated') ||
        msg.includes('502') ||
        msg.includes('503') ||
        msg.includes('504') ||
        msg.includes('econnreset') ||
        msg.includes('etimedout') ||
        msg.includes('network')) {
        return { recoverable: 'retry' };
    }
    // Rate limit — transient but the orchestrator should back off.
    if (msg.includes('rate_limit') || msg.includes('429')) {
        return { recoverable: 'retry' };
    }
    // Default: assume the wizard hit a bug. Don't loop on fatal errors —
    // route to the bug report flow.
    return {
        recoverable: 'fatal',
        suggestedAction: {
            command: ['amplitude-wizard', 'feedback'],
        },
    };
}
// ── Log truncation ──────────────────────────────────────────────────
//
// Inner-agent errors can include the entire failing SSE response body
// (model id, signature blobs, cache token counts, partial JSON
// deltas — kilobytes of internals). Past sessions surfaced 50KB+
// `log.message` strings that polluted orchestrator context, leaked
// internal model identifiers, and rendered as walls of unreadable text.
// We truncate in the emitter so a single misbehaving caller can't blow
// up downstream parsers regardless of where the noise originated.
/**
 * Maximum length of a `log.message` string in NDJSON output. Spillover
 * is dropped from the wire and pointed at the on-disk verbose log so
 * orchestrators see a readable status line and the operator still has
 * the full payload for debugging.
 */
exports.MAX_LOG_MESSAGE_LENGTH = 2048;
/**
 * Truncate a log message for inclusion in NDJSON output. Idempotent
 * (already-short strings pass through unchanged) and stable (the
 * suffix is appended exactly once even on double-truncation).
 *
 *   - `<= MAX_LOG_MESSAGE_LENGTH` → returned verbatim
 *   - otherwise                  → `<head>… [truncated …; see verbose log]`
 *
 * Pure for unit testing.
 */
function truncateLogMessage(message, max = exports.MAX_LOG_MESSAGE_LENGTH) {
    if (message.length <= max)
        return message;
    const suffix = '… [truncated; see verbose log]';
    // Reserve room for the suffix so the final string is always exactly
    // `max` bytes long (or shorter when `max` itself is too small for
    // the suffix — defensive, never happens in practice).
    const headroom = Math.max(0, max - suffix.length);
    return message.slice(0, headroom) + suffix;
}
/**
 * Map a Claude write-tool name to the operation kind the wire format
 * exposes. `Write` always creates (or overwrites), `Edit` / `MultiEdit` /
 * `NotebookEdit` modify. Returns null for non-write tools so callers can
 * skip emission cleanly.
 */
function classifyWriteOperation(toolName) {
    switch (toolName) {
        case 'Write':
            return 'create';
        case 'Edit':
        case 'MultiEdit':
        case 'NotebookEdit':
            return 'modify';
        default:
            return null;
    }
}
