@basein/runner 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.
Files changed (100) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +276 -0
  3. package/dist/auth/client.d.ts +85 -0
  4. package/dist/auth/client.js +284 -0
  5. package/dist/bin/bir-hooks.d.ts +48 -0
  6. package/dist/bin/bir-hooks.js +201 -0
  7. package/dist/bin/bir-proxy.d.ts +45 -0
  8. package/dist/bin/bir-proxy.js +207 -0
  9. package/dist/bin/bir-scenario.d.ts +24 -0
  10. package/dist/bin/bir-scenario.js +177 -0
  11. package/dist/bin/bir.d.ts +21 -0
  12. package/dist/bin/bir.js +876 -0
  13. package/dist/config/adapters/claude-code.d.ts +76 -0
  14. package/dist/config/adapters/claude-code.js +181 -0
  15. package/dist/config/adapters/generic.d.ts +17 -0
  16. package/dist/config/adapters/generic.js +36 -0
  17. package/dist/config/generate.d.ts +127 -0
  18. package/dist/config/generate.js +114 -0
  19. package/dist/config/resolve.d.ts +68 -0
  20. package/dist/config/resolve.js +132 -0
  21. package/dist/control/client.d.ts +56 -0
  22. package/dist/control/client.js +86 -0
  23. package/dist/control/correlation.d.ts +86 -0
  24. package/dist/control/correlation.js +0 -0
  25. package/dist/control/discovery.d.ts +50 -0
  26. package/dist/control/discovery.js +123 -0
  27. package/dist/control/ordering.d.ts +38 -0
  28. package/dist/control/ordering.js +44 -0
  29. package/dist/control/paths.d.ts +32 -0
  30. package/dist/control/paths.js +56 -0
  31. package/dist/control/server.d.ts +272 -0
  32. package/dist/control/server.js +1131 -0
  33. package/dist/control/transcript.d.ts +75 -0
  34. package/dist/control/transcript.js +241 -0
  35. package/dist/index.d.ts +37 -0
  36. package/dist/index.js +32 -0
  37. package/dist/jsonrpc/framing.d.ts +49 -0
  38. package/dist/jsonrpc/framing.js +143 -0
  39. package/dist/jsonrpc/types.d.ts +52 -0
  40. package/dist/jsonrpc/types.js +46 -0
  41. package/dist/proxy/intercept.d.ts +55 -0
  42. package/dist/proxy/intercept.js +147 -0
  43. package/dist/proxy/relay.d.ts +97 -0
  44. package/dist/proxy/relay.js +166 -0
  45. package/dist/proxy/session.d.ts +116 -0
  46. package/dist/proxy/session.js +319 -0
  47. package/dist/record/housekeeping.d.ts +34 -0
  48. package/dist/record/housekeeping.js +39 -0
  49. package/dist/record/queue.d.ts +48 -0
  50. package/dist/record/queue.js +96 -0
  51. package/dist/record/recorder.d.ts +111 -0
  52. package/dist/record/recorder.js +39 -0
  53. package/dist/record/redact.d.ts +37 -0
  54. package/dist/record/redact.js +119 -0
  55. package/dist/record/remote-recorder.d.ts +110 -0
  56. package/dist/record/remote-recorder.js +301 -0
  57. package/dist/record/truncate.d.ts +36 -0
  58. package/dist/record/truncate.js +85 -0
  59. package/dist/replay/bundle.d.ts +36 -0
  60. package/dist/replay/bundle.js +89 -0
  61. package/dist/replay/controller.d.ts +300 -0
  62. package/dist/replay/controller.js +807 -0
  63. package/dist/replay/coverage.d.ts +41 -0
  64. package/dist/replay/coverage.js +56 -0
  65. package/dist/replay/derive.d.ts +58 -0
  66. package/dist/replay/derive.js +166 -0
  67. package/dist/replay/executor.d.ts +78 -0
  68. package/dist/replay/executor.js +233 -0
  69. package/dist/replay/logic.d.ts +31 -0
  70. package/dist/replay/logic.js +50 -0
  71. package/dist/replay/plan.d.ts +181 -0
  72. package/dist/replay/plan.js +397 -0
  73. package/dist/replay/pricing.d.ts +41 -0
  74. package/dist/replay/pricing.js +76 -0
  75. package/dist/replay/source-run.d.ts +50 -0
  76. package/dist/replay/source-run.js +98 -0
  77. package/dist/replay/tool-error.d.ts +22 -0
  78. package/dist/replay/tool-error.js +60 -0
  79. package/dist/replay/types.d.ts +116 -0
  80. package/dist/replay/types.js +35 -0
  81. package/dist/upstream/client.d.ts +78 -0
  82. package/dist/upstream/client.js +114 -0
  83. package/dist/upstream/http-client.d.ts +78 -0
  84. package/dist/upstream/http-client.js +261 -0
  85. package/dist/upstream/lazy-client.d.ts +31 -0
  86. package/dist/upstream/lazy-client.js +53 -0
  87. package/dist/upstream/stdio-client.d.ts +57 -0
  88. package/dist/upstream/stdio-client.js +203 -0
  89. package/dist/util/log.d.ts +27 -0
  90. package/dist/util/log.js +51 -0
  91. package/dist/util/version.d.ts +2 -0
  92. package/dist/util/version.js +40 -0
  93. package/docs/BaseInstRunner.md +621 -0
  94. package/docs/calculatedReplay.md +1185 -0
  95. package/docs/calculatedReplayGuide.md +448 -0
  96. package/docs/installRun.md +413 -0
  97. package/docs/mcpmark.md +752 -0
  98. package/docs/quickstart.md +201 -0
  99. package/docs/t-bench.md +394 -0
  100. package/package.json +56 -0
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bir-proxy — the recording MCP proxy. One process per wrapped upstream.
4
+ *
5
+ * Usage:
6
+ * bir-proxy --server-name <key> -- <command> [args…] # stdio upstream
7
+ * bir-proxy --server-name <key> --url <url> [--transport http|sse]
8
+ * [--header 'K: V']… # remote upstream
9
+ *
10
+ * Options:
11
+ * --server-name <key> the client's config key for this server (required)
12
+ * --transport <t> `http` (default) or `sse`, with `--url`
13
+ * --header 'K: V' extra header for a remote upstream; repeatable
14
+ * --standalone skip control-server discovery; record as Tier 2
15
+ * --no-correlation never relax tool schemas; join on argument fingerprints
16
+ * --lazy create the upstream on the first host message, not at
17
+ * startup (see upstream/lazy-client.ts for the caveat)
18
+ * --restart restart a dead stdio upstream once
19
+ * --discovery-ms <n> how long to look for a control server (default 5000)
20
+ *
21
+ * Environment:
22
+ * BIR_CONTROL_URL / BIR_CONTROL_TOKEN control server, bypassing discovery
23
+ * BIR_AUTH_URL BaseIn service, for Tier 2 recording
24
+ * BIR_VERBOSE=1 per-message detail on stderr
25
+ *
26
+ * STDOUT IS SACRED. It is the host's JSON-RPC stream; every log line, every
27
+ * upstream stderr line and every diagnostic goes to stderr instead. A single
28
+ * stray byte here corrupts the session and surfaces somewhere else entirely.
29
+ */
30
+ import { StdioUpstreamClient } from "../upstream/stdio-client.js";
31
+ import { HttpUpstreamClient } from "../upstream/http-client.js";
32
+ import { LazyUpstreamClient } from "../upstream/lazy-client.js";
33
+ import { Relay } from "../proxy/relay.js";
34
+ import { RecordingInterceptor } from "../proxy/intercept.js";
35
+ import { ProxySession } from "../proxy/session.js";
36
+ import { logLine, errText } from "../util/log.js";
37
+ function usage(message) {
38
+ if (message)
39
+ process.stderr.write(`[bir] ${message}\n`);
40
+ process.stderr.write("Usage:\n" +
41
+ " bir-proxy --server-name <key> -- <command> [args…]\n" +
42
+ " bir-proxy --server-name <key> --url <url> [--transport http|sse] [--header 'K: V']…\n");
43
+ process.exit(2);
44
+ }
45
+ export function parseArgs(argv) {
46
+ const out = {
47
+ serverName: "",
48
+ transport: "http",
49
+ headers: {},
50
+ commandArgs: [],
51
+ standalone: false,
52
+ noCorrelation: false,
53
+ lazy: false,
54
+ restart: false,
55
+ };
56
+ let i = 0;
57
+ for (; i < argv.length; i += 1) {
58
+ const arg = argv[i];
59
+ if (arg === "--") {
60
+ // Everything after `--` is the upstream's own invocation, verbatim.
61
+ out.command = argv[i + 1];
62
+ out.commandArgs = argv.slice(i + 2);
63
+ break;
64
+ }
65
+ switch (arg) {
66
+ case "--server-name":
67
+ out.serverName = argv[++i] ?? "";
68
+ break;
69
+ case "--url":
70
+ out.url = argv[++i];
71
+ break;
72
+ case "--transport": {
73
+ const t = argv[++i];
74
+ if (t !== "http" && t !== "sse")
75
+ usage(`unknown transport: ${t}`);
76
+ out.transport = t;
77
+ break;
78
+ }
79
+ case "--header": {
80
+ const raw = argv[++i] ?? "";
81
+ const sep = raw.indexOf(":");
82
+ if (sep <= 0)
83
+ usage(`bad --header (expected 'Name: value'): ${raw}`);
84
+ out.headers[raw.slice(0, sep).trim()] = raw.slice(sep + 1).trim();
85
+ break;
86
+ }
87
+ case "--standalone":
88
+ out.standalone = true;
89
+ break;
90
+ case "--no-correlation":
91
+ out.noCorrelation = true;
92
+ break;
93
+ case "--lazy":
94
+ out.lazy = true;
95
+ break;
96
+ case "--restart":
97
+ out.restart = true;
98
+ break;
99
+ case "--discovery-ms":
100
+ out.discoveryMs = Number(argv[++i]);
101
+ break;
102
+ case "-h":
103
+ case "--help":
104
+ usage();
105
+ break;
106
+ default:
107
+ usage(`unknown option: ${arg}`);
108
+ }
109
+ }
110
+ if (!out.serverName)
111
+ usage("--server-name is required");
112
+ if (!out.url && !out.command)
113
+ usage("give either --url or -- <command>");
114
+ return out;
115
+ }
116
+ function buildUpstream(args) {
117
+ const create = () => {
118
+ if (args.url) {
119
+ return new HttpUpstreamClient({
120
+ url: args.url,
121
+ transport: args.transport,
122
+ headers: args.headers,
123
+ label: args.serverName,
124
+ });
125
+ }
126
+ return new StdioUpstreamClient({
127
+ command: args.command,
128
+ args: args.commandArgs,
129
+ label: args.serverName,
130
+ restartDelayMs: args.restart ? 500 : 0,
131
+ });
132
+ };
133
+ // `--lazy` defers to the first host message, not to `tools/list` — see the
134
+ // header comment in upstream/lazy-client.ts for why that is the honest limit.
135
+ return args.lazy ? new LazyUpstreamClient(create) : create();
136
+ }
137
+ async function main() {
138
+ const args = parseArgs(process.argv.slice(2));
139
+ // The upstream is built first so the session can be handed it: on a matched
140
+ // turn the control server runs the scenario's steps on this very connection
141
+ // (docs/calculatedReplay.md §16.2), which is what makes replay cost nothing.
142
+ const upstream = buildUpstream(args);
143
+ const session = new ProxySession({
144
+ serverName: args.serverName,
145
+ cwd: process.cwd(),
146
+ standalone: args.standalone,
147
+ noCorrelation: args.noCorrelation,
148
+ discoveryWindowMs: args.discoveryMs,
149
+ upstream,
150
+ });
151
+ // Negotiation runs in the background: relaying must start now, because the
152
+ // host may already be writing `initialize` at us.
153
+ session.start();
154
+ upstream.ready.catch((err) => {
155
+ // §10: the upstream failing to start is one broken server, not a broken
156
+ // session. The relay stays up and answers requests with a tool-level error.
157
+ logLine("upstream.unavailable", { server: args.serverName, error: errText(err) });
158
+ });
159
+ const relay = new Relay({
160
+ upstream,
161
+ hostIn: process.stdin,
162
+ hostOut: process.stdout,
163
+ interceptor: new RecordingInterceptor({ serverName: args.serverName, session }),
164
+ label: args.serverName,
165
+ onHostDisconnect: () => void shutdown(0),
166
+ });
167
+ relay.start();
168
+ logLine("proxy.started", {
169
+ server: args.serverName,
170
+ upstream: args.url ?? args.command,
171
+ transport: args.url ? args.transport : "stdio",
172
+ pid: process.pid,
173
+ });
174
+ let shuttingDown = false;
175
+ async function shutdown(code) {
176
+ if (shuttingDown)
177
+ return;
178
+ shuttingDown = true;
179
+ relay.stop();
180
+ upstream.close();
181
+ // Give the recorder its last chance; a run that loses its final steps is a
182
+ // worse artefact than a proxy that takes an extra moment to exit.
183
+ await Promise.race([
184
+ session.close(),
185
+ new Promise((resolve) => {
186
+ const t = setTimeout(resolve, 3_000);
187
+ t.unref?.();
188
+ }),
189
+ ]);
190
+ logLine("proxy.stopped", { server: args.serverName, tier: session.tier });
191
+ process.exit(code);
192
+ }
193
+ process.on("SIGINT", () => void shutdown(0));
194
+ process.on("SIGTERM", () => void shutdown(0));
195
+ process.on("uncaughtException", (err) => {
196
+ // Never take the host's session down. Log, and keep relaying if we can.
197
+ logLine("proxy.uncaught", { server: args.serverName, error: errText(err) });
198
+ });
199
+ process.on("unhandledRejection", (err) => {
200
+ logLine("proxy.unhandled_rejection", { server: args.serverName, error: errText(err) });
201
+ });
202
+ }
203
+ void main().catch((err) => {
204
+ logLine("proxy.fatal", { error: errText(err) });
205
+ process.exit(1);
206
+ });
207
+ //# sourceMappingURL=bir-proxy.js.map
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bir-scenario — a first-party MCP server with exactly one tool
4
+ * (docs/calculatedReplay.md §6.3).
5
+ *
6
+ * WHY THIS EXISTS. On a fully-wrapped match the whole scenario runs server-side,
7
+ * through proxies that are already connected, and the model's only job is to read
8
+ * the results. Delivering those results as `additionalContext` was the obvious
9
+ * shortcut and is wrong three times over: it must finish inside
10
+ * `UserPromptSubmit`'s timeout; it is *asserted* rather than corroborated by the
11
+ * transcript; and RRepeat learned expensively that a model reads an injected
12
+ * `deny` reason as adversarial interception. A genuine `tool_result` has none of
13
+ * those problems — which is why RRepeat eventually grew a whole MCP bridge to
14
+ * fake one, and why we simply serve one.
15
+ *
16
+ * It holds no state, no credentials, and no upstream connections. It finds the
17
+ * control server exactly as a proxy does — `~/.baseinstrunner/control/<key>.json`,
18
+ * mode 0600, bearer token — and forwards one call.
19
+ *
20
+ * STDOUT IS SACRED, as in every MCP server here: it is the host's JSON-RPC
21
+ * stream, and every log line goes to stderr instead.
22
+ */
23
+ export {};
24
+ //# sourceMappingURL=bir-scenario.d.ts.map
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bir-scenario — a first-party MCP server with exactly one tool
4
+ * (docs/calculatedReplay.md §6.3).
5
+ *
6
+ * WHY THIS EXISTS. On a fully-wrapped match the whole scenario runs server-side,
7
+ * through proxies that are already connected, and the model's only job is to read
8
+ * the results. Delivering those results as `additionalContext` was the obvious
9
+ * shortcut and is wrong three times over: it must finish inside
10
+ * `UserPromptSubmit`'s timeout; it is *asserted* rather than corroborated by the
11
+ * transcript; and RRepeat learned expensively that a model reads an injected
12
+ * `deny` reason as adversarial interception. A genuine `tool_result` has none of
13
+ * those problems — which is why RRepeat eventually grew a whole MCP bridge to
14
+ * fake one, and why we simply serve one.
15
+ *
16
+ * It holds no state, no credentials, and no upstream connections. It finds the
17
+ * control server exactly as a proxy does — `~/.baseinstrunner/control/<key>.json`,
18
+ * mode 0600, bearer token — and forwards one call.
19
+ *
20
+ * STDOUT IS SACRED, as in every MCP server here: it is the host's JSON-RPC
21
+ * stream, and every log line goes to stderr instead.
22
+ */
23
+ import { readFrames, writeFrame } from "../jsonrpc/framing.js";
24
+ import { JsonRpcErrorCode, errorResponse, isRequest, paramsObject, } from "../jsonrpc/types.js";
25
+ import { resolveControl } from "../control/discovery.js";
26
+ import { logLine, logDetail, errText } from "../util/log.js";
27
+ /** Must match `DIRECT_TOOL_NAME`'s suffix in the replay controller. */
28
+ const TOOL_NAME = "run_scenario";
29
+ /** How long to look for a control server before answering that there is none. */
30
+ const DISCOVERY_MS = 5_000;
31
+ /** The plan can be long; the whole-plan budget lives on the control server. */
32
+ const RUN_TIMEOUT_MS = 180_000;
33
+ const TOOL = {
34
+ name: TOOL_NAME,
35
+ description: "Run the pre-calculated tool sequence BaseInstRunner has armed for the current request, " +
36
+ "and return its results. Call once, with no arguments, when instructed to.",
37
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
38
+ };
39
+ /** Ask the control server to run the armed plan. Never throws. */
40
+ async function runArmedPlan(cwd) {
41
+ const found = await resolveControl(cwd, DISCOVERY_MS);
42
+ if (!found?.url)
43
+ return { ok: false, why: "no control server for this directory" };
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), RUN_TIMEOUT_MS);
46
+ timer.unref?.();
47
+ try {
48
+ const res = await fetch(`${found.url.replace(/\/+$/, "")}/scenario/run`, {
49
+ method: "POST",
50
+ headers: {
51
+ "content-type": "application/json",
52
+ authorization: `Bearer ${found.token}`,
53
+ },
54
+ body: "{}",
55
+ signal: controller.signal,
56
+ });
57
+ if (!res.ok)
58
+ return { ok: false, why: `control server answered HTTP ${res.status}` };
59
+ return (await res.json());
60
+ }
61
+ catch (err) {
62
+ return { ok: false, why: errText(err) };
63
+ }
64
+ finally {
65
+ clearTimeout(timer);
66
+ }
67
+ }
68
+ /**
69
+ * A `CallToolResult`. A turn with no armed plan is a **tool failure**, not a
70
+ * protocol error: the model called a tool that had nothing to do — usually
71
+ * because it remembered the tool from an earlier turn — and reacting to that is
72
+ * something models do well and routinely.
73
+ */
74
+ function toolResult(result) {
75
+ if (!result.ok || !result.text) {
76
+ const why = result.why === "no_plan"
77
+ ? "no scenario is armed for this turn — answer the user's request normally"
78
+ : (result.why ?? "the scenario could not be run");
79
+ return { content: [{ type: "text", text: why }], isError: true };
80
+ }
81
+ const notes = [];
82
+ if (result.partial)
83
+ notes.push("(partial: the plan's time budget was reached)");
84
+ const structured = result.responseModel && Object.keys(result.responseModel).length > 0
85
+ ? `\n\nResponse model (derived by the scenario's own logic):\n${JSON.stringify(result.responseModel)}`
86
+ : "";
87
+ // NO `structuredContent`, AND THAT IS THE WHOLE POINT.
88
+ //
89
+ // A client that receives both MAY render `structuredContent` and drop
90
+ // `content` — Claude Code does. The replay then reaches the model as the bare
91
+ // response model: a few hundred bytes of derived keys, with no bundle header,
92
+ // no per-step calls and no tool results. Observed in a real session as a
93
+ // 744-character `{"services":[…],"worstService":…}` where a 30 KB, 8-step
94
+ // bundle should have been.
95
+ //
96
+ // The model behaved correctly given what it saw — it could not answer from
97
+ // that, so it re-ran all eight tools itself. The replay had executed
98
+ // perfectly, cost nothing, and bought nothing, because its results never
99
+ // arrived. The bundle is the payload; the response model rides inside it as
100
+ // text rather than competing with it for the client's attention.
101
+ return {
102
+ content: [
103
+ { type: "text", text: `${result.text}${structured}${notes.length ? `\n\n${notes.join(" ")}` : ""}` },
104
+ ],
105
+ isError: false,
106
+ };
107
+ }
108
+ function main() {
109
+ const cwd = process.cwd();
110
+ let closing = false;
111
+ const reply = (msg) => writeFrame(process.stdout, msg);
112
+ readFrames(process.stdin, {
113
+ onMessage: (msg) => {
114
+ if (!isRequest(msg))
115
+ return; // notifications need no answer
116
+ const { id, method } = msg;
117
+ if (method === "initialize") {
118
+ const params = paramsObject(msg.params);
119
+ reply({
120
+ jsonrpc: "2.0",
121
+ id,
122
+ result: {
123
+ // Echo the host's protocol version when it names one; a server that
124
+ // insists on its own is a server the host may refuse to talk to.
125
+ protocolVersion: String(params.protocolVersion ?? "2025-06-18"),
126
+ capabilities: { tools: {} },
127
+ serverInfo: { name: "bir-scenario", version: "1" },
128
+ },
129
+ });
130
+ return;
131
+ }
132
+ if (method === "tools/list") {
133
+ reply({ jsonrpc: "2.0", id, result: { tools: [TOOL] } });
134
+ return;
135
+ }
136
+ if (method === "ping") {
137
+ reply({ jsonrpc: "2.0", id, result: {} });
138
+ return;
139
+ }
140
+ if (method === "tools/call") {
141
+ const params = paramsObject(msg.params);
142
+ if (String(params.name ?? "") !== TOOL_NAME) {
143
+ reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `unknown tool: ${params.name}`));
144
+ return;
145
+ }
146
+ logDetail("scenario.call", { cwd });
147
+ void runArmedPlan(cwd).then((result) => {
148
+ logLine("scenario.answered", {
149
+ ok: result.ok ?? false,
150
+ steps: result.steps,
151
+ why: result.ok ? undefined : result.why,
152
+ });
153
+ reply({ jsonrpc: "2.0", id, result: toolResult(result) });
154
+ });
155
+ return;
156
+ }
157
+ // Anything else: this server advertises only tools, so an unknown method
158
+ // is a protocol error rather than something to guess at.
159
+ reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `unsupported method: ${method}`));
160
+ },
161
+ onParseError: (detail) => logLine("scenario.frame_error", { reason: detail.reason }),
162
+ onEnd: () => {
163
+ // The host closed our stdin: the session is over.
164
+ if (closing)
165
+ return;
166
+ closing = true;
167
+ process.exit(0);
168
+ },
169
+ });
170
+ logLine("scenario.started", { cwd, pid: process.pid });
171
+ process.on("SIGINT", () => process.exit(0));
172
+ process.on("SIGTERM", () => process.exit(0));
173
+ process.on("uncaughtException", (err) => logLine("scenario.uncaught", { error: errText(err) }));
174
+ process.on("unhandledRejection", (err) => logLine("scenario.unhandled_rejection", { error: errText(err) }));
175
+ }
176
+ main();
177
+ //# sourceMappingURL=bir-scenario.js.map
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bir — the command line (Phase 7).
4
+ *
5
+ * bir install [--config <path>] [--server <name>]… [--local] [--no-hooks]
6
+ * bir uninstall [--config <path>]
7
+ * bir status
8
+ * bir doctor
9
+ * bir wrap --server-name <key> -- <command> [args…]
10
+ * bir login | logout
11
+ * bir --version
12
+ *
13
+ * `bir install` is reversible by construction: every file it edits is stashed
14
+ * verbatim first, so `bir uninstall` restores it byte-for-byte unless somebody
15
+ * else edited it in the meantime (in which case it repairs the entries and says
16
+ * so). `bir doctor` proves the install *works* by asking the control server which
17
+ * proxies actually registered — never by reading the config file back, and never
18
+ * from `serverInfo`, which must stay the upstream's (Phase 3).
19
+ */
20
+ export {};
21
+ //# sourceMappingURL=bir.d.ts.map