@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
package/dist/control/daemon.d.ts
CHANGED
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
* with a working directory that must equal the project's, each needing the
|
|
23
23
|
* person to know which one they are on. The hook already runs in the right
|
|
24
24
|
* directory with the right environment at exactly the right moment, and Claude
|
|
25
|
-
* Code already supervises it.
|
|
26
|
-
*
|
|
25
|
+
* Code already supervises it. Restart-on-crash mid-session is the proxies'
|
|
26
|
+
* part: a bound proxy (or `bir-scenario`) that finds the recorder gone —
|
|
27
|
+
* refusing connections, its pid dead — spawns `bir-hooks revive`
|
|
28
|
+
* ({@link reviveDaemon}), rate limited, unless `bir down` stopped it on
|
|
29
|
+
* purpose.
|
|
27
30
|
*
|
|
28
31
|
* HOW IT IS LAUNCHED. The daemon opens its own log file (`BIR_DAEMON_LOG`) and
|
|
29
32
|
* inherits nothing from our stdio. On POSIX that is a `detached` spawn with
|
|
@@ -32,50 +35,177 @@
|
|
|
32
35
|
* short-lived `powershell.exe`: libuv's CreateProcess inherits *every*
|
|
33
36
|
* inheritable handle of the parent, not only the three it was told about, and
|
|
34
37
|
* a node started under a PowerShell layer (the `irm | iex` bootstrap, a `.ps1`
|
|
35
|
-
* shim, a captured `bir up
|
|
36
|
-
* daemon that inherits it holds that pipe open for days, and
|
|
37
|
-
* it never sees EOF. `Start-Process` passes the environment
|
|
38
|
+
* shim, a captured `bir up`, a `$r = claude -p …`) carries an extra copy of its
|
|
39
|
+
* own stdout pipe. A daemon that inherits it holds that pipe open for days, and
|
|
40
|
+
* whoever is reading it never sees EOF. `Start-Process` passes the environment
|
|
41
|
+
* and no handles. It costs about a second, once per project per boot; a direct
|
|
42
|
+
* spawn is faster but only safe when nothing above the hook captured its
|
|
43
|
+
* output, so it is an opt-in (`BIR_WINDOWS_LAUNCH=spawn`), not the default.
|
|
44
|
+
*
|
|
45
|
+
* WHERE IT RUNS. The daemon's working directory is the runner's own state
|
|
46
|
+
* directory, never the project: on Windows a process's current directory
|
|
47
|
+
* cannot be renamed or deleted, and an invisible recorder holding the project
|
|
48
|
+
* folder made "delete the folder", `git worktree remove` and a re-clone fail
|
|
49
|
+
* with "in use by another program". The project is passed as `BIR_DAEMON_CWD`.
|
|
50
|
+
*
|
|
51
|
+
* WHAT IT MAY NOT DO. Every path is bounded: the SessionStart hook has a budget
|
|
52
|
+
* well inside Claude Code's hook timeout (ensure-hook.ts), a recorder that is
|
|
53
|
+
* alive but slow is never mistaken for a dead one (that started a duplicate
|
|
54
|
+
* that could not bind), and nothing here touches the network — the recorder
|
|
55
|
+
* listens before it signs in (recorder-keeper.ts).
|
|
38
56
|
*/
|
|
39
|
-
import { type DiscoveryInfo } from "./discovery.js";
|
|
40
|
-
|
|
57
|
+
import { type DiscoveryInfo, type SupersededRecorder } from "./discovery.js";
|
|
58
|
+
import { type InstalledSidecar } from "../config/generate.js";
|
|
59
|
+
export { listSupersededRecorders, type SupersededRecorder } from "./discovery.js";
|
|
60
|
+
/** A log file larger than this is rolled over to `.1` (at start, and while it runs). */
|
|
41
61
|
export declare const LOG_ROTATE_BYTES: number;
|
|
42
62
|
/** Marks a `bir-hooks` as started by this module; it says so in its discovery file. */
|
|
43
63
|
export declare const DAEMON_ENV = "BIR_DAEMON";
|
|
44
64
|
/** Where that process writes its audit lines. It opens the file itself. */
|
|
45
65
|
export declare const DAEMON_LOG_ENV = "BIR_DAEMON_LOG";
|
|
66
|
+
/**
|
|
67
|
+
* The project a background recorder serves. It runs in the runner's state
|
|
68
|
+
* directory, not the project (see WHERE IT RUNS above), so it cannot learn the
|
|
69
|
+
* project from its own working directory.
|
|
70
|
+
*/
|
|
71
|
+
export declare const DAEMON_CWD_ENV = "BIR_DAEMON_CWD";
|
|
46
72
|
/**
|
|
47
73
|
* The setup token's environment variable (`bir setup` reads it when `--token`
|
|
48
74
|
* is absent). Never handed to the recorder: it is a one-use secret for signing
|
|
49
75
|
* in, and a process that lives for days has no business holding it.
|
|
50
76
|
*/
|
|
51
77
|
export declare const SETUP_TOKEN_ENV = "BIR_SETUP_TOKEN";
|
|
78
|
+
/**
|
|
79
|
+
* An Anthropic key meant for the runner's derivation, and nothing else. Set
|
|
80
|
+
* explicitly; it is the only key the recorder uses by default.
|
|
81
|
+
*/
|
|
82
|
+
export declare const DERIVE_KEY_ENV = "BIR_DERIVE_API_KEY";
|
|
83
|
+
/**
|
|
84
|
+
* `1` lets the recorder use an `ANTHROPIC_API_KEY` from its environment for
|
|
85
|
+
* derivation, as it did before. Off by default: the hook-started recorder has
|
|
86
|
+
* Claude Code's environment, and an API-billed Claude Code has its key there —
|
|
87
|
+
* using it silently spent the person's own money on something the service
|
|
88
|
+
* does for a signed-in runner.
|
|
89
|
+
*/
|
|
90
|
+
export declare const USE_API_KEY_ENV = "BIR_USE_ANTHROPIC_API_KEY";
|
|
91
|
+
/** `spawn` launches the Windows daemon directly instead of through PowerShell. */
|
|
92
|
+
export declare const WINDOWS_LAUNCH_ENV = "BIR_WINDOWS_LAUNCH";
|
|
93
|
+
/**
|
|
94
|
+
* `bir-hooks revive --cwd <project>`: bring the project's recorder back when
|
|
95
|
+
* it is gone. Spawned detached by a bound `bir-proxy` or `bir-scenario` whose
|
|
96
|
+
* recorder refuses connections and whose pid is dead (see {@link reviveDaemon}).
|
|
97
|
+
*/
|
|
98
|
+
export declare const REVIVE_ARG = "revive";
|
|
99
|
+
/**
|
|
100
|
+
* {@link EnsureOptions.probeIdle}: the gap between the two `/health` probes
|
|
101
|
+
* that tell a recorder that only *reports* a run open from one doing work.
|
|
102
|
+
*/
|
|
103
|
+
export declare const IDLE_PROBE_GAP_MS = 2500;
|
|
52
104
|
export interface DaemonStatus {
|
|
53
105
|
info: DiscoveryInfo;
|
|
54
106
|
health: Record<string, unknown>;
|
|
55
107
|
}
|
|
108
|
+
/** The recorder is running but did not answer in time: busy, not gone. */
|
|
109
|
+
export declare class RecorderBusyError extends Error {
|
|
110
|
+
readonly pid: number;
|
|
111
|
+
constructor(pid: number);
|
|
112
|
+
}
|
|
113
|
+
/** A revive found the project's recorder stopped on purpose (`bir down`): it is left down. */
|
|
114
|
+
export declare class RecorderStoppedError extends Error {
|
|
115
|
+
constructor();
|
|
116
|
+
}
|
|
56
117
|
/** Absolute path of `dist/bin/bir-hooks.js` in the package this module runs from. */
|
|
57
118
|
export declare function hooksScriptPath(): string;
|
|
119
|
+
/** What answered on a recorder's address, told apart as far as HTTP can. */
|
|
120
|
+
export type HealthProbe =
|
|
121
|
+
/** A recorder that accepted the token. */
|
|
122
|
+
{
|
|
123
|
+
kind: "ok";
|
|
124
|
+
health: Record<string, unknown>;
|
|
125
|
+
}
|
|
126
|
+
/** Nothing listening (connection refused, or unreachable). */
|
|
127
|
+
| {
|
|
128
|
+
kind: "refused";
|
|
129
|
+
reason: string;
|
|
130
|
+
}
|
|
131
|
+
/** Something accepted the connection and did not answer in time. */
|
|
132
|
+
| {
|
|
133
|
+
kind: "timeout";
|
|
134
|
+
}
|
|
135
|
+
/** A listener that refused the token. */
|
|
136
|
+
| {
|
|
137
|
+
kind: "unauthorized";
|
|
138
|
+
status: number;
|
|
139
|
+
}
|
|
140
|
+
/** A listener that answered, but not as a recorder. */
|
|
141
|
+
| {
|
|
142
|
+
kind: "foreign";
|
|
143
|
+
status: number;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* `GET /health` with the bearer token, bounded, with the failure kept apart:
|
|
147
|
+
* "refused" means gone, "timeout" means busy, and the difference is the one
|
|
148
|
+
* that decides whether a second recorder is started.
|
|
149
|
+
*/
|
|
150
|
+
export declare function probeHealth(url: string, token: string, timeoutMs?: number): Promise<HealthProbe>;
|
|
58
151
|
/**
|
|
59
152
|
* The live control server for `cwd`, or undefined.
|
|
60
153
|
*
|
|
61
|
-
* "Live" means the discovery file
|
|
62
|
-
* answers `/health` with its token. A file left by a crash fails the
|
|
63
|
-
* test; a process that hung, or a recycled pid, fails the
|
|
154
|
+
* "Live" means the discovery file names a pid that is alive **and** that
|
|
155
|
+
* answers `/health` with its token. A file left by a crash fails the first
|
|
156
|
+
* test; a process that hung, or a recycled pid, fails the second.
|
|
64
157
|
*/
|
|
65
158
|
export declare function daemonStatus(cwd: string): Promise<DaemonStatus | undefined>;
|
|
159
|
+
/**
|
|
160
|
+
* This project's recorder, found by its port rather than its discovery file,
|
|
161
|
+
* and its file written again. For a recorder whose file was removed (by hand,
|
|
162
|
+
* by an older runner that took a slow recorder for a dead one): starting a
|
|
163
|
+
* second one would only find the port taken.
|
|
164
|
+
*
|
|
165
|
+
* A file that already names the recorder that answered is left as it is. What
|
|
166
|
+
* is written here is rebuilt from `/health`, which does not say whether the
|
|
167
|
+
* process runs in the background or where its log is: overwriting the
|
|
168
|
+
* recorder's own, complete record with it made `bir status` lose the log path
|
|
169
|
+
* and the start time, and hid the recorder from the reinstall check (which
|
|
170
|
+
* only replaces background recorders) — for the half hour until its own
|
|
171
|
+
* heartbeat. The recorder also rewrites a record of itself that differs from
|
|
172
|
+
* what it would write (recorder-main.ts), so what this writes is short-lived.
|
|
173
|
+
*/
|
|
174
|
+
export declare function rediscover(cwd: string, port: number, token: string, timeoutMs?: number): Promise<DaemonStatus | undefined>;
|
|
66
175
|
export interface EnsureOptions {
|
|
67
176
|
/** `dist/bin/bir-hooks.js` to run. Defaults to this package's. */
|
|
68
177
|
hooksScript?: string;
|
|
69
178
|
/** Environment for the new process. Defaults to this process's. */
|
|
70
179
|
env?: NodeJS.ProcessEnv;
|
|
71
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* The whole call's budget: finding, replacing and starting the recorder.
|
|
182
|
+
* Default 15 s. The SessionStart hook passes less (ensure-hook.ts).
|
|
183
|
+
*/
|
|
72
184
|
timeoutMs?: number;
|
|
73
185
|
/**
|
|
74
|
-
* Restart a running recorder
|
|
75
|
-
* is mid-run. Off by default: only `bir setup`/`bir up` and the
|
|
76
|
-
* hook — the moments a restart costs nothing — ask for it.
|
|
186
|
+
* Restart a running recorder that is an older version than ours, provided
|
|
187
|
+
* no session is mid-run. Off by default: only `bir setup`/`bir up` and the
|
|
188
|
+
* SessionStart hook — the moments a restart costs nothing — ask for it. A
|
|
189
|
+
* newer recorder (another runner copy on this machine) is kept.
|
|
77
190
|
*/
|
|
78
191
|
restartOnSkew?: boolean;
|
|
192
|
+
/**
|
|
193
|
+
* Do not take the word of a recorder that predates the idle bound. A 0.2.12
|
|
194
|
+
* (or older) recorder's `/health` has no `sessions[].idleMs`, and its
|
|
195
|
+
* `active` means "a run was opened and never finished" — an Esc-interrupted
|
|
196
|
+
* turn, a crashed window or a `claude -p` run leaves one open for the
|
|
197
|
+
* recorder's whole life, and it was then never replaced. With this set, such
|
|
198
|
+
* a recorder counts as idle — and replaceable — when two `/health` probes
|
|
199
|
+
* {@link IDLE_PROBE_GAP_MS} apart show no change in its sessions' steps and
|
|
200
|
+
* no registered proxy of it is alive. Costs that gap, so only `bir setup`
|
|
201
|
+
* and `bir up` pass it; the SessionStart hook stays conservative.
|
|
202
|
+
*/
|
|
203
|
+
probeIdle?: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Set by {@link reviveDaemon} only: honour a `bir down` (the down marker)
|
|
206
|
+
* instead of clearing it, and throw {@link RecorderStoppedError} when found.
|
|
207
|
+
*/
|
|
208
|
+
revive?: boolean;
|
|
79
209
|
}
|
|
80
210
|
export interface EnsureResult {
|
|
81
211
|
status: DaemonStatus;
|
|
@@ -86,14 +216,150 @@ export interface EnsureResult {
|
|
|
86
216
|
}
|
|
87
217
|
/** How many of the recorder's sessions are mid-run right now. */
|
|
88
218
|
export declare function activeSessions(health: Record<string, unknown>): number;
|
|
219
|
+
/**
|
|
220
|
+
* `a` against `b` as versions: -1, 0 or 1, or undefined when either is not
|
|
221
|
+
* `x.y.z` (with an optional pre-release, which sorts below its release, and
|
|
222
|
+
* build metadata, which is ignored).
|
|
223
|
+
*/
|
|
224
|
+
export declare function compareVersions(a: string, b: string): -1 | 0 | 1 | undefined;
|
|
225
|
+
/**
|
|
226
|
+
* True for the `/health` of a recorder that predates the idle bound (0.2.12
|
|
227
|
+
* and older): it lists sessions, and none of them says how long it has been
|
|
228
|
+
* idle. Its `active` is then "a run is open", with nothing to close one that
|
|
229
|
+
* was abandoned. An empty list says nothing either way (and counts none active).
|
|
230
|
+
*/
|
|
231
|
+
export declare function isLegacyHealth(health: Record<string, unknown>): boolean;
|
|
232
|
+
/** A TCP port number, or undefined for anything that is not one. */
|
|
233
|
+
export declare function validPort(value: unknown): number | undefined;
|
|
234
|
+
/** The address this project's hooks were written with, as far as anything recorded it. */
|
|
235
|
+
export interface ProjectEndpoint {
|
|
236
|
+
/**
|
|
237
|
+
* The port the hooks post to: a valid `BIR_CONTROL_PORT`, else the
|
|
238
|
+
* project's record. Undefined when neither names one — the recorder then
|
|
239
|
+
* takes the machine-wide or default port and may fall back to another.
|
|
240
|
+
*/
|
|
241
|
+
port?: number;
|
|
242
|
+
/** The project's token, when the project has a record. */
|
|
243
|
+
token?: string;
|
|
244
|
+
/** True when {@link port} is known: a recorder must bind exactly it. */
|
|
245
|
+
pinned: boolean;
|
|
246
|
+
/**
|
|
247
|
+
* True when `BIR_CONTROL_PORT=0` asks for any free port: the recorder binds
|
|
248
|
+
* an ephemeral one and says which in its discovery file, `/health` and its
|
|
249
|
+
* log. {@link port} is then undefined — no port is expected — so a running
|
|
250
|
+
* recorder is never replaced for listening on the "wrong" one. Absent
|
|
251
|
+
* otherwise. For tests and the smoke run, which must never take the real
|
|
252
|
+
* default port; a project's hooks name a fixed port, so 0 is not for them.
|
|
253
|
+
*/
|
|
254
|
+
ephemeral?: true;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* `BIR_CONTROL_PORT` read once, for everything that reads it: `"0"` is any
|
|
258
|
+
* free port, a valid port is that port, and anything else (unset, empty, not
|
|
259
|
+
* a number, out of range) is no setting at all.
|
|
260
|
+
*/
|
|
261
|
+
export declare function controlPortFromEnv(env?: NodeJS.ProcessEnv): number | undefined;
|
|
262
|
+
/**
|
|
263
|
+
* The one reading of "which port and token does this project use", shared by
|
|
264
|
+
* the recorder (which binds it) and by {@link ensureDaemon} (which checks a
|
|
265
|
+
* running recorder against it). They used to read `BIR_CONTROL_PORT`
|
|
266
|
+
* differently: the recorder took an empty or invalid value as "the default
|
|
267
|
+
* port, pinned", the check ignored it and used the project's port — and every
|
|
268
|
+
* session start then "replaced" a recorder that could only ever bind the
|
|
269
|
+
* default port again. An invalid value is now ignored by both, and `0` means
|
|
270
|
+
* "any free port" to both (see {@link ProjectEndpoint.ephemeral}).
|
|
271
|
+
*/
|
|
272
|
+
export declare function projectEndpoint(cwd: string, env?: NodeJS.ProcessEnv, sidecar?: InstalledSidecar): ProjectEndpoint;
|
|
89
273
|
/**
|
|
90
274
|
* The control server for `cwd`, started in the background if none is live.
|
|
91
275
|
*
|
|
92
|
-
* Idempotent: a live one is returned as-is
|
|
93
|
-
*
|
|
94
|
-
*
|
|
276
|
+
* Idempotent: a live one is returned as-is — unless it is an older version
|
|
277
|
+
* with nothing mid-run (`restartOnSkew`; with `probeIdle`, a legacy
|
|
278
|
+
* recorder whose "mid-run" is a run nobody will finish counts as idle), or a
|
|
279
|
+
* background recorder that the project's current hooks do not reach
|
|
280
|
+
* (another port or token). Any call but a revive ends a `bir down`.
|
|
281
|
+
*
|
|
282
|
+
* WHAT A REINSTALL MAY NOT BREAK. Claude Code reads a session's hooks when the
|
|
283
|
+
* session starts and keeps them: every window opened before `bir install
|
|
284
|
+
* --port N` or `bir uninstall` + `bir setup` still posts to the old port with
|
|
285
|
+
* the old token, until it is closed. The old recorder is what answers them. So
|
|
286
|
+
*
|
|
287
|
+
* - another **port**: the old recorder is left running for the sessions it
|
|
288
|
+
* has seen, and a second one is started on the new port — two ports, no
|
|
289
|
+
* conflict. The new one takes the discovery file; the old one steps back
|
|
290
|
+
* from it, is recorded as superseded (`listSupersededRecorders`, so
|
|
291
|
+
* `bir status`/`bir down` can find it), writes a log of its own, and
|
|
292
|
+
* exits once it has had nothing left to serve for a day
|
|
293
|
+
* (recorder-main.ts). A recorder that has never seen a session is simply
|
|
294
|
+
* replaced;
|
|
295
|
+
* - another **token** on the same port: only one process can listen there.
|
|
296
|
+
* It is replaced between turns (a restart then costs the old windows their
|
|
297
|
+
* next hooks, and gives every new one working hooks), never mid-run.
|
|
298
|
+
*
|
|
299
|
+
* A recorder that is alive but slow to answer is never replaced and never
|
|
300
|
+
* duplicated on its own port: this throws {@link RecorderBusyError} instead.
|
|
301
|
+
*
|
|
302
|
+
* Throws when a fresh process did not come up within the budget, with the
|
|
303
|
+
* tail of its log, because "it did not start" is the one message that must
|
|
304
|
+
* carry its own diagnosis.
|
|
95
305
|
*/
|
|
96
306
|
export declare function ensureDaemon(cwd: string, opts?: EnsureOptions): Promise<EnsureResult>;
|
|
307
|
+
/**
|
|
308
|
+
* Mark `cwd`'s recorder as stopped on purpose. {@link stopDaemon} does it; a
|
|
309
|
+
* revive then leaves the project without a recorder until something that
|
|
310
|
+
* asks for one on purpose — a session start, `bir up`, `bir setup` — clears
|
|
311
|
+
* it. Never throws.
|
|
312
|
+
*/
|
|
313
|
+
export declare function markDown(cwd: string): void;
|
|
314
|
+
/** Remove the {@link markDown} marker. Never throws. */
|
|
315
|
+
export declare function clearDownMarker(cwd: string): void;
|
|
316
|
+
/** True while `cwd`'s recorder is marked as stopped on purpose. */
|
|
317
|
+
export declare function isMarkedDown(cwd: string): boolean;
|
|
318
|
+
export interface ReviveResult {
|
|
319
|
+
outcome:
|
|
320
|
+
/** The project has no install record: nothing is started for it. */
|
|
321
|
+
"not_installed"
|
|
322
|
+
/** `bir down` stopped it on purpose, and nothing started it since. */
|
|
323
|
+
| "stopped_on_purpose"
|
|
324
|
+
/** A recorder was already there (another revive, or a session start, won). */
|
|
325
|
+
| "running"
|
|
326
|
+
/** This call started it. */
|
|
327
|
+
| "started"
|
|
328
|
+
/** Alive but not answering: left alone. */
|
|
329
|
+
| "busy" | "failed";
|
|
330
|
+
pid?: number;
|
|
331
|
+
error?: string;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* `bir-hooks revive --cwd <project>` (K1): the recorder of an installed
|
|
335
|
+
* project, started again when it is gone.
|
|
336
|
+
*
|
|
337
|
+
* Every hook but SessionStart is an HTTP hook to the recorder, and a refused
|
|
338
|
+
* one is shown in the session as a hook error — on every prompt and every tool
|
|
339
|
+
* call — until the next SessionStart. A recorder killed mid-session (an
|
|
340
|
+
* antivirus, the OOM killer, a restart that missed its budget) used to stay
|
|
341
|
+
* gone until then. A bound `bir-proxy` or `bir-scenario` that finds it
|
|
342
|
+
* refusing connections with its pid dead spawns this, detached and rate
|
|
343
|
+
* limited, so the window of refused hooks is seconds.
|
|
344
|
+
*
|
|
345
|
+
* {@link ensureDaemon} with `restartOnSkew: false`, under the same start lock,
|
|
346
|
+
* so several revives at once start one recorder. Only for a project with an
|
|
347
|
+
* install record, and never after `bir down` (the down marker). Started even
|
|
348
|
+
* while the runner is off: the off recorder is what answers every hook `{}`.
|
|
349
|
+
* Never throws.
|
|
350
|
+
*/
|
|
351
|
+
export declare function reviveDaemon(cwd: string, opts?: Pick<EnsureOptions, "hooksScript" | "env" | "timeoutMs">): Promise<ReviveResult>;
|
|
352
|
+
/**
|
|
353
|
+
* The environment a background recorder gets: the caller's, marked as a
|
|
354
|
+
* daemon, told its project, and without the secrets it has no use for.
|
|
355
|
+
*
|
|
356
|
+
* Nor `BIR_OFF` (K2), in any spelling: the recorder serves every window of
|
|
357
|
+
* the project, and a `BIR_OFF=1` in the environment of whichever window
|
|
358
|
+
* happened to start it switched all of them off for the recorder's life. That
|
|
359
|
+
* window's SessionStart hook tells the recorder its own session is off
|
|
360
|
+
* instead (ensure-hook.ts); the flag files still switch everything.
|
|
361
|
+
*/
|
|
362
|
+
export declare function daemonEnv(base: NodeJS.ProcessEnv, cwd: string, logFile: string): NodeJS.ProcessEnv;
|
|
97
363
|
/**
|
|
98
364
|
* Stop the control server for `cwd`.
|
|
99
365
|
*
|
|
@@ -103,14 +369,130 @@ export declare function ensureDaemon(cwd: string, opts?: EnsureOptions): Promise
|
|
|
103
369
|
* the polite route exists. Only a process that answers `/health` as the
|
|
104
370
|
* recorder is ever signalled: a discovery file survives a crash, and its pid
|
|
105
371
|
* is recycled — on Windows quickly — so "the pid in the file is alive" is not
|
|
106
|
-
* evidence that it is ours.
|
|
372
|
+
* evidence that it is ours. One that is alive but too busy to answer is asked
|
|
373
|
+
* and waited for, never signalled.
|
|
107
374
|
*/
|
|
108
375
|
export declare function stopDaemon(cwd: string, opts?: {
|
|
109
376
|
timeoutMs?: number;
|
|
377
|
+
/**
|
|
378
|
+
* Mark the project's recorder as stopped on purpose (default true), so a
|
|
379
|
+
* proxy that finds it gone does not bring it back (`bir-hooks revive`)
|
|
380
|
+
* until a session start, `bir up` or `bir setup` asks for one. A restart
|
|
381
|
+
* inside {@link ensureDaemon} passes false.
|
|
382
|
+
*/
|
|
383
|
+
markDown?: boolean;
|
|
110
384
|
}): Promise<{
|
|
111
385
|
stopped: boolean;
|
|
112
386
|
pid?: number;
|
|
113
387
|
stale?: boolean;
|
|
114
388
|
forced?: boolean;
|
|
115
389
|
}>;
|
|
390
|
+
/**
|
|
391
|
+
* Stop one superseded recorder (see {@link listSupersededRecorders}) the same
|
|
392
|
+
* way {@link stopDaemon} stops the project's: asked, and signalled only when
|
|
393
|
+
* it answers `/health` as its own pid. Its record is removed once it is gone
|
|
394
|
+
* (its own exit does that too), or when nothing answers as it.
|
|
395
|
+
*/
|
|
396
|
+
export declare function stopSupersededRecorder(record: SupersededRecorder, opts?: {
|
|
397
|
+
timeoutMs?: number;
|
|
398
|
+
}): Promise<{
|
|
399
|
+
stopped: boolean;
|
|
400
|
+
pid: number;
|
|
401
|
+
stale?: boolean;
|
|
402
|
+
forced?: boolean;
|
|
403
|
+
}>;
|
|
404
|
+
/**
|
|
405
|
+
* Stop every superseded recorder of `cwd` (or of every project, without
|
|
406
|
+
* one). For `bir down` and `bir uninstall`; the project's own recorder is
|
|
407
|
+
* {@link stopDaemon}'s.
|
|
408
|
+
*/
|
|
409
|
+
export declare function stopSupersededRecorders(cwd?: string, opts?: {
|
|
410
|
+
timeoutMs?: number;
|
|
411
|
+
}): Promise<Array<{
|
|
412
|
+
record: SupersededRecorder;
|
|
413
|
+
stopped: boolean;
|
|
414
|
+
stale?: boolean;
|
|
415
|
+
forced?: boolean;
|
|
416
|
+
}>>;
|
|
417
|
+
/**
|
|
418
|
+
* Why the background recorder for a project could not bind the port its
|
|
419
|
+
* hooks name. Written by that recorder as it exits, read by `bir doctor` and
|
|
420
|
+
* the next SessionStart, removed by the first recorder that binds it.
|
|
421
|
+
*/
|
|
422
|
+
export interface PortProblem {
|
|
423
|
+
port: number;
|
|
424
|
+
/** `EADDRINUSE` or `EACCES`. */
|
|
425
|
+
code: string;
|
|
426
|
+
/**
|
|
427
|
+
* `excluded`: the system reserved the port (Windows/Hyper-V/WSL ranges).
|
|
428
|
+
* `foreign`: another program answers there. `unauthorized`: a listener
|
|
429
|
+
* that refused this project's token — possibly a recorder from an earlier
|
|
430
|
+
* install. `other-project`: another project's recorder. `unknown`: busy,
|
|
431
|
+
* and nothing answered.
|
|
432
|
+
*/
|
|
433
|
+
holder: "excluded" | "foreign" | "unauthorized" | "other-project" | "unknown";
|
|
434
|
+
at: number;
|
|
435
|
+
/** One sentence for a person. */
|
|
436
|
+
detail: string;
|
|
437
|
+
/** What to run. */
|
|
438
|
+
fix: string;
|
|
439
|
+
}
|
|
440
|
+
export declare function readPortProblem(cwd: string): PortProblem | undefined;
|
|
441
|
+
export declare function writePortProblem(cwd: string, problem: PortProblem): void;
|
|
442
|
+
export declare function clearPortProblem(cwd: string): void;
|
|
443
|
+
/**
|
|
444
|
+
* The log a background recorder writes its audit lines to.
|
|
445
|
+
*
|
|
446
|
+
* Three things the plain stream did not do. It never fails the process: a
|
|
447
|
+
* file (or a logs directory) that cannot be opened or written — a full disk,
|
|
448
|
+
* a read-only or locked profile — used to be an unhandled `'error'` that
|
|
449
|
+
* killed the recorder at start, and with it every hook of the session. Now the
|
|
450
|
+
* lines are dropped instead. It comes back: a failure is usually brief (the
|
|
451
|
+
* disk full for a minute, an antivirus scan holding the file), and a recorder
|
|
452
|
+
* that runs for days must not stay silent for all of them — the minute tick
|
|
453
|
+
* ({@link rotateIfLarge}) opens the file again. And it stays bounded while the
|
|
454
|
+
* recorder runs for weeks: rolled to `.1` once it passes
|
|
455
|
+
* {@link LOG_ROTATE_BYTES}, not only when a new one starts.
|
|
456
|
+
*
|
|
457
|
+
* And it shares a path safely. Two recorders of one project can run at once
|
|
458
|
+
* (a reinstall moved the hooks to another port while a window was open), and
|
|
459
|
+
* `ensureDaemon` rolls the log before a start. Whoever rolled it, the other
|
|
460
|
+
* writer's stream follows the renamed file — all its lines went to `.1`, which
|
|
461
|
+
* then grew without bound, and on Windows the held `.1` made every later roll
|
|
462
|
+
* fail. So the minute tick first checks that the file at the path is the one
|
|
463
|
+
* it writes, and opens the path again when it is not. A recorder superseded
|
|
464
|
+
* like that also moves to a file of its own ({@link moveTo}).
|
|
465
|
+
*/
|
|
466
|
+
export declare class DaemonLog {
|
|
467
|
+
private stream?;
|
|
468
|
+
/** The descriptor {@link stream} writes to, once it is open. */
|
|
469
|
+
private fd?;
|
|
470
|
+
private current;
|
|
471
|
+
private readonly maxBytes;
|
|
472
|
+
/** Set once lines have been dropped, so the reopen can say so. */
|
|
473
|
+
private droppedSince?;
|
|
474
|
+
constructor(path: string, maxBytes?: number);
|
|
475
|
+
/** The file lines go to now. */
|
|
476
|
+
get path(): string;
|
|
477
|
+
/** Open the file and point `console` (every `logLine`) at it. Never throws. */
|
|
478
|
+
open(): void;
|
|
479
|
+
/**
|
|
480
|
+
* Write to `path` from now on — a superseded recorder's own file, so the
|
|
481
|
+
* project's log keeps one writer and one rotation. Never throws.
|
|
482
|
+
*/
|
|
483
|
+
moveTo(path: string): void;
|
|
484
|
+
private silence;
|
|
485
|
+
private attach;
|
|
486
|
+
/**
|
|
487
|
+
* Roll the file to `.1` if it is past the limit, and open it again if it
|
|
488
|
+
* failed earlier, or if the file at the path is no longer the one written
|
|
489
|
+
* (another writer rolled or removed it). True when it rolled. Never throws.
|
|
490
|
+
*/
|
|
491
|
+
rotateIfLarge(): boolean;
|
|
492
|
+
/** Whether `onDisk` is the file this log's stream writes (true when it cannot tell). */
|
|
493
|
+
private writes;
|
|
494
|
+
/** After a failure: try the file again. A stream that fails again just stays silent. */
|
|
495
|
+
private reopen;
|
|
496
|
+
close(): void;
|
|
497
|
+
}
|
|
116
498
|
//# sourceMappingURL=daemon.d.ts.map
|