unreal-mcp-proxy 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HyunJin Yoo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # unreal-mcp-proxy
2
+
3
+ **Flight recorder for [Epic's Unreal MCP](https://dev.epicgames.com/documentation/unreal-engine/unreal-mcp-in-unreal-editor) (UE 5.8+).**
4
+ A transparent local proxy that records every MCP call your AI agent makes to the Unreal
5
+ Editor, an offline single-file viewer to inspect sessions, and a skill that lets the agent
6
+ query its own history.
7
+
8
+ Unreal Engine 5.8 ships an MCP server inside the editor, so agents like Claude Code and
9
+ Cursor can drive it. What it doesn't ship is any way to see what actually happened:
10
+ the official debugging story is an output-log filter. This fills that gap.
11
+
12
+ - **Session recording** - every `tools/call` with request/response bodies, timing, and
13
+ errors, appended to plain JSONL. An observation session survives editor *and* proxy
14
+ restarts; it only rolls over when you explicitly clear it.
15
+ - **Offline viewer** - one self-contained `viewer.html`. Double-click it, drop a session
16
+ file in, and you get a thread timeline, a tool-flow graph with call-order replay, and
17
+ unwrapped request/response views (no `params.arguments.arguments` archaeology, no
18
+ escaped-JSON-inside-SSE archaeology). The running proxy also serves it live at
19
+ `/sessions/{id}` with shareable `?call=` deep links.
20
+ - **Agent skill** - `similar-failures`, `tool-stats`, `call-detail`, `annotate`. When a
21
+ tool call fails, the agent checks whether a past session already diagnosed it, and
22
+ records new diagnoses for future sessions. Record once, recall always.
23
+ - **Unreal-aware** - unwraps the `call_tool(toolset_name, tool_name, arguments)` envelope,
24
+ extracts target actors into the flow graph, and normalizes the completion boundary of
25
+ Unreal's SSE responses (the editor keeps the connection open until keep-alive expires;
26
+ without this every call looks ~15s slow).
27
+ - Zero runtime dependencies. Secrets in headers/bodies are redacted before anything is
28
+ written to disk.
29
+
30
+ ![Live session view: thread timeline, tool flow graph, and call list from a real Unreal Editor session](docs/screenshot-session.png)
31
+
32
+ *A real session against a live UE 5.8 editor. Failed calls break the thread and outline
33
+ their graph nodes; the `A1` badge marks a call carrying an agent annotation.*
34
+
35
+ ![Call detail: an agent annotation on a real schema failure, with the unwrapped request and the raw schema error](docs/screenshot-call-detail.png)
36
+
37
+ *The failure loop in action: `find_actors` failed against the real schema, the agent
38
+ recorded the diagnosis, and `similar-failures` will recall it in every future session.*
39
+
40
+ ## How it fits together
41
+
42
+ Epic's Unreal MCP server is not a separate program - **it runs inside the Unreal Editor
43
+ process** and listens over HTTP (default port 35000). This proxy is a separate,
44
+ long-running local process that sits in front of it. Your agent's `.mcp.json` entry is
45
+ just a URL, so "installing" the proxy means pointing that URL at the proxy's port
46
+ instead of the editor's:
47
+
48
+ ```text
49
+ agent (.mcp.json: :35100) ──HTTP──▶ unreal-mcp-proxy (:35100, you run this)
50
+ │ records to data/sessions/*.jsonl
51
+ ▼ HTTP
52
+ Unreal Editor's built-in MCP server (:35000)
53
+ ```
54
+
55
+ Because their lifecycles are independent, the proxy (and its observation session)
56
+ survives editor restarts, and calls made while the editor is down are recorded as
57
+ failures instead of vanishing. Only the *viewer* needs no server - `viewer.html` works
58
+ from a double-click. The proxy itself must be running to record.
59
+
60
+ ## Prerequisites
61
+
62
+ - **Unreal Engine 5.8+ with the Unreal MCP server enabled** - this proxy does not
63
+ install or launch it. Follow [Epic's Unreal MCP documentation](https://dev.epicgames.com/documentation/unreal-engine/unreal-mcp-in-unreal-editor),
64
+ or start the editor with:
65
+
66
+ ```text
67
+ UnrealEditor.exe <project> -ModelContextProtocolStartServer
68
+ ```
69
+
70
+ - **Node.js 20+** for the proxy.
71
+ - An MCP client already talking to Unreal MCP (Claude Code, Cursor, MCP Inspector, ...).
72
+
73
+ ## Quick start
74
+
75
+ 1. Run the proxy (keep it running - a separate terminal, or register it as a
76
+ service/startup task; it is safe to leave up across editor restarts):
77
+
78
+ ```bash
79
+ npx unreal-mcp-proxy
80
+ # unreal-mcp-proxy listening: http://127.0.0.1:35100/mcp -> http://127.0.0.1:35000/mcp
81
+ # session: http://127.0.0.1:35100/sessions/<session-id>
82
+ ```
83
+
84
+ 2. **Change the port in your agent's `.mcp.json`** from the editor's (35000) to the
85
+ proxy's (35100). If you already had Unreal MCP configured, this is the only edit:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "unreal-mcp": { "type": "http", "url": "http://127.0.0.1:35100/mcp" }
91
+ }
92
+ }
93
+ ```
94
+
95
+ 3. Work as usual. Open the session URL to watch calls live, or open `viewer.html` and
96
+ drop a `data/sessions/*.jsonl` file to inspect a recording offline - the viewer
97
+ needs no server at all.
98
+
99
+ If your editor's MCP server runs on a non-default port, set
100
+ `UNREAL_MCP_UPSTREAM_URL=http://127.0.0.1:<port>/mcp`.
101
+
102
+ ### Auto-start (no separate terminal)
103
+
104
+ Don't want to run step 1 by hand every time? Register the proxy as a **stdio** server
105
+ instead - your MCP client then starts it automatically whenever a session connects:
106
+
107
+ ```json
108
+ {
109
+ "mcpServers": {
110
+ "unreal-mcp": {
111
+ "command": "npx",
112
+ "args": ["unreal-mcp-proxy", "--stdio"]
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ In this mode the spawned process is a thin shim: it checks whether the proxy daemon is
119
+ already running on the configured port, **starts it (detached) if not**, and bridges
120
+ stdio to it. The daemon - and your observation session, live viewer, and sinks - keeps
121
+ running after the client session ends, and every concurrent client shares the same
122
+ recorder. Configure it with the same `UNREAL_MCP_PROXY_*` environment variables (set
123
+ them in the `env` field of the entry if needed).
124
+
125
+ ## Configuration
126
+
127
+ Environment variables (or a JSON file via `UNREAL_MCP_PROXY_CONFIG`):
128
+
129
+ | Variable | Default | |
130
+ | --- | --- | --- |
131
+ | `UNREAL_MCP_UPSTREAM_URL` | `http://127.0.0.1:35000/mcp` | The editor's MCP endpoint |
132
+ | `UNREAL_MCP_PROXY_PORT` | `35100` | Proxy listen port |
133
+ | `UNREAL_MCP_PROXY_HOST` | `127.0.0.1` | Bind `0.0.0.0` to share session links on your LAN |
134
+ | `UNREAL_MCP_PROXY_DATA_DIR` | `./data` | Where sessions are recorded |
135
+
136
+ `POST /api/session/clear` starts a new observation session (history is kept).
137
+
138
+ ## Event sinks (central monitoring)
139
+
140
+ Recording is local-first, but every event can also be forwarded elsewhere - a team
141
+ server, a log shipper, a queue - through **sinks**. A sink is an ES module the proxy
142
+ loads at startup and feeds every recorded event. Both the module and its settings are
143
+ injected from the config - nothing is hardcoded:
144
+
145
+ ```json
146
+ {
147
+ "sinks": [
148
+ { "module": "./team-sink.mjs", "options": { "url": "https://collector.internal/api/v1/events", "token": "..." } }
149
+ ]
150
+ }
151
+ ```
152
+
153
+ ```js
154
+ // team-sink.mjs - forward events to a central collector, resilient to its downtime
155
+ export default function createSink({ config, options, log }) {
156
+ const queue = [];
157
+ const timer = setInterval(async () => {
158
+ if (!queue.length) return;
159
+ const batch = queue.splice(0, 100);
160
+ try {
161
+ await fetch(options.url, {
162
+ method: "POST",
163
+ headers: {
164
+ "content-type": "application/json",
165
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {})
166
+ },
167
+ body: JSON.stringify({ events: batch })
168
+ });
169
+ } catch {
170
+ queue.unshift(...batch); // collector down: keep and retry next tick
171
+ }
172
+ }, 5000);
173
+ return {
174
+ onEvent(event) { queue.push(event); },
175
+ close() { clearInterval(timer); }
176
+ };
177
+ }
178
+ ```
179
+
180
+ Sink contract:
181
+
182
+ - `onEvent(event)` is awaited inside the recording write chain, so a durable sink can
183
+ persist events in order. A sink that **throws never breaks recording** (errors are
184
+ swallowed per event). A sink that fails to **load** fails startup - misconfigured
185
+ monitoring should be visible.
186
+ - `close()` (optional) runs on shutdown.
187
+ - A sink entry is either a plain module string or `{ module, options }`. `options` is
188
+ passed to the factory verbatim - put collector URLs, tokens, and batch settings there,
189
+ not in the sink code.
190
+ - `module` accepts relative paths (resolved against the config file's directory),
191
+ absolute paths, or **bare npm specifiers** - so a team can publish its sink as a
192
+ private package and inject it: `{ "module": "@myteam/collector-sink", "options": {...} }`.
193
+ `UNREAL_MCP_PROXY_SINKS` (comma-separated module specifiers) works too.
194
+ - Writing one in TypeScript? `import type { SinkFactory } from "unreal-mcp-proxy"` -
195
+ the package ships a library entry with all types.
196
+
197
+ ## Agent skill
198
+
199
+ Copy (or symlink) `skills/unreal-mcp-observer/` into your agent's skill directory
200
+ (for Claude Code: `.claude/skills/`). The skill teaches the agent to:
201
+
202
+ 1. check `similar-failures` before retrying a failed Unreal tool call - past sessions may
203
+ have already recorded the root cause and fix (asset read-only under LFS locking, etc.),
204
+ 2. compare slow calls against recorded `tool-stats` baselines,
205
+ 3. `annotate` new diagnoses so they are recalled forever after.
206
+
207
+ The query CLI also works standalone:
208
+
209
+ ```bash
210
+ node skills/unreal-mcp-observer/scripts/query.mjs recent-failures
211
+ node skills/unreal-mcp-observer/scripts/query.mjs similar-failures "as it is read only"
212
+ ```
213
+
214
+ ## API
215
+
216
+ | Route | |
217
+ | --- | --- |
218
+ | `POST /mcp` | Transparent MCP forwarding (this is what agents talk to) |
219
+ | `GET /health` | Proxy + active session status |
220
+ | `GET /api/session` · `POST /api/session/clear` | Active observation session |
221
+ | `GET /api/sessions/{id}` | Session model (calls, graph, annotations) |
222
+ | `GET /api/sessions/{id}/events` | Raw recorded events |
223
+ | `GET /api/sessions/{id}/stream` | SSE change stream |
224
+ | `POST /api/sessions/{id}/annotations` | Attach a diagnosis to a call |
225
+ | `GET /sessions/{id}` · `GET /viewer` | Viewer (served mode) |
226
+
227
+ ## Development
228
+
229
+ TypeScript throughout. Node 20+ to run; Node 24+ to develop (tests run `.ts` natively).
230
+
231
+ ```bash
232
+ npm install
233
+ npm run build # tsc -> dist, viewer -> dist/viewer.html
234
+ npm test
235
+ npm run typecheck
236
+ ```
237
+
238
+ ## License
239
+
240
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { loadConfig } from "./config.js";
5
+ import { SessionStore } from "./session-store.js";
6
+ import { createProxyServer } from "./proxy-server.js";
7
+ import { loadSinks, closeSinks } from "./sinks.js";
8
+ import { runStdioBridge } from "./stdio-bridge.js";
9
+ const config = await loadConfig();
10
+ // stdio mode: MCP clients spawn this per session; it ensures the HTTP daemon and bridges stdio to it.
11
+ if (process.argv.includes("--stdio")) {
12
+ await runStdioBridge(config);
13
+ process.exit(0);
14
+ }
15
+ const store = new SessionStore(config.dataDir, config.webBaseUrl);
16
+ await store.initialize();
17
+ const sinks = await loadSinks(store, config, (message) => console.log(message));
18
+ const viewerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "viewer.html");
19
+ const server = createProxyServer(config, store, { viewerPath });
20
+ server.listen(config.listenPort, config.listenHost, async () => {
21
+ await store.append("proxy_started", {
22
+ listenUrl: `http://${config.listenHost}:${config.listenPort}/mcp`,
23
+ upstreamUrl: config.upstreamUrl,
24
+ processId: process.pid
25
+ });
26
+ console.log(`unreal-mcp-proxy listening: http://${config.listenHost}:${config.listenPort}/mcp -> ${config.upstreamUrl}`);
27
+ console.log(`session: ${store.describe().url}`);
28
+ });
29
+ async function shutdown(signal) {
30
+ await store.append("proxy_stopping", { signal });
31
+ await closeSinks(sinks);
32
+ server.close(() => process.exit(0));
33
+ setTimeout(() => process.exit(0), 2000).unref();
34
+ }
35
+ process.on("SIGINT", () => void shutdown("SIGINT"));
36
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
37
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;AAElC,sGAAsG;AACtG,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;IACrC,MAAM,cAAc,CAAC,MAAM,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AACD,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;AAClE,MAAM,KAAK,CAAC,UAAU,EAAE,CAAC;AACzB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAEhF,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;AAC7F,MAAM,MAAM,GAAG,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;AAEhE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,IAAI,EAAE;IAC7D,MAAM,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE;QAClC,SAAS,EAAE,UAAU,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,MAAM;QACjE,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,SAAS,EAAE,OAAO,CAAC,GAAG;KACvB,CAAC,CAAC;IACH,OAAO,CAAC,GAAG,CAAC,sCAAsC,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,WAAW,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACzH,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,KAAK,UAAU,QAAQ,CAAC,MAAc;IACpC,MAAM,KAAK,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IACjD,MAAM,UAAU,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;AAClD,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACpD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { RedactionConfig } from "./redaction.js";
2
+ import type { SinkSpec } from "./sinks.js";
3
+ export interface ProxyConfig {
4
+ listenHost: string;
5
+ listenPort: number;
6
+ upstreamUrl: string;
7
+ dataDir: string;
8
+ webBaseUrl: string;
9
+ redaction: RedactionConfig;
10
+ /** Event sinks (docs: README "Event sinks"): a module path/specifier, or { module, options }. */
11
+ sinks: SinkSpec[];
12
+ /** Directory sink paths resolve against: the config file's directory, or cwd. */
13
+ baseDir: string;
14
+ }
15
+ export declare function loadConfig(configPath?: string | undefined): Promise<ProxyConfig>;
package/dist/config.js ADDED
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const defaults = {
4
+ listenHost: "127.0.0.1",
5
+ listenPort: 35100,
6
+ // Epic's Unreal MCP default: UnrealEditor -ModelContextProtocolStartServer (port 35000)
7
+ upstreamUrl: "http://127.0.0.1:35000/mcp",
8
+ dataDir: "./data",
9
+ webBaseUrl: "",
10
+ redaction: {
11
+ headers: ["authorization", "cookie", "set-cookie", "x-api-key"],
12
+ jsonKeys: ["token", "password", "secret", "apiKey", "authorization"],
13
+ maxBodyBytes: 262144
14
+ },
15
+ sinks: [],
16
+ baseDir: ""
17
+ };
18
+ function merge(base, override) {
19
+ const result = { ...base };
20
+ for (const [key, value] of Object.entries(override ?? {})) {
21
+ if (value === undefined)
22
+ continue;
23
+ const current = result[key];
24
+ result[key] = value && typeof value === "object" && !Array.isArray(value)
25
+ ? merge((current ?? {}), value)
26
+ : value;
27
+ }
28
+ return result;
29
+ }
30
+ export async function loadConfig(configPath = process.env.UNREAL_MCP_PROXY_CONFIG) {
31
+ let config = defaults;
32
+ if (configPath) {
33
+ config = merge(config, JSON.parse(await readFile(configPath, "utf8")));
34
+ }
35
+ config = merge(config, {
36
+ listenHost: process.env.UNREAL_MCP_PROXY_HOST,
37
+ listenPort: process.env.UNREAL_MCP_PROXY_PORT ? Number(process.env.UNREAL_MCP_PROXY_PORT) : undefined,
38
+ upstreamUrl: process.env.UNREAL_MCP_UPSTREAM_URL,
39
+ dataDir: process.env.UNREAL_MCP_PROXY_DATA_DIR,
40
+ webBaseUrl: process.env.UNREAL_MCP_PROXY_WEB_BASE_URL,
41
+ sinks: process.env.UNREAL_MCP_PROXY_SINKS
42
+ ? process.env.UNREAL_MCP_PROXY_SINKS.split(",").map((entry) => entry.trim()).filter(Boolean)
43
+ : undefined
44
+ });
45
+ config.dataDir = path.resolve(config.dataDir);
46
+ config.baseDir = configPath ? path.dirname(path.resolve(configPath)) : process.cwd();
47
+ if (!config.webBaseUrl)
48
+ config.webBaseUrl = `http://127.0.0.1:${config.listenPort}`;
49
+ return config;
50
+ }
51
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAiB7B,MAAM,QAAQ,GAAgB;IAC5B,UAAU,EAAE,WAAW;IACvB,UAAU,EAAE,KAAK;IACjB,wFAAwF;IACxF,WAAW,EAAE,4BAA4B;IACzC,OAAO,EAAE,QAAQ;IACjB,UAAU,EAAE,EAAE;IACd,SAAS,EAAE;QACT,OAAO,EAAE,CAAC,eAAe,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,CAAC;QAC/D,QAAQ,EAAE,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,eAAe,CAAC;QACpE,YAAY,EAAE,MAAM;KACrB;IACD,KAAK,EAAE,EAAE;IACT,OAAO,EAAE,EAAE;CACZ,CAAC;AAEF,SAAS,KAAK,CAAC,IAA6B,EAAE,QAA6C;IACzF,MAAM,MAAM,GAA4B,EAAE,GAAG,IAAI,EAAE,CAAC;IACpD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1D,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACvE,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,IAAI,EAAE,CAA4B,EAAE,KAAgC,CAAC;YACrF,CAAC,CAAC,KAAK,CAAC;IACZ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB;IAC/E,IAAI,MAAM,GAAgB,QAAQ,CAAC;IACnC,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,GAAG,KAAK,CAAC,MAA4C,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAA2B,CAAC;IACzI,CAAC;IACD,MAAM,GAAG,KAAK,CAAC,MAA4C,EAAE;QAC3D,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB;QAC7C,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,SAAS;QACrG,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,uBAAuB;QAChD,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,yBAAyB;QAC9C,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,6BAA6B;QACrD,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,sBAAsB;YACvC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;YAC5F,CAAC,CAAC,SAAS;KACd,CAA2B,CAAC;IAC7B,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,CAAC,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;IACrF,IAAI,CAAC,MAAM,CAAC,UAAU;QAAE,MAAM,CAAC,UAAU,GAAG,oBAAoB,MAAM,CAAC,UAAU,EAAE,CAAC;IACpF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /** Library entry: embed the recorder or write custom sinks against these APIs. */
2
+ export { loadConfig, type ProxyConfig } from "./config.js";
3
+ export { SessionStore, type SessionStoreOptions, type EventListener } from "./session-store.js";
4
+ export { createProxyServer, type ProxyServerOptions } from "./proxy-server.js";
5
+ export { buildSessionModel, normalizeError } from "./session-model.js";
6
+ export { loadSinks, closeSinks, type Sink, type SinkFactory, type SinkContext, type SinkSpec } from "./sinks.js";
7
+ export { runStdioBridge, ensureDaemon } from "./stdio-bridge.js";
8
+ export { redactHeaders, redactValue, summarizeBody, type RedactionConfig } from "./redaction.js";
9
+ export type * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Library entry: embed the recorder or write custom sinks against these APIs. */
2
+ export { loadConfig } from "./config.js";
3
+ export { SessionStore } from "./session-store.js";
4
+ export { createProxyServer } from "./proxy-server.js";
5
+ export { buildSessionModel, normalizeError } from "./session-model.js";
6
+ export { loadSinks, closeSinks } from "./sinks.js";
7
+ export { runStdioBridge, ensureDaemon } from "./stdio-bridge.js";
8
+ export { redactHeaders, redactValue, summarizeBody } from "./redaction.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,OAAO,EAAE,UAAU,EAAoB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAgD,MAAM,oBAAoB,CAAC;AAChG,OAAO,EAAE,iBAAiB,EAA2B,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAgE,MAAM,YAAY,CAAC;AACjH,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAwB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type Server } from "node:http";
2
+ import type { ProxyConfig } from "./config.js";
3
+ import type { SessionStore } from "./session-store.js";
4
+ export interface ProxyServerOptions {
5
+ /** Path to the built single-file viewer HTML. Served at /sessions/{id} and /viewer. */
6
+ viewerPath?: string;
7
+ }
8
+ export declare function createProxyServer(config: ProxyConfig, store: SessionStore, options?: ProxyServerOptions): Server;