@basein/runner 0.2.12 → 0.3.1
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 -369
- 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 -21
- package/dist/bin/bir-scenario.js +593 -122
- package/dist/bin/bir.d.ts +12 -6
- package/dist/bin/bir.js +2935 -489
- package/dist/bin/investigate.js +40 -15
- package/dist/bin/scenario-edit.d.ts +7 -1
- package/dist/bin/scenario-edit.js +43 -21
- 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 +2787 -302
- 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/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 +369 -20
- package/dist/replay/controller.js +1176 -107
- 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 -1304
- package/docs/calculatedReplayGuide.md +1212 -1087
- 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 -311
- 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/config/resolve.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Claude Code merges three scopes, highest precedence first. The whole winning
|
|
10
10
|
* entry is used — fields are never merged across scopes:
|
|
11
|
-
* 1. **Local** — `~/.claude.json` → `projects[<
|
|
11
|
+
* 1. **Local** — `~/.claude.json` → `projects[<project key>].mcpServers`,
|
|
12
|
+
* where the key is the canonical git root (see
|
|
13
|
+
* {@link claudeProjectKey}), not necessarily the cwd
|
|
12
14
|
* 2. **Project** — `<cwd>/.mcp.json` → `mcpServers`
|
|
13
15
|
* 3. **User** — `~/.claude.json` → top-level `mcpServers`
|
|
14
16
|
*
|
|
@@ -26,11 +28,17 @@ export interface StdioServerConfig {
|
|
|
26
28
|
args?: string[];
|
|
27
29
|
env?: Record<string, string>;
|
|
28
30
|
}
|
|
29
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* A remote server entry (streamable HTTP or legacy SSE). `headersHelper` and
|
|
33
|
+
* `oauth` are Claude Code's own ways to authenticate one; nothing here can
|
|
34
|
+
* reproduce either, which is why such an entry is never wrapped by default.
|
|
35
|
+
*/
|
|
30
36
|
export interface RemoteServerConfig {
|
|
31
37
|
type: "http" | "sse";
|
|
32
38
|
url: string;
|
|
33
39
|
headers?: Record<string, string>;
|
|
40
|
+
headersHelper?: string;
|
|
41
|
+
oauth?: unknown;
|
|
34
42
|
}
|
|
35
43
|
export type McpServerConfig = StdioServerConfig | RemoteServerConfig;
|
|
36
44
|
export type Scope = "local" | "project" | "user" | "explicit";
|
|
@@ -51,17 +59,71 @@ export interface ResolveOptions {
|
|
|
51
59
|
export declare function isRemote(config: McpServerConfig): config is RemoteServerConfig;
|
|
52
60
|
/** Parse a JSON file. `undefined` if absent; throws if present but malformed. */
|
|
53
61
|
export declare function parseJsonFile(path: string): unknown;
|
|
54
|
-
export declare function claudeJsonPath(override?: string): string;
|
|
55
62
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
63
|
+
* Parse the text of a JSON file (BOM tolerated). Throws, naming `path`, when
|
|
64
|
+
* it is not JSON — a file with comments (JSONC) included, which is exactly the
|
|
65
|
+
* file that must be refused rather than rewritten without its comments.
|
|
66
|
+
*/
|
|
67
|
+
export declare function parseJsonText(raw: string, path: string): unknown;
|
|
68
|
+
/**
|
|
69
|
+
* The file Claude Code keeps its user and local scopes in.
|
|
70
|
+
*
|
|
71
|
+
* `~/.claude.json` — unless `CLAUDE_CONFIG_DIR` is set, when Claude Code reads
|
|
72
|
+
* `$CLAUDE_CONFIG_DIR/.claude.json` instead (checked against Claude Code
|
|
73
|
+
* itself). Writing the home-directory file then wraps servers in a file no
|
|
74
|
+
* session reads: `bir setup` says "wrapped" and nothing is. `BIR_CLAUDE_JSON`
|
|
75
|
+
* (tests, a second profile) still names it outright. An empty variable counts
|
|
76
|
+
* as unset, the way Claude Code treats one.
|
|
77
|
+
*/
|
|
78
|
+
export declare function claudeJsonPath(override?: string, env?: NodeJS.ProcessEnv): string;
|
|
79
|
+
/**
|
|
80
|
+
* `CLAUDE_CONFIG_DIR` when it is set: where Claude Code keeps `.claude.json`,
|
|
81
|
+
* `settings.json` and the rest of what normally lives in `~/.claude`.
|
|
82
|
+
*/
|
|
83
|
+
export declare function claudeConfigHome(env?: NodeJS.ProcessEnv): string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The directory Claude Code keys this cwd's local scope (and its trust and
|
|
86
|
+
* permission state) under: the canonical git root, else `cwd` itself.
|
|
87
|
+
*/
|
|
88
|
+
export declare function claudeProjectRoot(cwd: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* The linked worktrees (`git worktree add`) of the repository whose main
|
|
91
|
+
* checkout is `root`, as directories that still exist.
|
|
92
|
+
*
|
|
93
|
+
* Claude Code keys their local scope under `root` too (see
|
|
94
|
+
* {@link canonicalGitRoot}), so a local-scope server written for `root` also
|
|
95
|
+
* runs in every one of them — sessions nobody set up, on branches whose
|
|
96
|
+
* `.mcp.json` may say something else. `bir install` names them, and leaves a
|
|
97
|
+
* server alone where a worktree's own `.mcp.json` defines it differently.
|
|
98
|
+
*/
|
|
99
|
+
export declare function linkedWorktrees(root: string): string[];
|
|
100
|
+
/** {@link claudeProjectRoot} spelled the way Claude Code spells a `projects` key. */
|
|
101
|
+
export declare function claudeProjectKey(cwd: string, platform?: NodeJS.Platform): string;
|
|
102
|
+
/**
|
|
103
|
+
* Every existing `projects` key Claude Code could read for this cwd — the
|
|
104
|
+
* spellings of its canonical root that it wrote (forward slashes on Windows;
|
|
105
|
+
* both drive-letter cases when both exist) — or, when it has never run here,
|
|
106
|
+
* the one key it will create. Never a backslash key: Claude Code does not read
|
|
107
|
+
* one, and writing there is the trap that made entries invisible to `/mcp`.
|
|
108
|
+
*/
|
|
109
|
+
export declare function projectKeysFor(projects: Record<string, unknown> | undefined, cwd: string, platform?: NodeJS.Platform): string[];
|
|
110
|
+
/**
|
|
111
|
+
* The local scope's servers for `cwd`, as Claude Code sees them: the first key
|
|
112
|
+
* it reads that holds any. Only when it has no key here at all is a legacy
|
|
113
|
+
* spelling of `cwd` itself (a backslash key, or the directory rather than its
|
|
114
|
+
* git root) consulted — so an install made by an older runner is still found
|
|
115
|
+
* and can be repaired, without reporting entries Claude Code never uses as live.
|
|
60
116
|
*/
|
|
61
117
|
export declare function projectScope(projects: Record<string, {
|
|
62
118
|
mcpServers?: Record<string, McpServerConfig>;
|
|
63
119
|
}> | undefined, cwd: string): Record<string, McpServerConfig>;
|
|
64
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* The key under which Claude Code stores (or would store) this cwd's local
|
|
122
|
+
* scope. A key spelled exactly like `cwd` is kept (it is where an older install
|
|
123
|
+
* put an entry, and where uninstall must look for it); otherwise the key
|
|
124
|
+
* Claude Code reads. It used to fall back to the raw cwd, which on Windows is a
|
|
125
|
+
* backslash key nothing ever reads.
|
|
126
|
+
*/
|
|
65
127
|
export declare function projectKeyFor(projects: Record<string, unknown> | undefined, cwd: string): string;
|
|
66
128
|
/** Every MCP server that would be live for `cwd`, with the scope that won. */
|
|
67
129
|
export declare function resolveServers(cwd: string, opts?: ResolveOptions): ResolvedServer[];
|
package/dist/config/resolve.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Claude Code merges three scopes, highest precedence first. The whole winning
|
|
10
10
|
* entry is used — fields are never merged across scopes:
|
|
11
|
-
* 1. **Local** — `~/.claude.json` → `projects[<
|
|
11
|
+
* 1. **Local** — `~/.claude.json` → `projects[<project key>].mcpServers`,
|
|
12
|
+
* where the key is the canonical git root (see
|
|
13
|
+
* {@link claudeProjectKey}), not necessarily the cwd
|
|
12
14
|
* 2. **Project** — `<cwd>/.mcp.json` → `mcpServers`
|
|
13
15
|
* 3. **User** — `~/.claude.json` → top-level `mcpServers`
|
|
14
16
|
*
|
|
@@ -19,9 +21,9 @@
|
|
|
19
21
|
* config. Matching the literal key misses every time; matching a *normalised*
|
|
20
22
|
* path, and refusing to stop at an empty twin, is what makes local scope work.
|
|
21
23
|
*/
|
|
22
|
-
import { readFileSync } from "node:fs";
|
|
24
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
23
25
|
import { homedir } from "node:os";
|
|
24
|
-
import { join } from "node:path";
|
|
26
|
+
import { basename, dirname, join, normalize as normalizeNative, resolve as resolvePath } from "node:path";
|
|
25
27
|
import { normalizePath } from "../control/paths.js";
|
|
26
28
|
export function isRemote(config) {
|
|
27
29
|
return (config.type === "http" || config.type === "sse");
|
|
@@ -47,6 +49,14 @@ export function parseJsonFile(path) {
|
|
|
47
49
|
catch {
|
|
48
50
|
return undefined; // absent / unreadable — "not configured"
|
|
49
51
|
}
|
|
52
|
+
return parseJsonText(raw, path);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse the text of a JSON file (BOM tolerated). Throws, naming `path`, when
|
|
56
|
+
* it is not JSON — a file with comments (JSONC) included, which is exactly the
|
|
57
|
+
* file that must be refused rather than rewritten without its comments.
|
|
58
|
+
*/
|
|
59
|
+
export function parseJsonText(raw, path) {
|
|
50
60
|
try {
|
|
51
61
|
return JSON.parse(stripBom(raw));
|
|
52
62
|
}
|
|
@@ -63,18 +73,172 @@ function readMcpServers(path, required) {
|
|
|
63
73
|
}
|
|
64
74
|
return parsed.mcpServers ?? {};
|
|
65
75
|
}
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The file Claude Code keeps its user and local scopes in.
|
|
78
|
+
*
|
|
79
|
+
* `~/.claude.json` — unless `CLAUDE_CONFIG_DIR` is set, when Claude Code reads
|
|
80
|
+
* `$CLAUDE_CONFIG_DIR/.claude.json` instead (checked against Claude Code
|
|
81
|
+
* itself). Writing the home-directory file then wraps servers in a file no
|
|
82
|
+
* session reads: `bir setup` says "wrapped" and nothing is. `BIR_CLAUDE_JSON`
|
|
83
|
+
* (tests, a second profile) still names it outright. An empty variable counts
|
|
84
|
+
* as unset, the way Claude Code treats one.
|
|
85
|
+
*/
|
|
86
|
+
export function claudeJsonPath(override, env = process.env) {
|
|
87
|
+
if (override)
|
|
88
|
+
return override;
|
|
89
|
+
if (env.BIR_CLAUDE_JSON)
|
|
90
|
+
return env.BIR_CLAUDE_JSON;
|
|
91
|
+
return join(claudeConfigHome(env) ?? homedir(), ".claude.json");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `CLAUDE_CONFIG_DIR` when it is set: where Claude Code keeps `.claude.json`,
|
|
95
|
+
* `settings.json` and the rest of what normally lives in `~/.claude`.
|
|
96
|
+
*/
|
|
97
|
+
export function claudeConfigHome(env = process.env) {
|
|
98
|
+
return env.CLAUDE_CONFIG_DIR || undefined;
|
|
99
|
+
}
|
|
100
|
+
// ── the local scope's key, as Claude Code computes it ──────────────────────
|
|
101
|
+
//
|
|
102
|
+
// Read out of Claude Code itself rather than guessed, because a local-scope
|
|
103
|
+
// entry written under any other key is silently never used: Claude Code keys
|
|
104
|
+
// `projects` by the project's CANONICAL GIT ROOT when the directory is inside
|
|
105
|
+
// a repository (walking up for a `.git` directory or file; a linked worktree
|
|
106
|
+
// maps to its main checkout), and by the directory itself otherwise — then
|
|
107
|
+
// normalises it and, on Windows, turns every backslash into a forward slash.
|
|
108
|
+
// The drive letter keeps whatever case the launching shell reported, which is
|
|
109
|
+
// why `c:/x` and `C:/x` routinely sit side by side: one per way Claude Code has
|
|
110
|
+
// been started there.
|
|
111
|
+
/** True when `dir` has a `.git` directory or gitfile (not a symlink). */
|
|
112
|
+
function hasGitEntry(dir) {
|
|
113
|
+
try {
|
|
114
|
+
const st = lstatSync(join(dir, ".git"));
|
|
115
|
+
return st.isDirectory() || st.isFile();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* A linked worktree's main checkout, the way Claude Code resolves it: only when
|
|
123
|
+
* the gitfile, the `commondir` file and the back-pointing `gitdir` file all
|
|
124
|
+
* agree. Anything unusual (a submodule, a hand-edited file) keeps `root`.
|
|
125
|
+
*/
|
|
126
|
+
function canonicalGitRoot(root) {
|
|
127
|
+
try {
|
|
128
|
+
const dotGit = join(root, ".git");
|
|
129
|
+
if (!statSync(dotGit).isFile())
|
|
130
|
+
return root;
|
|
131
|
+
const text = readFileSync(dotGit, "utf8").trim();
|
|
132
|
+
if (!text.startsWith("gitdir:"))
|
|
133
|
+
return root;
|
|
134
|
+
const gitdir = resolvePath(root, text.slice("gitdir:".length).trim());
|
|
135
|
+
const commondirFile = join(gitdir, "commondir");
|
|
136
|
+
if (!existsSync(commondirFile))
|
|
137
|
+
return root;
|
|
138
|
+
const common = resolvePath(gitdir, readFileSync(commondirFile, "utf8").trim());
|
|
139
|
+
if (normalizePath(dirname(gitdir)) !== normalizePath(join(common, "worktrees")))
|
|
140
|
+
return root;
|
|
141
|
+
const back = readFileSync(join(gitdir, "gitdir"), "utf8").trim();
|
|
142
|
+
if (normalizePath(resolvePath(gitdir, back)) !== normalizePath(dotGit))
|
|
143
|
+
return root;
|
|
144
|
+
if (basename(common) !== ".git")
|
|
145
|
+
return hasGitEntry(common) ? root : common;
|
|
146
|
+
return dirname(common);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return root;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The directory Claude Code keys this cwd's local scope (and its trust and
|
|
154
|
+
* permission state) under: the canonical git root, else `cwd` itself.
|
|
155
|
+
*/
|
|
156
|
+
export function claudeProjectRoot(cwd) {
|
|
157
|
+
let dir = resolvePath(cwd);
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (hasGitEntry(dir))
|
|
160
|
+
return canonicalGitRoot(dir);
|
|
161
|
+
const parent = dirname(dir);
|
|
162
|
+
if (parent === dir)
|
|
163
|
+
return resolvePath(cwd);
|
|
164
|
+
dir = parent;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The linked worktrees (`git worktree add`) of the repository whose main
|
|
169
|
+
* checkout is `root`, as directories that still exist.
|
|
170
|
+
*
|
|
171
|
+
* Claude Code keys their local scope under `root` too (see
|
|
172
|
+
* {@link canonicalGitRoot}), so a local-scope server written for `root` also
|
|
173
|
+
* runs in every one of them — sessions nobody set up, on branches whose
|
|
174
|
+
* `.mcp.json` may say something else. `bir install` names them, and leaves a
|
|
175
|
+
* server alone where a worktree's own `.mcp.json` defines it differently.
|
|
176
|
+
*/
|
|
177
|
+
export function linkedWorktrees(root) {
|
|
178
|
+
const admin = join(root, ".git", "worktrees");
|
|
179
|
+
let names;
|
|
180
|
+
try {
|
|
181
|
+
if (!statSync(join(root, ".git")).isDirectory())
|
|
182
|
+
return [];
|
|
183
|
+
names = readdirSync(admin);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const found = [];
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
try {
|
|
191
|
+
const dotGit = resolvePath(join(admin, name), readFileSync(join(admin, name, "gitdir"), "utf8").trim());
|
|
192
|
+
const dir = dirname(dotGit);
|
|
193
|
+
if (existsSync(dotGit) && normalizePath(dir) !== normalizePath(root))
|
|
194
|
+
found.push(dir);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* a pruned or half-made worktree */
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return found.sort();
|
|
201
|
+
}
|
|
202
|
+
/** {@link claudeProjectRoot} spelled the way Claude Code spells a `projects` key. */
|
|
203
|
+
export function claudeProjectKey(cwd, platform = process.platform) {
|
|
204
|
+
const root = normalizeNative(claudeProjectRoot(cwd));
|
|
205
|
+
return platform === "win32" ? root.replace(/\\/g, "/") : root;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Every existing `projects` key Claude Code could read for this cwd — the
|
|
209
|
+
* spellings of its canonical root that it wrote (forward slashes on Windows;
|
|
210
|
+
* both drive-letter cases when both exist) — or, when it has never run here,
|
|
211
|
+
* the one key it will create. Never a backslash key: Claude Code does not read
|
|
212
|
+
* one, and writing there is the trap that made entries invisible to `/mcp`.
|
|
213
|
+
*/
|
|
214
|
+
export function projectKeysFor(projects, cwd, platform = process.platform) {
|
|
215
|
+
const canonical = claudeProjectKey(cwd, platform);
|
|
216
|
+
const want = normalizePath(canonical);
|
|
217
|
+
const found = Object.keys(projects ?? {}).filter((key) => normalizePath(key) === want && !(platform === "win32" && key.includes("\\")));
|
|
218
|
+
if (found.length === 0)
|
|
219
|
+
return [canonical];
|
|
220
|
+
// The exact canonical spelling first, so single-key callers use it.
|
|
221
|
+
return found.sort((a, b) => Number(b === canonical) - Number(a === canonical));
|
|
68
222
|
}
|
|
69
223
|
/**
|
|
70
|
-
* The local scope's servers for `cwd
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
224
|
+
* The local scope's servers for `cwd`, as Claude Code sees them: the first key
|
|
225
|
+
* it reads that holds any. Only when it has no key here at all is a legacy
|
|
226
|
+
* spelling of `cwd` itself (a backslash key, or the directory rather than its
|
|
227
|
+
* git root) consulted — so an install made by an older runner is still found
|
|
228
|
+
* and can be repaired, without reporting entries Claude Code never uses as live.
|
|
74
229
|
*/
|
|
75
230
|
export function projectScope(projects, cwd) {
|
|
76
231
|
if (!projects)
|
|
77
232
|
return {};
|
|
233
|
+
const readable = projectKeysFor(projects, cwd).filter((key) => key in projects);
|
|
234
|
+
if (readable.length > 0) {
|
|
235
|
+
for (const key of readable) {
|
|
236
|
+
const servers = projects[key]?.mcpServers;
|
|
237
|
+
if (servers && Object.keys(servers).length > 0)
|
|
238
|
+
return servers;
|
|
239
|
+
}
|
|
240
|
+
return projects[readable[0]]?.mcpServers ?? {};
|
|
241
|
+
}
|
|
78
242
|
const exact = projects[cwd]?.mcpServers;
|
|
79
243
|
if (exact && Object.keys(exact).length > 0)
|
|
80
244
|
return exact;
|
|
@@ -86,18 +250,25 @@ export function projectScope(projects, cwd) {
|
|
|
86
250
|
}
|
|
87
251
|
return exact ?? {};
|
|
88
252
|
}
|
|
89
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* The key under which Claude Code stores (or would store) this cwd's local
|
|
255
|
+
* scope. A key spelled exactly like `cwd` is kept (it is where an older install
|
|
256
|
+
* put an entry, and where uninstall must look for it); otherwise the key
|
|
257
|
+
* Claude Code reads. It used to fall back to the raw cwd, which on Windows is a
|
|
258
|
+
* backslash key nothing ever reads.
|
|
259
|
+
*/
|
|
90
260
|
export function projectKeyFor(projects, cwd) {
|
|
91
|
-
if (
|
|
92
|
-
return cwd;
|
|
93
|
-
if (cwd in projects)
|
|
261
|
+
if (projects && cwd in projects)
|
|
94
262
|
return cwd;
|
|
263
|
+
const keys = projectKeysFor(projects, cwd);
|
|
264
|
+
if (projects && keys[0] in projects)
|
|
265
|
+
return keys[0];
|
|
95
266
|
const want = normalizePath(cwd);
|
|
96
|
-
for (const key of Object.keys(projects)) {
|
|
267
|
+
for (const key of Object.keys(projects ?? {})) {
|
|
97
268
|
if (normalizePath(key) === want)
|
|
98
269
|
return key;
|
|
99
270
|
}
|
|
100
|
-
return
|
|
271
|
+
return keys[0];
|
|
101
272
|
}
|
|
102
273
|
/** Every MCP server that would be live for `cwd`, with the scope that won. */
|
|
103
274
|
export function resolveServers(cwd, opts = {}) {
|
package/dist/control/client.d.ts
CHANGED
|
@@ -23,6 +23,19 @@ export interface ProxyWork {
|
|
|
23
23
|
arguments: unknown;
|
|
24
24
|
timeoutMs: number;
|
|
25
25
|
}
|
|
26
|
+
/** What one long-poll came back with (see {@link ControlClient.pollWork}). */
|
|
27
|
+
export interface PollOutcome {
|
|
28
|
+
/** False when the control server could not be reached, or refused us. */
|
|
29
|
+
reachable: boolean;
|
|
30
|
+
work?: ProxyWork;
|
|
31
|
+
/**
|
|
32
|
+
* The server said calculated replay is off here (`{ replay: false }`). A
|
|
33
|
+
* proxy drops to its idle pace (one poll every 30 s) rather than ask again
|
|
34
|
+
* four times a second — but does not stop: replay is turned back on by a
|
|
35
|
+
* restart that never makes a bound proxy register again.
|
|
36
|
+
*/
|
|
37
|
+
replayOff?: boolean;
|
|
38
|
+
}
|
|
26
39
|
/**
|
|
27
40
|
* Who a proxy is, sent with every register, poll and step report. The control
|
|
28
41
|
* server uses it to run a session's replay steps on that session's own upstream
|
|
@@ -65,9 +78,18 @@ export declare class ControlClient {
|
|
|
65
78
|
* with a small delay so a dead server is not busy-looped.
|
|
66
79
|
*/
|
|
67
80
|
poll(serverName: string, holdMs: number): Promise<ProxyWork | undefined>;
|
|
81
|
+
/**
|
|
82
|
+
* {@link poll}, with enough of the answer to pace the loop: whether the
|
|
83
|
+
* server was reachable at all, and whether it said replay is off. An empty
|
|
84
|
+
* answer that came back at once means the server is not holding polls for
|
|
85
|
+
* us, and the caller backs off.
|
|
86
|
+
*/
|
|
87
|
+
pollWork(serverName: string, holdMs: number): Promise<PollOutcome>;
|
|
68
88
|
/** Hand back one dispatched call's result, or the reason it could not run. */
|
|
69
89
|
result(workId: string, result?: unknown, error?: string): Promise<void>;
|
|
70
90
|
private post;
|
|
71
91
|
private request;
|
|
92
|
+
/** One HTTP exchange. Never throws; `ok: false` covers refused, timed out and non-2xx. */
|
|
93
|
+
private exchange;
|
|
72
94
|
}
|
|
73
95
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/control/client.js
CHANGED
|
@@ -47,8 +47,20 @@ export class ControlClient {
|
|
|
47
47
|
* with a small delay so a dead server is not busy-looped.
|
|
48
48
|
*/
|
|
49
49
|
async poll(serverName, holdMs) {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
return (await this.pollWork(serverName, holdMs)).work;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* {@link poll}, with enough of the answer to pace the loop: whether the
|
|
54
|
+
* server was reachable at all, and whether it said replay is off. An empty
|
|
55
|
+
* answer that came back at once means the server is not holding polls for
|
|
56
|
+
* us, and the caller backs off.
|
|
57
|
+
*/
|
|
58
|
+
async pollWork(serverName, holdMs) {
|
|
59
|
+
const outcome = await this.exchange("POST", "/proxy/poll", { ...this.identity, serverName, holdMs }, holdMs + 10_000);
|
|
60
|
+
if (!outcome.ok)
|
|
61
|
+
return { reachable: false };
|
|
62
|
+
const body = outcome.body;
|
|
63
|
+
return { reachable: true, work: body?.work, replayOff: body?.replay === false || undefined };
|
|
52
64
|
}
|
|
53
65
|
/** Hand back one dispatched call's result, or the reason it could not run. */
|
|
54
66
|
async result(workId, result, error) {
|
|
@@ -58,6 +70,11 @@ export class ControlClient {
|
|
|
58
70
|
return this.request("POST", path, body);
|
|
59
71
|
}
|
|
60
72
|
async request(method, path, body, timeoutMs = this.timeoutMs) {
|
|
73
|
+
const outcome = await this.exchange(method, path, body, timeoutMs);
|
|
74
|
+
return outcome.ok ? outcome.body : undefined;
|
|
75
|
+
}
|
|
76
|
+
/** One HTTP exchange. Never throws; `ok: false` covers refused, timed out and non-2xx. */
|
|
77
|
+
async exchange(method, path, body, timeoutMs = this.timeoutMs) {
|
|
61
78
|
const controller = new AbortController();
|
|
62
79
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
80
|
timer.unref?.();
|
|
@@ -73,21 +90,23 @@ export class ControlClient {
|
|
|
73
90
|
});
|
|
74
91
|
if (!res.ok) {
|
|
75
92
|
logDetail("control.http_error", { path, status: res.status });
|
|
76
|
-
|
|
93
|
+
// Drain, so the connection is released rather than held to its timeout.
|
|
94
|
+
await res.text().catch(() => undefined);
|
|
95
|
+
return { ok: false, status: res.status };
|
|
77
96
|
}
|
|
78
97
|
const text = await res.text();
|
|
79
98
|
if (!text)
|
|
80
|
-
return {};
|
|
99
|
+
return { ok: true, status: res.status, body: {} };
|
|
81
100
|
try {
|
|
82
|
-
return JSON.parse(text);
|
|
101
|
+
return { ok: true, status: res.status, body: JSON.parse(text) };
|
|
83
102
|
}
|
|
84
103
|
catch {
|
|
85
|
-
return {};
|
|
104
|
+
return { ok: true, status: res.status, body: {} };
|
|
86
105
|
}
|
|
87
106
|
}
|
|
88
107
|
catch (err) {
|
|
89
108
|
logDetail("control.unreachable", { path, error: errText(err) });
|
|
90
|
-
return
|
|
109
|
+
return { ok: false };
|
|
91
110
|
}
|
|
92
111
|
finally {
|
|
93
112
|
clearTimeout(timer);
|
|
@@ -10,21 +10,32 @@
|
|
|
10
10
|
* - the **proxy** contributes the true upstream result, `isError`,
|
|
11
11
|
* `structuredContent` and real execution duration.
|
|
12
12
|
*
|
|
13
|
-
* MECHANISM. At `PreToolUse` the hook
|
|
13
|
+
* MECHANISM. At `PreToolUse` the hook may answer with
|
|
14
14
|
* `updatedInput = { ...original, [BIR_CALL_ID]: id }`. Claude Code passes those
|
|
15
15
|
* arguments through as the call's `arguments`, the proxy strips the key before
|
|
16
16
|
* relaying, and echoes it on the step report. The control server joins on it.
|
|
17
17
|
*
|
|
18
|
+
* NOT BY DEFAULT. Carrying an id means answering `allow`, and `allow` skips
|
|
19
|
+
* the permission prompt the user would otherwise have seen for that call. So
|
|
20
|
+
* the control server injects only where that changes nothing (a session in
|
|
21
|
+
* `bypassPermissions` mode, a replay-pinned step) or where the operator opted
|
|
22
|
+
* in (`BIR_CORRELATION_DECISION=allow`); everywhere else the hook stays silent
|
|
23
|
+
* and the report joins by fingerprint — below.
|
|
24
|
+
*
|
|
18
25
|
* THE COST, INHERITED KNOWINGLY from RRepeat's sentinel: carrying an extra
|
|
19
26
|
* argument requires **relaxing the tool's schema** (`additionalProperties: true`
|
|
20
27
|
* plus the declared key), and that relaxation is visible to the model on every
|
|
21
28
|
* call, not only recorded ones. Hence {@link relaxSchema} is applied only when
|
|
22
29
|
* Tier 1 correlation is actually active, and only to wrapped servers.
|
|
23
30
|
*
|
|
24
|
-
* THE
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
31
|
+
* THE FINGERPRINT JOIN — the default, and all `--no-correlation` uses — matches
|
|
32
|
+
* on `(serverName, toolName, hash(args))`, the args hashed *raw*, as the model
|
|
33
|
+
* wrote them, before any redaction or clipping (the proxy sends that hash as
|
|
34
|
+
* `argsFingerprint`; an older proxy's report is hashed as reported), within
|
|
35
|
+
* the time a user may take to approve the call. It is lossy
|
|
36
|
+
* under identical concurrent calls — two parallel calls with the same arguments
|
|
37
|
+
* are genuinely indistinguishable without an id — and says so rather than
|
|
38
|
+
* pretending otherwise.
|
|
28
39
|
*/
|
|
29
40
|
/** The argument key that carries the call id from hook to proxy. */
|
|
30
41
|
export declare const BIR_CALL_ID = "__bir_call_id__";
|
|
@@ -42,6 +53,13 @@ export interface ProxyStepReport {
|
|
|
42
53
|
qualifiedName: string;
|
|
43
54
|
/** Post-redaction. */
|
|
44
55
|
args: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* The fingerprint join key over the call's *raw* arguments, taken by the
|
|
58
|
+
* proxy before redaction and clipping (proxy/intercept.ts). `args` above is
|
|
59
|
+
* what is recorded, and hashing it would miss the hook-side twin whenever
|
|
60
|
+
* redaction or clipping changed anything. Absent from older proxies.
|
|
61
|
+
*/
|
|
62
|
+
argsFingerprint?: string;
|
|
45
63
|
/** Whole CallToolResult, post-redaction and truncation. */
|
|
46
64
|
result?: unknown;
|
|
47
65
|
isError: boolean;
|
|
Binary file
|