@basein/runner 0.3.8 → 0.3.10
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 +46 -5
- package/dist/auth/client.d.ts +15 -0
- package/dist/auth/client.js +34 -12
- package/dist/bin/bir-hooks.d.ts +1 -1
- package/dist/bin/bir-hooks.js +1 -1
- package/dist/bin/bir.d.ts +2 -0
- package/dist/bin/bir.js +60 -6
- package/dist/bin/investigate.d.ts +5 -0
- package/dist/bin/investigate.js +117 -15
- package/dist/bin/setup.d.ts +12 -0
- package/dist/bin/setup.js +185 -56
- package/dist/bin/update.d.ts +81 -0
- package/dist/bin/update.js +194 -0
- package/dist/config/adapters/claude-code.d.ts +17 -0
- package/dist/config/adapters/claude-code.js +28 -1
- package/dist/config/generate.d.ts +14 -0
- package/dist/control/daemon.d.ts +26 -1
- package/dist/control/daemon.js +53 -3
- package/dist/control/pin-guard.d.ts +6 -1
- package/dist/control/pin-guard.js +15 -1
- package/dist/control/recorder-keeper.js +2 -1
- package/dist/control/server.d.ts +200 -2
- package/dist/control/server.js +741 -48
- package/dist/control/transcript.d.ts +4 -0
- package/dist/control/transcript.js +4 -0
- package/dist/proxy/session.js +1 -0
- package/dist/record/housekeeping.d.ts +7 -1
- package/dist/record/housekeeping.js +11 -3
- package/dist/record/recorder.d.ts +34 -0
- package/dist/record/remote-recorder.d.ts +14 -0
- package/dist/record/remote-recorder.js +107 -13
- package/dist/record/switchable-recorder.js +1 -0
- package/dist/record/truncate.d.ts +11 -0
- package/dist/record/truncate.js +11 -0
- package/dist/replay/controller.d.ts +146 -4
- package/dist/replay/controller.js +340 -31
- package/dist/replay/derive.js +2 -0
- package/dist/replay/handover.d.ts +13 -1
- package/dist/replay/handover.js +19 -0
- package/dist/replay/plan.d.ts +6 -0
- package/dist/replay/plan.js +13 -3
- package/dist/replay/safety.d.ts +15 -4
- package/dist/replay/safety.js +19 -6
- package/dist/replay/source-run.js +2 -1
- package/dist/replay/types.d.ts +16 -0
- package/dist/util/service-headers.d.ts +24 -0
- package/dist/util/service-headers.js +47 -0
- package/docs/BaseInstRunner.md +115 -1
- package/docs/calculatedReplay.md +239 -27
- package/docs/calculatedReplayGuide.md +134 -12
- package/docs/installRun.md +91 -10
- package/docs/quickstart.md +64 -9
- package/package.json +1 -1
- package/production.md +81 -16
- package/scripts/install.ps1 +25 -1
package/README.md
CHANGED
|
@@ -59,7 +59,9 @@ cd the-project-you-record-from
|
|
|
59
59
|
bir setup --auth-url https://api.bi2202.com # everything below, in one go
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
or the pieces `bir setup` is made of
|
|
62
|
+
or the pieces `bir setup` is made of (on a machine with no stored address yet,
|
|
63
|
+
`bir login` needs `BIR_AUTH_URL=https://api.bi2202.com` set: it does not read
|
|
64
|
+
`--auth-url`):
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
bir login # once; approve it in the browser (or --token <t> from the console)
|
|
@@ -152,7 +154,11 @@ work through (not recording, a proxy not registered yet, the service
|
|
|
152
154
|
unreachable): exit 0, or 1 with `--strict`. `--quick` does not call the service.
|
|
153
155
|
`bir status` also shows the recorder's view of the service, the off switch, a
|
|
154
156
|
port the recorder could not take, a STALE local copy, and the other projects set
|
|
155
|
-
up on this machine.
|
|
157
|
+
up on this machine. In a folder set up for another host
|
|
158
|
+
([A LangChain application](#a-langchain-application)) both add a `Host` line:
|
|
159
|
+
it names the host and says that no Claude Code hooks are needed there, or, in
|
|
160
|
+
a folder set up for both, names Claude Code too. `bir status --json` carries
|
|
161
|
+
`hosts`, and `bir doctor` does not report missing hooks in such a folder.
|
|
156
162
|
|
|
157
163
|
### Any other MCP client
|
|
158
164
|
|
|
@@ -164,6 +170,37 @@ bir install --config /path/to/that/client/mcp.json
|
|
|
164
170
|
bir wrap --server-name my-server -- node my-server.js # prints an entry to paste
|
|
165
171
|
```
|
|
166
172
|
|
|
173
|
+
### A LangChain application
|
|
174
|
+
|
|
175
|
+
A Python program whose agent is built with LangChain's `create_agent` is set up
|
|
176
|
+
in another way: nothing of Claude Code is wired. In the folder the application
|
|
177
|
+
starts in:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
bir setup --host langchain # a machine with no stored address: add --auth-url https://api.bi2202.com
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
It signs in, gives the folder an entry of its own in
|
|
184
|
+
`~/.baseinstrunner/installed.json` — a fixed port, a token, and replay on
|
|
185
|
+
(`--no-replay` stores it off; `bir replay on` / `off` work there as anywhere) —
|
|
186
|
+
starts the recorder in the background, and prints the two lines of Python to
|
|
187
|
+
add to the agent. It writes nothing under `.claude/` and nothing into
|
|
188
|
+
`~/.claude.json`: no hooks, no wrapped servers, no `bir` server. A folder
|
|
189
|
+
already set up for Claude Code keeps that set-up as it is. While a folder is
|
|
190
|
+
not set up for Claude Code as well, its recorder is started with a small
|
|
191
|
+
environment — `PATH`, the home and temp folders, what the system needs to
|
|
192
|
+
start a process, the proxy and certificate settings, and the `BIR_*`
|
|
193
|
+
variables — so the application's keys and passwords are not in the process that
|
|
194
|
+
runs scenario code unsandboxed ([What it will not do](#what-it-will-not-do)).
|
|
195
|
+
A runner that does not know `--host` (0.3.9 and older) prints its usage and
|
|
196
|
+
exits 2; it sets nothing up.
|
|
197
|
+
|
|
198
|
+
Those two lines load a LangChain middleware from the Python package
|
|
199
|
+
`langchain-basic2202`, which reports every call to the recorder. That package
|
|
200
|
+
and the service's LangChain part are needed too, and neither is out yet. The
|
|
201
|
+
how-to is [langChainGuide.md](langChainGuide.md) in the repository's root; it
|
|
202
|
+
is not in the npm package.
|
|
203
|
+
|
|
167
204
|
### Taking it out
|
|
168
205
|
|
|
169
206
|
```bash
|
|
@@ -177,7 +214,9 @@ leaves it running switched off for this project: restart that session, then
|
|
|
177
214
|
`bir down` (`--keep-recorder` leaves the recorder alone). While anything here
|
|
178
215
|
still runs through the runner it exits 1 with `Not everything could be undone`
|
|
179
216
|
and a list; fix what it names and run it again. `--server <name>` takes out only
|
|
180
|
-
that server, and `--replay` only the scenario server, switching replay off.
|
|
217
|
+
that server, and `--replay` only the scenario server, switching replay off. In
|
|
218
|
+
a folder set up for another host it removes the folder's entry — its port,
|
|
219
|
+
token and replay switch — and stops its recorder.
|
|
181
220
|
|
|
182
221
|
Remove the package (`npm rm -g @basein/runner`, or delete `~/.baseinstrunner`,
|
|
183
222
|
where the install line's copy lives) only after `bir uninstall --all` has
|
|
@@ -244,7 +283,8 @@ between the two is not guaranteed.
|
|
|
244
283
|
else. Calculated replay — running a matched prompt's scenario instead of
|
|
245
284
|
rediscovering it — ships, but needs `bir install --replay`, which `bir setup`
|
|
246
285
|
runs unless given `--no-replay`. Once installed it is on; `bir replay off` or
|
|
247
|
-
`BIR_REPLAY=0` turns it off
|
|
286
|
+
`BIR_REPLAY=0` turns it off from the recorder's next start
|
|
287
|
+
(`bir up --restart`), for the reason in the next section.
|
|
248
288
|
- **Change what the model does.** Tool descriptions are never edited. The
|
|
249
289
|
model-visible changes are one declared schema key (below), the first-party
|
|
250
290
|
`bir` server's `run_scenario` (only with `--replay`), and replay's own
|
|
@@ -448,7 +488,7 @@ what to do when they register, so there is one switch and no second copy of it:
|
|
|
448
488
|
| `ANTHROPIC_API_KEY` | Not used by the background recorder, which has Claude Code's environment and would bill your own key — unless `BIR_USE_ANTHROPIC_API_KEY=1`. A `bir-hooks` run by hand in a terminal uses that terminal's key. |
|
|
449
489
|
| `BIR_DERIVE_MODEL` | Derivation model, when you set a key of your own (default `claude-haiku-4-5-20251001`). |
|
|
450
490
|
| `BIR_MATCH_BUDGET_MS` | How long the prompt hook waits for a match (default `3000`). After a match slower than `BIR_MATCH_SLOW_MS` (default `2000`), or one that timed out, prompts stop waiting for 5 min, doubling up to 30 min; a fast match ends that. |
|
|
451
|
-
| `BIR_DERIVE_BUDGET_MS` / `BIR_HOOK_DERIVE_BUDGET_MS` | How long a direct run, and a hook, waits for parameters (default `8000` / `
|
|
491
|
+
| `BIR_DERIVE_BUDGET_MS` / `BIR_HOOK_DERIVE_BUDGET_MS` | How long a direct run, and a hook, waits for parameters (default `8000` / `8000`; a hook waits the smaller, and the prompt hook never past its own 6 s). |
|
|
452
492
|
| `BIR_PRETOOL_REPLAY_BUDGET_MS` | Everything replay does inside one `PreToolUse` (default `9000`, at most `10000`). |
|
|
453
493
|
| `BIR_REPLAY_BUDGET_MS` / `BIR_STEP_TIMEOUT_MS` | Whole-plan and per-step ceilings for direct execution (default `100000` / `60000`; `BIR_REPLAY_PLAN_BUDGET_MS` also sets the first). A direct run's parameter wait plus its plan are held under 270 s. |
|
|
454
494
|
| `BIR_LOGIC_TIMEOUT_MS` | How long one scenario logic body may run before it is stopped and the plan hands over (default `1000`, at most `10000`). |
|
|
@@ -487,6 +527,7 @@ The governing rule: **a host session never fails because of BaseInstRunner.**
|
|
|
487
527
|
| Control server dies mid-session | the proxy records to a Tier 2 run of its own (flagged lossy), starts the recorder again in the background (not after `bir down`), and binds to it once it answers |
|
|
488
528
|
| A hook the recorder cannot answer in time | every route has a hard deadline (prompt 7 s, `PreToolUse` 12 s, the others 1–2 s) and answers `{}`; so does a hook with a stale token or a malformed body |
|
|
489
529
|
| BaseIn service slow or down | hooks stop waiting on it after one failure (a cool-down of 5 s, doubling to 60 s); sends go on in the background, retried once only when they cannot have arrived (429, 503, a refused connection); past 500 queued sends or 32 MiB the oldest steps are dropped |
|
|
530
|
+
| BaseIn service answers `429`: the account is over one of its limits | the send — a run's create, a step, a report — goes once more, at most 2 s later, and is then dropped, with `recorder.send_failed … HTTP 429` on the log (one line per run). A dropped step is a hole in its recording; a run whose create was refused runs without its plan; a `429` the second try did not cure starts the cool-down above. The limits are settings of the service, per account — by default 120 runs and 3,000 steps a minute (a tool call is two steps), 10 title requests a minute, and 20 Calculate, Prefer and Recalculate requests in any one hour — and are built there, not deployed yet |
|
|
490
531
|
| Upstream fails to start, or exits | calls in flight are answered (`MCP server "<name>" is not available`, `isError: true` for a `tools/call`), then `bir-proxy` exits with the server's own status, so Claude Code shows the server as failed, as it would unwrapped. `BIR_PROXY_STAY_UP=1` keeps the proxy up answering that instead |
|
|
491
532
|
| Remote upstream unreachable or refusing | the request gets a short error (`upstream unreachable: …`, `upstream requires sign-in (HTTP 401)`), never the server's body; only connecting is bounded (10 s), never how long a tool runs |
|
|
492
533
|
| Result over 128 MiB | the call ends with an error saying the result was too large to deliver, instead of hanging |
|
package/dist/auth/client.d.ts
CHANGED
|
@@ -197,6 +197,21 @@ export declare const AUTH_URL_HINT: string;
|
|
|
197
197
|
* strict about the content type, which is what separates a service from a page.
|
|
198
198
|
*/
|
|
199
199
|
export declare function describeAuthService(baseUrl: string, fetchImpl?: typeof fetch): Promise<string | undefined>;
|
|
200
|
+
/** What `GET /health` said about the service at an address. */
|
|
201
|
+
export interface AuthServiceView {
|
|
202
|
+
/** One line saying what answered instead of a BaseIn service; absent when one did. */
|
|
203
|
+
problem?: string;
|
|
204
|
+
/** The newest contract the service speaks (util/service-headers.ts). Absent from an older service. */
|
|
205
|
+
contract?: number;
|
|
206
|
+
/** The lowest contract a runner needs to be handed a plan. Absent from an older service. */
|
|
207
|
+
minRunner?: number;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* {@link describeAuthService}, plus the two numbers a service puts on its
|
|
211
|
+
* `/health` so a runner can tell that it is too old to be handed a plan
|
|
212
|
+
* (`bir doctor` says so). An answer without them is an older service.
|
|
213
|
+
*/
|
|
214
|
+
export declare function inspectAuthService(baseUrl: string, fetchImpl?: typeof fetch): Promise<AuthServiceView>;
|
|
200
215
|
/**
|
|
201
216
|
* Authenticate and return a valid session, or `undefined` when none can be
|
|
202
217
|
* obtained. Callers degrade to not-recording rather than failing the session.
|
package/dist/auth/client.js
CHANGED
|
@@ -46,6 +46,7 @@ import { configDir, configPath, ensureDir } from "../control/paths.js";
|
|
|
46
46
|
import { join } from "node:path";
|
|
47
47
|
import { logLine, errText } from "../util/log.js";
|
|
48
48
|
import { packageVersion } from "../util/version.js";
|
|
49
|
+
import { serviceHeaders } from "../util/service-headers.js";
|
|
49
50
|
import { writeFileAtomic } from "../util/atomic-write.js";
|
|
50
51
|
import { isUnhealthyStatus, serviceHealth, serviceTimeoutMs, timeoutSignal } from "../util/service-health.js";
|
|
51
52
|
/** A non-2xx answer from the service, with its status. */
|
|
@@ -301,6 +302,7 @@ export async function normalizeAuthUrl(baseUrl, fetchImpl = fetch) {
|
|
|
301
302
|
// timeout (10-20 s) and a host that accepts and never answers cost 300 s.
|
|
302
303
|
res = await fetchImpl(`${base}/health`, {
|
|
303
304
|
method: "GET",
|
|
305
|
+
headers: serviceHeaders(),
|
|
304
306
|
redirect: "manual",
|
|
305
307
|
signal: timeoutSignal(PROBE_TIMEOUT_MS),
|
|
306
308
|
});
|
|
@@ -375,9 +377,18 @@ function causeText(err) {
|
|
|
375
377
|
* strict about the content type, which is what separates a service from a page.
|
|
376
378
|
*/
|
|
377
379
|
export async function describeAuthService(baseUrl, fetchImpl = fetch) {
|
|
380
|
+
return (await inspectAuthService(baseUrl, fetchImpl)).problem;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* {@link describeAuthService}, plus the two numbers a service puts on its
|
|
384
|
+
* `/health` so a runner can tell that it is too old to be handed a plan
|
|
385
|
+
* (`bir doctor` says so). An answer without them is an older service.
|
|
386
|
+
*/
|
|
387
|
+
export async function inspectAuthService(baseUrl, fetchImpl = fetch) {
|
|
388
|
+
const problem = (text) => ({ problem: text });
|
|
378
389
|
const base = baseUrl.replace(/\/+$/, "");
|
|
379
390
|
if (!base)
|
|
380
|
-
return "no URL configured";
|
|
391
|
+
return problem("no URL configured");
|
|
381
392
|
// Not reported to the health verdict, unlike every other request here: this
|
|
382
393
|
// is asked of *candidate* addresses (a website someone pasted, a typo), and
|
|
383
394
|
// their failure says nothing about the service the runner actually uses.
|
|
@@ -385,26 +396,37 @@ export async function describeAuthService(baseUrl, fetchImpl = fetch) {
|
|
|
385
396
|
try {
|
|
386
397
|
res = await fetchImpl(`${base}/health`, {
|
|
387
398
|
method: "GET",
|
|
388
|
-
headers: { accept: "application/json" },
|
|
399
|
+
headers: { accept: "application/json", ...serviceHeaders() },
|
|
389
400
|
redirect: "manual",
|
|
390
401
|
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
391
402
|
});
|
|
392
403
|
}
|
|
393
404
|
catch (err) {
|
|
394
|
-
return `unreachable (${causeText(err)})
|
|
405
|
+
return problem(`unreachable (${causeText(err)})`);
|
|
395
406
|
}
|
|
396
407
|
if (res.status >= 300 && res.status < 400) {
|
|
397
|
-
return `redirects to ${res.headers.get("location") ?? "somewhere else"} — use the final address
|
|
408
|
+
return problem(`redirects to ${res.headers.get("location") ?? "somewhere else"} — use the final address`);
|
|
398
409
|
}
|
|
399
410
|
const type = (res.headers.get("content-type") ?? "").toLowerCase();
|
|
400
411
|
if (!res.ok) {
|
|
401
|
-
return `GET /health answered HTTP ${res.status}${type.includes("html") ? " with a web page" : ""}
|
|
412
|
+
return problem(`GET /health answered HTTP ${res.status}${type.includes("html") ? " with a web page" : ""}`);
|
|
402
413
|
}
|
|
403
414
|
if (!type.includes("json")) {
|
|
404
415
|
const what = type.includes("html") ? "a web page" : type ? `'${type}'` : "no content type";
|
|
405
|
-
return `GET /health answered ${what} instead of JSON — that is a website, not the BaseIn API
|
|
416
|
+
return problem(`GET /health answered ${what} instead of JSON — that is a website, not the BaseIn API`);
|
|
406
417
|
}
|
|
407
|
-
|
|
418
|
+
// Still loose about the body: one that cannot be read, or has neither
|
|
419
|
+
// number, is a service that answers — an older one.
|
|
420
|
+
let body;
|
|
421
|
+
try {
|
|
422
|
+
body = JSON.parse(await res.text());
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
const b = (body && typeof body === "object" ? body : {});
|
|
428
|
+
const count = (v) => (typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : undefined);
|
|
429
|
+
return { contract: count(b.contract), minRunner: count(b.minRunner) };
|
|
408
430
|
}
|
|
409
431
|
function prompt(question) {
|
|
410
432
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -463,7 +485,7 @@ async function postJson(url, body) {
|
|
|
463
485
|
// seconds, not minutes.
|
|
464
486
|
res = await fetch(url, {
|
|
465
487
|
method: "POST",
|
|
466
|
-
headers: { "content-type": "application/json" },
|
|
488
|
+
headers: { "content-type": "application/json", ...serviceHeaders() },
|
|
467
489
|
body: JSON.stringify(body),
|
|
468
490
|
signal: timeoutSignal(serviceTimeoutMs()),
|
|
469
491
|
});
|
|
@@ -604,7 +626,7 @@ export async function startDeviceCode(authUrl, client, fetchImpl = fetch) {
|
|
|
604
626
|
try {
|
|
605
627
|
res = await fetchImpl(`${authUrl}/auth/device/code`, {
|
|
606
628
|
method: "POST",
|
|
607
|
-
headers: { "content-type": "application/json" },
|
|
629
|
+
headers: { "content-type": "application/json", ...serviceHeaders() },
|
|
608
630
|
body: JSON.stringify({ clientName: client.name, clientVersion: client.version }),
|
|
609
631
|
signal: timeoutSignal(serviceTimeoutMs()),
|
|
610
632
|
});
|
|
@@ -666,7 +688,7 @@ export async function pollDeviceToken(authUrl, deviceCode, opts) {
|
|
|
666
688
|
// the request in flight rather than after the service answers it.
|
|
667
689
|
res = await fetchImpl(`${authUrl}/auth/device/token`, {
|
|
668
690
|
method: "POST",
|
|
669
|
-
headers: { "content-type": "application/json" },
|
|
691
|
+
headers: { "content-type": "application/json", ...serviceHeaders() },
|
|
670
692
|
body: JSON.stringify({ deviceCode }),
|
|
671
693
|
signal: timeoutSignal(serviceTimeoutMs(), opts.signal),
|
|
672
694
|
});
|
|
@@ -832,7 +854,7 @@ export async function tokenLogin(authUrl, token, opts = {}) {
|
|
|
832
854
|
try {
|
|
833
855
|
res = await fetchImpl(`${authUrl}/auth/device/token`, {
|
|
834
856
|
method: "POST",
|
|
835
|
-
headers: { "content-type": "application/json" },
|
|
857
|
+
headers: { "content-type": "application/json", ...serviceHeaders() },
|
|
836
858
|
body: JSON.stringify({ deviceCode: token.trim() }),
|
|
837
859
|
signal: timeoutSignal(serviceTimeoutMs()),
|
|
838
860
|
});
|
|
@@ -942,7 +964,7 @@ export async function logout(authUrl) {
|
|
|
942
964
|
try {
|
|
943
965
|
const res = await fetch(`${authUrl}/auth/logout`, {
|
|
944
966
|
method: "POST",
|
|
945
|
-
headers: { "content-type": "application/json" },
|
|
967
|
+
headers: { "content-type": "application/json", ...serviceHeaders() },
|
|
946
968
|
body: JSON.stringify({ refreshToken: cached.refreshToken }),
|
|
947
969
|
signal: timeoutSignal(LOGOUT_TIMEOUT_MS),
|
|
948
970
|
});
|
package/dist/bin/bir-hooks.d.ts
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
* BIR_MATCH_BUDGET_MS prompt-hook match wait (default 3000)
|
|
92
92
|
* BIR_DERIVE_BUDGET_MS derivation wait before a direct run (default 8000);
|
|
93
93
|
* inside a hook it is at most BIR_HOOK_DERIVE_BUDGET_MS
|
|
94
|
-
* (default
|
|
94
|
+
* (default 8000)
|
|
95
95
|
* BIR_DERIVE_RECENT_RESULTS how many of this prompt's tool results derivation
|
|
96
96
|
* reads, so a target named by an earlier step can be
|
|
97
97
|
* found (default 5; 0 off)
|
package/dist/bin/bir-hooks.js
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
* BIR_MATCH_BUDGET_MS prompt-hook match wait (default 3000)
|
|
92
92
|
* BIR_DERIVE_BUDGET_MS derivation wait before a direct run (default 8000);
|
|
93
93
|
* inside a hook it is at most BIR_HOOK_DERIVE_BUDGET_MS
|
|
94
|
-
* (default
|
|
94
|
+
* (default 8000)
|
|
95
95
|
* BIR_DERIVE_RECENT_RESULTS how many of this prompt's tool results derivation
|
|
96
96
|
* reads, so a target named by an earlier step can be
|
|
97
97
|
* found (default 5; 0 off)
|
package/dist/bin/bir.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* bir — the command line (Phase 7).
|
|
4
4
|
*
|
|
5
5
|
* bir setup [--auth-url <url>] [--token <t>] [--project <dir>] ← the one command
|
|
6
|
+
* bir setup --host langchain ← the folder of a LangChain application: an entry and a recorder, no Claude Code wiring
|
|
6
7
|
* bir up [--restart] | bir down
|
|
7
8
|
* bir off [--here] | bir on [--here] ← the kill switch: inert at once, nothing uninstalled
|
|
8
9
|
* bir install [--config <path>] [--server <name>]… [--local] [--no-hooks]
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
* bir doctor [--quick]
|
|
12
13
|
* bir wrap --server-name <key> -- <command> [args…]
|
|
13
14
|
* bir login | logout
|
|
15
|
+
* bir update ← the newest version, into the copy that is running (update.ts)
|
|
14
16
|
* bir --version
|
|
15
17
|
*
|
|
16
18
|
* bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
|
package/dist/bin/bir.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* bir — the command line (Phase 7).
|
|
4
4
|
*
|
|
5
5
|
* bir setup [--auth-url <url>] [--token <t>] [--project <dir>] ← the one command
|
|
6
|
+
* bir setup --host langchain ← the folder of a LangChain application: an entry and a recorder, no Claude Code wiring
|
|
6
7
|
* bir up [--restart] | bir down
|
|
7
8
|
* bir off [--here] | bir on [--here] ← the kill switch: inert at once, nothing uninstalled
|
|
8
9
|
* bir install [--config <path>] [--server <name>]… [--local] [--no-hooks]
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
* bir doctor [--quick]
|
|
12
13
|
* bir wrap --server-name <key> -- <command> [args…]
|
|
13
14
|
* bir login | logout
|
|
15
|
+
* bir update ← the newest version, into the copy that is running (update.ts)
|
|
14
16
|
* bir --version
|
|
15
17
|
*
|
|
16
18
|
* bir scenario list | show <runId|scnId> [--step <n>] | calc <runId|scnId> [--force [--discard-edits]]
|
|
@@ -52,11 +54,12 @@ import { claudeUserSettingsPath, managedMcpPolicyBlocker, mcpjsonApprovals } fro
|
|
|
52
54
|
import { checkOverrides, recordServerName } from "../config/overrides.js";
|
|
53
55
|
import { awaitWithin, isUnhealthyStatus, serviceHealth, serviceTimeoutMs, timeoutSignal } from "../util/service-health.js";
|
|
54
56
|
import { runSetup } from "./setup.js";
|
|
57
|
+
import { runUpdate } from "./update.js";
|
|
55
58
|
import { claudeConfigHome, claudeProjectKey, claudeProjectRoot, isRemote, linkedWorktrees, parseJsonFile, projectKeyFor, projectKeysFor, projectScope, resolveServers, } from "../config/resolve.js";
|
|
56
|
-
import { addAllowRules, allowRules, assertEditableJson, BIR_TOOLS_RULE, buildHooksBlock, claudeCodePaths, disableMcpjsonServers, enableMcpjsonServers, ENSURE_ARG, ensureGitExcludeLine, hasBirHooks, installHooks, isBirHook, LOCAL_SETTINGS_EXCLUDE, readTextOrNull, removeAllowRules, removeGitExclude, setLocalEntries, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
|
|
59
|
+
import { addAllowRules, allowRules, assertEditableJson, BIR_TOOLS_RULE, buildHooksBlock, claudeCodePaths, claudeCodeSetUp, disableMcpjsonServers, enableMcpjsonServers, ENSURE_ARG, ensureGitExcludeLine, hasBirHooks, installHooks, isBirHook, LOCAL_SETTINGS_EXCLUDE, otherHosts, readTextOrNull, removeAllowRules, removeGitExclude, setLocalEntries, setServerEntry, sha256, uninstallHooks, } from "../config/adapters/claude-code.js";
|
|
57
60
|
import { writeFileAtomic } from "../util/atomic-write.js";
|
|
58
61
|
import { readGenericServers, setGenericServerEntry } from "../config/adapters/generic.js";
|
|
59
|
-
import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, clearCredentials, deviceLogin, describeAuthService, legacyPasswordLogin, tokenLogin, normalizeAuthUrl, rememberAuthUrl, resolveAuthUrl, } from "../auth/client.js";
|
|
62
|
+
import { AUTH_URL_HINT, DeviceFlowAborted, DeviceFlowUnsupported, authenticate, clearCredentials, deviceLogin, describeAuthService, inspectAuthService, legacyPasswordLogin, tokenLogin, normalizeAuthUrl, rememberAuthUrl, resolveAuthUrl, } from "../auth/client.js";
|
|
60
63
|
import { DEFAULT_CONTROL_PORT } from "../control/server.js";
|
|
61
64
|
import { errText, logLine } from "../util/log.js";
|
|
62
65
|
import { journalPath, readJournal } from "../util/journal.js";
|
|
@@ -64,6 +67,7 @@ import { investigateCommand } from "./investigate.js";
|
|
|
64
67
|
import { SCENARIO_EDIT_SUBCOMMANDS, scenarioEditCommand } from "./scenario-edit.js";
|
|
65
68
|
import { loadCredentials } from "../auth/client.js";
|
|
66
69
|
import { packageVersion } from "../util/version.js";
|
|
70
|
+
import { runnerOutdated, serviceHeaders } from "../util/service-headers.js";
|
|
67
71
|
const VERSION = packageVersion();
|
|
68
72
|
/**
|
|
69
73
|
* The npx SessionStart hook's own budget, passed as `ensure --budget-ms <n>`
|
|
@@ -82,6 +86,8 @@ function usage(code = 0) {
|
|
|
82
86
|
Commands:
|
|
83
87
|
setup the one command: sign in, wire this project, start the recorder, and give
|
|
84
88
|
Claude Code every bir tool (run_scenario, investigate, scenario_*) with no prompt
|
|
89
|
+
(--host langchain: for a LangChain application instead — sign in, give the
|
|
90
|
+
folder its own port and token, start the recorder; nothing of Claude Code is wired)
|
|
85
91
|
up start the recorder for this project in the background (--restart: replace it)
|
|
86
92
|
down stop it (open sessions here then show a hook error until they restart;
|
|
87
93
|
\`bir off --here\` is the quiet way to take it out of the way)
|
|
@@ -99,8 +105,10 @@ Commands:
|
|
|
99
105
|
wrap print a proxied entry for one server (any MCP client)
|
|
100
106
|
login sign in to the BaseIn service (opens your browser)
|
|
101
107
|
logout revoke this machine's session and forget it
|
|
108
|
+
update install the newest version over this copy of bir, wherever it was installed
|
|
109
|
+
(the console's install line or npm); then run \`bir setup\` in each project
|
|
102
110
|
|
|
103
|
-
scenario list
|
|
111
|
+
scenario list recorded runs and their calculated scenarios
|
|
104
112
|
scenario show <runId|scnId> [--step <n>]
|
|
105
113
|
a scenario: intent, params, steps (--step: just that one)
|
|
106
114
|
scenario calc <runId|scnId> [--force [--discard-edits]]
|
|
@@ -176,6 +184,8 @@ Options:
|
|
|
176
184
|
--auth-url <url> setup: the BaseIn API address (else BIR_AUTH_URL, else the stored one)
|
|
177
185
|
--project <dir> setup: the directory to record (default: this one)
|
|
178
186
|
--no-replay setup: wire recording only; add no scenario server
|
|
187
|
+
--host <name> setup: the host the folder's agent runs in, when it is not Claude Code
|
|
188
|
+
(langchain). Writes nothing under .claude/ and nothing into ~/.claude.json
|
|
179
189
|
--allow-servers <a,b> setup: servers replay may call unattended (default: all wrapped)
|
|
180
190
|
--no-daemon setup: wire everything, start nothing
|
|
181
191
|
--restart up: replace a running recorder
|
|
@@ -292,6 +302,11 @@ function parseArgs(argv) {
|
|
|
292
302
|
case "--no-replay":
|
|
293
303
|
args.noReplay = true;
|
|
294
304
|
break;
|
|
305
|
+
case "--host":
|
|
306
|
+
args.host = (argv[++i] ?? "").trim().toLowerCase();
|
|
307
|
+
if (!args.host)
|
|
308
|
+
usage(2);
|
|
309
|
+
break;
|
|
295
310
|
case "--allow-servers":
|
|
296
311
|
args.allowServers = argv[++i];
|
|
297
312
|
if (args.allowServers === undefined)
|
|
@@ -1746,11 +1761,16 @@ function uninstallProject(cwd, sidecar, opts) {
|
|
|
1746
1761
|
const myHookFiles = (sidecar.hookFiles ?? []).filter((f) => fileProjectDir(f) === here);
|
|
1747
1762
|
const hookedNow = [paths.settingsLocal, paths.settings].filter((f) => hasBirHooks(f));
|
|
1748
1763
|
const before = runnerLeftovers(cwd, sidecar, opts.configPath, otherClaude);
|
|
1764
|
+
// A folder set up for another host (`bir setup --host langchain`) has no
|
|
1765
|
+
// hooks and no wrapped server, and still has something to undo: its entry,
|
|
1766
|
+
// and the recorder the caller stops. It goes past this point.
|
|
1767
|
+
const hostEntry = !narrow && otherHosts(cwd, sidecar).length > 0;
|
|
1749
1768
|
if (mine.length === 0 &&
|
|
1750
1769
|
myHookFiles.length === 0 &&
|
|
1751
1770
|
hookedNow.length === 0 &&
|
|
1752
1771
|
before.leftovers.length === 0 &&
|
|
1753
|
-
before.unreadable.length === 0
|
|
1772
|
+
before.unreadable.length === 0 &&
|
|
1773
|
+
!hostEntry) {
|
|
1754
1774
|
return { nothing: true, remaining: [] };
|
|
1755
1775
|
}
|
|
1756
1776
|
// 1. Byte-for-byte: a project file we wrote, nobody touched since, and whose
|
|
@@ -1946,6 +1966,9 @@ function uninstallProject(cwd, sidecar, opts) {
|
|
|
1946
1966
|
const record = projectRecord(sidecar, cwd);
|
|
1947
1967
|
if (sidecar.projects)
|
|
1948
1968
|
delete sidecar.projects[here];
|
|
1969
|
+
if (record?.hosts?.length) {
|
|
1970
|
+
out(` ← this folder's entry for ${record.hosts.join(", ")} removed (its port, token and replay switch)`);
|
|
1971
|
+
}
|
|
1949
1972
|
if (record?.excludeAdded !== false && existsSync(cwd) && !sharesRepository(cwd, sidecar)) {
|
|
1950
1973
|
const exclude = removeGitExclude(cwd);
|
|
1951
1974
|
if (exclude)
|
|
@@ -2686,8 +2709,11 @@ async function status(args) {
|
|
|
2686
2709
|
const portProblem = openPortProblem(cwd, health ? portOf(discovery?.url) : undefined, hookPorts.length === 1 ? hookPorts[0] : undefined);
|
|
2687
2710
|
// Recorders a reinstall on another port left serving older windows (LC-1).
|
|
2688
2711
|
const superseded = await supersededRecorders(projectDir);
|
|
2712
|
+
// Hosts other than Claude Code this folder was set up for (`bir setup --host`).
|
|
2713
|
+
const hosts = args.configPath ? [] : otherHosts(projectDir, sidecar);
|
|
2689
2714
|
const report = {
|
|
2690
2715
|
cwd,
|
|
2716
|
+
...(hosts.length ? { hosts } : {}),
|
|
2691
2717
|
authUrl: resolveAuthUrl() || null,
|
|
2692
2718
|
controlServer: discovery ? { url: discovery.url, pid: discovery.pid, answering: health !== undefined } : null,
|
|
2693
2719
|
discoveryFile: discoveryPath(cwd),
|
|
@@ -2719,6 +2745,11 @@ async function status(args) {
|
|
|
2719
2745
|
return 0;
|
|
2720
2746
|
}
|
|
2721
2747
|
out(`Directory : ${report.cwd}`);
|
|
2748
|
+
if (hosts.length) {
|
|
2749
|
+
out(`Host : set up for ${hosts.join(", ")}${claudeCodeSetUp(projectDir, sidecar)
|
|
2750
|
+
? ", and for Claude Code"
|
|
2751
|
+
: " — its adapter reports every call, so no Claude Code hooks or wrapped servers are needed here"}`);
|
|
2752
|
+
}
|
|
2722
2753
|
out(`BaseIn : ${report.authUrl ?? "(not set — run `bir setup`, or set BIR_AUTH_URL; nothing is recorded)"}`);
|
|
2723
2754
|
out(`Control : ${discovery
|
|
2724
2755
|
? `${discovery.url} (pid ${discovery.pid}${discovery.daemon ? "; background" : ""})${health ? "" : " — not answering; `bir doctor` says more"}`
|
|
@@ -2883,6 +2914,8 @@ async function doctor(args) {
|
|
|
2883
2914
|
}
|
|
2884
2915
|
const wantWrapped = entries.filter((e) => isWrapped(e.config)).map((e) => e.name);
|
|
2885
2916
|
const hooks = hookFacts(paths);
|
|
2917
|
+
// Hosts other than Claude Code this folder was set up for (`bir setup --host`).
|
|
2918
|
+
const hosts = otherHosts(projectDir, sidecar);
|
|
2886
2919
|
for (const file of hooks.unreadable) {
|
|
2887
2920
|
warnings.push(`${file} is not plain JSON, so its hooks could not be checked (and \`bir setup\` will not rewrite it)`);
|
|
2888
2921
|
}
|
|
@@ -2907,6 +2940,12 @@ async function doctor(args) {
|
|
|
2907
2940
|
notes.push("the recorder is not running; the SessionStart hook starts it with the next session (`bir up` starts it now)" +
|
|
2908
2941
|
(wantWrapped.length && !off ? ` — ${wantWrapped.join(", ")} register with it then` : ""));
|
|
2909
2942
|
}
|
|
2943
|
+
else if (hosts.length > 0) {
|
|
2944
|
+
// Set up for another host (`bir setup --host langchain`): its adapter runs
|
|
2945
|
+
// inside the application and reports every call itself, so there are no
|
|
2946
|
+
// hooks to miss — and it starts the recorder when the application runs.
|
|
2947
|
+
notes.push(`the recorder is not running; the ${hosts.join(", ")} application starts it when it next runs (\`bir up\` starts it now)`);
|
|
2948
|
+
}
|
|
2910
2949
|
else {
|
|
2911
2950
|
notes.push("no hooks here: only MCP calls are recorded (standalone, Tier 2) — `bir setup` wires the hooks");
|
|
2912
2951
|
}
|
|
@@ -3105,11 +3144,19 @@ async function doctor(args) {
|
|
|
3105
3144
|
// until a password has been posted to it and it says `405`. A warning, not a
|
|
3106
3145
|
// problem: sessions do not use this shell. `--quick` skips the network call.
|
|
3107
3146
|
const shellAuthUrl = resolveAuthUrl();
|
|
3108
|
-
const
|
|
3147
|
+
const shellService = shellAuthUrl && !args.quick ? await inspectAuthService(shellAuthUrl) : undefined;
|
|
3148
|
+
const shellAuthProblem = shellService?.problem;
|
|
3109
3149
|
if (shellAuthProblem) {
|
|
3110
3150
|
warnings.push(`BIR_AUTH_URL in this shell (${shellAuthUrl}) is not a BaseIn service: ${shellAuthProblem}. ` +
|
|
3111
3151
|
`\`bir login\` and \`bir scenario\` here will fail — ${AUTH_URL_HINT}`);
|
|
3112
3152
|
}
|
|
3153
|
+
// THE SERVICE HANDS PLANS ONLY TO A NEWER RUNNER. Its `/health` names the
|
|
3154
|
+
// lowest contract it serves (util/service-headers.ts). Under it, every match
|
|
3155
|
+
// comes back with no plan (`runner_update_required`) and the agent does each
|
|
3156
|
+
// task: sessions work and recording goes on, but nothing is replayed.
|
|
3157
|
+
const outdated = runnerOutdated(shellService);
|
|
3158
|
+
if (outdated)
|
|
3159
|
+
warnings.push(outdated);
|
|
3113
3160
|
// THE SESSIONSTART HOOK NAMES A NODE BY ABSOLUTE PATH. A version manager's
|
|
3114
3161
|
// per-shell directory, an uninstalled Node, a moved private Node: the hook
|
|
3115
3162
|
// then fails on every session and the recorder is never started, which
|
|
@@ -3318,6 +3365,9 @@ async function doctor(args) {
|
|
|
3318
3365
|
else if (health) {
|
|
3319
3366
|
out("Replay : off");
|
|
3320
3367
|
}
|
|
3368
|
+
if (hosts.length) {
|
|
3369
|
+
out(`Host : ${hosts.join(", ")}${claudeCodeSetUp(projectDir, sidecar) ? ", and Claude Code" : " (no Claude Code hooks are needed here)"}`);
|
|
3370
|
+
}
|
|
3321
3371
|
out(`Wrapped in config : ${wantWrapped.join(", ") || "(none)"}`);
|
|
3322
3372
|
out(`Registered proxies: ${[...registered].join(", ") || "(none)"}`);
|
|
3323
3373
|
out(`Control server : ${discovery?.url ?? "not running"}`);
|
|
@@ -3381,6 +3431,7 @@ async function service(method, path, body) {
|
|
|
3381
3431
|
headers: {
|
|
3382
3432
|
"content-type": "application/json",
|
|
3383
3433
|
authorization: `Bearer ${session.accessToken}`,
|
|
3434
|
+
...serviceHeaders(),
|
|
3384
3435
|
},
|
|
3385
3436
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
3386
3437
|
signal: timeoutSignal(serviceTimeoutMs()),
|
|
@@ -3710,7 +3761,7 @@ async function revokeRefreshToken(configured, refreshToken) {
|
|
|
3710
3761
|
const authUrl = await normalizeAuthUrl(configured);
|
|
3711
3762
|
const res = await fetch(`${authUrl}/auth/logout`, {
|
|
3712
3763
|
method: "POST",
|
|
3713
|
-
headers: { "content-type": "application/json" },
|
|
3764
|
+
headers: { "content-type": "application/json", ...serviceHeaders() },
|
|
3714
3765
|
body: JSON.stringify({ refreshToken }),
|
|
3715
3766
|
signal: timeoutSignal(LOGOUT_REVOKE_BUDGET_MS),
|
|
3716
3767
|
});
|
|
@@ -3814,6 +3865,7 @@ async function main() {
|
|
|
3814
3865
|
daemon: !args.noDaemon,
|
|
3815
3866
|
allowServers: args.allowServers,
|
|
3816
3867
|
portExplicit: args.portExplicit,
|
|
3868
|
+
host: args.host,
|
|
3817
3869
|
}, {
|
|
3818
3870
|
out,
|
|
3819
3871
|
err: (line) => process.stderr.write(`[bir] ${line}\n`),
|
|
@@ -3904,6 +3956,8 @@ async function main() {
|
|
|
3904
3956
|
case "off":
|
|
3905
3957
|
case "on":
|
|
3906
3958
|
return offCommand(args, args.command === "off");
|
|
3959
|
+
case "update":
|
|
3960
|
+
return runUpdate({ out, err: (line) => process.stderr.write(`[bir] ${line}\n`) });
|
|
3907
3961
|
case "login": {
|
|
3908
3962
|
// Settle the URL the way every other command does, then refuse to go on
|
|
3909
3963
|
// when what is there is not the service. "Signed in" must mean the service
|
|
@@ -21,6 +21,11 @@ import type { JournalEntry } from "../util/journal.js";
|
|
|
21
21
|
export interface Turn {
|
|
22
22
|
runId: string;
|
|
23
23
|
sessionId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The host the turn ran in, when its session said it is not Claude Code
|
|
26
|
+
* (`langchain`): `run.start` names it. Absent for a Claude Code turn.
|
|
27
|
+
*/
|
|
28
|
+
host?: string;
|
|
24
29
|
startedAt: string;
|
|
25
30
|
finishedAt?: string;
|
|
26
31
|
prompt?: string;
|