@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.
- package/LICENSE +201 -0
- package/README.md +276 -0
- package/dist/auth/client.d.ts +85 -0
- package/dist/auth/client.js +284 -0
- package/dist/bin/bir-hooks.d.ts +48 -0
- package/dist/bin/bir-hooks.js +201 -0
- package/dist/bin/bir-proxy.d.ts +45 -0
- package/dist/bin/bir-proxy.js +207 -0
- package/dist/bin/bir-scenario.d.ts +24 -0
- package/dist/bin/bir-scenario.js +177 -0
- package/dist/bin/bir.d.ts +21 -0
- package/dist/bin/bir.js +876 -0
- package/dist/config/adapters/claude-code.d.ts +76 -0
- package/dist/config/adapters/claude-code.js +181 -0
- package/dist/config/adapters/generic.d.ts +17 -0
- package/dist/config/adapters/generic.js +36 -0
- package/dist/config/generate.d.ts +127 -0
- package/dist/config/generate.js +114 -0
- package/dist/config/resolve.d.ts +68 -0
- package/dist/config/resolve.js +132 -0
- package/dist/control/client.d.ts +56 -0
- package/dist/control/client.js +86 -0
- package/dist/control/correlation.d.ts +86 -0
- package/dist/control/correlation.js +0 -0
- package/dist/control/discovery.d.ts +50 -0
- package/dist/control/discovery.js +123 -0
- package/dist/control/ordering.d.ts +38 -0
- package/dist/control/ordering.js +44 -0
- package/dist/control/paths.d.ts +32 -0
- package/dist/control/paths.js +56 -0
- package/dist/control/server.d.ts +272 -0
- package/dist/control/server.js +1131 -0
- package/dist/control/transcript.d.ts +75 -0
- package/dist/control/transcript.js +241 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +32 -0
- package/dist/jsonrpc/framing.d.ts +49 -0
- package/dist/jsonrpc/framing.js +143 -0
- package/dist/jsonrpc/types.d.ts +52 -0
- package/dist/jsonrpc/types.js +46 -0
- package/dist/proxy/intercept.d.ts +55 -0
- package/dist/proxy/intercept.js +147 -0
- package/dist/proxy/relay.d.ts +97 -0
- package/dist/proxy/relay.js +166 -0
- package/dist/proxy/session.d.ts +116 -0
- package/dist/proxy/session.js +319 -0
- package/dist/record/housekeeping.d.ts +34 -0
- package/dist/record/housekeeping.js +39 -0
- package/dist/record/queue.d.ts +48 -0
- package/dist/record/queue.js +96 -0
- package/dist/record/recorder.d.ts +111 -0
- package/dist/record/recorder.js +39 -0
- package/dist/record/redact.d.ts +37 -0
- package/dist/record/redact.js +119 -0
- package/dist/record/remote-recorder.d.ts +110 -0
- package/dist/record/remote-recorder.js +301 -0
- package/dist/record/truncate.d.ts +36 -0
- package/dist/record/truncate.js +85 -0
- package/dist/replay/bundle.d.ts +36 -0
- package/dist/replay/bundle.js +89 -0
- package/dist/replay/controller.d.ts +300 -0
- package/dist/replay/controller.js +807 -0
- package/dist/replay/coverage.d.ts +41 -0
- package/dist/replay/coverage.js +56 -0
- package/dist/replay/derive.d.ts +58 -0
- package/dist/replay/derive.js +166 -0
- package/dist/replay/executor.d.ts +78 -0
- package/dist/replay/executor.js +233 -0
- package/dist/replay/logic.d.ts +31 -0
- package/dist/replay/logic.js +50 -0
- package/dist/replay/plan.d.ts +181 -0
- package/dist/replay/plan.js +397 -0
- package/dist/replay/pricing.d.ts +41 -0
- package/dist/replay/pricing.js +76 -0
- package/dist/replay/source-run.d.ts +50 -0
- package/dist/replay/source-run.js +98 -0
- package/dist/replay/tool-error.d.ts +22 -0
- package/dist/replay/tool-error.js +60 -0
- package/dist/replay/types.d.ts +116 -0
- package/dist/replay/types.js +35 -0
- package/dist/upstream/client.d.ts +78 -0
- package/dist/upstream/client.js +114 -0
- package/dist/upstream/http-client.d.ts +78 -0
- package/dist/upstream/http-client.js +261 -0
- package/dist/upstream/lazy-client.d.ts +31 -0
- package/dist/upstream/lazy-client.js +53 -0
- package/dist/upstream/stdio-client.d.ts +57 -0
- package/dist/upstream/stdio-client.js +203 -0
- package/dist/util/log.d.ts +27 -0
- package/dist/util/log.js +51 -0
- package/dist/util/version.d.ts +2 -0
- package/dist/util/version.js +40 -0
- package/docs/BaseInstRunner.md +621 -0
- package/docs/calculatedReplay.md +1185 -0
- package/docs/calculatedReplayGuide.md +448 -0
- package/docs/installRun.md +413 -0
- package/docs/mcpmark.md +752 -0
- package/docs/quickstart.md +201 -0
- package/docs/t-bench.md +394 -0
- 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
|