@basein/runner 0.2.10 → 0.2.11
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/README.md +22 -1
- package/dist/bin/bir-scenario.d.ts +18 -2
- package/dist/bin/bir-scenario.js +374 -4
- package/dist/bin/bir.d.ts +10 -0
- package/dist/bin/bir.js +140 -43
- package/dist/bin/investigate.js +1 -1
- package/dist/bin/scenario-edit.d.ts +173 -0
- package/dist/bin/scenario-edit.js +771 -0
- package/dist/config/generate.d.ts +21 -0
- package/dist/config/generate.js +16 -0
- package/docs/calculatedReplay.md +51 -0
- package/docs/calculatedReplayGuide.md +318 -8
- package/docs/quickstart.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,9 @@ between the two is not guaranteed.
|
|
|
147
147
|
section.
|
|
148
148
|
- **Change what the model does.** Tool descriptions are never edited. The
|
|
149
149
|
model-visible changes are schema relaxation, below, and — only with
|
|
150
|
-
`--replay` —
|
|
150
|
+
`--replay` — the first-party `bir` server's tools: `run_scenario`, and three
|
|
151
|
+
that only read a scenario. The three that change one appear only after
|
|
152
|
+
`bir scenario editing on` in that project.
|
|
151
153
|
|
|
152
154
|
---
|
|
153
155
|
|
|
@@ -192,6 +194,25 @@ sequence runs through connections the proxies already hold — **zero model toke
|
|
|
192
194
|
scenarios steer the live session instead, pinning each step's arguments. Either
|
|
193
195
|
way the turn reports what it cost, so the savings ledger measures something real.
|
|
194
196
|
|
|
197
|
+
**Fixing a step.** A calculated step is code, and code can be wrong.
|
|
198
|
+
`bir investigate` names such a step; `bir scenario edit` fixes it, and the
|
|
199
|
+
service checks the change against the recording before it saves it. **A change
|
|
200
|
+
is saved only if it reproduces the recording — or on purpose, with
|
|
201
|
+
`--force --note "why"`.** Only the owner can change a scenario. Claude can do it
|
|
202
|
+
too: in its terminal, or with the `bir` server's tools once
|
|
203
|
+
`bir scenario editing on` was run in the project. Built 2026-09-25, not yet
|
|
204
|
+
released: it needs the next `@basein/runner` version and a service deploy. Step
|
|
205
|
+
by step: [the guide's §9.2](docs/calculatedReplayGuide.md).
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
bir scenario show scn_… --step 3 # read one step
|
|
209
|
+
bir scenario check scn_… --step 3 --input-logic step3.js # try a change; nothing is saved
|
|
210
|
+
bir scenario edit scn_… --step 3 --input-logic step3.js --note "why" # the same check, then save
|
|
211
|
+
bir scenario edits scn_… # the history
|
|
212
|
+
bir scenario undo scn_… # undo the newest edit
|
|
213
|
+
bir scenario editing on # let Claude's `bir` tools change scenarios here
|
|
214
|
+
```
|
|
215
|
+
|
|
195
216
|
---
|
|
196
217
|
|
|
197
218
|
## Two things to know before you turn it on
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* bir-scenario —
|
|
4
|
-
*
|
|
3
|
+
* bir-scenario — the first-party `bir` MCP server (docs/calculatedReplay.md §6.3):
|
|
4
|
+
* `run_scenario`, plus the tools that read and fix a calculated scenario
|
|
5
|
+
* (editSteps.md in the BaseIn repository; docs/calculatedReplayGuide.md §9.2).
|
|
5
6
|
*
|
|
6
7
|
* WHY THIS EXISTS. On a fully-wrapped match the whole scenario runs server-side,
|
|
7
8
|
* through proxies that are already connected, and the model's only job is to read
|
|
@@ -17,6 +18,21 @@
|
|
|
17
18
|
* control server exactly as a proxy does — `~/.baseinstrunner/control/<key>.json`,
|
|
18
19
|
* mode 0600, bearer token — and forwards one call.
|
|
19
20
|
*
|
|
21
|
+
* THE SCENARIO TOOLS RUN `bir` ITSELF. `scenario_show`, `scenario_check` and
|
|
22
|
+
* the rest are the `bir scenario …` commands, spawned from this same package
|
|
23
|
+
* with `--json` (D1: one path, whether a person types it or the model calls
|
|
24
|
+
* it). That keeps this server credential-free — `bir` signs in the way it
|
|
25
|
+
* always does — and means the model sees exactly the answer a person would.
|
|
26
|
+
* Logic bodies travel through temporary files, never argv, and a non-zero exit
|
|
27
|
+
* is an `isError` result that carries the output, refusal and report included.
|
|
28
|
+
*
|
|
29
|
+
* The tools that CHANGE a scenario are offered only where a person ran
|
|
30
|
+
* `bir scenario editing on` (D2): this server runs in every session of every
|
|
31
|
+
* installed project, a fleet included, and a plan must not change because some
|
|
32
|
+
* agent there decided it should. The switch is read when the host lists the
|
|
33
|
+
* tools, and again on every call, so a call to a tool that was never offered —
|
|
34
|
+
* or was offered before the switch went off — is refused rather than run.
|
|
35
|
+
*
|
|
20
36
|
* STDOUT IS SACRED, as in every MCP server here: it is the host's JSON-RPC
|
|
21
37
|
* stream, and every log line goes to stderr instead.
|
|
22
38
|
*/
|
package/dist/bin/bir-scenario.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* bir-scenario —
|
|
4
|
-
*
|
|
3
|
+
* bir-scenario — the first-party `bir` MCP server (docs/calculatedReplay.md §6.3):
|
|
4
|
+
* `run_scenario`, plus the tools that read and fix a calculated scenario
|
|
5
|
+
* (editSteps.md in the BaseIn repository; docs/calculatedReplayGuide.md §9.2).
|
|
5
6
|
*
|
|
6
7
|
* WHY THIS EXISTS. On a fully-wrapped match the whole scenario runs server-side,
|
|
7
8
|
* through proxies that are already connected, and the model's only job is to read
|
|
@@ -17,12 +18,33 @@
|
|
|
17
18
|
* control server exactly as a proxy does — `~/.baseinstrunner/control/<key>.json`,
|
|
18
19
|
* mode 0600, bearer token — and forwards one call.
|
|
19
20
|
*
|
|
21
|
+
* THE SCENARIO TOOLS RUN `bir` ITSELF. `scenario_show`, `scenario_check` and
|
|
22
|
+
* the rest are the `bir scenario …` commands, spawned from this same package
|
|
23
|
+
* with `--json` (D1: one path, whether a person types it or the model calls
|
|
24
|
+
* it). That keeps this server credential-free — `bir` signs in the way it
|
|
25
|
+
* always does — and means the model sees exactly the answer a person would.
|
|
26
|
+
* Logic bodies travel through temporary files, never argv, and a non-zero exit
|
|
27
|
+
* is an `isError` result that carries the output, refusal and report included.
|
|
28
|
+
*
|
|
29
|
+
* The tools that CHANGE a scenario are offered only where a person ran
|
|
30
|
+
* `bir scenario editing on` (D2): this server runs in every session of every
|
|
31
|
+
* installed project, a fleet included, and a plan must not change because some
|
|
32
|
+
* agent there decided it should. The switch is read when the host lists the
|
|
33
|
+
* tools, and again on every call, so a call to a tool that was never offered —
|
|
34
|
+
* or was offered before the switch went off — is refused rather than run.
|
|
35
|
+
*
|
|
20
36
|
* STDOUT IS SACRED, as in every MCP server here: it is the host's JSON-RPC
|
|
21
37
|
* stream, and every log line goes to stderr instead.
|
|
22
38
|
*/
|
|
23
39
|
import { readFrames, writeFrame } from "../jsonrpc/framing.js";
|
|
24
40
|
import { JsonRpcErrorCode, errorResponse, isRequest, paramsObject, } from "../jsonrpc/types.js";
|
|
41
|
+
import { spawn } from "node:child_process";
|
|
42
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
43
|
+
import { tmpdir } from "node:os";
|
|
44
|
+
import { dirname, join, resolve as resolvePath } from "node:path";
|
|
45
|
+
import { fileURLToPath } from "node:url";
|
|
25
46
|
import { resolveControl } from "../control/discovery.js";
|
|
47
|
+
import { editingEnabled, nodeFlagsToCarry, readSidecar } from "../config/generate.js";
|
|
26
48
|
import { logLine, logDetail, errText } from "../util/log.js";
|
|
27
49
|
/** Must match `DIRECT_TOOL_NAME`'s suffix in the replay controller. */
|
|
28
50
|
const TOOL_NAME = "run_scenario";
|
|
@@ -105,6 +127,337 @@ function toolResult(result) {
|
|
|
105
127
|
isError: false,
|
|
106
128
|
};
|
|
107
129
|
}
|
|
130
|
+
// ── the scenario tools: `bir scenario …`, spawned ─────────────────────────────
|
|
131
|
+
/** How long one `bir` command may take. A check replays the chain on the service. */
|
|
132
|
+
const BIR_TIMEOUT_MS = 120_000;
|
|
133
|
+
class BadArguments extends Error {
|
|
134
|
+
}
|
|
135
|
+
const TARGET = {
|
|
136
|
+
type: "string",
|
|
137
|
+
description: "The scenario's scn_ id, or the run_ id of the recording it was calculated from — as investigate and scenario_show print them.",
|
|
138
|
+
};
|
|
139
|
+
const STEP = {
|
|
140
|
+
type: "integer",
|
|
141
|
+
minimum: 0,
|
|
142
|
+
description: "The step's stepIndex, as scenario_show prints it (the first step is 0).",
|
|
143
|
+
};
|
|
144
|
+
const INPUT_LOGIC = {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "The new input logic: the BODY of a JavaScript function of (parameters, intent, respParams) that ends in `return { … }` — the tool's input. Only JavaScript's built-ins exist when the service runs it.",
|
|
147
|
+
};
|
|
148
|
+
const OUTPUT_LOGIC = {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "The new output logic: the BODY of a JavaScript function of (toolOutput, parameters, intent, respParams) — toolOutput is the tool's answer as text — returning the values later steps read.",
|
|
151
|
+
};
|
|
152
|
+
/** A `run_`/`scn_` id: letters, digits, `_` and `-`, so it can never read as a flag. */
|
|
153
|
+
function target(args) {
|
|
154
|
+
const value = args.target;
|
|
155
|
+
if (typeof value !== "string" || !/^(run|scn)_[\w-]+$/.test(value)) {
|
|
156
|
+
throw new BadArguments("`target` must be a scn_… or run_… id");
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function step(args, required) {
|
|
161
|
+
const value = typeof args.step === "string" && /^\d+$/.test(args.step) ? Number(args.step) : args.step;
|
|
162
|
+
if (value === undefined || value === null) {
|
|
163
|
+
if (required)
|
|
164
|
+
throw new BadArguments("`step` is required: the stepIndex scenario_show prints");
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
168
|
+
throw new BadArguments("`step` must be a whole number from 0");
|
|
169
|
+
}
|
|
170
|
+
return ["--step", String(value)];
|
|
171
|
+
}
|
|
172
|
+
function logic(args, key) {
|
|
173
|
+
const value = args[key];
|
|
174
|
+
if (value === undefined || value === null)
|
|
175
|
+
return undefined;
|
|
176
|
+
if (typeof value !== "string")
|
|
177
|
+
throw new BadArguments(`\`${key}\` must be a string: a JavaScript function body`);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function flag(args, key, name) {
|
|
181
|
+
const value = args[key];
|
|
182
|
+
if (value === undefined || value === null || value === false)
|
|
183
|
+
return [];
|
|
184
|
+
if (value !== true)
|
|
185
|
+
throw new BadArguments(`\`${key}\` must be true or false`);
|
|
186
|
+
return [name];
|
|
187
|
+
}
|
|
188
|
+
const SCENARIO_TOOLS = [
|
|
189
|
+
{
|
|
190
|
+
name: "scenario_show",
|
|
191
|
+
changes: false,
|
|
192
|
+
description: "Read a calculated scenario of this account: the whole plan, or with `step` just that step — its input logic, " +
|
|
193
|
+
"output logic, whether it is marked *needs a judgement* (nondeterministic) and the recorded input and output " +
|
|
194
|
+
"it has to reproduce. Read a step here before changing it.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: { target: TARGET, step: STEP },
|
|
198
|
+
required: ["target"],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
},
|
|
201
|
+
plan: (a) => ({ argv: ["scenario", "show", target(a), ...step(a, false)] }),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "scenario_edits",
|
|
205
|
+
changes: false,
|
|
206
|
+
description: "List the hand edits of a calculated scenario, newest first: which step changed, the note, the revision, " +
|
|
207
|
+
"and whether it was saved on purpose (forced), undone, or replaced by a recalculation.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: { target: TARGET },
|
|
211
|
+
required: ["target"],
|
|
212
|
+
additionalProperties: false,
|
|
213
|
+
},
|
|
214
|
+
plan: (a) => ({ argv: ["scenario", "edits", target(a)] }),
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "investigate",
|
|
218
|
+
changes: false,
|
|
219
|
+
description: "Explain why a turn in this project did or did not replay its calculated scenario, what it cost and saved, " +
|
|
220
|
+
"and what to fix. Without `id`, the newest turn here; with a run_, scn_ or sexec_ id, that one. A finding " +
|
|
221
|
+
"that names a step can be read with scenario_show and, where editing is on, fixed with scenario_check and scenario_edit.",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
id: { type: "string", description: "A run_, scn_ or sexec_ id. Omit it for the newest turn in this project." },
|
|
226
|
+
},
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
},
|
|
229
|
+
plan: (a) => {
|
|
230
|
+
if (a.id === undefined || a.id === null || a.id === "")
|
|
231
|
+
return { argv: ["investigate"] };
|
|
232
|
+
if (typeof a.id !== "string" || !/^(run|scn|sexec)_[\w-]+$/.test(a.id)) {
|
|
233
|
+
throw new BadArguments("`id` must be a run_…, scn_… or sexec_… id");
|
|
234
|
+
}
|
|
235
|
+
return { argv: ["investigate", a.id] };
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "scenario_check",
|
|
240
|
+
changes: true,
|
|
241
|
+
description: "Try a change to one step of a calculated scenario without saving anything: the service runs the new " +
|
|
242
|
+
"`inputLogic` and/or `outputLogic` with the recording's values and reports whether this step and every later " +
|
|
243
|
+
"step still reproduce the recorded calls, and what happens to the step's *needs a judgement* mark. The workflow " +
|
|
244
|
+
"is scenario_show → scenario_check until it passes → scenario_edit with the same change and a `note` saying why. " +
|
|
245
|
+
"A failing check means the code is wrong, not that the check is in the way: fix the code.",
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
target: TARGET,
|
|
250
|
+
step: STEP,
|
|
251
|
+
inputLogic: INPUT_LOGIC,
|
|
252
|
+
outputLogic: OUTPUT_LOGIC,
|
|
253
|
+
unfreeze: { type: "boolean", description: "Ask whether the *needs a judgement* mark could be removed." },
|
|
254
|
+
},
|
|
255
|
+
required: ["target", "step"],
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
},
|
|
258
|
+
plan: (a) => ({
|
|
259
|
+
argv: ["scenario", "check", target(a), ...step(a, true), ...flag(a, "unfreeze", "--unfreeze")],
|
|
260
|
+
inputLogic: logic(a, "inputLogic"),
|
|
261
|
+
outputLogic: logic(a, "outputLogic"),
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: "scenario_edit",
|
|
266
|
+
changes: true,
|
|
267
|
+
description: "Save a change to one step of a calculated scenario. It runs the same check as scenario_check and saves only " +
|
|
268
|
+
"if the step reproduces the recording and is not a copy of the recorded call; always give a `note` saying why, " +
|
|
269
|
+
"and `revision` (the chainRevision scenario_show printed) so a plan that changed meanwhile is not overwritten. " +
|
|
270
|
+
"`force` saves what the check refused and is only for a recording that was itself wrong — never a way past a " +
|
|
271
|
+
"failing check. Every save can be undone with scenario_undo.",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
target: TARGET,
|
|
276
|
+
step: STEP,
|
|
277
|
+
inputLogic: INPUT_LOGIC,
|
|
278
|
+
outputLogic: OUTPUT_LOGIC,
|
|
279
|
+
freeze: {
|
|
280
|
+
type: "boolean",
|
|
281
|
+
description: "Mark the step *needs a judgement*: runs then hand over to the agent in front of it.",
|
|
282
|
+
},
|
|
283
|
+
unfreeze: { type: "boolean", description: "Remove that mark, if the check says the step reproduces." },
|
|
284
|
+
force: {
|
|
285
|
+
type: "boolean",
|
|
286
|
+
description: "Save although the check refused it. Needs `note`. Only when the recording itself was wrong.",
|
|
287
|
+
},
|
|
288
|
+
note: { type: "string", description: "Why the step is changed; kept in the history. Required with `force`." },
|
|
289
|
+
revision: {
|
|
290
|
+
type: "integer",
|
|
291
|
+
minimum: 1,
|
|
292
|
+
description: "The chainRevision the change was made against; the save is refused if the plan moved on.",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
required: ["target", "step"],
|
|
296
|
+
additionalProperties: false,
|
|
297
|
+
},
|
|
298
|
+
plan: (a) => {
|
|
299
|
+
if (a.freeze === true && a.unfreeze === true)
|
|
300
|
+
throw new BadArguments("`freeze` and `unfreeze` ask for opposite things");
|
|
301
|
+
const argv = [
|
|
302
|
+
"scenario",
|
|
303
|
+
"edit",
|
|
304
|
+
target(a),
|
|
305
|
+
...step(a, true),
|
|
306
|
+
...flag(a, "freeze", "--freeze"),
|
|
307
|
+
...flag(a, "unfreeze", "--unfreeze"),
|
|
308
|
+
...flag(a, "force", "--force"),
|
|
309
|
+
];
|
|
310
|
+
if (a.note !== undefined && a.note !== null) {
|
|
311
|
+
if (typeof a.note !== "string")
|
|
312
|
+
throw new BadArguments("`note` must be a string");
|
|
313
|
+
argv.push("--note", a.note);
|
|
314
|
+
}
|
|
315
|
+
if (a.revision !== undefined && a.revision !== null) {
|
|
316
|
+
if (typeof a.revision !== "number" || !Number.isInteger(a.revision) || a.revision < 1) {
|
|
317
|
+
throw new BadArguments("`revision` must be a whole number from 1");
|
|
318
|
+
}
|
|
319
|
+
argv.push("--revision", String(a.revision));
|
|
320
|
+
}
|
|
321
|
+
return { argv, inputLogic: logic(a, "inputLogic"), outputLogic: logic(a, "outputLogic") };
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "scenario_undo",
|
|
326
|
+
changes: true,
|
|
327
|
+
description: "Put one step of a calculated scenario back as it was before its newest hand edit — without `edit`, the newest " +
|
|
328
|
+
"edit that can be undone. The undo is itself an edit, so undoing it again redoes the change.",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {
|
|
332
|
+
target: TARGET,
|
|
333
|
+
edit: { type: "string", description: "The sedit_ id to undo, as scenario_edits lists it." },
|
|
334
|
+
},
|
|
335
|
+
required: ["target"],
|
|
336
|
+
additionalProperties: false,
|
|
337
|
+
},
|
|
338
|
+
plan: (a) => {
|
|
339
|
+
const argv = ["scenario", "undo", target(a)];
|
|
340
|
+
if (a.edit !== undefined && a.edit !== null) {
|
|
341
|
+
if (typeof a.edit !== "string" || !/^sedit_[\w-]+$/.test(a.edit)) {
|
|
342
|
+
throw new BadArguments("`edit` must be a sedit_… id");
|
|
343
|
+
}
|
|
344
|
+
argv.push("--edit", a.edit);
|
|
345
|
+
}
|
|
346
|
+
return { argv };
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
];
|
|
350
|
+
/** Whether this project may be offered the tools that change a scenario. Never throws. */
|
|
351
|
+
function editingOn(cwd) {
|
|
352
|
+
try {
|
|
353
|
+
return editingEnabled(readSidecar(), cwd);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** The tools to list for `cwd`, read now: the switch may have moved since the last list. */
|
|
360
|
+
function listTools(cwd) {
|
|
361
|
+
const editing = editingOn(cwd);
|
|
362
|
+
return [
|
|
363
|
+
TOOL,
|
|
364
|
+
...SCENARIO_TOOLS.filter((t) => !t.changes || editing).map(({ name, description, inputSchema }) => ({
|
|
365
|
+
name,
|
|
366
|
+
description,
|
|
367
|
+
inputSchema,
|
|
368
|
+
})),
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
/** This package's own `bir`, beside this file — never whatever `bir` is first on PATH. */
|
|
372
|
+
function birPath() {
|
|
373
|
+
return resolvePath(dirname(fileURLToPath(import.meta.url)), "bir.js");
|
|
374
|
+
}
|
|
375
|
+
function runBir(argv, cwd) {
|
|
376
|
+
return new Promise((resolve) => {
|
|
377
|
+
const child = spawn(process.execPath, [...nodeFlagsToCarry(), birPath(), ...argv], {
|
|
378
|
+
cwd,
|
|
379
|
+
env: process.env,
|
|
380
|
+
// Nothing is ever read from stdin: logic arrives as files.
|
|
381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
382
|
+
windowsHide: true,
|
|
383
|
+
});
|
|
384
|
+
let stdout = "";
|
|
385
|
+
let stderr = "";
|
|
386
|
+
let timedOut = false;
|
|
387
|
+
child.stdout.setEncoding("utf8");
|
|
388
|
+
child.stderr.setEncoding("utf8");
|
|
389
|
+
child.stdout.on("data", (c) => {
|
|
390
|
+
stdout += c;
|
|
391
|
+
});
|
|
392
|
+
child.stderr.on("data", (c) => {
|
|
393
|
+
stderr += c;
|
|
394
|
+
});
|
|
395
|
+
const timer = setTimeout(() => {
|
|
396
|
+
timedOut = true;
|
|
397
|
+
child.kill();
|
|
398
|
+
}, BIR_TIMEOUT_MS);
|
|
399
|
+
timer.unref?.();
|
|
400
|
+
child.on("error", (err) => {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
resolve({ code: null, stdout, stderr: `${stderr}${errText(err)}`, timedOut });
|
|
403
|
+
});
|
|
404
|
+
child.on("close", (code) => {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
resolve({ code, stdout, stderr, timedOut });
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const failed = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
411
|
+
/**
|
|
412
|
+
* Run one scenario tool. Never throws. The logic bodies are written to a
|
|
413
|
+
* directory of their own under the OS temp directory, owner-only, and the
|
|
414
|
+
* directory is removed whatever happens — the code may carry a table name or a
|
|
415
|
+
* host the person would not want left lying around.
|
|
416
|
+
*/
|
|
417
|
+
async function callScenarioTool(tool, args, cwd) {
|
|
418
|
+
let plan;
|
|
419
|
+
try {
|
|
420
|
+
plan = tool.plan(args);
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
return failed(`invalid arguments for ${tool.name}: ${errText(err)}`);
|
|
424
|
+
}
|
|
425
|
+
let dir;
|
|
426
|
+
try {
|
|
427
|
+
const argv = [...plan.argv];
|
|
428
|
+
if (plan.inputLogic !== undefined || plan.outputLogic !== undefined) {
|
|
429
|
+
dir = mkdtempSync(join(tmpdir(), "bir-logic-"));
|
|
430
|
+
if (plan.inputLogic !== undefined) {
|
|
431
|
+
const file = join(dir, "input.js");
|
|
432
|
+
writeFileSync(file, plan.inputLogic, { encoding: "utf8", mode: 0o600 });
|
|
433
|
+
argv.push("--input-logic", file);
|
|
434
|
+
}
|
|
435
|
+
if (plan.outputLogic !== undefined) {
|
|
436
|
+
const file = join(dir, "output.js");
|
|
437
|
+
writeFileSync(file, plan.outputLogic, { encoding: "utf8", mode: 0o600 });
|
|
438
|
+
argv.push("--output-logic", file);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
argv.push("--json");
|
|
442
|
+
const run = await runBir(argv, cwd);
|
|
443
|
+
if (run.timedOut)
|
|
444
|
+
return failed(`bir did not finish in ${BIR_TIMEOUT_MS / 1000} s`);
|
|
445
|
+
const stdout = run.stdout.trim();
|
|
446
|
+
if (run.code === 0)
|
|
447
|
+
return { content: [{ type: "text", text: stdout || "(no output)" }], isError: false };
|
|
448
|
+
// The refusal and its report are on stdout; why bir itself failed, if it
|
|
449
|
+
// did (not signed in, no service address), is on stderr. The model needs both.
|
|
450
|
+
const text = [stdout, run.stderr.trim()].filter(Boolean).join("\n");
|
|
451
|
+
return failed(text || `bir exited with code ${run.code ?? "?"}`);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
return failed(`could not run bir: ${errText(err)}`);
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
if (dir)
|
|
458
|
+
rmSync(dir, { recursive: true, force: true });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
108
461
|
function main() {
|
|
109
462
|
const cwd = process.cwd();
|
|
110
463
|
let closing = false;
|
|
@@ -130,7 +483,7 @@ function main() {
|
|
|
130
483
|
return;
|
|
131
484
|
}
|
|
132
485
|
if (method === "tools/list") {
|
|
133
|
-
reply({ jsonrpc: "2.0", id, result: { tools:
|
|
486
|
+
reply({ jsonrpc: "2.0", id, result: { tools: listTools(cwd) } });
|
|
134
487
|
return;
|
|
135
488
|
}
|
|
136
489
|
if (method === "ping") {
|
|
@@ -139,7 +492,24 @@ function main() {
|
|
|
139
492
|
}
|
|
140
493
|
if (method === "tools/call") {
|
|
141
494
|
const params = paramsObject(msg.params);
|
|
142
|
-
|
|
495
|
+
const name = String(params.name ?? "");
|
|
496
|
+
const scenarioTool = SCENARIO_TOOLS.find((t) => t.name === name);
|
|
497
|
+
if (scenarioTool) {
|
|
498
|
+
// Asked again, not remembered from tools/list: a host may call a tool
|
|
499
|
+
// it listed before the switch went off, or one it was never offered.
|
|
500
|
+
if (scenarioTool.changes && !editingOn(cwd)) {
|
|
501
|
+
reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `${name} is not offered in this project: scenario editing is off. ` +
|
|
502
|
+
"A person turns it on with `bir scenario editing on` in this directory, then restarts the session."));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
logDetail("scenario.tool", { cwd, tool: name });
|
|
506
|
+
void callScenarioTool(scenarioTool, paramsObject(params.arguments), cwd).then((result) => {
|
|
507
|
+
logLine("scenario.tool_answered", { tool: name, isError: result.isError === true });
|
|
508
|
+
reply({ jsonrpc: "2.0", id, result });
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (name !== TOOL_NAME) {
|
|
143
513
|
reply(errorResponse(id, JsonRpcErrorCode.MethodNotFound, `unknown tool: ${params.name}`));
|
|
144
514
|
return;
|
|
145
515
|
}
|
package/dist/bin/bir.d.ts
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
* bir login | logout
|
|
13
13
|
* bir --version
|
|
14
14
|
*
|
|
15
|
+
* bir scenario list | show <runId|scnId> [--step <n>] | calc <runId> [--force [--discard-edits]]
|
|
16
|
+
* bir scenario check|edit <runId|scnId> --step <n> --input-logic <file|-> … ← fix one step
|
|
17
|
+
* bir scenario edits | undo <runId|scnId> [--edit <sedit_id>]
|
|
18
|
+
* bir scenario editing on|off|status ← may this project's `bir` MCP server change a scenario?
|
|
19
|
+
* bir scenario replay <scnId> --prompt "…" [--dry] | bir replay status|on|off|allow …
|
|
20
|
+
* bir investigate [<id>] | list | executions
|
|
21
|
+
*
|
|
22
|
+
* The `scenario` words that read or change one step live in scenario-edit.ts
|
|
23
|
+
* (editSteps.md in the BaseIn repository); `investigate` in investigate.ts.
|
|
24
|
+
*
|
|
15
25
|
* `bir install` is reversible by construction: every file it edits is stashed
|
|
16
26
|
* verbatim first, so `bir uninstall` restores it byte-for-byte unless somebody
|
|
17
27
|
* else edited it in the meantime (in which case it repairs the entries and says
|