@cleocode/runtime 2026.6.12 → 2026.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SubsystemRegistry,
3
3
  createSubsystemLogger,
4
4
  defineSubsystem
5
- } from "./chunk-WSQQ5DQL.js";
5
+ } from "./chunk-3K3LFSDF.js";
6
6
 
7
7
  // src/index.ts
8
8
  import { conduit } from "@cleocode/core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/runtime",
3
- "version": "2026.6.12",
3
+ "version": "2026.6.14",
4
4
  "description": "Long-running process layer for CLEO — agent polling, SSE connections, heartbeat, key rotation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,8 +33,8 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "node-cron": "^4.2.1",
36
- "@cleocode/contracts": "2026.6.12",
37
- "@cleocode/core": "2026.6.12"
36
+ "@cleocode/contracts": "2026.6.14",
37
+ "@cleocode/core": "2026.6.14"
38
38
  },
39
39
  "devDependencies": {
40
40
  "tsup": "^8.0.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/gateway/rpc/codec.ts","../src/gateway/rpc/server.ts"],"sourcesContent":["/**\n * NDJSON codec for the CLI-RPC transport.\n *\n * Implements the SAME framing discipline as the FROZEN `supervisor-ipc` v1.0\n * contract — one zod-validated JSON envelope per newline-delimited line, with\n * an explicit `protocol_version` negotiation step — but for the gateway-RPC\n * frame set (`@cleocode/contracts/gateway/rpc`). The supervisor-ipc module is\n * frozen and is NOT edited; this codec re-applies its pattern.\n *\n * The codec is intentionally socket-agnostic: it owns line buffering, JSON\n * parse, zod decode, and version negotiation, returning typed results the\n * server maps onto `node:net` reads/writes. This keeps the parse/validate logic\n * unit-testable without binding a real socket.\n *\n * @task T11449\n * @epic T11254\n * @saga T11243\n */\n\nimport {\n GATEWAY_RPC_PROTOCOL_VERSION,\n type GatewayRpcErrorCode,\n type GatewayRpcErrorFrame,\n type GatewayRpcFrame,\n GatewayRpcFrameSchema,\n type GatewayRpcRequestFrame,\n type GatewayRpcResponseFrame,\n isFrozenGatewayRpcVersion,\n} from '@cleocode/contracts/gateway/rpc';\n\n/** Newline delimiter for the NDJSON wire (mirrors supervisor-ipc). */\nconst NEWLINE = '\\n';\n\n/**\n * Encode any RPC frame to a single NDJSON line (JSON + trailing `\\n`).\n *\n * @param frame - The response or error frame to serialize.\n * @returns A newline-terminated JSON string ready to write to the socket.\n */\nexport function encodeFrame(frame: GatewayRpcResponseFrame | GatewayRpcErrorFrame): string {\n return `${JSON.stringify(frame)}${NEWLINE}`;\n}\n\n/**\n * Build a protocol-level error frame.\n *\n * @param id - Correlation id (`'0'` when the originating frame's id is unknown).\n * @param code - One of the frozen {@link GatewayRpcErrorCode} values.\n * @param message - Human-readable detail.\n * @returns A fully-formed {@link GatewayRpcErrorFrame}.\n */\nexport function buildErrorFrame(\n id: string,\n code: GatewayRpcErrorCode,\n message: string,\n): GatewayRpcErrorFrame {\n return {\n protocol_version: GATEWAY_RPC_PROTOCOL_VERSION,\n id,\n direction: 'error',\n error: { code, message },\n };\n}\n\n/**\n * The discriminated outcome of decoding a single inbound NDJSON line.\n *\n * - `request` — a valid, version-matched request frame ready to route.\n * - `error` — a protocol-level error frame the server should write straight\n * back (parse failure, version mismatch, or malformed frame). The\n * server never routes these to the gateway.\n */\nexport type DecodeResult =\n | { kind: 'request'; frame: GatewayRpcRequestFrame }\n | { kind: 'error'; frame: GatewayRpcErrorFrame };\n\n/**\n * Decode + validate a single inbound NDJSON line into a routable request or a\n * protocol-level error frame.\n *\n * Steps (the supervisor-ipc pattern, applied to gateway frames):\n * 1. JSON parse — failure → `E_RPC_PARSE`.\n * 2. zod decode against {@link GatewayRpcFrameSchema} — failure → `E_RPC_BAD_FRAME`.\n * 3. version negotiation via {@link isFrozenGatewayRpcVersion} —\n * mismatch → `E_RPC_BAD_VERSION`.\n * 4. direction must be `request` (a server never receives response/error\n * frames) — otherwise → `E_RPC_BAD_FRAME`.\n *\n * The best-effort correlation `id` is recovered from the raw JSON when possible\n * so the client can match the error to its request even on a decode failure.\n *\n * @param line - One trimmed NDJSON line (no trailing newline).\n * @returns A {@link DecodeResult}.\n */\nexport function decodeLine(line: string): DecodeResult {\n let raw: unknown;\n try {\n raw = JSON.parse(line);\n } catch {\n return { kind: 'error', frame: buildErrorFrame('0', 'E_RPC_PARSE', 'invalid JSON line') };\n }\n\n // Best-effort id recovery for error correlation before full validation.\n const recoveredId = recoverId(raw);\n\n const parsed = GatewayRpcFrameSchema.safeParse(raw);\n if (!parsed.success) {\n return {\n kind: 'error',\n frame: buildErrorFrame(recoveredId, 'E_RPC_BAD_FRAME', 'frame failed schema validation'),\n };\n }\n\n const frame: GatewayRpcFrame = parsed.data;\n\n if (!isFrozenGatewayRpcVersion(frame.protocol_version)) {\n return {\n kind: 'error',\n frame: buildErrorFrame(\n frame.id,\n 'E_RPC_BAD_VERSION',\n `unsupported protocol version ${frame.protocol_version}; server speaks ${GATEWAY_RPC_PROTOCOL_VERSION}`,\n ),\n };\n }\n\n if (frame.direction !== 'request') {\n return {\n kind: 'error',\n frame: buildErrorFrame(\n frame.id,\n 'E_RPC_BAD_FRAME',\n `server cannot accept a ${frame.direction} frame`,\n ),\n };\n }\n\n return { kind: 'request', frame };\n}\n\n/**\n * Recover a best-effort correlation id from raw decoded JSON, used so a\n * validation failure can still be correlated to the client's request.\n *\n * @param raw - The result of `JSON.parse` on the inbound line.\n * @returns The string `id` if present and non-empty, else `'0'`.\n */\nfunction recoverId(raw: unknown): string {\n if (raw && typeof raw === 'object' && 'id' in raw) {\n const id = (raw as { id: unknown }).id;\n if (typeof id === 'string' && id.length > 0) return id;\n }\n return '0';\n}\n\n/**\n * A streaming line splitter for the NDJSON wire.\n *\n * `node:net` sockets deliver arbitrary chunk boundaries; a single `data` event\n * may carry a partial line or several lines. This buffer accumulates bytes and\n * yields only complete lines (split on `\\n`), matching the supervisor-ipc\n * client's `readline`-style framing without depending on `readline` (so it can\n * be driven directly off socket chunks).\n */\nexport class LineBuffer {\n /** Pending bytes that have not yet completed a line. */\n private buffer = '';\n\n /**\n * Push a decoded UTF-8 chunk; return every complete line it completes.\n *\n * @param chunk - A UTF-8 string chunk from the socket.\n * @returns Zero or more complete lines (trailing newline stripped, blanks\n * skipped).\n */\n push(chunk: string): string[] {\n this.buffer += chunk;\n const lines: string[] = [];\n let idx = this.buffer.indexOf(NEWLINE);\n while (idx !== -1) {\n const line = this.buffer.slice(0, idx).trim();\n this.buffer = this.buffer.slice(idx + 1);\n if (line.length > 0) lines.push(line);\n idx = this.buffer.indexOf(NEWLINE);\n }\n return lines;\n }\n}\n","/**\n * CLI-RPC unix-socket transport adapter — routes NDJSON frames through the gateway.\n *\n * Serves the gateway over a long-lived unix-domain socket using newline-delimited\n * JSON (NDJSON): one zod-validated {@link GatewayRpcFrame} per line. Any local\n * client (the `cleo` CLI talking to a warm daemon, Studio's server process, a\n * test harness) can open the socket, write `request` frames, and read back\n * `response`/`error` frames — without paying the per-invocation cold-start of\n * spawning a fresh `cleo` process.\n *\n * This is the THIRD transport adapter, mirroring the just-merged MCP stdio\n * adapter (`packages/runtime/src/gateway/mcp/`):\n *\n * RPC request frame { direction:'request', request:{…} }\n * → decode + validate (`@cleocode/contracts/gateway/rpc`)\n * → DispatchRequest { source: 'rpc', … } (re-validated at the boundary)\n * → injected {@link GatewayHandler}.handle()\n * → DispatchResponse (LAFS envelope) → RPC response frame\n *\n * The adapter owns ONLY wire concerns: socket lifecycle, line framing, version\n * negotiation, error rendering, and per-connection cleanup. All domain logic\n * (resolution, validation, middleware, handler execution) lives behind the\n * {@link GatewayHandler} — exactly mirroring the CLI and MCP adapters. There is\n * NO `process.exit` and NO error-render inside the handlers (R3 contract); the\n * adapter never calls `process.exit` at all (lifecycle is the caller's).\n *\n * The request `source` is forced to `'rpc'` regardless of what the client sent,\n * so a client cannot impersonate another transport.\n *\n * @task T11449\n * @epic T11254\n * @saga T11243\n */\n\nimport { existsSync, unlinkSync } from 'node:fs';\nimport { createServer, type Socket } from 'node:net';\nimport type { DispatchRequest } from '@cleocode/contracts/gateway';\nimport type {\n GatewayRpcRequestFrame,\n GatewayRpcResponseFrame,\n} from '@cleocode/contracts/gateway/rpc';\nimport { GATEWAY_RPC_PROTOCOL_VERSION } from '@cleocode/contracts/gateway/rpc';\nimport { getLogger } from '@cleocode/core';\nimport type { GatewayHandler } from '../index.js';\nimport { buildErrorFrame, decodeLine, encodeFrame, LineBuffer } from './codec.js';\nimport type { RpcServerHandle, RpcServerOptions } from './types.js';\n\n/**\n * Route a single decoded request frame through the gateway handler and build\n * the correlated response frame.\n *\n * The request's `source` is forced to `'rpc'`; the frame `id` is reused as the\n * response correlation id (and, when the client omitted a `requestId` shaped\n * for tracing, the frame id flows through as the dispatch `requestId`). Thrown\n * handler errors are caught and rendered as a protocol-level `E_RPC_INTERNAL`\n * error frame — never propagated as an unhandled rejection or `process.exit`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param frame - A validated, version-matched request frame.\n * @returns The response or error frame to write back on the same connection.\n */\nexport async function routeFrame(\n handler: GatewayHandler,\n frame: GatewayRpcRequestFrame,\n): Promise<GatewayRpcResponseFrame | ReturnType<typeof buildErrorFrame>> {\n const request: DispatchRequest = {\n ...frame.request,\n // Force the transport of origin — a client cannot impersonate cli/mcp/http.\n source: 'rpc',\n requestId: frame.request.requestId,\n };\n\n try {\n const response = await handler.handle(request);\n return {\n protocol_version: GATEWAY_RPC_PROTOCOL_VERSION,\n id: frame.id,\n direction: 'response',\n response,\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return buildErrorFrame(frame.id, 'E_RPC_INTERNAL', message);\n }\n}\n\n/**\n * Handle a single client connection: buffer NDJSON lines, decode each, route\n * request frames through the gateway, and write back response/error frames.\n *\n * Each connection owns its own {@link LineBuffer}, so partial-line chunk\n * boundaries are reassembled per socket. Frames are processed concurrently;\n * each correlated response carries the originating frame `id`, so the client\n * can demultiplex out-of-order completions.\n *\n * @param handler - The injected gateway handler.\n * @param socket - The accepted `node:net` connection.\n * @param log - The adapter's pino logger.\n */\nfunction handleConnection(\n handler: GatewayHandler,\n socket: Socket,\n log: ReturnType<typeof getLogger>,\n): void {\n socket.setEncoding('utf8');\n const lines = new LineBuffer();\n\n socket.on('data', (chunk: string) => {\n for (const line of lines.push(chunk)) {\n const decoded = decodeLine(line);\n if (decoded.kind === 'error') {\n socket.write(encodeFrame(decoded.frame));\n continue;\n }\n routeFrame(handler, decoded.frame)\n .then((out) => {\n if (!socket.destroyed) socket.write(encodeFrame(out));\n })\n .catch((err: unknown) => {\n // routeFrame already traps handler errors; this guards the framing path.\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'rpc frame routing failed');\n if (!socket.destroyed) {\n socket.write(encodeFrame(buildErrorFrame(decoded.frame.id, 'E_RPC_INTERNAL', message)));\n }\n });\n }\n });\n\n socket.on('error', (err) => {\n log.warn({ err }, 'rpc connection error');\n });\n}\n\n/**\n * Start the CLI-RPC unix-socket server over the gateway.\n *\n * Binds {@link RpcServerOptions.socketPath}, accepts connections, and routes\n * each inbound NDJSON `request` frame through the injected {@link GatewayHandler}.\n * The caller assembles the handler (via `createGatewayHandler` with its domain\n * handlers + middleware) and injects it here — this adapter never builds\n * handlers itself, keeping the runtime free of any `@cleocode/cleo` dependency.\n *\n * Returns once the socket is listening. The returned {@link RpcServerHandle}\n * exposes an idempotent `close()` for deterministic teardown (daemon shutdown,\n * tests). The adapter never calls `process.exit`.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Socket path + lifecycle options.\n * @returns A promise resolving to the live {@link RpcServerHandle}.\n */\nexport function startRpcServer(\n handler: GatewayHandler,\n opts: RpcServerOptions,\n): Promise<RpcServerHandle> {\n const log = getLogger('gateway-rpc');\n const removeStale = opts.removeStaleSocket ?? true;\n\n if (removeStale && existsSync(opts.socketPath)) {\n try {\n unlinkSync(opts.socketPath);\n } catch (err) {\n log.warn({ err, socketPath: opts.socketPath }, 'failed to unlink stale rpc socket');\n }\n }\n\n const server = createServer((socket) => handleConnection(handler, socket, log));\n\n return new Promise<RpcServerHandle>((resolve, reject) => {\n server.once('error', reject);\n server.listen(opts.socketPath, () => {\n server.removeListener('error', reject);\n log.info(\n { socketPath: opts.socketPath, version: GATEWAY_RPC_PROTOCOL_VERSION },\n 'rpc server listening',\n );\n\n let closed = false;\n const handle: RpcServerHandle = {\n server,\n socketPath: opts.socketPath,\n close(): Promise<void> {\n if (closed) return Promise.resolve();\n closed = true;\n return new Promise<void>((res) => {\n server.close(() => {\n if (existsSync(opts.socketPath)) {\n try {\n unlinkSync(opts.socketPath);\n } catch {\n // best-effort cleanup; the OS reclaims the node on exit anyway.\n }\n }\n res();\n });\n });\n },\n };\n resolve(handle);\n });\n });\n}\n"],"mappings":"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","names":["GATEWAY_RPC_PROTOCOL_VERSION","GATEWAY_RPC_PROTOCOL_VERSION"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/gateway/mcp/server.ts","../src/gateway/mcp/tool-naming.ts","../src/gateway/registry.ts","../src/gateway/mcp/tools-list.ts"],"sourcesContent":["/**\n * MCP stdio transport adapter — routes `tools/call` through the gateway.\n *\n * Serves the Model Context Protocol over stdio (newline-delimited JSON-RPC 2.0)\n * so any MCP-compatible client (Claude Code, LLM-agnostic tools) can discover\n * and invoke CLEO operations. Unlike the legacy standalone `@cleocode/mcp-adapter`\n * — which called the `@cleocode/core` SDK directly — this adapter is a thin\n * TRANSPORT over the unified gateway:\n *\n * MCP `tools/call` { name, arguments }\n * → {@link GatewayOperationKey} (via the `mcpExposed` registry subset)\n * → DispatchRequest { source: 'mcp', gateway, domain, operation, params }\n * → injected {@link GatewayHandler}.handle()\n * → DispatchResponse (LAFS envelope) → MCP tool result (JSON text content)\n *\n * The adapter owns ONLY wire concerns: JSON-RPC framing, the stdio read loop,\n * `tools/list` generation, error rendering, and `process.exit` on stream close.\n * All domain logic (resolution, validation, middleware, handler execution) lives\n * behind the {@link GatewayHandler} — exactly mirroring how the CLI adapter\n * wraps the same handler. `process.exit` and error-render stay HERE, never in\n * the handlers (R3-T4 contract).\n *\n * `tools/list` is generated from the OPERATIONS registry behind the default-deny\n * `mcpExposed` flag, so the external tool surface is identical to the historical\n * standalone adapter (no behavior change).\n *\n * @task T11448\n * @epic T11254\n * @saga T11243\n */\n\nimport { randomUUID } from 'node:crypto';\nimport * as readline from 'node:readline';\nimport type { DispatchRequest, DispatchResponse } from '@cleocode/contracts/gateway';\nimport { getLogger } from '@cleocode/core';\nimport type { GatewayHandler } from '../index.js';\nimport { toolNameToOperationKey } from './tool-naming.js';\nimport { buildToolsList, exposedOperations } from './tools-list.js';\nimport type { McpTool, McpToolResult } from './types.js';\n\n// ---------------------------------------------------------------------------\n// JSON-RPC 2.0 envelope helpers\n// ---------------------------------------------------------------------------\n\n/** Inbound JSON-RPC request frame. */\ninterface JsonRpcRequest {\n jsonrpc: '2.0';\n id: number | string | null;\n method: string;\n params?: unknown;\n}\n\n/** Outbound JSON-RPC response frame. */\ninterface JsonRpcResponse {\n jsonrpc: '2.0';\n id: number | string | null;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n}\n\n/** Build a JSON-RPC success response. */\nfunction ok(id: number | string | null, result: unknown): JsonRpcResponse {\n return { jsonrpc: '2.0', id, result };\n}\n\n/** Build a JSON-RPC error response. */\nfunction rpcError(id: number | string | null, code: number, message: string): JsonRpcResponse {\n return { jsonrpc: '2.0', id, error: { code, message } };\n}\n\n// ---------------------------------------------------------------------------\n// Server identity\n// ---------------------------------------------------------------------------\n\n/** Advertised MCP server name. */\nconst SERVER_NAME = 'cleo-gateway-mcp';\n/** MCP protocol revision the adapter speaks. */\nconst PROTOCOL_VERSION = '2024-11-05';\n/** Version string advertised in the `initialize` handshake. */\nconst GATEWAY_MCP_VERSION = '1.0.0';\n\n// ---------------------------------------------------------------------------\n// tools/call → gateway routing\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a {@link DispatchResponse} envelope into an MCP tool result.\n *\n * The full LAFS envelope is serialized as the text content so MCP clients see\n * the same `{ success, data, meta, error }` payload every other transport\n * returns. `isError` is set from the envelope's `success` flag.\n *\n * @param response - The gateway dispatch response.\n * @returns An MCP {@link McpToolResult}.\n */\nfunction responseToToolResult(response: DispatchResponse): McpToolResult {\n return {\n content: [{ type: 'text', text: JSON.stringify(response) }],\n isError: response.success ? undefined : true,\n };\n}\n\n/**\n * Route a single MCP `tools/call` through the gateway handler.\n *\n * Resolves the tool name to a `(domain, operation)` key against the exposed\n * (`mcpExposed`) registry subset, builds a `source: 'mcp'` {@link DispatchRequest},\n * and delegates to the injected {@link GatewayHandler}. Unknown tool names and\n * thrown handler errors are rendered as MCP error results — never propagated as\n * `process.exit` from this layer.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param toolName - The MCP `tools/call` tool name.\n * @param args - The parsed tool arguments (already JSON-decoded by the client).\n * @returns An MCP {@link McpToolResult}.\n */\nexport async function callTool(\n handler: GatewayHandler,\n toolName: string,\n args: Record<string, unknown>,\n): Promise<McpToolResult> {\n const exposed = exposedOperations();\n const key = toolNameToOperationKey(toolName, exposed);\n if (!key) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],\n isError: true,\n };\n }\n\n // The exposed def carries the CQRS gateway — no second lookup needed.\n const def = exposed.find((op) => op.domain === key.domain && op.operation === key.operation);\n if (!def) {\n return {\n content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],\n isError: true,\n };\n }\n\n const request: DispatchRequest = {\n gateway: def.gateway,\n domain: key.domain,\n operation: key.operation,\n params: args,\n source: 'mcp',\n requestId: randomUUID(),\n };\n\n try {\n const response = await handler.handle(request);\n return responseToToolResult(response);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n content: [{ type: 'text', text: `Tool execution failed: ${message}` }],\n isError: true,\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// JSON-RPC method handlers\n// ---------------------------------------------------------------------------\n\n/**\n * Handle a single decoded JSON-RPC request frame.\n *\n * @param handler - The injected gateway handler.\n * @param req - The decoded JSON-RPC request.\n * @returns The JSON-RPC response frame to write back.\n */\nasync function handleRequest(\n handler: GatewayHandler,\n req: JsonRpcRequest,\n): Promise<JsonRpcResponse> {\n const { id, method, params } = req;\n\n switch (method) {\n case 'initialize':\n return ok(id, {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: { name: SERVER_NAME, version: GATEWAY_MCP_VERSION },\n });\n\n case 'tools/list':\n return ok(id, { tools: buildToolsList() satisfies McpTool[] });\n\n case 'tools/call': {\n const p = params as { name?: string; arguments?: Record<string, unknown> } | undefined;\n const toolName = p?.name;\n if (!toolName) {\n return rpcError(id, -32602, 'Missing tool name in params.name');\n }\n const result = await callTool(handler, toolName, p?.arguments ?? {});\n return ok(id, result);\n }\n\n case 'notifications/initialized':\n case 'ping':\n return ok(id, {});\n\n default:\n return rpcError(id, -32601, `Method not found: ${method}`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// stdio server entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link startMcpServer}.\n */\nexport interface McpServerOptions {\n /** Inbound stream (defaults to `process.stdin`). */\n input?: NodeJS.ReadableStream;\n /** Outbound stream (defaults to `process.stdout`). */\n output?: NodeJS.WritableStream;\n /**\n * Whether to call `process.exit(0)` when the input stream closes.\n * Defaults to `true` (production stdio). Tests pass `false`.\n */\n exitOnClose?: boolean;\n}\n\n/**\n * Start the MCP stdio server over the gateway.\n *\n * Reads newline-delimited JSON-RPC 2.0 requests from the input stream, routes\n * `tools/call` through the injected {@link GatewayHandler}, and writes responses\n * to the output stream. Each inbound line must be one complete JSON-RPC request.\n *\n * The caller assembles the {@link GatewayHandler} (via `createGatewayHandler`\n * with its domain handlers + middleware) and injects it here — this adapter\n * never builds handlers itself, keeping the runtime free of any\n * `@cleocode/cleo` dependency.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Optional stream + lifecycle overrides (defaults to stdio).\n */\nexport function startMcpServer(handler: GatewayHandler, opts?: McpServerOptions): void {\n const input = opts?.input ?? process.stdin;\n const output = opts?.output ?? process.stdout;\n const exitOnClose = opts?.exitOnClose ?? true;\n const log = getLogger('gateway-mcp');\n\n const rl = readline.createInterface({ input, terminal: false });\n\n rl.on('line', (line) => {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n let req: JsonRpcRequest;\n try {\n req = JSON.parse(trimmed) as JsonRpcRequest;\n } catch {\n output.write(`${JSON.stringify(rpcError(null, -32700, 'Parse error: invalid JSON'))}\\n`);\n return;\n }\n\n handleRequest(handler, req)\n .then((response) => {\n output.write(`${JSON.stringify(response)}\\n`);\n })\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'mcp request handling failed');\n output.write(\n `${JSON.stringify(rpcError(req.id ?? null, -32603, `Internal error: ${message}`))}\\n`,\n );\n });\n });\n\n rl.on('close', () => {\n if (exitOnClose) process.exit(0);\n });\n}\n","/**\n * MCP tool-name <-> gateway-operation mapping.\n *\n * The MCP wire protocol identifies a tool by a single flat `name` string\n * (snake_case, e.g. `cleo_sentient_propose_list`), whereas the gateway routes\n * on a `(gateway, domain, operation)` triple (e.g.\n * `query / sentient / propose.list`). This module is the bijective bridge\n * between the two surfaces:\n *\n * gateway op → MCP tool name {@link operationToToolName}\n * MCP tool name → domain+operation {@link toolNameToOperationKey}\n *\n * ## Naming convention (frozen — matches the legacy `@cleocode/mcp-adapter`)\n *\n * `cleo_<domain>_<operation>` where every `.` and `-` in the dotted operation\n * name becomes `_`. The three historically-exposed tools therefore map as:\n *\n * | gateway op | MCP tool name |\n * |-----------------------------|--------------------------------|\n * | `sentient` · `status` | `cleo_sentient_status` |\n * | `sentient` · `propose.list` | `cleo_sentient_propose_list` |\n * | `sentient` · `propose.enable` | `cleo_sentient_propose_enable` |\n *\n * Preserving this exact mapping is what guarantees R3-T4's \"no behavior change\n * to the MCP tool surface\" acceptance criterion.\n *\n * @task T11448\n * @epic T11254\n * @saga T11243\n */\n\nimport type { OperationDef } from '@cleocode/contracts';\n\n/** Prefix every CLEO MCP tool name carries. */\nexport const MCP_TOOL_PREFIX = 'cleo_';\n\n/**\n * Identifies a gateway operation by its `(domain, operation)` pair. The CQRS\n * gateway resolves which `Gateway` ('query' | 'mutate') the pair belongs to via\n * the operation registry, so the MCP adapter does not need to carry it.\n */\nexport interface GatewayOperationKey {\n /** Canonical domain (e.g. `'sentient'`). */\n readonly domain: string;\n /** Dotted operation name (e.g. `'propose.list'`). */\n readonly operation: string;\n}\n\n/**\n * Convert a dotted operation segment into its MCP-safe snake_case form.\n *\n * @param segment - A domain or operation string (may contain `.`/`-`).\n * @returns The segment with every `.`/`-` replaced by `_`.\n */\nfunction toSnake(segment: string): string {\n return segment.replace(/[.-]/g, '_');\n}\n\n/**\n * Derive the flat MCP tool name for a gateway operation.\n *\n * @param op - An {@link OperationDef} (or any object carrying `domain` +\n * `operation`) from the registry.\n * @returns The MCP tool name, e.g. `'cleo_sentient_propose_list'`.\n */\nexport function operationToToolName(op: Pick<OperationDef, 'domain' | 'operation'>): string {\n return `${MCP_TOOL_PREFIX}${toSnake(op.domain)}_${toSnake(op.operation)}`;\n}\n\n/**\n * Resolve an MCP tool name back to its `(domain, operation)` gateway key.\n *\n * The reverse of {@link operationToToolName} cannot recover dots vs dashes from\n * the snake_case wire form, so resolution is performed against the provided\n * `exposed` set (the same `mcpExposed: true` subset used to build `tools/list`).\n * This keeps the mapping exact and rejects unknown tool names.\n *\n * @param toolName - The MCP `tools/call` tool name.\n * @param exposed - The exposed operation defs (the `mcpExposed` subset).\n * @returns The matching {@link GatewayOperationKey}, or `undefined` when the\n * tool name does not correspond to any exposed operation.\n */\nexport function toolNameToOperationKey(\n toolName: string,\n exposed: readonly OperationDef[],\n): GatewayOperationKey | undefined {\n const match = exposed.find((op) => operationToToolName(op) === toolName);\n return match ? { domain: match.domain, operation: match.operation } : undefined;\n}\n","/**\n * Unified CQRS Dispatch Layer -- Operation Registry (shim)\n *\n * The `OPERATIONS` data array was relocated to\n * `@cleocode/contracts/dispatch/operations-registry` in T10061 / T9833b\n * (E-CLI-BOUNDARY · Saga T9831 SG-ARCH-SOLID). This file is now a thin\n * re-export shim that:\n *\n * 1. Re-exports `OPERATIONS` from contracts so every downstream consumer\n * that already imports `{ OPERATIONS }` from this path continues to\n * compile unchanged.\n * 2. Keeps the pure utility / derivation functions (`resolve`,\n * `validateRequiredParams`, `getByDomain`, etc.) that operate on\n * the data and are rightly colocated with the CLI package.\n *\n * @epic T4820\n * @task T4814, T5241, T5615, T10061\n */\n\n// OperationDef + Resolution live in @cleocode/contracts (SSoT — promoted in\n// T9954 / Phase 0b of SG-ARCH-SOLID Saga T9831 · E-CONTRACTS-FOUNDATION\n// T9832). Imported below for internal use AND re-exported so that every\n// downstream consumer that already imports from './registry.js' continues\n// to compile unchanged.\nimport type { OperationDef, Resolution } from '@cleocode/contracts';\nimport { OPERATIONS as _OPERATIONS } from '@cleocode/contracts';\nimport type { CanonicalDomain, Gateway, Tier } from '@cleocode/contracts/gateway';\n\nexport type { OperationDef, Resolution };\n\n/** The single source of truth for all operations in CLEO (data lives in @cleocode/contracts). */\nexport const OPERATIONS: OperationDef[] = _OPERATIONS;\n\n// ---------------------------------------------------------------------------\n// Gateway Matrix Derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Derive a gateway operation matrix from the registry.\n *\n * Returns `Record<string, string[]>` containing:\n * - All canonical domains with their operations\n *\n * This is the SINGLE derivation point — gateways use this instead of\n * maintaining independent operation lists.\n */\nexport function deriveGatewayMatrix(gateway: Gateway): Record<string, string[]> {\n const matrix: Record<string, string[]> = {};\n\n for (const op of OPERATIONS) {\n if (op.gateway !== gateway) continue;\n if (!matrix[op.domain]) matrix[op.domain] = [];\n matrix[op.domain].push(op.operation);\n }\n\n return matrix;\n}\n\n/**\n * Get all accepted domain names for a gateway (canonical only).\n */\nexport function getGatewayDomains(gateway: Gateway): string[] {\n return Object.keys(deriveGatewayMatrix(gateway));\n}\n\n// ---------------------------------------------------------------------------\n// Lookup & Validation\n// ---------------------------------------------------------------------------\n\n/**\n * Resolves a domain + operation to its registered definition.\n */\nexport function resolve(\n gateway: Gateway,\n domain: string,\n operation: string,\n): Resolution | undefined {\n const def = OPERATIONS.find(\n (o) => o.gateway === gateway && o.domain === domain && o.operation === operation,\n );\n\n if (!def) return undefined;\n\n return { domain: def.domain, operation: def.operation, def };\n}\n\n/**\n * Validates that all required parameters are present in the request.\n * Returns an array of missing parameter keys.\n */\nexport function validateRequiredParams(\n def: OperationDef,\n params?: Record<string, unknown>,\n): string[] {\n if (!def.requiredParams || def.requiredParams.length === 0) return [];\n const provided = params || {};\n // Build a quick lookup so we can honor per-param `allowEmpty` declarations.\n // When a ParamDef sets `allowEmpty: true`, the empty string `\"\"` is a\n // valid value rather than \"missing\" — e.g. `provenance.backfill --since \"\"`\n // walks every reachable tag from the beginning of history.\n const allowEmptyKeys = new Set<string>();\n if (def.params) {\n for (const p of def.params) {\n if (p.allowEmpty === true) allowEmptyKeys.add(p.name);\n }\n }\n return def.requiredParams.filter((key) => {\n const v = provided[key];\n if (v === undefined || v === null) return true;\n if (v === '' && !allowEmptyKeys.has(key)) return true;\n return false;\n });\n}\n\n/** Get all operations for a specific canonical domain. */\nexport function getByDomain(domain: CanonicalDomain): OperationDef[] {\n return OPERATIONS.filter((o) => o.domain === domain);\n}\n\n/** Get all operations for a specific gateway. */\nexport function getByGateway(gateway: Gateway): OperationDef[] {\n return OPERATIONS.filter((o) => o.gateway === gateway);\n}\n\n/** Get all operations available at or below a specific tier. */\nexport function getByTier(tier: Tier): OperationDef[] {\n return OPERATIONS.filter((o) => o.tier <= tier);\n}\n\n/** Get a list of canonical domains that actually have operations registered. */\nexport function getActiveDomains(): CanonicalDomain[] {\n const active = new Set(OPERATIONS.map((o) => o.domain));\n return Array.from(active);\n}\n\n/**\n * Returns summary counts of operations for module validation.\n */\nexport function getCounts(): { query: number; mutate: number; total: number } {\n return {\n query: OPERATIONS.filter((o) => o.gateway === 'query').length,\n mutate: OPERATIONS.filter((o) => o.gateway === 'mutate').length,\n total: OPERATIONS.length,\n };\n}\n\n// Module load validation (dynamic, no hardcoded operation totals)\nconst counts = getCounts();\nif (counts.total !== OPERATIONS.length) {\n console.warn(\n `[Registry] Operation count mismatch: total=${counts.total}, registry=${OPERATIONS.length}`,\n );\n}\n","/**\n * MCP `tools/list` generation from the OPERATIONS registry.\n *\n * The MCP tool surface is **default-deny**: only {@link OperationDef} entries\n * that explicitly set `mcpExposed: true` are surfaced. This replaces the legacy\n * `@cleocode/mcp-adapter`'s hand-maintained `ALL_TOOLS` array with a single\n * generation point driven by the canonical registry — promoting a new tool is\n * now one `mcpExposed: true` edit in `@cleocode/contracts`, not a fork-and-copy\n * into a standalone adapter.\n *\n * @task T11448\n * @epic T11254\n * @saga T11243\n */\n\nimport type { OperationDef, ParamDef } from '@cleocode/contracts';\nimport { OPERATIONS } from '../registry.js';\nimport { operationToToolName } from './tool-naming.js';\nimport type { McpInputProperty, McpTool } from './types.js';\n\n/**\n * The subset of OPERATIONS surfaced over MCP (default-deny `mcpExposed` filter).\n *\n * @returns Every {@link OperationDef} with `mcpExposed === true`, in registry\n * order. Empty when no operation opts in.\n */\nexport function exposedOperations(): OperationDef[] {\n return OPERATIONS.filter((op) => op.mcpExposed === true);\n}\n\n/**\n * Map a JSON-ish {@link ParamDef} type to its JSON Schema primitive.\n *\n * MCP clients consume standard JSON Schema, so `'array'` widens to `'string'`\n * the same way the CLI's citty bridge does (callers split/parse) and every\n * other type passes through unchanged.\n *\n * @param type - The {@link ParamDef} runtime type.\n * @returns The JSON Schema `type` keyword.\n */\nfunction paramTypeToJsonSchema(type: ParamDef['type']): string {\n return type === 'array' ? 'string' : type;\n}\n\n/**\n * Build the MCP `inputSchema` for a single operation from its `params`.\n *\n * Hidden params (`hidden: true`) are excluded from the public tool surface,\n * matching the JSON Schema generation policy declared on {@link ParamDef}.\n *\n * @param op - The exposed operation definition.\n * @returns A JSON Schema object describing the tool's input.\n */\nfunction buildInputSchema(op: OperationDef): McpTool['inputSchema'] {\n const properties: Record<string, McpInputProperty> = {};\n const required: string[] = [];\n\n for (const param of op.params ?? []) {\n if (param.hidden === true) continue;\n const prop: McpInputProperty = {\n type: paramTypeToJsonSchema(param.type),\n description: param.description,\n };\n if (param.enum !== undefined && param.enum.length > 0) {\n prop.enum = param.enum;\n }\n properties[param.name] = prop;\n if (param.required) required.push(param.name);\n }\n\n const schema: McpTool['inputSchema'] = { type: 'object', properties };\n if (required.length > 0) schema.required = required;\n return schema;\n}\n\n/**\n * Convert a single exposed {@link OperationDef} into its MCP tool definition.\n *\n * @param op - An operation with `mcpExposed === true`.\n * @returns The MCP {@link McpTool} (name + description + inputSchema).\n */\nexport function operationToMcpTool(op: OperationDef): McpTool {\n return {\n name: operationToToolName(op),\n description: op.description,\n inputSchema: buildInputSchema(op),\n };\n}\n\n/**\n * Generate the full MCP `tools/list` payload from the registry.\n *\n * @returns Every exposed operation rendered as an {@link McpTool}, in registry\n * order (stable across calls).\n */\nexport function buildToolsList(): McpTool[] {\n return exposedOperations().map(operationToMcpTool);\n}\n"],"mappings":";AA+BA,SAAS,kBAAkB;AAC3B,YAAY,cAAc;AAE1B,SAAS,iBAAiB;;;ACAnB,IAAM,kBAAkB;AAoB/B,SAAS,QAAQ,SAAyB;AACxC,SAAO,QAAQ,QAAQ,SAAS,GAAG;AACrC;AASO,SAAS,oBAAoB,IAAwD;AAC1F,SAAO,GAAG,eAAe,GAAG,QAAQ,GAAG,MAAM,CAAC,IAAI,QAAQ,GAAG,SAAS,CAAC;AACzE;AAeO,SAAS,uBACd,UACA,SACiC;AACjC,QAAM,QAAQ,QAAQ,KAAK,CAAC,OAAO,oBAAoB,EAAE,MAAM,QAAQ;AACvE,SAAO,QAAQ,EAAE,QAAQ,MAAM,QAAQ,WAAW,MAAM,UAAU,IAAI;AACxE;;;AC/DA,SAAS,cAAc,mBAAmB;AAMnC,IAAM,aAA6B;AAenC,SAAS,oBAAoB,SAA4C;AAC9E,QAAM,SAAmC,CAAC;AAE1C,aAAW,MAAM,YAAY;AAC3B,QAAI,GAAG,YAAY,QAAS;AAC5B,QAAI,CAAC,OAAO,GAAG,MAAM,EAAG,QAAO,GAAG,MAAM,IAAI,CAAC;AAC7C,WAAO,GAAG,MAAM,EAAE,KAAK,GAAG,SAAS;AAAA,EACrC;AAEA,SAAO;AACT;AAKO,SAAS,kBAAkB,SAA4B;AAC5D,SAAO,OAAO,KAAK,oBAAoB,OAAO,CAAC;AACjD;AASO,SAAS,QACd,SACA,QACA,WACwB;AACxB,QAAM,MAAM,WAAW;AAAA,IACrB,CAAC,MAAM,EAAE,YAAY,WAAW,EAAE,WAAW,UAAU,EAAE,cAAc;AAAA,EACzE;AAEA,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,EAAE,QAAQ,IAAI,QAAQ,WAAW,IAAI,WAAW,IAAI;AAC7D;AAMO,SAAS,uBACd,KACA,QACU;AACV,MAAI,CAAC,IAAI,kBAAkB,IAAI,eAAe,WAAW,EAAG,QAAO,CAAC;AACpE,QAAM,WAAW,UAAU,CAAC;AAK5B,QAAM,iBAAiB,oBAAI,IAAY;AACvC,MAAI,IAAI,QAAQ;AACd,eAAW,KAAK,IAAI,QAAQ;AAC1B,UAAI,EAAE,eAAe,KAAM,gBAAe,IAAI,EAAE,IAAI;AAAA,IACtD;AAAA,EACF;AACA,SAAO,IAAI,eAAe,OAAO,CAAC,QAAQ;AACxC,UAAM,IAAI,SAAS,GAAG;AACtB,QAAI,MAAM,UAAa,MAAM,KAAM,QAAO;AAC1C,QAAI,MAAM,MAAM,CAAC,eAAe,IAAI,GAAG,EAAG,QAAO;AACjD,WAAO;AAAA,EACT,CAAC;AACH;AAGO,SAAS,YAAY,QAAyC;AACnE,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,WAAW,MAAM;AACrD;AAGO,SAAS,aAAa,SAAkC;AAC7D,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO;AACvD;AAGO,SAAS,UAAU,MAA4B;AACpD,SAAO,WAAW,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI;AAChD;AAGO,SAAS,mBAAsC;AACpD,QAAM,SAAS,IAAI,IAAI,WAAW,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC;AACtD,SAAO,MAAM,KAAK,MAAM;AAC1B;AAKO,SAAS,YAA8D;AAC5E,SAAO;AAAA,IACL,OAAO,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,OAAO,EAAE;AAAA,IACvD,QAAQ,WAAW,OAAO,CAAC,MAAM,EAAE,YAAY,QAAQ,EAAE;AAAA,IACzD,OAAO,WAAW;AAAA,EACpB;AACF;AAGA,IAAM,SAAS,UAAU;AACzB,IAAI,OAAO,UAAU,WAAW,QAAQ;AACtC,UAAQ;AAAA,IACN,8CAA8C,OAAO,KAAK,cAAc,WAAW,MAAM;AAAA,EAC3F;AACF;;;AC9HO,SAAS,oBAAoC;AAClD,SAAO,WAAW,OAAO,CAAC,OAAO,GAAG,eAAe,IAAI;AACzD;AAYA,SAAS,sBAAsB,MAAgC;AAC7D,SAAO,SAAS,UAAU,WAAW;AACvC;AAWA,SAAS,iBAAiB,IAA0C;AAClE,QAAM,aAA+C,CAAC;AACtD,QAAM,WAAqB,CAAC;AAE5B,aAAW,SAAS,GAAG,UAAU,CAAC,GAAG;AACnC,QAAI,MAAM,WAAW,KAAM;AAC3B,UAAM,OAAyB;AAAA,MAC7B,MAAM,sBAAsB,MAAM,IAAI;AAAA,MACtC,aAAa,MAAM;AAAA,IACrB;AACA,QAAI,MAAM,SAAS,UAAa,MAAM,KAAK,SAAS,GAAG;AACrD,WAAK,OAAO,MAAM;AAAA,IACpB;AACA,eAAW,MAAM,IAAI,IAAI;AACzB,QAAI,MAAM,SAAU,UAAS,KAAK,MAAM,IAAI;AAAA,EAC9C;AAEA,QAAM,SAAiC,EAAE,MAAM,UAAU,WAAW;AACpE,MAAI,SAAS,SAAS,EAAG,QAAO,WAAW;AAC3C,SAAO;AACT;AAQO,SAAS,mBAAmB,IAA2B;AAC5D,SAAO;AAAA,IACL,MAAM,oBAAoB,EAAE;AAAA,IAC5B,aAAa,GAAG;AAAA,IAChB,aAAa,iBAAiB,EAAE;AAAA,EAClC;AACF;AAQO,SAAS,iBAA4B;AAC1C,SAAO,kBAAkB,EAAE,IAAI,kBAAkB;AACnD;;;AHpCA,SAAS,GAAG,IAA4B,QAAkC;AACxE,SAAO,EAAE,SAAS,OAAO,IAAI,OAAO;AACtC;AAGA,SAAS,SAAS,IAA4B,MAAc,SAAkC;AAC5F,SAAO,EAAE,SAAS,OAAO,IAAI,OAAO,EAAE,MAAM,QAAQ,EAAE;AACxD;AAOA,IAAM,cAAc;AAEpB,IAAM,mBAAmB;AAEzB,IAAM,sBAAsB;AAgB5B,SAAS,qBAAqB,UAA2C;AACvE,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,QAAQ,EAAE,CAAC;AAAA,IAC1D,SAAS,SAAS,UAAU,SAAY;AAAA,EAC1C;AACF;AAgBA,eAAsB,SACpB,SACA,UACA,MACwB;AACxB,QAAM,UAAU,kBAAkB;AAClC,QAAM,MAAM,uBAAuB,UAAU,OAAO;AACpD,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,MACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAiB,QAAQ,GAAG,CAAC;AAAA,MAC7D,SAAS;AAAA,IACX;AAAA,EACF;AAGA,QAAM,MAAM,QAAQ,KAAK,CAAC,OAAO,GAAG,WAAW,IAAI,UAAU,GAAG,cAAc,IAAI,SAAS;AAC3F,MAAI,CAAC,KAAK;AACR,WAAO;AAAA,MACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,iBAAiB,QAAQ,GAAG,CAAC;AAAA,MAC7D,SAAS;AAAA,IACX;AAAA,EACF;AAEA,QAAM,UAA2B;AAAA,IAC/B,SAAS,IAAI;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,WAAW,WAAW;AAAA,EACxB;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,QAAQ,OAAO,OAAO;AAC7C,WAAO,qBAAqB,QAAQ;AAAA,EACtC,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO;AAAA,MACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,0BAA0B,OAAO,GAAG,CAAC;AAAA,MACrE,SAAS;AAAA,IACX;AAAA,EACF;AACF;AAaA,eAAe,cACb,SACA,KAC0B;AAC1B,QAAM,EAAE,IAAI,QAAQ,OAAO,IAAI;AAE/B,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,GAAG,IAAI;AAAA,QACZ,iBAAiB;AAAA,QACjB,cAAc,EAAE,OAAO,CAAC,EAAE;AAAA,QAC1B,YAAY,EAAE,MAAM,aAAa,SAAS,oBAAoB;AAAA,MAChE,CAAC;AAAA,IAEH,KAAK;AACH,aAAO,GAAG,IAAI,EAAE,OAAO,eAAe,EAAsB,CAAC;AAAA,IAE/D,KAAK,cAAc;AACjB,YAAM,IAAI;AACV,YAAM,WAAW,GAAG;AACpB,UAAI,CAAC,UAAU;AACb,eAAO,SAAS,IAAI,QAAQ,kCAAkC;AAAA,MAChE;AACA,YAAM,SAAS,MAAM,SAAS,SAAS,UAAU,GAAG,aAAa,CAAC,CAAC;AACnE,aAAO,GAAG,IAAI,MAAM;AAAA,IACtB;AAAA,IAEA,KAAK;AAAA,IACL,KAAK;AACH,aAAO,GAAG,IAAI,CAAC,CAAC;AAAA,IAElB;AACE,aAAO,SAAS,IAAI,QAAQ,qBAAqB,MAAM,EAAE;AAAA,EAC7D;AACF;AAoCO,SAAS,eAAe,SAAyB,MAA+B;AACrF,QAAM,QAAQ,MAAM,SAAS,QAAQ;AACrC,QAAM,SAAS,MAAM,UAAU,QAAQ;AACvC,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,MAAM,UAAU,aAAa;AAEnC,QAAM,KAAc,yBAAgB,EAAE,OAAO,UAAU,MAAM,CAAC;AAE9D,KAAG,GAAG,QAAQ,CAAC,SAAS;AACtB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AAEd,QAAI;AACJ,QAAI;AACF,YAAM,KAAK,MAAM,OAAO;AAAA,IAC1B,QAAQ;AACN,aAAO,MAAM,GAAG,KAAK,UAAU,SAAS,MAAM,QAAQ,2BAA2B,CAAC,CAAC;AAAA,CAAI;AACvF;AAAA,IACF;AAEA,kBAAc,SAAS,GAAG,EACvB,KAAK,CAAC,aAAa;AAClB,aAAO,MAAM,GAAG,KAAK,UAAU,QAAQ,CAAC;AAAA,CAAI;AAAA,IAC9C,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,UAAI,KAAK,EAAE,IAAI,GAAG,6BAA6B;AAC/C,aAAO;AAAA,QACL,GAAG,KAAK,UAAU,SAAS,IAAI,MAAM,MAAM,QAAQ,mBAAmB,OAAO,EAAE,CAAC,CAAC;AAAA;AAAA,MACnF;AAAA,IACF,CAAC;AAAA,EACL,CAAC;AAED,KAAG,GAAG,SAAS,MAAM;AACnB,QAAI,YAAa,SAAQ,KAAK,CAAC;AAAA,EACjC,CAAC;AACH;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/daemon/define-subsystem.ts","../src/daemon/logger.ts","../src/daemon/registry.ts","../src/daemon/supervisor-client.ts"],"sourcesContent":["/**\n * `defineSubsystem()` — declare a supervised daemon subsystem.\n *\n * The single factory R4-R7 adapters call to express a long-running concern\n * (Studio supervision, GC cron, web server, docs-viewer, runtime poller) as a\n * uniform {@link Subsystem}. The returned descriptor is frozen so the registry\n * can drive `start → healthProbe → shutdown` without the definition mutating\n * underfoot.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport type { Subsystem, SubsystemDefinition } from '@cleocode/contracts';\n\n/**\n * Declare a daemon subsystem.\n *\n * Validates that the four required surfaces (`name`, `start`, `healthProbe`,\n * `shutdown`) are present and that `name` is a non-empty string, then returns a\n * frozen {@link Subsystem} ready to register with a `SubsystemRegistry`.\n *\n * @typeParam TContext - Opaque context threaded from `start` into `shutdown`\n * (e.g. a child handle or resolved config). Defaults to `void`.\n * @param definition - The subsystem descriptor.\n * @returns A frozen, registrable {@link Subsystem}.\n * @throws {TypeError} When `name` is empty or a required method is missing.\n *\n * @example\n * ```ts\n * const studio = defineSubsystem({\n * name: 'studio',\n * start: () => supervisor.start(),\n * healthProbe: () => ({ child_id: 'studio', pid: supervisor.pid ?? 0,\n * state: supervisor.status === 'running' ? 'running' : 'stopped',\n * restart_count: 0 }),\n * shutdown: () => supervisor.stop(),\n * });\n * ```\n */\nexport function defineSubsystem<TContext = void>(\n definition: SubsystemDefinition<TContext>,\n): Subsystem<TContext> {\n if (typeof definition.name !== 'string' || definition.name.length === 0) {\n throw new TypeError('defineSubsystem: `name` must be a non-empty string');\n }\n if (typeof definition.start !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`start\\` must be a function`);\n }\n if (typeof definition.healthProbe !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`healthProbe\\` must be a function`);\n }\n if (typeof definition.shutdown !== 'function') {\n throw new TypeError(`defineSubsystem(${definition.name}): \\`shutdown\\` must be a function`);\n }\n\n return Object.freeze({\n name: definition.name,\n start: definition.start,\n healthProbe: definition.healthProbe,\n shutdown: definition.shutdown,\n });\n}\n","/**\n * Daemon subsystem log-routing primitive.\n *\n * `createSubsystemLogger(name)` is the **single** logging surface the\n * `@cleocode/runtime/daemon` submodule exposes. It routes through the canonical\n * pino factory `getLogger` (`packages/core/src/logger.ts`) — NOT raw\n * `createWriteStream` and NOT `console.*` — so every daemon log line lands in\n * the rotating, retention-managed CLEO log files with a consistent structured\n * shape and a per-subsystem `subsystem` field.\n *\n * It supersedes the **5 ad-hoc logging conventions** the legacy daemons grew,\n * which R4-R7 will migrate onto this primitive:\n *\n * 1. `sentient/daemon.ts` — `createWriteStream(sentient.log)` /\n * `createWriteStream(sentient.err)` + `process.stderr.write` lines.\n * 2. `gc/daemon.ts` — `createWriteStream(gc.log)` / `createWriteStream(gc.err)`.\n * 3. `web.ts` — `console.*`.\n * 4. `docs-viewer.ts` — ad-hoc stdout/stderr writes.\n * 5. the `runtime` services — bespoke per-service logging.\n *\n * ## Migration guidance for R4-R7 adapters\n *\n * Each adapter (Studio, GC, web, docs-viewer) replaces its bespoke sink with:\n *\n * ```ts\n * import { createSubsystemLogger } from '@cleocode/runtime/daemon';\n * const log = createSubsystemLogger('studio');\n * log.info({ pid }, 'Studio started'); // not process.stderr.write(...)\n * log.error({ err }, 'Studio crashed'); // not createWriteStream('sentient.err')\n * ```\n *\n * Do not import pino directly and do not open file streams — the canonical\n * factory owns rotation, retention, level, and the `projectHash` correlation\n * field. Routing through it is what makes the daemon's logs observable via the\n * same `cleo` log surface as the rest of the system.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11368 — log routing primitive (pino getLogger canonical sink)\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport { getLogger } from '@cleocode/core';\n\n/**\n * A structured log payload — a plain object of serializable fields merged into\n * the emitted log record (e.g. `{ pid, err }`).\n */\nexport type SubsystemLogFields = Record<string, unknown>;\n\n/**\n * The narrow logging surface a daemon subsystem uses.\n *\n * Each method accepts an optional structured `fields` object followed by the\n * message, mirroring pino's `(obj, msg)` call shape. This is intentionally\n * smaller than the full pino `Logger` so the daemon submodule does not leak the\n * pino type across its public boundary while still routing through the\n * canonical pino sink.\n */\nexport interface SubsystemLogger {\n /** Log at `debug` level. */\n debug: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `info` level. */\n info: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `warn` level. */\n warn: (fields: SubsystemLogFields, msg: string) => void;\n /** Log at `error` level. */\n error: (fields: SubsystemLogFields, msg: string) => void;\n}\n\n/**\n * Create the single per-subsystem logger for the daemon submodule.\n *\n * Routes through the canonical pino factory `getLogger('daemon')` and namespaces\n * the result with a per-subsystem child binding (`{ subsystem: name }`), so all\n * daemon log lines share the `daemon` subsystem root while remaining\n * individually attributable. Never opens a file stream and never writes to\n * `console`.\n *\n * @param name - The logical subsystem name (e.g. `'studio'`, `'gc'`, `'web'`).\n * @returns A {@link SubsystemLogger} bound to `name`.\n *\n * @example\n * ```ts\n * const log = createSubsystemLogger('studio');\n * log.info({ pid: 4242, port: 3456 }, 'Studio started');\n * ```\n */\nexport function createSubsystemLogger(name: string): SubsystemLogger {\n // Bind under the canonical 'daemon' root, then a per-subsystem child so each\n // subsystem's lines carry `{ subsystem: 'daemon', daemonSubsystem: name }`.\n const log = getLogger('daemon').child({ daemonSubsystem: name });\n return {\n debug: (fields, msg) => log.debug(fields, msg),\n info: (fields, msg) => log.info(fields, msg),\n warn: (fields, msg) => log.warn(fields, msg),\n error: (fields, msg) => log.error(fields, msg),\n };\n}\n","/**\n * `SubsystemRegistry` — drives the uniform daemon subsystem lifecycle.\n *\n * Aggregates {@link Subsystem}s declared via `defineSubsystem` and runs them\n * through a uniform `start → healthProbe → shutdown` lifecycle, firing the\n * typed {@link DaemonLifecycleHooks} in order. Health probes are aggregated\n * into a {@link HealthStatus} that projects onto the FROZEN `supervisor-ipc`\n * `MonitorResponse` the Rust supervisor consumes (AC4).\n *\n * Shutdown runs in reverse registration order (LIFO) so dependants stop before\n * their dependencies — the conventional supervised-process teardown ordering.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport {\n type DaemonLifecycleHooks,\n type HealthStatus,\n type Subsystem,\n type SubsystemHealth,\n type SubsystemLifecyclePhase,\n summarizeHealth,\n} from '@cleocode/contracts';\n\n/**\n * A registered subsystem plus its runtime bookkeeping.\n *\n * The `context` is the value `start()` resolved to, threaded back into\n * `shutdown()`. It is type-erased to `unknown` inside the registry because the\n * registry holds subsystems of heterogeneous context types; each entry's\n * `subsystem.shutdown` is the only consumer and re-narrows it at the call site\n * via the subsystem's own typing — no `any`, and the erased value never escapes\n * the registry surface.\n */\ninterface RegisteredSubsystem {\n readonly subsystem: Subsystem<unknown>;\n started: boolean;\n context: unknown;\n}\n\n/** Normalize an unknown thrown value into an `Error`. */\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\n/**\n * The registry that owns and drives the daemon's subsystems.\n *\n * Construct with optional {@link DaemonLifecycleHooks}; register subsystems,\n * then `startAll()` / `aggregateHealth()` / `shutdownAll()`.\n */\nexport class SubsystemRegistry {\n readonly #entries: RegisteredSubsystem[] = [];\n readonly #hooks: DaemonLifecycleHooks;\n\n /**\n * @param hooks - Optional lifecycle hooks fired around start/shutdown and on\n * any lifecycle error.\n */\n constructor(hooks: DaemonLifecycleHooks = {}) {\n this.#hooks = hooks;\n }\n\n /**\n * Register a subsystem. Registration order defines start order; shutdown runs\n * in reverse.\n *\n * @typeParam TContext - The subsystem's start→shutdown context type.\n * @param subsystem - A subsystem produced by `defineSubsystem`.\n * @throws {Error} When a subsystem with the same `name` is already registered.\n */\n register<TContext>(subsystem: Subsystem<TContext>): void {\n if (this.#entries.some((entry) => entry.subsystem.name === subsystem.name)) {\n throw new Error(`SubsystemRegistry: duplicate subsystem name '${subsystem.name}'`);\n }\n // Widen the context type to `unknown` for heterogeneous storage. The\n // subsystem's own `start`/`shutdown` remain individually well-typed; only\n // the registry's internal handle is erased, and it never leaks out.\n this.#entries.push({\n subsystem: subsystem as Subsystem<unknown>,\n started: false,\n context: undefined,\n });\n }\n\n /** The names of all registered subsystems, in registration order. */\n get names(): readonly string[] {\n return this.#entries.map((entry) => entry.subsystem.name);\n }\n\n /**\n * Start every registered subsystem in registration order.\n *\n * For each subsystem: awaits `start()`, stores the resolved context, then\n * fires `onStart`. If `start()` throws, fires `onError` with phase `start`\n * and re-throws — a failed start is surfaced, never swallowed.\n *\n * @throws The first subsystem `start()` error (after firing `onError`).\n */\n async startAll(): Promise<void> {\n for (const entry of this.#entries) {\n try {\n entry.context = await entry.subsystem.start();\n entry.started = true;\n await this.#hooks.onStart?.(entry.subsystem.name);\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'start');\n throw toError(cause);\n }\n }\n }\n\n /**\n * Probe every registered subsystem and aggregate into a {@link HealthStatus}.\n *\n * A subsystem whose `healthProbe()` rejects does not abort the aggregate:\n * `onError` fires with phase `healthProbe` and the subsystem is reported as a\n * `stopped` row carrying the error in its `detail`, so the supervisor still\n * receives a complete snapshot.\n *\n * @returns The aggregate health snapshot (projects onto `MonitorResponse`).\n */\n async aggregateHealth(): Promise<HealthStatus> {\n const rows: SubsystemHealth[] = [];\n for (const entry of this.#entries) {\n try {\n rows.push(await entry.subsystem.healthProbe());\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'healthProbe');\n rows.push({\n child_id: entry.subsystem.name,\n pid: 0,\n state: 'stopped',\n restart_count: 0,\n detail: `healthProbe failed: ${toError(cause).message}`,\n });\n }\n }\n return summarizeHealth(rows);\n }\n\n /**\n * Shut down every started subsystem in reverse registration order (LIFO).\n *\n * Each subsystem's `shutdown(context)` is awaited and `onShutdown` fires. A\n * throwing `shutdown` fires `onError` with phase `shutdown` but does NOT\n * abort the teardown of the remaining subsystems — best-effort graceful\n * shutdown of the whole daemon.\n */\n async shutdownAll(): Promise<void> {\n for (let i = this.#entries.length - 1; i >= 0; i -= 1) {\n const entry = this.#entries[i];\n if (entry === undefined || !entry.started) continue;\n try {\n await entry.subsystem.shutdown(entry.context);\n entry.started = false;\n await this.#hooks.onShutdown?.(entry.subsystem.name);\n } catch (cause) {\n await this.#fireError(entry.subsystem.name, cause, 'shutdown');\n // Continue tearing down the rest — do not abort on one failure.\n }\n }\n }\n\n /** Fire the `onError` hook, swallowing any error the hook itself throws. */\n async #fireError(name: string, cause: unknown, phase: SubsystemLifecyclePhase): Promise<void> {\n try {\n await this.#hooks.onError?.(name, toError(cause), phase);\n } catch {\n // A throwing error-hook must not mask the original failure.\n }\n }\n}\n","/**\n * Typed NDJSON client for the FROZEN `supervisor-ipc` v1.0 contract.\n *\n * The Rust `cleo-supervisor` is the server; a CLEO daemon process is a client\n * that sends {@link SupervisorIpcRequestEnvelope}s and receives\n * {@link SupervisorIpcResponseEnvelope}s, one newline-delimited JSON\n * (NDJSON) envelope per line. This module is a transport-agnostic codec: it\n * encodes outbound envelopes to NDJSON lines and decodes inbound lines through\n * the frozen Zod schemas. **Every inbound line is parsed** — a malformed or\n * schema-violating frame is rejected with a typed {@link MalformedIpcFrameError}\n * and never silently dropped (T11367 AC2).\n *\n * The codec does not own a socket — callers feed it raw lines from whatever\n * transport (Unix socket, named pipe, test pair) they own, keeping the daemon\n * submodule free of platform IO and trivially testable.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/daemon\n *\n * @epic T11253 R2 — `@cleocode/runtime/daemon` submodule\n * @task T11367 — defineSubsystem + lifecycle/health registry + IPC client\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport {\n SUPERVISOR_IPC_PROTOCOL_VERSION,\n type SupervisorIpcRequest,\n type SupervisorIpcRequestEnvelope,\n SupervisorIpcRequestEnvelopeSchema,\n type SupervisorIpcResponseEnvelope,\n SupervisorIpcResponseEnvelopeSchema,\n} from '@cleocode/contracts';\n\n/**\n * A typed error raised when an inbound NDJSON line cannot be decoded into a\n * valid {@link SupervisorIpcResponseEnvelope}.\n *\n * Carries the offending raw `line` and the underlying `cause` (a JSON\n * `SyntaxError` or a Zod validation error) so callers can log + correlate.\n * Invalid frames are surfaced as this error — never silently dropped.\n */\nexport class MalformedIpcFrameError extends Error {\n /** The raw NDJSON line that failed to decode. */\n readonly line: string;\n\n /**\n * @param line - The offending raw NDJSON line.\n * @param cause - The underlying JSON or Zod error.\n */\n constructor(line: string, cause: unknown) {\n super(`Malformed supervisor-ipc frame: ${cause instanceof Error ? cause.message : cause}`);\n this.name = 'MalformedIpcFrameError';\n this.line = line;\n this.cause = cause;\n }\n}\n\n/**\n * A transport-agnostic codec for the FROZEN `supervisor-ipc` v1.0 wire format.\n *\n * Construct via {@link createSupervisorIpcClient}. The client is stateful only\n * in its monotonic correlation-id counter; it owns no socket.\n */\nexport interface SupervisorIpcClient {\n /**\n * Encode a request body into a complete, newline-terminated NDJSON envelope\n * line ready to write to the transport.\n *\n * Stamps the frozen protocol version and a fresh correlation `id`, validates\n * the result against {@link SupervisorIpcRequestEnvelopeSchema}, and returns\n * the serialized line (including the trailing `\\n`).\n *\n * @param request - The request body (validated by the envelope schema).\n * @returns `{ id, line }` — the correlation id and the NDJSON line to send.\n */\n encodeRequest: (request: SupervisorIpcRequest) => { id: string; line: string };\n\n /**\n * Decode a single inbound NDJSON line into a typed response envelope.\n *\n * Parses the line as JSON, then validates it against\n * {@link SupervisorIpcResponseEnvelopeSchema}. A malformed or\n * schema-violating line throws {@link MalformedIpcFrameError} — it is never\n * silently dropped.\n *\n * @param line - A single NDJSON line (without the trailing newline).\n * @returns The decoded {@link SupervisorIpcResponseEnvelope}.\n * @throws {MalformedIpcFrameError} When the line is not a valid response frame.\n */\n decodeResponseLine: (line: string) => SupervisorIpcResponseEnvelope;\n}\n\n/**\n * Generate a process-unique correlation id for an IPC request envelope.\n *\n * @param seq - The client's monotonic sequence number.\n * @returns A correlation id of the form `c<seq>-<random>`.\n */\nfunction nextCorrelationId(seq: number): string {\n return `c${seq}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a typed NDJSON codec for the FROZEN `supervisor-ipc` v1.0 contract.\n *\n * @returns A {@link SupervisorIpcClient}.\n *\n * @example\n * ```ts\n * const client = createSupervisorIpcClient();\n * const { id, line } = client.encodeRequest({ kind: 'health' });\n * socket.write(line);\n * socket.on('line', (raw) => {\n * const env = client.decodeResponseLine(raw); // throws MalformedIpcFrameError on bad frame\n * if (env.response.kind === 'health') { /* ... *\\/ }\n * });\n * ```\n */\nexport function createSupervisorIpcClient(): SupervisorIpcClient {\n let seq = 0;\n\n return {\n encodeRequest(request: SupervisorIpcRequest): { id: string; line: string } {\n seq += 1;\n const id = nextCorrelationId(seq);\n const envelope: SupervisorIpcRequestEnvelope = {\n protocol_version: SUPERVISOR_IPC_PROTOCOL_VERSION,\n id,\n direction: 'request',\n request,\n };\n // Validate before serialization so we never emit an off-contract frame.\n const parsed = SupervisorIpcRequestEnvelopeSchema.parse(envelope);\n return { id, line: `${JSON.stringify(parsed)}\\n` };\n },\n\n decodeResponseLine(line: string): SupervisorIpcResponseEnvelope {\n let json: unknown;\n try {\n json = JSON.parse(line);\n } catch (cause) {\n throw new MalformedIpcFrameError(line, cause);\n }\n const result = SupervisorIpcResponseEnvelopeSchema.safeParse(json);\n if (!result.success) {\n throw new MalformedIpcFrameError(line, result.error);\n }\n return result.data;\n },\n };\n}\n"],"mappings":";AA4CO,SAAS,gBACd,YACqB;AACrB,MAAI,OAAO,WAAW,SAAS,YAAY,WAAW,KAAK,WAAW,GAAG;AACvE,UAAM,IAAI,UAAU,oDAAoD;AAAA,EAC1E;AACA,MAAI,OAAO,WAAW,UAAU,YAAY;AAC1C,UAAM,IAAI,UAAU,mBAAmB,WAAW,IAAI,iCAAiC;AAAA,EACzF;AACA,MAAI,OAAO,WAAW,gBAAgB,YAAY;AAChD,UAAM,IAAI,UAAU,mBAAmB,WAAW,IAAI,uCAAuC;AAAA,EAC/F;AACA,MAAI,OAAO,WAAW,aAAa,YAAY;AAC7C,UAAM,IAAI,UAAU,mBAAmB,WAAW,IAAI,oCAAoC;AAAA,EAC5F;AAEA,SAAO,OAAO,OAAO;AAAA,IACnB,MAAM,WAAW;AAAA,IACjB,OAAO,WAAW;AAAA,IAClB,aAAa,WAAW;AAAA,IACxB,UAAU,WAAW;AAAA,EACvB,CAAC;AACH;;;ACtBA,SAAS,iBAAiB;AA8CnB,SAAS,sBAAsB,MAA+B;AAGnE,QAAM,MAAM,UAAU,QAAQ,EAAE,MAAM,EAAE,iBAAiB,KAAK,CAAC;AAC/D,SAAO;AAAA,IACL,OAAO,CAAC,QAAQ,QAAQ,IAAI,MAAM,QAAQ,GAAG;AAAA,IAC7C,MAAM,CAAC,QAAQ,QAAQ,IAAI,KAAK,QAAQ,GAAG;AAAA,IAC3C,MAAM,CAAC,QAAQ,QAAQ,IAAI,KAAK,QAAQ,GAAG;AAAA,IAC3C,OAAO,CAAC,QAAQ,QAAQ,IAAI,MAAM,QAAQ,GAAG;AAAA,EAC/C;AACF;;;AChFA;AAAA,EAME;AAAA,OACK;AAmBP,SAAS,QAAQ,OAAuB;AACtC,SAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACjE;AAQO,IAAM,oBAAN,MAAwB;AAAA,EACpB,WAAkC,CAAC;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,QAA8B,CAAC,GAAG;AAC5C,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,SAAmB,WAAsC;AACvD,QAAI,KAAK,SAAS,KAAK,CAAC,UAAU,MAAM,UAAU,SAAS,UAAU,IAAI,GAAG;AAC1E,YAAM,IAAI,MAAM,gDAAgD,UAAU,IAAI,GAAG;AAAA,IACnF;AAIA,SAAK,SAAS,KAAK;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,MACT,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,IAAI,QAA2B;AAC7B,WAAO,KAAK,SAAS,IAAI,CAAC,UAAU,MAAM,UAAU,IAAI;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,WAA0B;AAC9B,eAAW,SAAS,KAAK,UAAU;AACjC,UAAI;AACF,cAAM,UAAU,MAAM,MAAM,UAAU,MAAM;AAC5C,cAAM,UAAU;AAChB,cAAM,KAAK,OAAO,UAAU,MAAM,UAAU,IAAI;AAAA,MAClD,SAAS,OAAO;AACd,cAAM,KAAK,WAAW,MAAM,UAAU,MAAM,OAAO,OAAO;AAC1D,cAAM,QAAQ,KAAK;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,kBAAyC;AAC7C,UAAM,OAA0B,CAAC;AACjC,eAAW,SAAS,KAAK,UAAU;AACjC,UAAI;AACF,aAAK,KAAK,MAAM,MAAM,UAAU,YAAY,CAAC;AAAA,MAC/C,SAAS,OAAO;AACd,cAAM,KAAK,WAAW,MAAM,UAAU,MAAM,OAAO,aAAa;AAChE,aAAK,KAAK;AAAA,UACR,UAAU,MAAM,UAAU;AAAA,UAC1B,KAAK;AAAA,UACL,OAAO;AAAA,UACP,eAAe;AAAA,UACf,QAAQ,uBAAuB,QAAQ,KAAK,EAAE,OAAO;AAAA,QACvD,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO,gBAAgB,IAAI;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,cAA6B;AACjC,aAAS,IAAI,KAAK,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AACrD,YAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,UAAI,UAAU,UAAa,CAAC,MAAM,QAAS;AAC3C,UAAI;AACF,cAAM,MAAM,UAAU,SAAS,MAAM,OAAO;AAC5C,cAAM,UAAU;AAChB,cAAM,KAAK,OAAO,aAAa,MAAM,UAAU,IAAI;AAAA,MACrD,SAAS,OAAO;AACd,cAAM,KAAK,WAAW,MAAM,UAAU,MAAM,OAAO,UAAU;AAAA,MAE/D;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,WAAW,MAAc,OAAgB,OAA+C;AAC5F,QAAI;AACF,YAAM,KAAK,OAAO,UAAU,MAAM,QAAQ,KAAK,GAAG,KAAK;AAAA,IACzD,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;ACzJA;AAAA,EACE;AAAA,EAGA;AAAA,EAEA;AAAA,OACK;AAUA,IAAM,yBAAN,cAAqC,MAAM;AAAA;AAAA,EAEvC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,MAAc,OAAgB;AACxC,UAAM,mCAAmC,iBAAiB,QAAQ,MAAM,UAAU,KAAK,EAAE;AACzF,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;AA2CA,SAAS,kBAAkB,KAAqB;AAC9C,SAAO,IAAI,GAAG,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3D;AAkBO,SAAS,4BAAiD;AAC/D,MAAI,MAAM;AAEV,SAAO;AAAA,IACL,cAAc,SAA6D;AACzE,aAAO;AACP,YAAM,KAAK,kBAAkB,GAAG;AAChC,YAAM,WAAyC;AAAA,QAC7C,kBAAkB;AAAA,QAClB;AAAA,QACA,WAAW;AAAA,QACX;AAAA,MACF;AAEA,YAAM,SAAS,mCAAmC,MAAM,QAAQ;AAChE,aAAO,EAAE,IAAI,MAAM,GAAG,KAAK,UAAU,MAAM,CAAC;AAAA,EAAK;AAAA,IACnD;AAAA,IAEA,mBAAmB,MAA6C;AAC9D,UAAI;AACJ,UAAI;AACF,eAAO,KAAK,MAAM,IAAI;AAAA,MACxB,SAAS,OAAO;AACd,cAAM,IAAI,uBAAuB,MAAM,KAAK;AAAA,MAC9C;AACA,YAAM,SAAS,oCAAoC,UAAU,IAAI;AACjE,UAAI,CAAC,OAAO,SAAS;AACnB,cAAM,IAAI,uBAAuB,MAAM,OAAO,KAAK;AAAA,MACrD;AACA,aAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}
@@ -1,305 +0,0 @@
1
- // src/gateway/http/listen.ts
2
- import { createServer } from "http";
3
- import { getLogger } from "@cleocode/core";
4
-
5
- // src/gateway/http/server.ts
6
- import { randomUUID } from "crypto";
7
- function statusForResponse(response) {
8
- if (response.success) return 200;
9
- const code = response.error?.code ?? "";
10
- if (code === "E_NOT_FOUND" || code.endsWith("_NOT_FOUND")) return 404;
11
- if (code === "E_UNAUTHORIZED" || code === "E_AUTH_REQUIRED") return 401;
12
- if (code === "E_FORBIDDEN") return 403;
13
- if (code === "E_RATE_LIMITED" || code === "E_RATE_LIMIT_EXCEEDED") return 429;
14
- if (code === "E_VALIDATION" || code.startsWith("E_VALIDATION") || code === "E_INVALID_INPUT" || code === "E_BAD_REQUEST") {
15
- return 400;
16
- }
17
- if (code === "E_NOT_IMPLEMENTED" || code === "E_UNSUPPORTED") return 501;
18
- return 500;
19
- }
20
- async function routeUnary(handler, req) {
21
- const requestId = req.requestId ?? randomUUID();
22
- const request = {
23
- gateway: req.gateway,
24
- domain: req.domain,
25
- operation: req.operation,
26
- params: req.params,
27
- // Force the transport of origin — a client cannot impersonate cli/mcp/rpc.
28
- source: "http",
29
- requestId,
30
- sessionId: req.sessionId
31
- };
32
- try {
33
- const body = await handler.handle(request);
34
- return { status: statusForResponse(body), body };
35
- } catch (err) {
36
- const message = err instanceof Error ? err.message : String(err);
37
- const body = {
38
- meta: {
39
- gateway: req.gateway,
40
- domain: req.domain,
41
- operation: req.operation,
42
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
43
- duration_ms: 0,
44
- source: "http",
45
- requestId
46
- },
47
- success: false,
48
- error: { code: "E_HTTP_INTERNAL", message }
49
- };
50
- return { status: 500, body };
51
- }
52
- }
53
-
54
- // src/gateway/http/listen.ts
55
- var GATEWAYS = /* @__PURE__ */ new Set(["query", "mutate"]);
56
- var MAX_BODY_BYTES = 1048576;
57
- function parseHttpRoute(method, url) {
58
- if (method !== "POST") {
59
- return {
60
- ok: false,
61
- status: 405,
62
- code: "E_HTTP_METHOD_NOT_ALLOWED",
63
- message: `method ${method ?? "<none>"} not allowed; gateway accepts POST`
64
- };
65
- }
66
- const pathname = new URL(url ?? "/", "http://localhost").pathname;
67
- const segments = pathname.split("/").filter((s) => s.length > 0);
68
- if (segments.length !== 3) {
69
- return {
70
- ok: false,
71
- status: 404,
72
- code: "E_HTTP_NOT_FOUND",
73
- message: `expected POST /<gateway>/<domain>/<operation>, got '${pathname}'`
74
- };
75
- }
76
- const [gateway, domain, operation] = segments;
77
- if (!GATEWAYS.has(gateway)) {
78
- return {
79
- ok: false,
80
- status: 404,
81
- code: "E_HTTP_NOT_FOUND",
82
- message: `unknown gateway '${gateway}'; expected one of query, mutate`
83
- };
84
- }
85
- return { ok: true, gateway, domain, operation };
86
- }
87
- function readBody(req) {
88
- return new Promise((resolve, reject) => {
89
- const chunks = [];
90
- let size = 0;
91
- req.on("data", (chunk) => {
92
- size += chunk.length;
93
- if (size > MAX_BODY_BYTES) {
94
- reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));
95
- req.destroy();
96
- return;
97
- }
98
- chunks.push(chunk);
99
- });
100
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
101
- req.on("error", reject);
102
- });
103
- }
104
- function writeEdgeError(res, status, code, message) {
105
- const body = {
106
- meta: {
107
- gateway: "query",
108
- domain: "",
109
- operation: "",
110
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
111
- duration_ms: 0,
112
- source: "http",
113
- requestId: ""
114
- },
115
- success: false,
116
- error: { code, message }
117
- };
118
- res.writeHead(status, { "Content-Type": "application/json" });
119
- res.end(JSON.stringify(body));
120
- }
121
- async function handleHttpRequest(handler, req, res, log) {
122
- const route = parseHttpRoute(req.method, req.url);
123
- if (!route.ok) {
124
- writeEdgeError(res, route.status, route.code, route.message);
125
- return;
126
- }
127
- let params;
128
- try {
129
- const raw = await readBody(req);
130
- if (raw.trim().length > 0) {
131
- const parsed = JSON.parse(raw);
132
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
133
- writeEdgeError(res, 400, "E_HTTP_BAD_REQUEST", "request body must be a JSON object");
134
- return;
135
- }
136
- params = parsed;
137
- }
138
- } catch (err) {
139
- const message = err instanceof Error ? err.message : String(err);
140
- writeEdgeError(res, 400, "E_HTTP_BAD_REQUEST", `invalid request body: ${message}`);
141
- return;
142
- }
143
- const unary = {
144
- gateway: route.gateway,
145
- domain: route.domain,
146
- operation: route.operation,
147
- params
148
- };
149
- const result = await routeUnary(handler, unary);
150
- res.writeHead(result.status, { "Content-Type": "application/json" });
151
- res.end(JSON.stringify(result.body));
152
- log.debug(
153
- {
154
- gateway: route.gateway,
155
- domain: route.domain,
156
- operation: route.operation,
157
- status: result.status
158
- },
159
- "http request routed"
160
- );
161
- }
162
- function startHttpServer(handler, opts) {
163
- const log = getLogger("gateway-http");
164
- const host = opts.host ?? "127.0.0.1";
165
- const server = createServer((req, res) => {
166
- handleHttpRequest(handler, req, res, log).catch((err) => {
167
- const message = err instanceof Error ? err.message : String(err);
168
- log.warn({ err }, "http request handling failed");
169
- if (!res.headersSent) {
170
- writeEdgeError(res, 500, "E_HTTP_INTERNAL", message);
171
- } else if (!res.writableEnded) {
172
- res.end();
173
- }
174
- });
175
- });
176
- return new Promise((resolve, reject) => {
177
- server.once("error", reject);
178
- server.listen(opts.port, host, () => {
179
- server.removeListener("error", reject);
180
- const address = server.address();
181
- const boundPort = typeof address === "object" && address !== null ? address.port : opts.port;
182
- log.info({ host, port: boundPort }, "http gateway server listening");
183
- let closed = false;
184
- const handle = {
185
- server,
186
- port: boundPort,
187
- host,
188
- close() {
189
- if (closed) return Promise.resolve();
190
- closed = true;
191
- return new Promise((res) => {
192
- server.close(() => res());
193
- server.closeIdleConnections();
194
- });
195
- }
196
- };
197
- resolve(handle);
198
- });
199
- });
200
- }
201
-
202
- // src/gateway/http/sse.ts
203
- var LF = "\n";
204
- var RECORD_END = "\n\n";
205
- function encodeSseFrame(frame) {
206
- let out = "";
207
- if (frame.event !== void 0) out += `event: ${frame.event}${LF}`;
208
- if (frame.id !== void 0) out += `id: ${frame.id}${LF}`;
209
- out += `data: ${JSON.stringify(frame.data)}${RECORD_END}`;
210
- return out;
211
- }
212
- function encodeStreamEvent(frame) {
213
- return encodeSseFrame({ id: String(frame.seq), data: frame });
214
- }
215
- var SSE_HEADERS = Object.freeze({
216
- "Content-Type": "text/event-stream",
217
- "Cache-Control": "no-cache",
218
- Connection: "keep-alive",
219
- "X-Accel-Buffering": "no"
220
- });
221
- function createSseStream(source, signal) {
222
- const encoder = new TextEncoder();
223
- return new ReadableStream({
224
- start(controller) {
225
- let closed = false;
226
- let teardown;
227
- let teardownDone = false;
228
- let sourceRunning = false;
229
- const onAbort = () => emitter.close();
230
- const runTeardown = () => {
231
- if (teardownDone) return;
232
- teardownDone = true;
233
- if (typeof teardown === "function") teardown();
234
- };
235
- const emitter = {
236
- get closed() {
237
- return closed;
238
- },
239
- send(frame) {
240
- if (closed) return;
241
- try {
242
- controller.enqueue(encoder.encode(encodeSseFrame(frame)));
243
- } catch {
244
- emitter.close();
245
- }
246
- },
247
- sendStreamEvent(frame) {
248
- if (closed) return;
249
- try {
250
- controller.enqueue(encoder.encode(encodeStreamEvent(frame)));
251
- } catch {
252
- emitter.close();
253
- }
254
- },
255
- sendRaw(record) {
256
- if (closed) return;
257
- try {
258
- controller.enqueue(encoder.encode(record));
259
- } catch {
260
- emitter.close();
261
- }
262
- },
263
- close() {
264
- if (closed) return;
265
- closed = true;
266
- if (signal) signal.removeEventListener("abort", onAbort);
267
- if (!sourceRunning) runTeardown();
268
- try {
269
- controller.close();
270
- } catch {
271
- }
272
- }
273
- };
274
- if (signal) {
275
- if (signal.aborted) {
276
- closed = true;
277
- teardownDone = true;
278
- try {
279
- controller.close();
280
- } catch {
281
- }
282
- return;
283
- }
284
- signal.addEventListener("abort", onAbort);
285
- }
286
- sourceRunning = true;
287
- const returned = source(emitter);
288
- teardown = typeof returned === "function" ? returned : void 0;
289
- sourceRunning = false;
290
- if (closed) runTeardown();
291
- }
292
- });
293
- }
294
-
295
- export {
296
- statusForResponse,
297
- routeUnary,
298
- parseHttpRoute,
299
- startHttpServer,
300
- encodeSseFrame,
301
- encodeStreamEvent,
302
- SSE_HEADERS,
303
- createSseStream
304
- };
305
- //# sourceMappingURL=chunk-YZBIPGMH.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/gateway/http/listen.ts","../src/gateway/http/server.ts","../src/gateway/http/sse.ts"],"sourcesContent":["/**\n * `node:http` server binding for the framework-agnostic HTTP gateway adapter.\n *\n * The HTTP adapter ({@link routeUnary} + the SSE primitives) is deliberately\n * framework-agnostic — it owns NO port binding and NO `Request`/`Response`\n * lifecycle, so an embedder (a SvelteKit route, a test harness) can drive it\n * directly. The **daemon**, however, is itself the embedder: it must expose the\n * gateway to external HTTP clients over a real listening socket. This module is\n * that thin embedder — a minimal `node:http` server that parses the request URL\n * into a `(gateway, domain, operation)` triple, reads the JSON body, and routes\n * it through {@link routeUnary}. It deliberately stays in the runtime (no\n * SvelteKit / no `@cleocode/cleo` / no drizzle dependency) so the daemon can\n * host the gateway without reaching across a package boundary.\n *\n * Routing contract (intentionally minimal — the wire vocabulary is the same as\n * every other transport, only the framing differs):\n *\n * `POST /<gateway>/<domain>/<operation>` with a JSON body → operation params\n *\n * where `<gateway>` is `query` | `mutate`. A non-POST method, an unparseable\n * path, or a body that is not a JSON object is rejected at the wire edge with a\n * LAFS-shaped error envelope — the injected {@link GatewayHandler} is never\n * invoked with a malformed request.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11451\n * @epic T11254\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport type { DispatchResponse, Gateway } from '@cleocode/contracts/gateway';\nimport { getLogger } from '@cleocode/core';\nimport type { GatewayHandler } from '../index.js';\nimport { routeUnary } from './server.js';\nimport type { HttpUnaryRequest } from './types.js';\n\n/** The two CQRS gateways a path segment may name. */\nconst GATEWAYS: ReadonlySet<string> = new Set<Gateway>(['query', 'mutate']);\n\n/** Maximum accepted request-body size (1 MiB) — a crude DoS guard at the edge. */\nconst MAX_BODY_BYTES = 1_048_576;\n\n/**\n * Options for {@link startHttpServer}.\n *\n * Mirrors the connection-oriented {@link RpcServerOptions} shape: the caller\n * resolves the bind coordinates so the runtime stays free of any\n * `@cleocode/paths` dependency.\n */\nexport interface HttpServerOptions {\n /**\n * TCP port to bind. `0` selects an ephemeral port (the bound port is then\n * readable from {@link HttpServerHandle.port}).\n */\n port: number;\n /**\n * Host/interface to bind. Defaults to `127.0.0.1` (loopback only) — the\n * gateway is local-process-facing; exposing it on a public interface is an\n * explicit caller decision.\n */\n host?: string;\n}\n\n/**\n * A live HTTP server handle returned by {@link startHttpServer}.\n *\n * Exposes the bound {@link Server}, the actually-bound `port` (resolved even\n * when `0` was requested), and an idempotent {@link close}. The adapter never\n * calls `process.exit` — lifecycle stays with the caller (R3 contract).\n */\nexport interface HttpServerHandle {\n /** The bound `node:http` server. */\n server: Server;\n /** The actually-bound TCP port (resolved when `0` was requested). */\n port: number;\n /** Host/interface the server is bound to. */\n host: string;\n /** Close the server + all live connections; resolves once fully closed. */\n close(): Promise<void>;\n}\n\n/**\n * The result of parsing a request line into routing coordinates: either a\n * resolved unary triple or a rejection reason for the wire edge.\n */\ntype ParsedRoute =\n | { ok: true; gateway: Gateway; domain: string; operation: string }\n | { ok: false; status: number; code: string; message: string };\n\n/**\n * Parse `POST /<gateway>/<domain>/<operation>` into a routing triple.\n *\n * Rejects a non-POST method (`405`) and any path that does not resolve to a\n * known `(gateway, domain, operation)` triple (`404`). The gateway segment is\n * validated against the two CQRS gateways so a client cannot smuggle an\n * arbitrary value into the dispatch request.\n *\n * @param method - The HTTP request method.\n * @param url - The HTTP request URL (path + query).\n * @returns The parsed route, or a typed rejection for the wire edge.\n */\nexport function parseHttpRoute(method: string | undefined, url: string | undefined): ParsedRoute {\n if (method !== 'POST') {\n return {\n ok: false,\n status: 405,\n code: 'E_HTTP_METHOD_NOT_ALLOWED',\n message: `method ${method ?? '<none>'} not allowed; gateway accepts POST`,\n };\n }\n // Parse against a dummy origin so only the path is consumed (query ignored).\n const pathname = new URL(url ?? '/', 'http://localhost').pathname;\n const segments = pathname.split('/').filter((s) => s.length > 0);\n if (segments.length !== 3) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `expected POST /<gateway>/<domain>/<operation>, got '${pathname}'`,\n };\n }\n const [gateway, domain, operation] = segments;\n if (!GATEWAYS.has(gateway)) {\n return {\n ok: false,\n status: 404,\n code: 'E_HTTP_NOT_FOUND',\n message: `unknown gateway '${gateway}'; expected one of query, mutate`,\n };\n }\n return { ok: true, gateway: gateway as Gateway, domain, operation };\n}\n\n/**\n * Read the full request body as a string, enforcing {@link MAX_BODY_BYTES}.\n *\n * @param req - The inbound request stream.\n * @returns The decoded UTF-8 body.\n * @throws {Error} When the body exceeds the size cap (the connection is then\n * destroyed by the caller).\n */\nfunction readBody(req: IncomingMessage): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n req.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n reject(new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n req.on('error', reject);\n });\n}\n\n/**\n * Write a LAFS-shaped error envelope to the response at the wire edge.\n *\n * Used for rejections that never reach the {@link GatewayHandler} (bad method,\n * unroutable path, malformed body), so a client always sees a well-formed LAFS\n * envelope rather than a bare HTTP error.\n *\n * @param res - The server response.\n * @param status - The HTTP status to set.\n * @param code - The machine-readable LAFS error code.\n * @param message - A human-readable message.\n */\nfunction writeEdgeError(res: ServerResponse, status: number, code: string, message: string): void {\n const body: DispatchResponse = {\n meta: {\n gateway: 'query',\n domain: '',\n operation: '',\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId: '',\n },\n success: false,\n error: { code, message },\n };\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Handle a single inbound HTTP request: parse the route, read + JSON-parse the\n * body, route it through the gateway handler, and serialize the LAFS response.\n *\n * All failure modes are rendered as LAFS envelopes — a bad route or malformed\n * body is rejected at the edge (handler never invoked); a thrown handler error\n * is already trapped inside {@link routeUnary} as a `500`. The function never\n * throws and never calls `process.exit`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param req - The inbound request.\n * @param res - The outbound response.\n * @param log - The adapter's pino logger.\n */\nasync function handleHttpRequest(\n handler: GatewayHandler,\n req: IncomingMessage,\n res: ServerResponse,\n log: ReturnType<typeof getLogger>,\n): Promise<void> {\n const route = parseHttpRoute(req.method, req.url);\n if (!route.ok) {\n writeEdgeError(res, route.status, route.code, route.message);\n return;\n }\n\n let params: Record<string, unknown> | undefined;\n try {\n const raw = await readBody(req);\n if (raw.trim().length > 0) {\n const parsed: unknown = JSON.parse(raw);\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n writeEdgeError(res, 400, 'E_HTTP_BAD_REQUEST', 'request body must be a JSON object');\n return;\n }\n params = parsed as Record<string, unknown>;\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n writeEdgeError(res, 400, 'E_HTTP_BAD_REQUEST', `invalid request body: ${message}`);\n return;\n }\n\n const unary: HttpUnaryRequest = {\n gateway: route.gateway,\n domain: route.domain,\n operation: route.operation,\n params,\n };\n const result = await routeUnary(handler, unary);\n res.writeHead(result.status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(result.body));\n log.debug(\n {\n gateway: route.gateway,\n domain: route.domain,\n operation: route.operation,\n status: result.status,\n },\n 'http request routed',\n );\n}\n\n/**\n * Start a `node:http` server that hosts the gateway over HTTP for external\n * clients.\n *\n * Binds {@link HttpServerOptions.port} on {@link HttpServerOptions.host}\n * (default loopback), routes every `POST /<gateway>/<domain>/<operation>`\n * through the injected {@link GatewayHandler} via {@link routeUnary}, and\n * resolves once listening. The returned {@link HttpServerHandle} exposes an\n * idempotent `close()` for deterministic teardown (daemon shutdown, tests). The\n * adapter never calls `process.exit`.\n *\n * The caller assembles the handler (via `createGatewayHandler` with its domain\n * handlers + middleware) and injects it here — this server never builds\n * handlers itself, keeping the runtime free of any `@cleocode/cleo` dependency.\n *\n * @param handler - The transport-neutral gateway handler to route through.\n * @param opts - Bind coordinates.\n * @returns A promise resolving to the live {@link HttpServerHandle}.\n */\nexport function startHttpServer(\n handler: GatewayHandler,\n opts: HttpServerOptions,\n): Promise<HttpServerHandle> {\n const log = getLogger('gateway-http');\n const host = opts.host ?? '127.0.0.1';\n\n const server = createServer((req, res) => {\n handleHttpRequest(handler, req, res, log).catch((err: unknown) => {\n // handleHttpRequest traps its own failures; this guards the wire path.\n const message = err instanceof Error ? err.message : String(err);\n log.warn({ err }, 'http request handling failed');\n if (!res.headersSent) {\n writeEdgeError(res, 500, 'E_HTTP_INTERNAL', message);\n } else if (!res.writableEnded) {\n res.end();\n }\n });\n });\n\n return new Promise<HttpServerHandle>((resolve, reject) => {\n server.once('error', reject);\n server.listen(opts.port, host, () => {\n server.removeListener('error', reject);\n const address = server.address();\n const boundPort = typeof address === 'object' && address !== null ? address.port : opts.port;\n log.info({ host, port: boundPort }, 'http gateway server listening');\n\n let closed = false;\n const handle: HttpServerHandle = {\n server,\n port: boundPort,\n host,\n close(): Promise<void> {\n if (closed) return Promise.resolve();\n closed = true;\n return new Promise<void>((res) => {\n server.close(() => res());\n // Drop keep-alive connections so close() resolves promptly.\n server.closeIdleConnections();\n });\n },\n };\n resolve(handle);\n });\n });\n}\n","/**\n * HTTP transport adapter — serves gateway unary + SSE over HTTP.\n *\n * The fourth and final gateway transport (after CLI, MCP, RPC). Unlike the\n * connection-oriented RPC/MCP adapters, the HTTP adapter is framework-agnostic\n * by design: it does NOT bind a port or own a `Request`/`Response` lifecycle.\n * Instead it exposes two pure routing primitives the embedder (a SvelteKit\n * route handler, the daemon's HTTP server, a test harness) calls directly:\n *\n * - {@link routeUnary} — one POST → `source: 'http'` {@link DispatchRequest}\n * → injected {@link GatewayHandler} → `{ status, body }` (LAFS envelope).\n * - the SSE primitives in `./sse.js` (`createSseStream`) for streaming ops.\n *\n * This keeps the runtime free of any HTTP-server / SvelteKit / `@cleocode/cleo`\n * dependency: the embedder owns wire concerns (parsing the URL into a\n * `(gateway, domain, operation)` triple, setting headers, `process.exit`,\n * error-render at the edge), exactly mirroring how the CLI adapter wraps the\n * same handler. There is NO `process.exit` and NO error-render in the handlers\n * (R3 contract); the adapter never calls `process.exit` at all.\n *\n * The request `source` is forced to `'http'` regardless of what the embedder\n * passes, so a client cannot impersonate another transport.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http\n *\n * @task T11450\n * @epic T11254\n * @saga T11243\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { DispatchRequest, DispatchResponse } from '@cleocode/contracts/gateway';\nimport type { GatewayHandler } from '../index.js';\nimport type { HttpUnaryRequest, HttpUnaryResult } from './types.js';\n\n/**\n * Map a LAFS dispatch error code to an HTTP status code.\n *\n * The canonical machine-readable error code (`E_NOT_FOUND`, `E_VALIDATION_…`,\n * `E_FORBIDDEN`, …) is the source of truth; the numeric LAFS exit code is a\n * secondary hint. Unknown error codes fall back to `500`. A successful envelope\n * is always `200`.\n *\n * @param response - The dispatch response.\n * @returns The HTTP status the embedder should set.\n */\nexport function statusForResponse(response: DispatchResponse): number {\n if (response.success) return 200;\n const code = response.error?.code ?? '';\n if (code === 'E_NOT_FOUND' || code.endsWith('_NOT_FOUND')) return 404;\n if (code === 'E_UNAUTHORIZED' || code === 'E_AUTH_REQUIRED') return 401;\n if (code === 'E_FORBIDDEN') return 403;\n if (code === 'E_RATE_LIMITED' || code === 'E_RATE_LIMIT_EXCEEDED') return 429;\n if (\n code === 'E_VALIDATION' ||\n code.startsWith('E_VALIDATION') ||\n code === 'E_INVALID_INPUT' ||\n code === 'E_BAD_REQUEST'\n ) {\n return 400;\n }\n if (code === 'E_NOT_IMPLEMENTED' || code === 'E_UNSUPPORTED') return 501;\n return 500;\n}\n\n/**\n * Route a single HTTP unary request through the gateway handler.\n *\n * Builds a `source: 'http'` {@link DispatchRequest} from the embedder-resolved\n * `(gateway, domain, operation, params)` triple and delegates to the injected\n * {@link GatewayHandler}. A thrown handler error is trapped and rendered as a\n * `500` LAFS error envelope — never propagated as an unhandled rejection or\n * `process.exit`. The `source` is always forced to `'http'`.\n *\n * @param handler - The injected transport-neutral gateway handler.\n * @param req - The embedder-resolved unary request coordinates + body params.\n * @returns The HTTP status + LAFS response body for the embedder to serialize.\n */\nexport async function routeUnary(\n handler: GatewayHandler,\n req: HttpUnaryRequest,\n): Promise<HttpUnaryResult> {\n const requestId = req.requestId ?? randomUUID();\n const request: DispatchRequest = {\n gateway: req.gateway,\n domain: req.domain,\n operation: req.operation,\n params: req.params,\n // Force the transport of origin — a client cannot impersonate cli/mcp/rpc.\n source: 'http',\n requestId,\n sessionId: req.sessionId,\n };\n\n try {\n const body = await handler.handle(request);\n return { status: statusForResponse(body), body };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const body: DispatchResponse = {\n meta: {\n gateway: req.gateway,\n domain: req.domain,\n operation: req.operation,\n timestamp: new Date().toISOString(),\n duration_ms: 0,\n source: 'http',\n requestId,\n },\n success: false,\n error: { code: 'E_HTTP_INTERNAL', message },\n };\n return { status: 500, body };\n }\n}\n","/**\n * Server-Sent Events (SSE) wire primitives for the `@cleocode/runtime/gateway/http`\n * adapter.\n *\n * The HTTP transport is the only gateway transport that streams: long-running\n * and subscription operations emit a sequence of {@link GatewayStreamEvent}\n * frames terminated by a `done` or `error` frame. This module owns ONLY the SSE\n * wire encoding + the `ReadableStream` lifecycle plumbing; the actual frame\n * production (polling a DB, tailing a job, etc.) lives in the caller's async\n * source. Keeping the encode/stream concern here lets BOTH the gateway-routed\n * streaming ops AND the existing Studio SSE endpoints (which carry their own\n * domain event shapes) share one tested, leak-free stream builder.\n *\n * Two encoders are provided so an existing stream can adopt this plumbing\n * WITHOUT changing its observed bytes:\n * - {@link encodeStreamEvent} — the canonical `data: <json>\\n\\n` form for\n * {@link GatewayStreamEvent} frames (gateway-routed streaming ops).\n * - {@link encodeSseFrame} — a generic `event:`/`id:`/`data:` writer for any\n * payload, so endpoints with a bespoke wire (a named `event:` field, a\n * `data:`-only frame) keep an IDENTICAL byte stream while still routing their\n * lifecycle through the shared, abort-safe builder.\n *\n * @packageDocumentation\n * @module @cleocode/runtime/gateway/http/sse\n *\n * @task T11450\n * @epic T11254\n * @saga T11243\n */\n\nimport type { GatewayStreamEvent } from '@cleocode/contracts/gateway';\n\n/** SSE field/record delimiter constants (the SSE wire is line-oriented). */\nconst LF = '\\n';\n/** A blank line terminates an SSE record (`\\n\\n`). */\nconst RECORD_END = '\\n\\n';\n\n/**\n * One generic SSE frame: an optional named `event`, an optional `id`, and a\n * `data` payload that is JSON-serialized.\n *\n * This is intentionally NOT {@link GatewayStreamEvent} — it is the raw wire\n * shape so a bespoke endpoint can reproduce its exact existing bytes (e.g. a\n * named `event: task-updated` line, or a `data:`-only frame with no `event`).\n */\nexport interface SseFrame {\n /** Optional SSE `event:` field name (omitted → a `data:`-only frame). */\n event?: string;\n /** Optional SSE `id:` field for client-side last-event-id resumption. */\n id?: string;\n /** The frame payload; JSON-serialized into the `data:` field. */\n data: unknown;\n}\n\n/**\n * Encode a generic {@link SseFrame} to its SSE wire bytes.\n *\n * Field order is `event:` (if present) → `id:` (if present) → `data:`, matching\n * the SSE spec record layout, then a blank line. When `event` is omitted the\n * frame is a `data:`-only record — byte-identical to a handwritten\n * `data: <json>\\n\\n`.\n *\n * @param frame - The frame to serialize.\n * @returns The SSE record as a string (UTF-8 caller-encoded).\n */\nexport function encodeSseFrame(frame: SseFrame): string {\n let out = '';\n if (frame.event !== undefined) out += `event: ${frame.event}${LF}`;\n if (frame.id !== undefined) out += `id: ${frame.id}${LF}`;\n out += `data: ${JSON.stringify(frame.data)}${RECORD_END}`;\n return out;\n}\n\n/**\n * Encode a canonical {@link GatewayStreamEvent} as a `data:`-only SSE record.\n *\n * Gateway-routed streaming ops carry the discriminator + sequence inside the\n * JSON payload (`{ kind, seq, data, error, requestId }`), so no named SSE\n * `event:` field is used — the client demultiplexes on `kind`/`seq`. The\n * monotonic `seq` is also surfaced as the SSE `id:` so a reconnecting client can\n * send `Last-Event-ID`.\n *\n * @param frame - The gateway stream event to serialize.\n * @returns The SSE record as a string.\n */\nexport function encodeStreamEvent(frame: GatewayStreamEvent): string {\n return encodeSseFrame({ id: String(frame.seq), data: frame });\n}\n\n/** The standard SSE response headers (no caching, no proxy buffering). */\nexport const SSE_HEADERS: Readonly<Record<string, string>> = Object.freeze({\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n});\n\n/**\n * A push-style SSE controller handed to an {@link SseSource} so it can emit\n * frames and close the stream without touching the underlying\n * `ReadableStreamDefaultController`.\n */\nexport interface SseEmitter {\n /** Emit one generic SSE frame; a no-op once the stream is closed. */\n send(frame: SseFrame): void;\n /** Emit one canonical {@link GatewayStreamEvent}; a no-op once closed. */\n sendStreamEvent(frame: GatewayStreamEvent): void;\n /**\n * Emit a pre-encoded SSE record verbatim; a no-op once closed. For endpoints\n * that own their own wire encoder (e.g. a bespoke `data: <json>\\n\\n`) and want\n * to keep emitting byte-identical records while still routing lifecycle\n * through this builder.\n */\n sendRaw(record: string): void;\n /** Whether the stream has been closed (client disconnect or `close()`). */\n readonly closed: boolean;\n /** Close the stream; idempotent. */\n close(): void;\n}\n\n/**\n * A teardown callback returned by an {@link SseSource}, invoked exactly once when\n * the stream ends (client disconnect, `emitter.close()`, or producer completion).\n */\nexport type SseTeardown = () => void;\n\n/**\n * A frame source for {@link createSseStream}. Receives an {@link SseEmitter} and\n * MAY return an {@link SseTeardown} callback (return nothing to skip teardown).\n *\n * The source is started SYNCHRONOUSLY inside `ReadableStream.start`, mirroring\n * the existing Studio endpoints (which schedule timers inside `start`). A source\n * may push frames immediately and/or schedule async work.\n */\nexport type SseSource = (emitter: SseEmitter) => SseTeardown | undefined;\n\n/**\n * Build a `text/event-stream` {@link ReadableStream} driven by an\n * {@link SseSource}.\n *\n * This is the shared, abort-safe stream builder every SSE endpoint routes\n * through. It guarantees:\n * - frames are dropped (never throw) after the stream closes;\n * - the source's teardown callback runs exactly once;\n * - an optional {@link AbortSignal} (the request's) closes the stream on client\n * disconnect, with its listener removed on teardown.\n *\n * The byte output is fully controlled by what the source emits — so an endpoint\n * adopting this builder reproduces its prior wire exactly by emitting the same\n * frames in the same order.\n *\n * @param source - The frame producer.\n * @param signal - Optional request abort signal that closes the stream on\n * client disconnect.\n * @returns A ReadableStream of UTF-8 SSE bytes for a `Response` body.\n */\nexport function createSseStream(\n source: SseSource,\n signal?: AbortSignal,\n): ReadableStream<Uint8Array> {\n const encoder = new TextEncoder();\n\n return new ReadableStream<Uint8Array>({\n start(controller): void {\n let closed = false;\n /** Set once the source returns; invoked exactly once when the stream ends. */\n let teardown: (() => void) | undefined;\n /** Guards against running the source teardown more than once. */\n let teardownDone = false;\n /** True while `source(emitter)` is still executing synchronously. */\n let sourceRunning = false;\n\n const onAbort = (): void => emitter.close();\n\n /** Invoke the source teardown at most once (no-op until the source returns). */\n const runTeardown = (): void => {\n if (teardownDone) return;\n teardownDone = true;\n if (typeof teardown === 'function') teardown();\n };\n\n const emitter: SseEmitter = {\n get closed(): boolean {\n return closed;\n },\n send(frame: SseFrame): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(encodeSseFrame(frame)));\n } catch {\n emitter.close();\n }\n },\n sendStreamEvent(frame: GatewayStreamEvent): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(encodeStreamEvent(frame)));\n } catch {\n emitter.close();\n }\n },\n sendRaw(record: string): void {\n if (closed) return;\n try {\n controller.enqueue(encoder.encode(record));\n } catch {\n emitter.close();\n }\n },\n close(): void {\n if (closed) return;\n closed = true;\n if (signal) signal.removeEventListener('abort', onAbort);\n // If the source closed synchronously mid-body, it has not yet returned\n // its teardown — defer it to the post-return reconciliation below.\n if (!sourceRunning) runTeardown();\n try {\n controller.close();\n } catch {\n // already closed by the runtime — nothing to do.\n }\n },\n };\n\n if (signal) {\n if (signal.aborted) {\n // Client already gone before we started — close immediately. The\n // source is never invoked, so there is no teardown to run.\n closed = true;\n teardownDone = true;\n try {\n controller.close();\n } catch {\n // already closed.\n }\n return;\n }\n signal.addEventListener('abort', onAbort);\n }\n\n sourceRunning = true;\n const returned = source(emitter);\n teardown = typeof returned === 'function' ? returned : undefined;\n sourceRunning = false;\n\n // The source may have called `emitter.close()` synchronously before\n // returning its teardown; run it now that the callback is in hand.\n if (closed) runTeardown();\n },\n });\n}\n"],"mappings":"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","names":[]}