@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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transcript — recover the model's reasoning for a tool call (Tier 1 only).
|
|
3
|
+
*
|
|
4
|
+
* Ported in miniature from RRepeat's `server/transcript.ts`. Claude Code writes
|
|
5
|
+
* each session to a JSONL file named by `transcript_path` on every hook payload:
|
|
6
|
+
*
|
|
7
|
+
* { "type": "assistant", "message": { "role": "assistant", "content": [
|
|
8
|
+
* { "type": "text", "text": "..." },
|
|
9
|
+
* { "type": "tool_use", "id": "toolu_…", "name": "Bash", "input": {…} } ] } }
|
|
10
|
+
*
|
|
11
|
+
* `PreToolUse` gives the tool but **not** the reasoning that preceded it, and
|
|
12
|
+
* §7's recorded step carries that reasoning as a `tool_selected` step's
|
|
13
|
+
* `context`. So it is recovered here by finding the assistant message that
|
|
14
|
+
* emitted a given `tool_use_id` and returning its text blocks.
|
|
15
|
+
*
|
|
16
|
+
* The schema drifts between Claude Code versions, so every accessor is
|
|
17
|
+
* defensive: an unknown shape yields `""` rather than throwing. Nothing here is
|
|
18
|
+
* ever required for a recording to be valid — a missing context is a missing
|
|
19
|
+
* nicety, not a failed step.
|
|
20
|
+
*/
|
|
21
|
+
/** The assistant text that accompanied `toolUseId`, or "" when unavailable. */
|
|
22
|
+
export declare function contextForToolUse(transcriptPath: string | undefined, toolUseId: string): string;
|
|
23
|
+
/** The last assistant text in the transcript — the turn's final answer. */
|
|
24
|
+
export declare function lastAssistantText(transcriptPath: string | undefined): string;
|
|
25
|
+
/**
|
|
26
|
+
* The turn's final answer, waiting briefly for the transcript to catch up.
|
|
27
|
+
*
|
|
28
|
+
* WHY THE WAIT EXISTS. `Stop` fires the moment the assistant finishes its turn,
|
|
29
|
+
* which is *before* Claude Code has flushed that turn's last message to the
|
|
30
|
+
* JSONL file. Reading the transcript at that instant returns the previous
|
|
31
|
+
* assistant text — usually the empty one that carried the tool call — so the run
|
|
32
|
+
* gets finished with no answer at all. Observed, not theorised.
|
|
33
|
+
*
|
|
34
|
+
* The condition is "changed from what we already had", not "non-empty", because
|
|
35
|
+
* an intermediate assistant message may be non-empty and still not be the answer.
|
|
36
|
+
* On timeout it returns the best text available rather than nothing: a late or
|
|
37
|
+
* imperfect answer beats an empty one.
|
|
38
|
+
*/
|
|
39
|
+
export declare function settledLastAssistantText(transcriptPath: string | undefined, opts?: {
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
confirmMs?: number;
|
|
42
|
+
pollMs?: number;
|
|
43
|
+
}): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Per-model token usage, as it appears on one assistant message.
|
|
46
|
+
*/
|
|
47
|
+
export interface ModelUsage {
|
|
48
|
+
model: string;
|
|
49
|
+
inputTokens: number;
|
|
50
|
+
outputTokens: number;
|
|
51
|
+
cacheReadTokens: number;
|
|
52
|
+
cacheCreationTokens: number;
|
|
53
|
+
}
|
|
54
|
+
/** Cumulative per-model totals, captured when a run starts. */
|
|
55
|
+
export type UsageMark = Map<string, ModelUsage>;
|
|
56
|
+
/**
|
|
57
|
+
* Snapshot cumulative usage at the moment a run starts.
|
|
58
|
+
*
|
|
59
|
+
* WHY A WATERMARK AND NOT A LINE INDEX. Claude Code writes one transcript per
|
|
60
|
+
* *session*, and this server rolls a new run on every prompt. Summing the whole
|
|
61
|
+
* file gives run 3 the cost of runs 1+2+3 — and every one of those numbers is a
|
|
62
|
+
* **baseline** on the service, so a three-prompt session would hand the savings
|
|
63
|
+
* ledger a baseline roughly three times the truth. Indexing by line does not fix
|
|
64
|
+
* it either: transcripts are appended concurrently and can be compacted. So
|
|
65
|
+
* snapshot the totals and subtract.
|
|
66
|
+
*/
|
|
67
|
+
export declare function markTranscriptUsage(transcriptPath: string | undefined): UsageMark | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Per-model delta since `mark`, **clamped at 0 per field**, so a compaction or a
|
|
70
|
+
* rotated transcript degrades to "no usage" rather than to a negative cost.
|
|
71
|
+
*/
|
|
72
|
+
export declare function usageSince(transcriptPath: string | undefined, mark: UsageMark | undefined): ModelUsage[];
|
|
73
|
+
/** The first user prompt in the transcript — the run's `input` when a hook omits it. */
|
|
74
|
+
export declare function firstUserPrompt(transcriptPath: string | undefined): string;
|
|
75
|
+
//# sourceMappingURL=transcript.d.ts.map
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transcript — recover the model's reasoning for a tool call (Tier 1 only).
|
|
3
|
+
*
|
|
4
|
+
* Ported in miniature from RRepeat's `server/transcript.ts`. Claude Code writes
|
|
5
|
+
* each session to a JSONL file named by `transcript_path` on every hook payload:
|
|
6
|
+
*
|
|
7
|
+
* { "type": "assistant", "message": { "role": "assistant", "content": [
|
|
8
|
+
* { "type": "text", "text": "..." },
|
|
9
|
+
* { "type": "tool_use", "id": "toolu_…", "name": "Bash", "input": {…} } ] } }
|
|
10
|
+
*
|
|
11
|
+
* `PreToolUse` gives the tool but **not** the reasoning that preceded it, and
|
|
12
|
+
* §7's recorded step carries that reasoning as a `tool_selected` step's
|
|
13
|
+
* `context`. So it is recovered here by finding the assistant message that
|
|
14
|
+
* emitted a given `tool_use_id` and returning its text blocks.
|
|
15
|
+
*
|
|
16
|
+
* The schema drifts between Claude Code versions, so every accessor is
|
|
17
|
+
* defensive: an unknown shape yields `""` rather than throwing. Nothing here is
|
|
18
|
+
* ever required for a recording to be valid — a missing context is a missing
|
|
19
|
+
* nicety, not a failed step.
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
function readLines(transcriptPath) {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = readFileSync(transcriptPath, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const line of raw.split("\n")) {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed)
|
|
34
|
+
continue;
|
|
35
|
+
try {
|
|
36
|
+
out.push(JSON.parse(trimmed));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Transcripts are appended concurrently; a half-written line is normal.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function blocksOf(line) {
|
|
45
|
+
const content = line.message?.content ?? line.content;
|
|
46
|
+
if (Array.isArray(content))
|
|
47
|
+
return content;
|
|
48
|
+
if (typeof content === "string")
|
|
49
|
+
return [{ type: "text", text: content }];
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
function textOf(blocks) {
|
|
53
|
+
return blocks
|
|
54
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
55
|
+
.map((b) => b.text)
|
|
56
|
+
.join("\n")
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
/** The assistant text that accompanied `toolUseId`, or "" when unavailable. */
|
|
60
|
+
export function contextForToolUse(transcriptPath, toolUseId) {
|
|
61
|
+
if (!transcriptPath || !toolUseId)
|
|
62
|
+
return "";
|
|
63
|
+
for (const line of readLines(transcriptPath)) {
|
|
64
|
+
const blocks = blocksOf(line);
|
|
65
|
+
if (blocks.some((b) => b.type === "tool_use" && b.id === toolUseId)) {
|
|
66
|
+
return textOf(blocks);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
/** The last assistant text in the transcript — the turn's final answer. */
|
|
72
|
+
export function lastAssistantText(transcriptPath) {
|
|
73
|
+
if (!transcriptPath)
|
|
74
|
+
return "";
|
|
75
|
+
const lines = readLines(transcriptPath);
|
|
76
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
const role = line.message?.role ?? line.type;
|
|
79
|
+
if (role !== "assistant")
|
|
80
|
+
continue;
|
|
81
|
+
const text = textOf(blocksOf(line));
|
|
82
|
+
if (text)
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The turn's final answer, waiting briefly for the transcript to catch up.
|
|
89
|
+
*
|
|
90
|
+
* WHY THE WAIT EXISTS. `Stop` fires the moment the assistant finishes its turn,
|
|
91
|
+
* which is *before* Claude Code has flushed that turn's last message to the
|
|
92
|
+
* JSONL file. Reading the transcript at that instant returns the previous
|
|
93
|
+
* assistant text — usually the empty one that carried the tool call — so the run
|
|
94
|
+
* gets finished with no answer at all. Observed, not theorised.
|
|
95
|
+
*
|
|
96
|
+
* The condition is "changed from what we already had", not "non-empty", because
|
|
97
|
+
* an intermediate assistant message may be non-empty and still not be the answer.
|
|
98
|
+
* On timeout it returns the best text available rather than nothing: a late or
|
|
99
|
+
* imperfect answer beats an empty one.
|
|
100
|
+
*/
|
|
101
|
+
export async function settledLastAssistantText(transcriptPath, opts = {}) {
|
|
102
|
+
if (!transcriptPath)
|
|
103
|
+
return "";
|
|
104
|
+
const pollMs = opts.pollMs ?? 150;
|
|
105
|
+
const before = lastAssistantText(transcriptPath);
|
|
106
|
+
// Waiting is only expensive when there is nothing to wait for. With text
|
|
107
|
+
// already in hand we only confirm that nothing more is coming; with none, the
|
|
108
|
+
// answer is genuinely still being written and is worth the longer window.
|
|
109
|
+
const timeoutMs = before ? (opts.confirmMs ?? 400) : (opts.timeoutMs ?? 2_000);
|
|
110
|
+
const deadline = Date.now() + timeoutMs;
|
|
111
|
+
let latest = before;
|
|
112
|
+
while (Date.now() < deadline) {
|
|
113
|
+
await new Promise((resolve) => {
|
|
114
|
+
const t = setTimeout(resolve, pollMs);
|
|
115
|
+
t.unref?.();
|
|
116
|
+
});
|
|
117
|
+
latest = lastAssistantText(transcriptPath);
|
|
118
|
+
if (latest && latest !== before)
|
|
119
|
+
return latest;
|
|
120
|
+
}
|
|
121
|
+
return latest || before;
|
|
122
|
+
}
|
|
123
|
+
const zeroUsage = (model) => ({
|
|
124
|
+
model,
|
|
125
|
+
inputTokens: 0,
|
|
126
|
+
outputTokens: 0,
|
|
127
|
+
cacheReadTokens: 0,
|
|
128
|
+
cacheCreationTokens: 0,
|
|
129
|
+
});
|
|
130
|
+
/**
|
|
131
|
+
* Sum every assistant message's `usage` in the whole file, keyed by model.
|
|
132
|
+
*
|
|
133
|
+
* DEDUPLICATED BY `message.id`, and that is not an optimisation — it is the
|
|
134
|
+
* difference between a cost and a multiple of it.
|
|
135
|
+
*
|
|
136
|
+
* Claude Code writes **one transcript line per content block**, and every one of
|
|
137
|
+
* those lines repeats the `usage` of the whole message. An assistant turn that
|
|
138
|
+
* emits five tool calls therefore appears five times, each claiming the full
|
|
139
|
+
* turn's tokens. Summing lines inflates the total by the block count — a factor
|
|
140
|
+
* that varies per turn, so it does not even cancel out between two runs being
|
|
141
|
+
* compared.
|
|
142
|
+
*
|
|
143
|
+
* Measured on a real session: 37 lines carrying usage, 14 distinct messages,
|
|
144
|
+
* total overstated 2.6x. That inflation is what made every replay look more
|
|
145
|
+
* expensive than the run it replaced.
|
|
146
|
+
*
|
|
147
|
+
* A line with no `message.id` is counted once on its own (older transcripts, and
|
|
148
|
+
* anything hand-written in a test).
|
|
149
|
+
*/
|
|
150
|
+
function totalUsage(transcriptPath) {
|
|
151
|
+
const totals = new Map();
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
for (const line of readLines(transcriptPath)) {
|
|
154
|
+
const usage = line.message?.usage;
|
|
155
|
+
if (!usage)
|
|
156
|
+
continue;
|
|
157
|
+
const id = line.message?.id;
|
|
158
|
+
if (id) {
|
|
159
|
+
if (seen.has(id))
|
|
160
|
+
continue;
|
|
161
|
+
seen.add(id);
|
|
162
|
+
}
|
|
163
|
+
const model = line.message?.model ?? "unknown";
|
|
164
|
+
const acc = totals.get(model) ?? zeroUsage(model);
|
|
165
|
+
acc.inputTokens += usage.input_tokens ?? 0;
|
|
166
|
+
acc.outputTokens += usage.output_tokens ?? 0;
|
|
167
|
+
acc.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
168
|
+
acc.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
169
|
+
totals.set(model, acc);
|
|
170
|
+
}
|
|
171
|
+
return totals;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Snapshot cumulative usage at the moment a run starts.
|
|
175
|
+
*
|
|
176
|
+
* WHY A WATERMARK AND NOT A LINE INDEX. Claude Code writes one transcript per
|
|
177
|
+
* *session*, and this server rolls a new run on every prompt. Summing the whole
|
|
178
|
+
* file gives run 3 the cost of runs 1+2+3 — and every one of those numbers is a
|
|
179
|
+
* **baseline** on the service, so a three-prompt session would hand the savings
|
|
180
|
+
* ledger a baseline roughly three times the truth. Indexing by line does not fix
|
|
181
|
+
* it either: transcripts are appended concurrently and can be compacted. So
|
|
182
|
+
* snapshot the totals and subtract.
|
|
183
|
+
*/
|
|
184
|
+
export function markTranscriptUsage(transcriptPath) {
|
|
185
|
+
// `undefined`, NOT an empty map. The two look alike and mean opposite things:
|
|
186
|
+
// an empty map is a real mark on a transcript with no usage yet (subtract
|
|
187
|
+
// nothing — correct), while a missing path means we could not take a mark at
|
|
188
|
+
// all, so `usageSince` would return the WHOLE FILE.
|
|
189
|
+
//
|
|
190
|
+
// That distinction is the difference between reporting this turn's cost and
|
|
191
|
+
// reporting the entire session's, and an empty Map is truthy — so a caller
|
|
192
|
+
// doing `Boolean(mark)` would call the second one "measured" and hand the
|
|
193
|
+
// savings ledger a baseline several times too large. Observed, not theorised:
|
|
194
|
+
// a run opened lazily by a proxy step (which carries no transcript path)
|
|
195
|
+
// reported $0.507 for a turn that cost $0.0009.
|
|
196
|
+
if (!transcriptPath)
|
|
197
|
+
return undefined;
|
|
198
|
+
return totalUsage(transcriptPath);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Per-model delta since `mark`, **clamped at 0 per field**, so a compaction or a
|
|
202
|
+
* rotated transcript degrades to "no usage" rather than to a negative cost.
|
|
203
|
+
*/
|
|
204
|
+
export function usageSince(transcriptPath, mark) {
|
|
205
|
+
if (!transcriptPath)
|
|
206
|
+
return [];
|
|
207
|
+
const now = totalUsage(transcriptPath);
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const [model, current] of now) {
|
|
210
|
+
const before = mark?.get(model);
|
|
211
|
+
const delta = {
|
|
212
|
+
model,
|
|
213
|
+
inputTokens: Math.max(0, current.inputTokens - (before?.inputTokens ?? 0)),
|
|
214
|
+
outputTokens: Math.max(0, current.outputTokens - (before?.outputTokens ?? 0)),
|
|
215
|
+
cacheReadTokens: Math.max(0, current.cacheReadTokens - (before?.cacheReadTokens ?? 0)),
|
|
216
|
+
cacheCreationTokens: Math.max(0, current.cacheCreationTokens - (before?.cacheCreationTokens ?? 0)),
|
|
217
|
+
};
|
|
218
|
+
if (delta.inputTokens ||
|
|
219
|
+
delta.outputTokens ||
|
|
220
|
+
delta.cacheReadTokens ||
|
|
221
|
+
delta.cacheCreationTokens) {
|
|
222
|
+
out.push(delta);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
/** The first user prompt in the transcript — the run's `input` when a hook omits it. */
|
|
228
|
+
export function firstUserPrompt(transcriptPath) {
|
|
229
|
+
if (!transcriptPath)
|
|
230
|
+
return "";
|
|
231
|
+
for (const line of readLines(transcriptPath)) {
|
|
232
|
+
const role = line.message?.role ?? line.type;
|
|
233
|
+
if (role !== "user")
|
|
234
|
+
continue;
|
|
235
|
+
const text = textOf(blocksOf(line));
|
|
236
|
+
if (text)
|
|
237
|
+
return text;
|
|
238
|
+
}
|
|
239
|
+
return "";
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=transcript.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseInstRunnerMCP — public surface.
|
|
3
|
+
*
|
|
4
|
+
* The product is three binaries (`bir`, `bir-proxy`, `bir-hooks`); this module
|
|
5
|
+
* exists so the pieces can be embedded — an Agent SDK harness that wants to host
|
|
6
|
+
* its own control server, or a test that drives the relay directly.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./jsonrpc/types.js";
|
|
9
|
+
export { readFrames, writeFrame, DEFAULT_MAX_LINE_BYTES } from "./jsonrpc/framing.js";
|
|
10
|
+
export type { UpstreamClient, UpstreamCloseInfo } from "./upstream/client.js";
|
|
11
|
+
export { BaseUpstreamClient, BIR_ID_PREFIX } from "./upstream/client.js";
|
|
12
|
+
export { StdioUpstreamClient, resolveCommand, whichSync } from "./upstream/stdio-client.js";
|
|
13
|
+
export { HttpUpstreamClient } from "./upstream/http-client.js";
|
|
14
|
+
export { LazyUpstreamClient } from "./upstream/lazy-client.js";
|
|
15
|
+
export { Relay } from "./proxy/relay.js";
|
|
16
|
+
export type { Interceptor, PendingRequest, HostDecision, RelayOptions } from "./proxy/relay.js";
|
|
17
|
+
export { RecordingInterceptor } from "./proxy/intercept.js";
|
|
18
|
+
export { ProxySession, DISCOVERY_WINDOW_MS } from "./proxy/session.js";
|
|
19
|
+
export type { Tier } from "./proxy/session.js";
|
|
20
|
+
export { ControlServer } from "./control/server.js";
|
|
21
|
+
export type { ControlServerOptions, ControlServerAddress, HookPayload } from "./control/server.js";
|
|
22
|
+
export { ControlClient } from "./control/client.js";
|
|
23
|
+
export * from "./control/correlation.js";
|
|
24
|
+
export { StepIndexAllocator } from "./control/ordering.js";
|
|
25
|
+
export * from "./control/discovery.js";
|
|
26
|
+
export * from "./control/paths.js";
|
|
27
|
+
export * from "./record/recorder.js";
|
|
28
|
+
export { RemoteRecorder } from "./record/remote-recorder.js";
|
|
29
|
+
export { redact, redactText, REDACTED } from "./record/redact.js";
|
|
30
|
+
export { serializeCapped, truncateString, truncateStrings, DEFAULT_MAX_PAYLOAD_BYTES, } from "./record/truncate.js";
|
|
31
|
+
export { StepQueue } from "./record/queue.js";
|
|
32
|
+
export * from "./config/resolve.js";
|
|
33
|
+
export * from "./config/generate.js";
|
|
34
|
+
export { authenticate, loadCredentials, saveCredentials, clearCredentials } from "./auth/client.js";
|
|
35
|
+
export type { AuthSession, AuthUser } from "./auth/client.js";
|
|
36
|
+
export { logLine, logDetail } from "./util/log.js";
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BaseInstRunnerMCP — public surface.
|
|
3
|
+
*
|
|
4
|
+
* The product is three binaries (`bir`, `bir-proxy`, `bir-hooks`); this module
|
|
5
|
+
* exists so the pieces can be embedded — an Agent SDK harness that wants to host
|
|
6
|
+
* its own control server, or a test that drives the relay directly.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./jsonrpc/types.js";
|
|
9
|
+
export { readFrames, writeFrame, DEFAULT_MAX_LINE_BYTES } from "./jsonrpc/framing.js";
|
|
10
|
+
export { BaseUpstreamClient, BIR_ID_PREFIX } from "./upstream/client.js";
|
|
11
|
+
export { StdioUpstreamClient, resolveCommand, whichSync } from "./upstream/stdio-client.js";
|
|
12
|
+
export { HttpUpstreamClient } from "./upstream/http-client.js";
|
|
13
|
+
export { LazyUpstreamClient } from "./upstream/lazy-client.js";
|
|
14
|
+
export { Relay } from "./proxy/relay.js";
|
|
15
|
+
export { RecordingInterceptor } from "./proxy/intercept.js";
|
|
16
|
+
export { ProxySession, DISCOVERY_WINDOW_MS } from "./proxy/session.js";
|
|
17
|
+
export { ControlServer } from "./control/server.js";
|
|
18
|
+
export { ControlClient } from "./control/client.js";
|
|
19
|
+
export * from "./control/correlation.js";
|
|
20
|
+
export { StepIndexAllocator } from "./control/ordering.js";
|
|
21
|
+
export * from "./control/discovery.js";
|
|
22
|
+
export * from "./control/paths.js";
|
|
23
|
+
export * from "./record/recorder.js";
|
|
24
|
+
export { RemoteRecorder } from "./record/remote-recorder.js";
|
|
25
|
+
export { redact, redactText, REDACTED } from "./record/redact.js";
|
|
26
|
+
export { serializeCapped, truncateString, truncateStrings, DEFAULT_MAX_PAYLOAD_BYTES, } from "./record/truncate.js";
|
|
27
|
+
export { StepQueue } from "./record/queue.js";
|
|
28
|
+
export * from "./config/resolve.js";
|
|
29
|
+
export * from "./config/generate.js";
|
|
30
|
+
export { authenticate, loadCredentials, saveCredentials, clearCredentials } from "./auth/client.js";
|
|
31
|
+
export { logLine, logDetail } from "./util/log.js";
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* framing — line-delimited JSON-RPC over a byte stream (Phase 1.1).
|
|
3
|
+
*
|
|
4
|
+
* MCP's stdio transport is one JSON value per line. Three things break naive
|
|
5
|
+
* implementations, and all three are handled here:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Partial reads.** A chunk boundary lands mid-message. Bytes are buffered
|
|
8
|
+
* until a newline arrives.
|
|
9
|
+
* 2. **Oversized lines.** A runaway upstream can otherwise grow the buffer
|
|
10
|
+
* without bound. Past `maxLineBytes` the reader drops bytes until the next
|
|
11
|
+
* newline and reports one error, rather than allocating until the process
|
|
12
|
+
* dies.
|
|
13
|
+
* 3. **Invalid JSON.** Reported to the caller (which answers `-32700` toward a
|
|
14
|
+
* host, or logs and drops toward an upstream). It never throws.
|
|
15
|
+
*
|
|
16
|
+
* Buffering is done on `Buffer`, not on a decoded string: a multi-byte UTF-8
|
|
17
|
+
* character split across two chunks decodes to replacement characters if you
|
|
18
|
+
* concatenate strings, which silently corrupts payloads containing non-ASCII.
|
|
19
|
+
*/
|
|
20
|
+
import type { Readable, Writable } from "node:stream";
|
|
21
|
+
import type { JsonRpcMessage } from "./types.js";
|
|
22
|
+
/** 32 MiB. Large enough for a base64 screenshot, small enough to be a bound. */
|
|
23
|
+
export declare const DEFAULT_MAX_LINE_BYTES: number;
|
|
24
|
+
export interface FrameHandlers {
|
|
25
|
+
onMessage: (msg: JsonRpcMessage) => void;
|
|
26
|
+
/** A line that was not valid JSON, or that exceeded `maxLineBytes`. */
|
|
27
|
+
onParseError?: (detail: {
|
|
28
|
+
reason: string;
|
|
29
|
+
bytes: number;
|
|
30
|
+
sample: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
/** The input stream ended or errored. Called at most once. */
|
|
33
|
+
onEnd?: (err?: Error) => void;
|
|
34
|
+
}
|
|
35
|
+
export interface FrameOptions {
|
|
36
|
+
maxLineBytes?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read newline-delimited JSON messages off `input`.
|
|
40
|
+
* Returns a detach function that stops the reader (it does not close `input`).
|
|
41
|
+
*/
|
|
42
|
+
export declare function readFrames(input: Readable, handlers: FrameHandlers, opts?: FrameOptions): () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Write one message as a single line. Serialization failure is swallowed after
|
|
45
|
+
* being reported: a proxy that throws while answering the host takes the host's
|
|
46
|
+
* session down with it, which rule 4 of §1 forbids.
|
|
47
|
+
*/
|
|
48
|
+
export declare function writeFrame(output: Writable, msg: unknown, onError?: (err: Error) => void): void;
|
|
49
|
+
//# sourceMappingURL=framing.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* framing — line-delimited JSON-RPC over a byte stream (Phase 1.1).
|
|
3
|
+
*
|
|
4
|
+
* MCP's stdio transport is one JSON value per line. Three things break naive
|
|
5
|
+
* implementations, and all three are handled here:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Partial reads.** A chunk boundary lands mid-message. Bytes are buffered
|
|
8
|
+
* until a newline arrives.
|
|
9
|
+
* 2. **Oversized lines.** A runaway upstream can otherwise grow the buffer
|
|
10
|
+
* without bound. Past `maxLineBytes` the reader drops bytes until the next
|
|
11
|
+
* newline and reports one error, rather than allocating until the process
|
|
12
|
+
* dies.
|
|
13
|
+
* 3. **Invalid JSON.** Reported to the caller (which answers `-32700` toward a
|
|
14
|
+
* host, or logs and drops toward an upstream). It never throws.
|
|
15
|
+
*
|
|
16
|
+
* Buffering is done on `Buffer`, not on a decoded string: a multi-byte UTF-8
|
|
17
|
+
* character split across two chunks decodes to replacement characters if you
|
|
18
|
+
* concatenate strings, which silently corrupts payloads containing non-ASCII.
|
|
19
|
+
*/
|
|
20
|
+
/** 32 MiB. Large enough for a base64 screenshot, small enough to be a bound. */
|
|
21
|
+
export const DEFAULT_MAX_LINE_BYTES = 32 * 1024 * 1024;
|
|
22
|
+
const NEWLINE = 0x0a;
|
|
23
|
+
/**
|
|
24
|
+
* Read newline-delimited JSON messages off `input`.
|
|
25
|
+
* Returns a detach function that stops the reader (it does not close `input`).
|
|
26
|
+
*/
|
|
27
|
+
export function readFrames(input, handlers, opts = {}) {
|
|
28
|
+
const maxLineBytes = opts.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
|
|
29
|
+
let buf = Buffer.alloc(0);
|
|
30
|
+
/** Set after an oversized line: drop bytes until the next newline resynchronises us. */
|
|
31
|
+
let discarding = false;
|
|
32
|
+
let discarded = 0;
|
|
33
|
+
let ended = false;
|
|
34
|
+
const finish = (err) => {
|
|
35
|
+
if (ended)
|
|
36
|
+
return;
|
|
37
|
+
ended = true;
|
|
38
|
+
handlers.onEnd?.(err);
|
|
39
|
+
};
|
|
40
|
+
const onData = (chunk) => {
|
|
41
|
+
buf = Buffer.concat([buf, typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk]);
|
|
42
|
+
for (;;) {
|
|
43
|
+
const nl = buf.indexOf(NEWLINE);
|
|
44
|
+
if (nl === -1) {
|
|
45
|
+
if (discarding) {
|
|
46
|
+
discarded += buf.length;
|
|
47
|
+
buf = Buffer.alloc(0);
|
|
48
|
+
}
|
|
49
|
+
else if (buf.length > maxLineBytes) {
|
|
50
|
+
// Enter discard mode; report once for the whole oversized line.
|
|
51
|
+
handlers.onParseError?.({
|
|
52
|
+
reason: `line exceeded ${maxLineBytes} bytes`,
|
|
53
|
+
bytes: buf.length,
|
|
54
|
+
sample: buf.subarray(0, 120).toString("utf8"),
|
|
55
|
+
});
|
|
56
|
+
discarding = true;
|
|
57
|
+
discarded = buf.length;
|
|
58
|
+
buf = Buffer.alloc(0);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const line = buf.subarray(0, nl);
|
|
63
|
+
buf = buf.subarray(nl + 1);
|
|
64
|
+
if (discarding) {
|
|
65
|
+
// The tail of an oversized line. Resynchronised — resume normal reading.
|
|
66
|
+
discarded += line.length;
|
|
67
|
+
discarding = false;
|
|
68
|
+
discarded = 0;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (line.length === 0)
|
|
72
|
+
continue; // keep-alive blank lines are legal noise
|
|
73
|
+
if (line.length > maxLineBytes) {
|
|
74
|
+
handlers.onParseError?.({
|
|
75
|
+
reason: `line exceeded ${maxLineBytes} bytes`,
|
|
76
|
+
bytes: line.length,
|
|
77
|
+
sample: line.subarray(0, 120).toString("utf8"),
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const text = line.toString("utf8");
|
|
82
|
+
let msg;
|
|
83
|
+
try {
|
|
84
|
+
msg = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
handlers.onParseError?.({
|
|
88
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
89
|
+
bytes: line.length,
|
|
90
|
+
sample: text.slice(0, 120),
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg)) {
|
|
95
|
+
// A bare scalar or a JSON-RPC batch. Neither appears in MCP; both would
|
|
96
|
+
// desynchronise a 1:1 relay, so they are reported rather than forwarded.
|
|
97
|
+
handlers.onParseError?.({
|
|
98
|
+
reason: "message was not a JSON object",
|
|
99
|
+
bytes: line.length,
|
|
100
|
+
sample: text.slice(0, 120),
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
handlers.onMessage(msg);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const onError = (err) => finish(err);
|
|
108
|
+
const onEnd = () => finish();
|
|
109
|
+
input.on("data", onData);
|
|
110
|
+
input.on("error", onError);
|
|
111
|
+
input.on("end", onEnd);
|
|
112
|
+
input.on("close", onEnd);
|
|
113
|
+
return () => {
|
|
114
|
+
input.off("data", onData);
|
|
115
|
+
input.off("error", onError);
|
|
116
|
+
input.off("end", onEnd);
|
|
117
|
+
input.off("close", onEnd);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Write one message as a single line. Serialization failure is swallowed after
|
|
122
|
+
* being reported: a proxy that throws while answering the host takes the host's
|
|
123
|
+
* session down with it, which rule 4 of §1 forbids.
|
|
124
|
+
*/
|
|
125
|
+
export function writeFrame(output, msg, onError) {
|
|
126
|
+
let line;
|
|
127
|
+
try {
|
|
128
|
+
line = JSON.stringify(msg);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (line === undefined)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
output.write(line + "\n");
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=framing.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 message types, kept deliberately loose.
|
|
3
|
+
*
|
|
4
|
+
* D7 is full transparent passthrough: the relay forwards methods it has never
|
|
5
|
+
* heard of. So nothing here enumerates MCP methods, and `params` / `result` are
|
|
6
|
+
* `unknown` — a typed MCP SDK would fight the one property that makes the proxy
|
|
7
|
+
* work.
|
|
8
|
+
*/
|
|
9
|
+
export type JsonRpcId = string | number | null;
|
|
10
|
+
export interface JsonRpcRequest {
|
|
11
|
+
jsonrpc: "2.0";
|
|
12
|
+
id: string | number;
|
|
13
|
+
method: string;
|
|
14
|
+
params?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface JsonRpcNotification {
|
|
17
|
+
jsonrpc: "2.0";
|
|
18
|
+
method: string;
|
|
19
|
+
params?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface JsonRpcError {
|
|
22
|
+
code: number;
|
|
23
|
+
message: string;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface JsonRpcResponse {
|
|
27
|
+
jsonrpc: "2.0";
|
|
28
|
+
id: JsonRpcId;
|
|
29
|
+
result?: unknown;
|
|
30
|
+
error?: JsonRpcError;
|
|
31
|
+
}
|
|
32
|
+
export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
|
|
33
|
+
/** Standard JSON-RPC error codes, plus the one we use for transport failure. */
|
|
34
|
+
export declare const JsonRpcErrorCode: {
|
|
35
|
+
readonly ParseError: -32700;
|
|
36
|
+
readonly InvalidRequest: -32600;
|
|
37
|
+
readonly MethodNotFound: -32601;
|
|
38
|
+
readonly InvalidParams: -32602;
|
|
39
|
+
readonly InternalError: -32603;
|
|
40
|
+
/** Implementation-defined server error — the upstream is gone. */
|
|
41
|
+
readonly UpstreamUnavailable: -32000;
|
|
42
|
+
};
|
|
43
|
+
/** A message with an `id` and a `method` — something that expects a response. */
|
|
44
|
+
export declare function isRequest(msg: JsonRpcMessage): msg is JsonRpcRequest;
|
|
45
|
+
/** A message with a `method` and no `id` — fire-and-forget. */
|
|
46
|
+
export declare function isNotification(msg: JsonRpcMessage): msg is JsonRpcNotification;
|
|
47
|
+
/** A message with an `id` and no `method` — an answer to somebody's request. */
|
|
48
|
+
export declare function isResponse(msg: JsonRpcMessage): msg is JsonRpcResponse;
|
|
49
|
+
export declare function errorResponse(id: JsonRpcId, code: number, message: string, data?: unknown): JsonRpcResponse;
|
|
50
|
+
/** `params` as an object, or `{}` — every caller wants this and none wants a throw. */
|
|
51
|
+
export declare function paramsObject(params: unknown): Record<string, unknown>;
|
|
52
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 message types, kept deliberately loose.
|
|
3
|
+
*
|
|
4
|
+
* D7 is full transparent passthrough: the relay forwards methods it has never
|
|
5
|
+
* heard of. So nothing here enumerates MCP methods, and `params` / `result` are
|
|
6
|
+
* `unknown` — a typed MCP SDK would fight the one property that makes the proxy
|
|
7
|
+
* work.
|
|
8
|
+
*/
|
|
9
|
+
/** Standard JSON-RPC error codes, plus the one we use for transport failure. */
|
|
10
|
+
export const JsonRpcErrorCode = {
|
|
11
|
+
ParseError: -32700,
|
|
12
|
+
InvalidRequest: -32600,
|
|
13
|
+
MethodNotFound: -32601,
|
|
14
|
+
InvalidParams: -32602,
|
|
15
|
+
InternalError: -32603,
|
|
16
|
+
/** Implementation-defined server error — the upstream is gone. */
|
|
17
|
+
UpstreamUnavailable: -32000,
|
|
18
|
+
};
|
|
19
|
+
/** A message with an `id` and a `method` — something that expects a response. */
|
|
20
|
+
export function isRequest(msg) {
|
|
21
|
+
const m = msg;
|
|
22
|
+
return typeof m.method === "string" && m.id !== undefined && m.id !== null;
|
|
23
|
+
}
|
|
24
|
+
/** A message with a `method` and no `id` — fire-and-forget. */
|
|
25
|
+
export function isNotification(msg) {
|
|
26
|
+
const m = msg;
|
|
27
|
+
return typeof m.method === "string" && (m.id === undefined || m.id === null);
|
|
28
|
+
}
|
|
29
|
+
/** A message with an `id` and no `method` — an answer to somebody's request. */
|
|
30
|
+
export function isResponse(msg) {
|
|
31
|
+
const m = msg;
|
|
32
|
+
return m.method === undefined && m.id !== undefined;
|
|
33
|
+
}
|
|
34
|
+
export function errorResponse(id, code, message, data) {
|
|
35
|
+
const error = { code, message };
|
|
36
|
+
if (data !== undefined)
|
|
37
|
+
error.data = data;
|
|
38
|
+
return { jsonrpc: "2.0", id, error };
|
|
39
|
+
}
|
|
40
|
+
/** `params` as an object, or `{}` — every caller wants this and none wants a throw. */
|
|
41
|
+
export function paramsObject(params) {
|
|
42
|
+
return params && typeof params === "object" && !Array.isArray(params)
|
|
43
|
+
? params
|
|
44
|
+
: {};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.js.map
|