@basein/runner 0.2.11 → 0.3.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/README.md +540 -350
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -10
- package/dist/bin/bir-scenario.js +704 -135
- package/dist/bin/bir.d.ts +14 -8
- package/dist/bin/bir.js +2937 -488
- package/dist/bin/investigate.js +41 -16
- package/dist/bin/scenario-edit.d.ts +43 -10
- package/dist/bin/scenario-edit.js +281 -80
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2803 -306
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/housekeeping.d.ts +71 -0
- package/dist/record/housekeeping.js +417 -0
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +371 -20
- package/dist/replay/controller.js +1185 -111
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1244
- package/docs/calculatedReplayGuide.md +1212 -879
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -271
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- package/scripts/install.sh +104 -7
|
@@ -27,8 +27,79 @@
|
|
|
27
27
|
* possibly be the user's task. `Read`, `Bash` and `Grep` are NOT here: they do
|
|
28
28
|
* real work, they belong in a recording, and reaching for one mid-replay
|
|
29
29
|
* usually *is* the model doing the task another way.
|
|
30
|
+
*
|
|
31
|
+
* THE RUNNER'S OWN `bir` IS HOUSEKEEPING TOO (editSteps.md D13), and that is
|
|
32
|
+
* the one reason this file now looks at a call's input and not only its name.
|
|
33
|
+
* A session that fixes a plan — `bir investigate`, then `bir scenario edit`, or
|
|
34
|
+
* the same through the `bir` MCP server's `scenario_*` tools — is exactly the
|
|
35
|
+
* kind of session that gets recorded and calculated. Recorded, it becomes a
|
|
36
|
+
* scenario whose steps *edit plans*, and a steered or direct replay of it would
|
|
37
|
+
* then change a plan unattended, on a prompt nobody meant as "change the plan":
|
|
38
|
+
* the very thing D2 keeps the editing tools switched off in a fleet to prevent.
|
|
39
|
+
* And mid-replay, a model that stops to read `bir investigate` has not left the
|
|
40
|
+
* plan; it is reading about it.
|
|
41
|
+
*
|
|
42
|
+
* So two more kinds of call are housekeeping, and both are still narrow:
|
|
43
|
+
*
|
|
44
|
+
* - every tool of the `bir` MCP server **except `run_scenario`**, which is
|
|
45
|
+
* the direct plan's delivery vehicle and keeps its own handling (it is
|
|
46
|
+
* recorded, tagged `pinnedBy`, while its plan is live);
|
|
47
|
+
* - a `Bash` call whose command is **nothing but** `bir` invocations
|
|
48
|
+
* ({@link isBirOnlyCommand}), and the same for a `PowerShell` call
|
|
49
|
+
* ({@link isBirOnlyPowerShell}) — Claude Code on Windows reaches for its
|
|
50
|
+
* PowerShell tool as readily as for Bash, and a `bir scenario edit` run
|
|
51
|
+
* there is the same act. `npm test && bir investigate` is not: the
|
|
52
|
+
* `npm test` half is real work, and hiding it from the recording would be
|
|
53
|
+
* hiding the task. When in doubt the answer is "not housekeeping", because
|
|
54
|
+
* a `bir` call recorded by mistake costs a noisy step, while real work
|
|
55
|
+
* skipped by mistake is a plan with a hole in it.
|
|
30
56
|
*/
|
|
31
57
|
/** Host tools that are never recorded as steps and never count as divergence. */
|
|
32
58
|
export declare const HOUSEKEEPING_TOOLS: ReadonlySet<string>;
|
|
59
|
+
/** By name alone — the host's own bookkeeping tools. See {@link isHousekeepingCall} for the full rule. */
|
|
33
60
|
export declare function isHousekeeping(toolName: string): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The direct plan's delivery vehicle. Must equal `DIRECT_TOOL_NAME` in the
|
|
63
|
+
* replay controller (a test holds them together); spelled out here because
|
|
64
|
+
* the controller imports this file.
|
|
65
|
+
*/
|
|
66
|
+
export declare const RUN_SCENARIO_TOOL = "mcp__bir__run_scenario";
|
|
67
|
+
/**
|
|
68
|
+
* Whether one call is host housekeeping: never recorded as a step, never a
|
|
69
|
+
* divergence from a plan. `toolInput` is what the hook saw (`tool_input`); only
|
|
70
|
+
* a `Bash` call's `command` is read from it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function isHousekeepingCall(toolName: string, toolInput?: unknown): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* {@link isBirOnlyCommand} for PowerShell, whose rules differ enough that the
|
|
75
|
+
* Bash reader would get it wrong: a backslash is a path separator, not an
|
|
76
|
+
* escape; the escape is the backtick; `$(…)` runs code even inside double
|
|
77
|
+
* quotes; `&` at the start of a part is the call operator. Deliberately
|
|
78
|
+
* narrower than the Bash reader: parts split on `;`, `&&`, `||` and newlines
|
|
79
|
+
* (outside quotes); each is `bir …` (as {@link isBirInvocation} reads it, also
|
|
80
|
+
* after a leading `&`) or a plain `cd`/`Set-Location`/`Push-Location <dir>`.
|
|
81
|
+
* A pipe, a backtick, `$(` or `@(` anywhere, a brace or parenthesis outside
|
|
82
|
+
* quotes, an `@` starting a word, or a quote left open makes the answer false — for the same
|
|
83
|
+
* reason as there: when unsure, record it.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isBirOnlyPowerShell(command: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* True when a shell command runs `bir` and nothing else: split on `&&`, `||`,
|
|
88
|
+
* `;`, `|`, `&` and newlines (outside quotes), every part is a `bir`
|
|
89
|
+
* invocation or a plain `cd <dir>`, and at least one is `bir`.
|
|
90
|
+
*
|
|
91
|
+
* A `bir` invocation is `bir` / `bir.cmd` / a path ending in `bir`, `bir.cmd`,
|
|
92
|
+
* `bir.js` or `bir.ps1`; `npx [-y] [-p @basein/runner…] [@basein/runner…] bir …`;
|
|
93
|
+
* or `node <path>/bir.js …` — each optionally after `NAME=value` assignments,
|
|
94
|
+
* which only set `bir`'s own environment. A heredoc is read as the data it is,
|
|
95
|
+
* because `bir scenario check … --input-logic - <<'EOF'` is the natural way to
|
|
96
|
+
* hand `bir` a logic body, and its lines are JavaScript, not commands.
|
|
97
|
+
*
|
|
98
|
+
* Anything that could run other code while looking like `bir` makes the answer
|
|
99
|
+
* false: a command substitution (`$(…)`, backticks) outside single quotes and
|
|
100
|
+
* quoted heredocs, and a subshell, group or process substitution (`(`, `{`).
|
|
101
|
+
* So does a quote or heredoc left open — a parse this side is not sure of is
|
|
102
|
+
* not one to hide a step on.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isBirOnlyCommand(command: string): boolean;
|
|
34
105
|
//# sourceMappingURL=housekeeping.d.ts.map
|
|
@@ -27,13 +27,430 @@
|
|
|
27
27
|
* possibly be the user's task. `Read`, `Bash` and `Grep` are NOT here: they do
|
|
28
28
|
* real work, they belong in a recording, and reaching for one mid-replay
|
|
29
29
|
* usually *is* the model doing the task another way.
|
|
30
|
+
*
|
|
31
|
+
* THE RUNNER'S OWN `bir` IS HOUSEKEEPING TOO (editSteps.md D13), and that is
|
|
32
|
+
* the one reason this file now looks at a call's input and not only its name.
|
|
33
|
+
* A session that fixes a plan — `bir investigate`, then `bir scenario edit`, or
|
|
34
|
+
* the same through the `bir` MCP server's `scenario_*` tools — is exactly the
|
|
35
|
+
* kind of session that gets recorded and calculated. Recorded, it becomes a
|
|
36
|
+
* scenario whose steps *edit plans*, and a steered or direct replay of it would
|
|
37
|
+
* then change a plan unattended, on a prompt nobody meant as "change the plan":
|
|
38
|
+
* the very thing D2 keeps the editing tools switched off in a fleet to prevent.
|
|
39
|
+
* And mid-replay, a model that stops to read `bir investigate` has not left the
|
|
40
|
+
* plan; it is reading about it.
|
|
41
|
+
*
|
|
42
|
+
* So two more kinds of call are housekeeping, and both are still narrow:
|
|
43
|
+
*
|
|
44
|
+
* - every tool of the `bir` MCP server **except `run_scenario`**, which is
|
|
45
|
+
* the direct plan's delivery vehicle and keeps its own handling (it is
|
|
46
|
+
* recorded, tagged `pinnedBy`, while its plan is live);
|
|
47
|
+
* - a `Bash` call whose command is **nothing but** `bir` invocations
|
|
48
|
+
* ({@link isBirOnlyCommand}), and the same for a `PowerShell` call
|
|
49
|
+
* ({@link isBirOnlyPowerShell}) — Claude Code on Windows reaches for its
|
|
50
|
+
* PowerShell tool as readily as for Bash, and a `bir scenario edit` run
|
|
51
|
+
* there is the same act. `npm test && bir investigate` is not: the
|
|
52
|
+
* `npm test` half is real work, and hiding it from the recording would be
|
|
53
|
+
* hiding the task. When in doubt the answer is "not housekeeping", because
|
|
54
|
+
* a `bir` call recorded by mistake costs a noisy step, while real work
|
|
55
|
+
* skipped by mistake is a plan with a hole in it.
|
|
30
56
|
*/
|
|
57
|
+
import { SCENARIO_SERVER_KEY } from "../config/generate.js";
|
|
31
58
|
/** Host tools that are never recorded as steps and never count as divergence. */
|
|
32
59
|
export const HOUSEKEEPING_TOOLS = new Set([
|
|
33
60
|
"ToolSearch",
|
|
34
61
|
"TodoWrite",
|
|
35
62
|
]);
|
|
63
|
+
/** By name alone — the host's own bookkeeping tools. See {@link isHousekeepingCall} for the full rule. */
|
|
36
64
|
export function isHousekeeping(toolName) {
|
|
37
65
|
return HOUSEKEEPING_TOOLS.has(toolName);
|
|
38
66
|
}
|
|
67
|
+
/** `mcp__bir__`: every tool the first-party `bir` MCP server offers is named under it. */
|
|
68
|
+
const SCENARIO_SERVER_PREFIX = `mcp__${SCENARIO_SERVER_KEY}__`;
|
|
69
|
+
/**
|
|
70
|
+
* The direct plan's delivery vehicle. Must equal `DIRECT_TOOL_NAME` in the
|
|
71
|
+
* replay controller (a test holds them together); spelled out here because
|
|
72
|
+
* the controller imports this file.
|
|
73
|
+
*/
|
|
74
|
+
export const RUN_SCENARIO_TOOL = `${SCENARIO_SERVER_PREFIX}run_scenario`;
|
|
75
|
+
/**
|
|
76
|
+
* Whether one call is host housekeeping: never recorded as a step, never a
|
|
77
|
+
* divergence from a plan. `toolInput` is what the hook saw (`tool_input`); only
|
|
78
|
+
* a `Bash` call's `command` is read from it.
|
|
79
|
+
*/
|
|
80
|
+
export function isHousekeepingCall(toolName, toolInput) {
|
|
81
|
+
if (HOUSEKEEPING_TOOLS.has(toolName))
|
|
82
|
+
return true;
|
|
83
|
+
if (toolName.startsWith(SCENARIO_SERVER_PREFIX))
|
|
84
|
+
return toolName !== RUN_SCENARIO_TOOL;
|
|
85
|
+
if (toolName === "Bash" || toolName === "PowerShell") {
|
|
86
|
+
const command = toolInput?.command;
|
|
87
|
+
if (typeof command !== "string")
|
|
88
|
+
return false;
|
|
89
|
+
return toolName === "Bash" ? isBirOnlyCommand(command) : isBirOnlyPowerShell(command);
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* {@link isBirOnlyCommand} for PowerShell, whose rules differ enough that the
|
|
95
|
+
* Bash reader would get it wrong: a backslash is a path separator, not an
|
|
96
|
+
* escape; the escape is the backtick; `$(…)` runs code even inside double
|
|
97
|
+
* quotes; `&` at the start of a part is the call operator. Deliberately
|
|
98
|
+
* narrower than the Bash reader: parts split on `;`, `&&`, `||` and newlines
|
|
99
|
+
* (outside quotes); each is `bir …` (as {@link isBirInvocation} reads it, also
|
|
100
|
+
* after a leading `&`) or a plain `cd`/`Set-Location`/`Push-Location <dir>`.
|
|
101
|
+
* A pipe, a backtick, `$(` or `@(` anywhere, a brace or parenthesis outside
|
|
102
|
+
* quotes, an `@` starting a word, or a quote left open makes the answer false — for the same
|
|
103
|
+
* reason as there: when unsure, record it.
|
|
104
|
+
*/
|
|
105
|
+
export function isBirOnlyPowerShell(command) {
|
|
106
|
+
if (command.includes("`") || command.includes("$(") || command.includes("@("))
|
|
107
|
+
return false;
|
|
108
|
+
const segments = [];
|
|
109
|
+
let words = [];
|
|
110
|
+
let word = "";
|
|
111
|
+
let inWord = false;
|
|
112
|
+
const endWord = () => {
|
|
113
|
+
if (inWord)
|
|
114
|
+
words.push(word);
|
|
115
|
+
word = "";
|
|
116
|
+
inWord = false;
|
|
117
|
+
};
|
|
118
|
+
const endSegment = () => {
|
|
119
|
+
endWord();
|
|
120
|
+
segments.push(words);
|
|
121
|
+
words = [];
|
|
122
|
+
};
|
|
123
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
124
|
+
const c = command[i];
|
|
125
|
+
if (c === "'" || c === '"') {
|
|
126
|
+
const close = command.indexOf(c, i + 1);
|
|
127
|
+
if (close < 0)
|
|
128
|
+
return false;
|
|
129
|
+
word += command.slice(i + 1, close);
|
|
130
|
+
inWord = true;
|
|
131
|
+
i = close;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === "\n" || c === "\r" || c === ";") {
|
|
135
|
+
endSegment();
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if ((c === "&" || c === "|") && command[i + 1] === c) {
|
|
139
|
+
endSegment();
|
|
140
|
+
i += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (c === "|" || c === "{" || c === "}" || c === "(" || c === ")")
|
|
144
|
+
return false;
|
|
145
|
+
// `@` opens an array, a hash or a splat only at the start of a word; inside
|
|
146
|
+
// one (`…\node_modules\@basein\runner\…`) it is a plain character.
|
|
147
|
+
if (c === "@" && !inWord)
|
|
148
|
+
return false;
|
|
149
|
+
if (c === "&") {
|
|
150
|
+
// The call operator, as a word of its own at the start of a part; a
|
|
151
|
+
// redirection's `2>&1` keeps its `&` inside the word.
|
|
152
|
+
if (inWord && /\d?>$/.test(word)) {
|
|
153
|
+
word += c;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (inWord || words.length > 0)
|
|
157
|
+
return false;
|
|
158
|
+
words.push("&");
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (c === " " || c === "\t") {
|
|
162
|
+
endWord();
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
word += c;
|
|
166
|
+
inWord = true;
|
|
167
|
+
}
|
|
168
|
+
endSegment();
|
|
169
|
+
let bir = false;
|
|
170
|
+
for (const raw of segments) {
|
|
171
|
+
const parts = raw[0] === "&" ? raw.slice(1) : raw;
|
|
172
|
+
if (parts.length === 0) {
|
|
173
|
+
if (raw.length > 0)
|
|
174
|
+
return false;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (raw[0] !== "&" && /^(cd|chdir|sl|set-location|pushd|push-location)$/i.test(parts[0])) {
|
|
178
|
+
if (parts.length > 2 || parts.slice(1).some(isRedirect))
|
|
179
|
+
return false;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (!isBirInvocation(parts.filter((w) => !isRedirect(w))))
|
|
183
|
+
return false;
|
|
184
|
+
bir = true;
|
|
185
|
+
}
|
|
186
|
+
return bir;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* True when a shell command runs `bir` and nothing else: split on `&&`, `||`,
|
|
190
|
+
* `;`, `|`, `&` and newlines (outside quotes), every part is a `bir`
|
|
191
|
+
* invocation or a plain `cd <dir>`, and at least one is `bir`.
|
|
192
|
+
*
|
|
193
|
+
* A `bir` invocation is `bir` / `bir.cmd` / a path ending in `bir`, `bir.cmd`,
|
|
194
|
+
* `bir.js` or `bir.ps1`; `npx [-y] [-p @basein/runner…] [@basein/runner…] bir …`;
|
|
195
|
+
* or `node <path>/bir.js …` — each optionally after `NAME=value` assignments,
|
|
196
|
+
* which only set `bir`'s own environment. A heredoc is read as the data it is,
|
|
197
|
+
* because `bir scenario check … --input-logic - <<'EOF'` is the natural way to
|
|
198
|
+
* hand `bir` a logic body, and its lines are JavaScript, not commands.
|
|
199
|
+
*
|
|
200
|
+
* Anything that could run other code while looking like `bir` makes the answer
|
|
201
|
+
* false: a command substitution (`$(…)`, backticks) outside single quotes and
|
|
202
|
+
* quoted heredocs, and a subshell, group or process substitution (`(`, `{`).
|
|
203
|
+
* So does a quote or heredoc left open — a parse this side is not sure of is
|
|
204
|
+
* not one to hide a step on.
|
|
205
|
+
*/
|
|
206
|
+
export function isBirOnlyCommand(command) {
|
|
207
|
+
const segments = splitCommand(command);
|
|
208
|
+
if (!segments)
|
|
209
|
+
return false;
|
|
210
|
+
let bir = false;
|
|
211
|
+
for (const words of segments) {
|
|
212
|
+
if (words.length === 0)
|
|
213
|
+
continue;
|
|
214
|
+
if (isPlainCd(words))
|
|
215
|
+
continue;
|
|
216
|
+
if (!isBirInvocation(words))
|
|
217
|
+
return false;
|
|
218
|
+
bir = true;
|
|
219
|
+
}
|
|
220
|
+
return bir;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The command's parts, each as its words (quotes removed, as the shell would),
|
|
224
|
+
* or null when it cannot be read with confidence. Redirections stay words:
|
|
225
|
+
* `bir … < step.js` and `bir … > out.json 2>&1` are still only `bir`.
|
|
226
|
+
*/
|
|
227
|
+
function splitCommand(command) {
|
|
228
|
+
const n = command.length;
|
|
229
|
+
const segments = [];
|
|
230
|
+
const heredocs = [];
|
|
231
|
+
let words = [];
|
|
232
|
+
let word = "";
|
|
233
|
+
let inWord = false;
|
|
234
|
+
const endWord = () => {
|
|
235
|
+
if (inWord)
|
|
236
|
+
words.push(word);
|
|
237
|
+
word = "";
|
|
238
|
+
inWord = false;
|
|
239
|
+
};
|
|
240
|
+
const endSegment = () => {
|
|
241
|
+
endWord();
|
|
242
|
+
segments.push(words);
|
|
243
|
+
words = [];
|
|
244
|
+
};
|
|
245
|
+
for (let i = 0; i < n; i += 1) {
|
|
246
|
+
const c = command[i];
|
|
247
|
+
if (c === "'") {
|
|
248
|
+
const close = command.indexOf("'", i + 1);
|
|
249
|
+
if (close === -1)
|
|
250
|
+
return null;
|
|
251
|
+
word += command.slice(i + 1, close);
|
|
252
|
+
inWord = true;
|
|
253
|
+
i = close;
|
|
254
|
+
}
|
|
255
|
+
else if (c === '"') {
|
|
256
|
+
let j = i + 1;
|
|
257
|
+
for (; j < n && command[j] !== '"'; j += 1) {
|
|
258
|
+
const d = command[j];
|
|
259
|
+
if (d === "`" || (d === "$" && command[j + 1] === "("))
|
|
260
|
+
return null;
|
|
261
|
+
if (d === "\\" && j + 1 < n) {
|
|
262
|
+
j += 1;
|
|
263
|
+
// Inside double quotes a backslash escapes only these; before anything else it stays.
|
|
264
|
+
if (!'"\$`\n'.includes(command[j]))
|
|
265
|
+
word += "\\";
|
|
266
|
+
}
|
|
267
|
+
word += command[j];
|
|
268
|
+
}
|
|
269
|
+
if (j >= n)
|
|
270
|
+
return null;
|
|
271
|
+
inWord = true;
|
|
272
|
+
i = j;
|
|
273
|
+
}
|
|
274
|
+
else if (c === "`" || (c === "$" && command[i + 1] === "(")) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
else if (c === "$" && command[i + 1] === "{") {
|
|
278
|
+
// `${NAME}` only reads a variable; a substitution inside it was refused above.
|
|
279
|
+
const close = command.indexOf("}", i + 2);
|
|
280
|
+
if (close === -1)
|
|
281
|
+
return null;
|
|
282
|
+
const inner = command.slice(i + 2, close);
|
|
283
|
+
if (/`|\$\(/.test(inner))
|
|
284
|
+
return null;
|
|
285
|
+
word += command.slice(i, close + 1);
|
|
286
|
+
inWord = true;
|
|
287
|
+
i = close;
|
|
288
|
+
}
|
|
289
|
+
else if (c === "\\") {
|
|
290
|
+
// A backslash-newline joins lines; any other escaped character is itself.
|
|
291
|
+
if (command[i + 1] === "\n") {
|
|
292
|
+
i += 1;
|
|
293
|
+
}
|
|
294
|
+
else if (command[i + 1] === "\r" && command[i + 2] === "\n") {
|
|
295
|
+
i += 2;
|
|
296
|
+
}
|
|
297
|
+
else if (i + 1 < n) {
|
|
298
|
+
word += command[i + 1];
|
|
299
|
+
inWord = true;
|
|
300
|
+
i += 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else if (c === "<" && command[i + 1] === "<" && command[i + 2] === "<") {
|
|
304
|
+
// A here-string: one word of data, read as the words that follow.
|
|
305
|
+
word += "<<<";
|
|
306
|
+
inWord = true;
|
|
307
|
+
i += 2;
|
|
308
|
+
}
|
|
309
|
+
else if (c === "<" && command[i + 1] === "<") {
|
|
310
|
+
endWord();
|
|
311
|
+
let j = i + 2;
|
|
312
|
+
const stripTabs = command[j] === "-";
|
|
313
|
+
if (stripTabs)
|
|
314
|
+
j += 1;
|
|
315
|
+
while (command[j] === " " || command[j] === "\t")
|
|
316
|
+
j += 1;
|
|
317
|
+
let delimiter = "";
|
|
318
|
+
let literal = false;
|
|
319
|
+
for (; j < n; j += 1) {
|
|
320
|
+
const d = command[j];
|
|
321
|
+
if (d === "'" || d === '"') {
|
|
322
|
+
const close = command.indexOf(d, j + 1);
|
|
323
|
+
if (close === -1)
|
|
324
|
+
return null;
|
|
325
|
+
delimiter += command.slice(j + 1, close);
|
|
326
|
+
literal = true;
|
|
327
|
+
j = close;
|
|
328
|
+
}
|
|
329
|
+
else if (d === "\\" && j + 1 < n) {
|
|
330
|
+
delimiter += command[j + 1];
|
|
331
|
+
literal = true;
|
|
332
|
+
j += 1;
|
|
333
|
+
}
|
|
334
|
+
else if (/[\s;&|<>(){}]/.test(d)) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
delimiter += d;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!delimiter)
|
|
342
|
+
return null;
|
|
343
|
+
heredocs.push({ delimiter, literal, stripTabs });
|
|
344
|
+
words.push("<<", delimiter);
|
|
345
|
+
i = j - 1;
|
|
346
|
+
}
|
|
347
|
+
else if (c === "\n") {
|
|
348
|
+
endSegment();
|
|
349
|
+
// The bodies of the heredocs this line opened come next, in order.
|
|
350
|
+
while (heredocs.length > 0) {
|
|
351
|
+
const h = heredocs.shift();
|
|
352
|
+
let closed = false;
|
|
353
|
+
for (let pos = i + 1; pos <= n;) {
|
|
354
|
+
let eol = command.indexOf("\n", pos);
|
|
355
|
+
if (eol === -1)
|
|
356
|
+
eol = n;
|
|
357
|
+
let line = command.slice(pos, eol);
|
|
358
|
+
if (line.endsWith("\r"))
|
|
359
|
+
line = line.slice(0, -1);
|
|
360
|
+
if ((h.stripTabs ? line.replace(/^\t+/, "") : line) === h.delimiter) {
|
|
361
|
+
i = eol;
|
|
362
|
+
closed = true;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (!h.literal && /`|\$\(/.test(line))
|
|
366
|
+
return null;
|
|
367
|
+
pos = eol + 1;
|
|
368
|
+
}
|
|
369
|
+
if (!closed)
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (c === ";" || c === "|") {
|
|
374
|
+
endSegment();
|
|
375
|
+
}
|
|
376
|
+
else if (c === "&") {
|
|
377
|
+
// `2>&1` and `&>file` redirect; every other `&` ends a command.
|
|
378
|
+
if (command[i - 1] === ">" || command[i - 1] === "<" || command[i + 1] === ">") {
|
|
379
|
+
word += c;
|
|
380
|
+
inWord = true;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
endSegment();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else if (c === "(" || c === ")" || c === "{" || c === "}") {
|
|
387
|
+
// A subshell, a group or a process substitution: not something this reads.
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
else if (c === " " || c === "\t" || c === "\r") {
|
|
391
|
+
endWord();
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
word += c;
|
|
395
|
+
inWord = true;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// A heredoc whose body never came.
|
|
399
|
+
if (heredocs.length > 0)
|
|
400
|
+
return null;
|
|
401
|
+
endSegment();
|
|
402
|
+
return segments;
|
|
403
|
+
}
|
|
404
|
+
/** `cd` or `cd <dir>`, and nothing more. */
|
|
405
|
+
function isPlainCd(words) {
|
|
406
|
+
return words[0] === "cd" && words.length <= 2 && !words.slice(1).some(isRedirect);
|
|
407
|
+
}
|
|
408
|
+
const isRedirect = (w) => /^\d*[<>]|^&>/.test(w);
|
|
409
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
410
|
+
/** The last path component, on either separator. */
|
|
411
|
+
const basename = (w) => w.slice(Math.max(w.lastIndexOf("/"), w.lastIndexOf("\\")) + 1);
|
|
412
|
+
/** `bir`, `bir.cmd`, `bir.ps1`, or any path to one of those or to `bir.js`. */
|
|
413
|
+
function isBirProgram(w) {
|
|
414
|
+
const base = basename(w);
|
|
415
|
+
if (/^bir(\.cmd|\.ps1)?$/i.test(base))
|
|
416
|
+
return true;
|
|
417
|
+
return /^bir\.js$/i.test(base) && base !== w;
|
|
418
|
+
}
|
|
419
|
+
const isNode = (w) => /^node(\.exe)?$/i.test(basename(w));
|
|
420
|
+
/** `@basein/runner`, `@basein/runner@0.2.11`, `@basein/runner@latest`. */
|
|
421
|
+
const isRunnerPackage = (w) => /^@basein\/runner(@[\w.+-]+)?$/.test(w);
|
|
422
|
+
function isBirInvocation(input) {
|
|
423
|
+
let words = input;
|
|
424
|
+
while (words.length > 0 && ASSIGNMENT.test(words[0]))
|
|
425
|
+
words = words.slice(1);
|
|
426
|
+
const first = words[0];
|
|
427
|
+
if (first === undefined)
|
|
428
|
+
return false;
|
|
429
|
+
if (isBirProgram(first))
|
|
430
|
+
return true;
|
|
431
|
+
if (isNode(first)) {
|
|
432
|
+
// Exactly `node <path>/bir.js …`: a node flag could load other code first.
|
|
433
|
+
const script = words[1];
|
|
434
|
+
return script !== undefined && /^bir\.js$/i.test(basename(script));
|
|
435
|
+
}
|
|
436
|
+
if (first === "npx") {
|
|
437
|
+
let i = 1;
|
|
438
|
+
for (; i < words.length; i += 1) {
|
|
439
|
+
const w = words[i];
|
|
440
|
+
if (w === "-y" || w === "--yes")
|
|
441
|
+
continue;
|
|
442
|
+
if ((w === "-p" || w === "--package") && isRunnerPackage(words[i + 1] ?? "")) {
|
|
443
|
+
i += 1;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (w.startsWith("--package=") && isRunnerPackage(w.slice("--package=".length)))
|
|
447
|
+
continue;
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
if (isRunnerPackage(words[i] ?? ""))
|
|
451
|
+
i += 1;
|
|
452
|
+
return words[i] === "bir";
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
39
456
|
//# sourceMappingURL=housekeeping.js.map
|
|
@@ -16,6 +16,17 @@ export interface RunMetrics {
|
|
|
16
16
|
costUsd?: number;
|
|
17
17
|
durationMs?: number;
|
|
18
18
|
}
|
|
19
|
+
/** How long a caller is prepared to wait for {@link Recorder.flush}. */
|
|
20
|
+
export interface FlushOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Stop waiting after this long; the sends carry on in the background. A
|
|
23
|
+
* recorder picks a bounded default when this is absent (RemoteRecorder: 2 s,
|
|
24
|
+
* `BIR_FLUSH_TIMEOUT_MS`), because a flush behind a dead service must never
|
|
25
|
+
* be what holds a hook or a shutdown — and it stops waiting sooner, at once,
|
|
26
|
+
* when the service is known to be failing (the health verdict's cool-down).
|
|
27
|
+
*/
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
19
30
|
export interface Recorder {
|
|
20
31
|
/**
|
|
21
32
|
* Create the run; returns the client-generated runId synchronously.
|
|
@@ -57,9 +68,39 @@ export interface Recorder {
|
|
|
57
68
|
answer: string;
|
|
58
69
|
}): string;
|
|
59
70
|
finishRun(runId: string, finalOutput?: string, metrics?: RunMetrics): void;
|
|
60
|
-
/**
|
|
61
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Await every queued send for a run. Called once at session end. Bounded:
|
|
73
|
+
* see {@link FlushOptions}.
|
|
74
|
+
*/
|
|
75
|
+
flush(runId: string, opts?: FlushOptions): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* What a recorder can say about its own sending, for `/health` and
|
|
79
|
+
* `bir doctor`. The queue a hook server keeps drains instantly; the real
|
|
80
|
+
* backlog is here, so this is where "lossy" is actually known.
|
|
81
|
+
*/
|
|
82
|
+
export interface RecorderHealth {
|
|
83
|
+
/**
|
|
84
|
+
* `recording`, or why nothing is being sent: `signed_out` (credentials.json
|
|
85
|
+
* was removed — `bir logout`), `auth_rejected` (the service refused the
|
|
86
|
+
* session and there was nothing newer to adopt), `other_service` (the
|
|
87
|
+
* credentials now belong to a different service address).
|
|
88
|
+
*/
|
|
89
|
+
state: "recording" | "signed_out" | "auth_rejected" | "other_service";
|
|
90
|
+
/** Sends queued and not yet started. */
|
|
91
|
+
pendingSends: number;
|
|
92
|
+
/** Their approximate size. */
|
|
93
|
+
pendingBytes: number;
|
|
94
|
+
/** Sends dropped since start: the backlog bound, a signed-out session, a run the service never created. */
|
|
95
|
+
droppedSends: number;
|
|
96
|
+
/** Sends that were tried and failed since start. */
|
|
97
|
+
failedSends: number;
|
|
98
|
+
}
|
|
99
|
+
/** Optional capability: a recorder that knows its own backlog. */
|
|
100
|
+
export interface RecorderHealthReporter {
|
|
101
|
+
recorderHealth(): RecorderHealth;
|
|
62
102
|
}
|
|
103
|
+
export declare function isRecorderHealthReporter(r: Recorder): r is Recorder & RecorderHealthReporter;
|
|
63
104
|
/**
|
|
64
105
|
* A similar-prompt hit reported by `POST /recordings/runs`. On a hit the server
|
|
65
106
|
* does **not** create a run, so every later step post for that id would 404 —
|
|
@@ -200,9 +241,16 @@ export interface IntentMatchAnswer {
|
|
|
200
241
|
bestStepSimilarity?: number;
|
|
201
242
|
segmentWouldArm?: SegmentWouldArm;
|
|
202
243
|
}
|
|
244
|
+
/** How long a probe's caller will wait; the request is abandoned past it. */
|
|
245
|
+
export interface IntentMatchOptions {
|
|
246
|
+
/** Abort the request (and free its socket) when the caller stops waiting. */
|
|
247
|
+
signal?: AbortSignal;
|
|
248
|
+
/** A deadline for the request itself. A recorder picks a short default. */
|
|
249
|
+
timeoutMs?: number;
|
|
250
|
+
}
|
|
203
251
|
/** Optional capability: intent matching needs a service. */
|
|
204
252
|
export interface IntentMatcher {
|
|
205
|
-
matchIntent(req: IntentMatchRequest): Promise<IntentMatchAnswer | null>;
|
|
253
|
+
matchIntent(req: IntentMatchRequest, opts?: IntentMatchOptions): Promise<IntentMatchAnswer | null>;
|
|
206
254
|
}
|
|
207
255
|
export declare function isIntentMatcher(r: Recorder): r is Recorder & IntentMatcher;
|
|
208
256
|
/**
|
package/dist/record/recorder.js
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* service needs no new endpoints for v1: a BaseInstRunner run is the same row
|
|
6
6
|
* shape as an RRepeat one, discriminated only by `metadata.recorder`.
|
|
7
7
|
*/
|
|
8
|
+
export function isRecorderHealthReporter(r) {
|
|
9
|
+
return typeof r.recorderHealth === "function";
|
|
10
|
+
}
|
|
8
11
|
export function isMatchAware(r) {
|
|
9
12
|
return typeof r.getMatch === "function";
|
|
10
13
|
}
|