{"version":3,"sources":["../src/cli/commands/ingest/tail.ts"],"sourcesContent":["import { setTimeout as wait } from \"node:timers/promises\";\nimport chalk from \"chalk\";\nimport { loadConfig, isLoggedIn } from \"@/cli/utils/governance/config\";\nimport {\n  getEventsForSource,\n  GovernanceCliError,\n  type ActivityEventDetailRow,\n} from \"@/cli/utils/governance/cli-api\";\n\n/**\n * `langwatch ingest tail <sourceId> [--limit N] [--follow] [--json]`\n *\n * Stream recent OCSF-normalised events for an IngestionSource. Wraps\n * the same `eventsForSource` query the per-source detail page uses,\n * so what you see in `tail` and what you see in the web UI are\n * guaranteed identical.\n *\n * --follow polls every 3s for new events (cursor-paginated by\n * eventTimestamp DESC); deduplicates by eventId so replays don't\n * print twice. Ctrl-C exits cleanly.\n */\nexport async function ingestTailCommand(\n  sourceId: string,\n  options: { limit?: number; follow?: boolean; json?: boolean },\n): Promise<void> {\n  const cfg = loadConfig();\n  if (!isLoggedIn(cfg)) {\n    process.stderr.write(\n      \"Not logged in. Run `langwatch login --device` first.\\n\",\n    );\n    process.exit(1);\n  }\n\n  const limit = options.limit ?? 50;\n\n  let initial: ActivityEventDetailRow[];\n  try {\n    initial = await getEventsForSource(cfg, sourceId, { limit });\n  } catch (err) {\n    if (err instanceof GovernanceCliError) {\n      process.stderr.write(`Error: ${err.message}\\n`);\n      process.exit(err.status === 404 ? 1 : 1);\n    }\n    process.stderr.write(`Error: ${String(err)}\\n`);\n    process.exit(1);\n  }\n\n  if (options.json) {\n    console.log(JSON.stringify(initial, null, 2));\n    if (!options.follow) return;\n  } else {\n    if (initial.length === 0) {\n      console.log(\n        chalk.gray(\n          \"No events for this source yet. Once your upstream platform \" +\n            \"starts sending OTel/audit logs to /api/ingest/* with the \" +\n            \"source's bearer secret, events will land here.\",\n        ),\n      );\n      if (!options.follow) return;\n    } else {\n      // Display oldest-first so a tail-like reader sees the chronology.\n      // (eventsForSource returns DESC; reverse for printing.)\n      const oldestFirst = [...initial].reverse();\n      for (const e of oldestFirst) {\n        printEventLine(e);\n      }\n    }\n  }\n\n  if (!options.follow) return;\n\n  // Poll every 3s. Track the most recent eventTimestamp + eventIds\n  // we've already printed within the same second so we don't dup\n  // events that share a timestamp.\n  let cursorIso = initial[0]?.eventTimestampIso ?? new Date().toISOString();\n  const seen = new Set<string>(initial.map((e) => e.eventId));\n  process.on(\"SIGINT\", () => {\n    process.stderr.write(chalk.gray(\"\\n^C — exiting tail\\n\"));\n    process.exit(0);\n  });\n  for (;;) {\n    await wait(3000);\n    let next: ActivityEventDetailRow[];\n    try {\n      // Query without beforeIso — we want the MOST RECENT, then\n      // filter in-memory to anything newer than cursorIso OR a new\n      // eventId at the cursorIso boundary.\n      next = await getEventsForSource(cfg, sourceId, { limit: 50 });\n    } catch (err) {\n      // Transient errors shouldn't kill the follow; print + retry.\n      const msg = err instanceof GovernanceCliError ? err.message : String(err);\n      process.stderr.write(chalk.yellow(`warn: ${msg} (retrying)\\n`));\n      continue;\n    }\n    const fresh = pickFreshEvents(next, { cursorIso, seen });\n    for (const e of fresh) {\n      if (options.json) {\n        console.log(JSON.stringify(e));\n      } else {\n        printEventLine(e);\n      }\n      seen.add(e.eventId);\n      if (e.eventTimestampIso > cursorIso) cursorIso = e.eventTimestampIso;\n    }\n  }\n}\n\n/**\n * Pure dedup filter for the --follow polling loop. Given the latest\n * batch from the server (DESC by eventTimestamp) and the current\n * `(cursorIso, seen)` watermark, returns the events that are new\n * AND have not yet been printed, in chronological (oldest-first) order.\n *\n * Two paths produce a \"new\" event:\n *   1. eventTimestampIso strictly greater than cursorIso.\n *   2. eventTimestampIso equal to cursorIso AND eventId not in `seen`\n *      — handles multiple events that share the same second-resolution\n *      timestamp on the server's clock.\n *\n * Exported for unit testing; the follow loop owns the mutable state\n * (Set + cursor) and advances them after each printed row.\n */\nexport function pickFreshEvents(\n  next: readonly ActivityEventDetailRow[],\n  state: { cursorIso: string; seen: ReadonlySet<string> },\n): ActivityEventDetailRow[] {\n  return next\n    .filter(\n      (e) =>\n        e.eventTimestampIso > state.cursorIso ||\n        (e.eventTimestampIso === state.cursorIso &&\n          !state.seen.has(e.eventId)),\n    )\n    .slice()\n    .reverse();\n}\n\n/**\n * Renders a single event row for the human (non-JSON) output mode.\n * Pure — no I/O — so it can be unit-tested by capturing stdout via\n * a spy or by calling `formatEventLine` directly.\n *\n * Cost is suppressed when ≤ 0 (no upstream cost attribute), tokens\n * are suppressed when both counts are zero. Both fields are rendered\n * as separate trailing meta cells separated by a single space.\n */\nexport function formatEventLine(e: ActivityEventDetailRow): string {\n  const ts = chalk.gray(e.eventTimestampIso);\n  const evt = chalk.cyan(e.eventType);\n  const action = chalk.white(e.action);\n  const target = chalk.magenta(e.target);\n  const cost = e.costUsd > 0 ? chalk.yellow(`$${e.costUsd.toFixed(4)}`) : \"\";\n  const tokens =\n    e.tokensInput || e.tokensOutput\n      ? chalk.gray(`${e.tokensInput}/${e.tokensOutput} tok`)\n      : \"\";\n  const meta = [cost, tokens].filter(Boolean).join(\" \");\n  return `${ts}  ${evt}  ${action} → ${target}  ${meta}`;\n}\n\nfunction printEventLine(e: ActivityEventDetailRow): void {\n  console.log(formatEventLine(e));\n}\n"],"mappings":"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","names":[]}