@savvy-web/mcp 3.4.17 → 3.4.18
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/index.d.ts +13 -6
- package/package.json +7 -7
- package/runtime.js +1 -1
- package/toolkit.js +15 -4
- package/tools/biome-check.js +122 -110
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Distribution } from "@effected/engine";
|
|
2
2
|
import { ToolRefusal } from "@effected/mcp";
|
|
3
3
|
import { FileSystem, Layer, Path, Schema, Stdio } from "effect";
|
|
4
|
+
import { ToolDiscovery } from "@effected/commands";
|
|
5
|
+
import { Git } from "@effected/git";
|
|
4
6
|
import { WorkspaceRoot } from "@effected/workspaces";
|
|
5
7
|
import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
|
|
6
8
|
import { ChildProcessSpawner } from "effect/process";
|
|
@@ -71,11 +73,13 @@ export type McpToolError = typeof McpToolError.Type;
|
|
|
71
73
|
* {@link makeSilkRuntimeLayer} passes both through onto its own output via an
|
|
72
74
|
* `Effect.context` identity layer, so they survive `main.ts`'s
|
|
73
75
|
* `Layer.provide(NodeServices.layer)` instead of being fully discharged by
|
|
74
|
-
* it.
|
|
76
|
+
* it. `ChildProcessSpawner` is passed through the same way, and `Git` (from the
|
|
77
|
+
* kit graph) plus the shared `ToolDiscovery` are exposed, because
|
|
78
|
+
* `biome_check` yields all three directly.
|
|
75
79
|
*
|
|
76
80
|
* @public
|
|
77
81
|
*/
|
|
78
|
-
type McpServices = SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore | Repos.ReposDrift | FileSystem.FileSystem | Path.Path;
|
|
82
|
+
type McpServices = SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore | Repos.ReposDrift | Git | ToolDiscovery | ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path;
|
|
79
83
|
/**
|
|
80
84
|
* Build the MCP runtime layer for a workspace root. Provides
|
|
81
85
|
* `SilkWorkspaceAnalyzer`, `WorkspaceRoot`, `Turbo.TurboInspector`,
|
|
@@ -137,8 +141,9 @@ interface ServerOptions {
|
|
|
137
141
|
export declare const ServerLayer: (cwd: string, options?: ServerOptions) => Layer.Layer<never, never, PlatformServices>;
|
|
138
142
|
/**
|
|
139
143
|
* The `biome_check` tool value. Mutating when `write`/`unsafe` is set, so it is
|
|
140
|
-
* annotated non-read-only and non-idempotent
|
|
141
|
-
*
|
|
144
|
+
* annotated non-read-only and non-idempotent. `dependencies` lists what the
|
|
145
|
+
* handler yields: `Git` (worktree containment), `ToolDiscovery` (the Biome
|
|
146
|
+
* binary) and core's `ChildProcessSpawner` (`Run.collect`).
|
|
142
147
|
*/
|
|
143
148
|
declare const biomeCheckTool: Tool.Tool<"biome_check", {
|
|
144
149
|
readonly parameters: Schema.Struct<{
|
|
@@ -170,7 +175,7 @@ declare const biomeCheckTool: Tool.Tool<"biome_check", {
|
|
|
170
175
|
}>;
|
|
171
176
|
readonly failure: Schema.Union<readonly [typeof WorkspaceNotFound, typeof EngineError, typeof BiomeFailed, typeof ToolRefusal]>;
|
|
172
177
|
readonly failureMode: "error";
|
|
173
|
-
},
|
|
178
|
+
}, ChildProcessSpawner.ChildProcessSpawner | Git | ToolDiscovery>;
|
|
174
179
|
/** The `changeset_deps_detect` tool value. */
|
|
175
180
|
declare const changesetDepsDetectTool: Tool.Tool<"changeset_deps_detect", {
|
|
176
181
|
readonly parameters: Schema.Struct<{
|
|
@@ -672,7 +677,9 @@ export declare const SilkToolkit: Toolkit.Toolkit<SilkTools>;
|
|
|
672
677
|
* The handler layer. `cwd` — the project directory `main.ts` resolved once at
|
|
673
678
|
* startup — is closed over as every handler's fallback when a call omits its
|
|
674
679
|
* own `cwd`; the handlers' declared service dependencies are discharged by
|
|
675
|
-
* `makeSilkRuntimeLayer(cwd)` in `server.ts`.
|
|
680
|
+
* `makeSilkRuntimeLayer(cwd)` in `server.ts`. Building the layer mints the
|
|
681
|
+
* server's ONE `Exclusive` write lane, so concurrent `biome_check` write passes
|
|
682
|
+
* are serialized per server.
|
|
676
683
|
*
|
|
677
684
|
* @public
|
|
678
685
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/mcp",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.18",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The savvy MCP server — Silk Suite tooling and library knowledge for coding agents",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/mcp",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@effect/platform-node": "^4.0.6",
|
|
40
|
-
"@effected/commands": "^0.
|
|
41
|
-
"@effected/engine": "^0.
|
|
42
|
-
"@effected/git": "^0.
|
|
43
|
-
"@effected/mcp": "^0.
|
|
40
|
+
"@effected/commands": "^0.17.0",
|
|
41
|
+
"@effected/engine": "^0.9.0",
|
|
42
|
+
"@effected/git": "^0.26.0",
|
|
43
|
+
"@effected/mcp": "^0.10.0",
|
|
44
44
|
"@effected/templates": "^0.14.1",
|
|
45
|
-
"@effected/workspaces": "^0.39.
|
|
46
|
-
"@savvy-web/silk-effects": "^10.
|
|
45
|
+
"@effected/workspaces": "^0.39.1",
|
|
46
|
+
"@savvy-web/silk-effects": "^10.2.0",
|
|
47
47
|
"effect": "^4.0.6"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/runtime.js
CHANGED
|
@@ -73,7 +73,7 @@ const makeSilkRuntimeLayer = (cwd) => {
|
|
|
73
73
|
*/
|
|
74
74
|
const repos = Layer.mergeAll(Repos.ReposConfigStore.layer, Repos.ReposManager.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer), Layer.provide(Repos.ReposLockdown.layer)), Repos.ReposDrift.layer.pipe(Layer.provide(Repos.ReposConfigStore.layer)));
|
|
75
75
|
const platformPassthrough = Layer.effectContext(Effect.context());
|
|
76
|
-
return Layer.mergeAll(SilkWorkspaceAnalyzer.layer.pipe(Layer.provide(analyzerDeps)), Turbo.TurboInspector.layer.pipe(Layer.provide(toolDiscovery)), inspectorAndAnalyzer, depsRegen, repos, platformPassthrough).pipe(Layer.provideMerge(kitGraph));
|
|
76
|
+
return Layer.mergeAll(SilkWorkspaceAnalyzer.layer.pipe(Layer.provide(analyzerDeps)), Turbo.TurboInspector.layer.pipe(Layer.provide(toolDiscovery)), toolDiscovery, inspectorAndAnalyzer, depsRegen, repos, platformPassthrough).pipe(Layer.provideMerge(kitGraph));
|
|
77
77
|
};
|
|
78
78
|
|
|
79
79
|
export { makeSilkRuntimeLayer };
|
package/toolkit.js
CHANGED
|
@@ -8,10 +8,19 @@ import { handleReposInspect, reposInspectTool } from "./tools/repos-inspect.js";
|
|
|
8
8
|
import { handleReposManage, reposManageTool } from "./tools/repos-manage.js";
|
|
9
9
|
import { handleTurboInspect, turboInspectTool } from "./tools/turbo-inspect.js";
|
|
10
10
|
import { handleWorkspaceInfo, workspaceInfoTool } from "./tools/workspace-info.js";
|
|
11
|
+
import { Effect } from "effect";
|
|
12
|
+
import { Exclusive } from "@effected/engine";
|
|
11
13
|
import { Toolkit } from "effect/ai";
|
|
12
14
|
|
|
13
15
|
// src/toolkit.ts
|
|
14
16
|
/**
|
|
17
|
+
* Where the ten tool values and their handlers meet: {@link SilkToolkit} is
|
|
18
|
+
* the served toolkit a structural test can import with no bin and no platform
|
|
19
|
+
* layer; {@link ToolsLayer} binds every handler to one project directory.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
15
24
|
* The ten savvy-mcp tools — seven read-only, three mutating (`biome_check`
|
|
16
25
|
* with `write`/`unsafe`, `changeset_deps_regen`, `repos_manage`) — in the
|
|
17
26
|
* order `tools/list` serves them.
|
|
@@ -23,11 +32,13 @@ const SilkToolkit = Toolkit.make(workspaceInfoTool, turboInspectTool, changesetI
|
|
|
23
32
|
* The handler layer. `cwd` — the project directory `main.ts` resolved once at
|
|
24
33
|
* startup — is closed over as every handler's fallback when a call omits its
|
|
25
34
|
* own `cwd`; the handlers' declared service dependencies are discharged by
|
|
26
|
-
* `makeSilkRuntimeLayer(cwd)` in `server.ts`.
|
|
35
|
+
* `makeSilkRuntimeLayer(cwd)` in `server.ts`. Building the layer mints the
|
|
36
|
+
* server's ONE `Exclusive` write lane, so concurrent `biome_check` write passes
|
|
37
|
+
* are serialized per server.
|
|
27
38
|
*
|
|
28
39
|
* @public
|
|
29
40
|
*/
|
|
30
|
-
const ToolsLayer = (cwd) => SilkToolkit.toLayer({
|
|
41
|
+
const ToolsLayer = (cwd) => SilkToolkit.toLayer(Effect.map(Exclusive.make, (biomeWriteLane) => ({
|
|
31
42
|
workspace_info: (params) => handleWorkspaceInfo(cwd, params),
|
|
32
43
|
turbo_inspect: (params) => handleTurboInspect(cwd, params),
|
|
33
44
|
changeset_inspect: (params) => handleChangesetInspect(cwd, params),
|
|
@@ -37,7 +48,7 @@ const ToolsLayer = (cwd) => SilkToolkit.toLayer({
|
|
|
37
48
|
changeset_deps_regen: (params) => handleChangesetDepsRegen(cwd, params),
|
|
38
49
|
repos_inspect: (params) => handleReposInspect(cwd, params),
|
|
39
50
|
repos_manage: handleReposManage(cwd),
|
|
40
|
-
biome_check: (params) => handleBiomeCheck(cwd, params)
|
|
41
|
-
});
|
|
51
|
+
biome_check: (params) => handleBiomeCheck(cwd, params, biomeWriteLane)
|
|
52
|
+
})));
|
|
42
53
|
|
|
43
54
|
export { SilkToolkit, ToolsLayer };
|
package/tools/biome-check.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { BiomeFailed, McpToolError } from "../errors.js";
|
|
2
2
|
import { ToolFailure, ToolRefusal } from "@effected/mcp";
|
|
3
|
-
import { Effect, Schema } from "effect";
|
|
4
|
-
import {
|
|
5
|
-
import { Tool } from "effect/ai";
|
|
6
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { Effect, Option, Schema } from "effect";
|
|
4
|
+
import { Run, Tool, ToolDiscovery } from "@effected/commands";
|
|
5
|
+
import { Tool as Tool$1 } from "effect/ai";
|
|
7
6
|
import { realpathSync } from "node:fs";
|
|
8
7
|
import { relative, resolve, sep } from "node:path";
|
|
8
|
+
import { Git, TreeIdentity } from "@effected/git";
|
|
9
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
9
10
|
|
|
10
11
|
// src/tools/biome-check.ts
|
|
11
12
|
/**
|
|
@@ -136,8 +137,10 @@ const buildBiomeResult = (params) => {
|
|
|
136
137
|
};
|
|
137
138
|
};
|
|
138
139
|
/**
|
|
139
|
-
* Canonicalize a path, falling back to lexical resolution when
|
|
140
|
-
* (a non-existent target cannot be a symlink pointing out of
|
|
140
|
+
* Canonicalize a caller-supplied path, falling back to lexical resolution when
|
|
141
|
+
* it does not exist (a non-existent target cannot be a symlink pointing out of
|
|
142
|
+
* tree). Also applied to a sibling worktree's `TreeIdentity.toplevel`, so the
|
|
143
|
+
* containment root is spelled the same way as the realpath'd caller paths.
|
|
141
144
|
*/
|
|
142
145
|
const canonicalize = (p) => {
|
|
143
146
|
try {
|
|
@@ -146,52 +149,43 @@ const canonicalize = (p) => {
|
|
|
146
149
|
return resolve(p);
|
|
147
150
|
}
|
|
148
151
|
};
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"--git-common-dir",
|
|
157
|
-
"--show-toplevel"
|
|
158
|
-
], {
|
|
159
|
-
encoding: "utf8",
|
|
160
|
-
timeout: 1e4
|
|
161
|
-
});
|
|
162
|
-
if (res.error || (res.status ?? 1) !== 0) return null;
|
|
163
|
-
const [commonDir, topLevel] = (res.stdout ?? "").trim().split("\n");
|
|
164
|
-
if (!commonDir || !topLevel) return null;
|
|
165
|
-
return {
|
|
166
|
-
commonDir: canonicalize(commonDir.trim()),
|
|
167
|
-
topLevel: canonicalize(topLevel.trim())
|
|
168
|
-
};
|
|
169
|
-
};
|
|
152
|
+
/**
|
|
153
|
+
* The working-tree identity of `dir`, or `Option.none` when it is in no
|
|
154
|
+
* working tree. A git failure (git missing, too old for `--path-format`) also
|
|
155
|
+
* answers `none`: the caller then rejects the cwd, which is what an unknown
|
|
156
|
+
* identity must mean for a mutating tool.
|
|
157
|
+
*/
|
|
158
|
+
const treeIdentity = (git, dir) => git.treeIdentity(dir).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
170
159
|
/**
|
|
171
160
|
* Decide which directory tree this run may touch.
|
|
172
161
|
*
|
|
173
162
|
* @remarks A cwd inside the server's root keeps that root. A cwd OUTSIDE it is
|
|
174
|
-
* accepted only when it belongs to a DIFFERENT worktree of the same repository
|
|
175
|
-
*
|
|
176
|
-
* that worktree rather than the server's start directory
|
|
177
|
-
* meant a call from a sibling
|
|
178
|
-
*
|
|
163
|
+
* accepted only when it belongs to a DIFFERENT worktree of the same repository
|
|
164
|
+
* (`TreeIdentity.relation` answers `"sibling-worktree"`), and containment then
|
|
165
|
+
* follows that worktree's toplevel rather than the server's start directory
|
|
166
|
+
* (systems#482). Binding to the start directory meant a call from a sibling
|
|
167
|
+
* worktree silently mutated the main checkout, which in a parallel multi-agent
|
|
168
|
+
* session is another agent's tree.
|
|
169
|
+
*
|
|
170
|
+
* `"same-tree"` is rejected: `root` can be a strict subdirectory of its own
|
|
171
|
+
* worktree (dev tooling launches the server from packages/mcp/), and a cwd
|
|
172
|
+
* elsewhere in that same checkout must not widen a --write pass to the whole
|
|
173
|
+
* repo, including the .repos/** vendored trees. `"different-repo"`, or either
|
|
174
|
+
* directory being in no working tree, is rejected too.
|
|
179
175
|
*
|
|
180
176
|
* @param root - the server's workspace root, already canonicalized
|
|
181
177
|
* @param cwd - the requested working directory, already canonicalized
|
|
182
|
-
* @
|
|
183
|
-
* @returns the directory tree to contain to, or null if the cwd must be rejected
|
|
178
|
+
* @returns the directory tree to contain to, or `Option.none` to reject
|
|
184
179
|
*/
|
|
185
|
-
const resolveContainmentRoot = (root, cwd
|
|
186
|
-
if (cwd === root || cwd.startsWith(`${root}${sep}`)) return root;
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
};
|
|
180
|
+
const resolveContainmentRoot = Effect.fn("biome_check.resolveContainmentRoot")(function* (root, cwd) {
|
|
181
|
+
if (cwd === root || cwd.startsWith(`${root}${sep}`)) return Option.some(root);
|
|
182
|
+
const git = yield* Git;
|
|
183
|
+
const from = yield* treeIdentity(git, cwd);
|
|
184
|
+
if (Option.isNone(from)) return Option.none();
|
|
185
|
+
const home = yield* treeIdentity(git, root);
|
|
186
|
+
if (Option.isNone(home)) return Option.none();
|
|
187
|
+
return TreeIdentity.relation(from.value, home.value) === "sibling-worktree" ? Option.some(canonicalize(from.value.toplevel)) : Option.none();
|
|
188
|
+
});
|
|
195
189
|
/** Remediation for a path or cwd that escapes the containment tree. */
|
|
196
190
|
const CONTAINMENT_REMEDIATION = { hint: "Keep cwd and every path inside the server's workspace root, or inside a git worktree of the same repository." };
|
|
197
191
|
/** Remediation when no Biome binary can be located. */
|
|
@@ -205,81 +199,100 @@ const biomeFailed = (raw, exitCode) => new BiomeFailed({
|
|
|
205
199
|
remediation: BIOME_FAILED_REMEDIATION
|
|
206
200
|
});
|
|
207
201
|
/**
|
|
202
|
+
* The Biome tool, global copy first — the same lookup order as silk-effects'
|
|
203
|
+
* `Lint.Biome.findBiome` (`Command.findTool` resolves
|
|
204
|
+
* `Tool.named(name, { prefer: "global" })`). One module-level value, so every
|
|
205
|
+
* call hits the same `ToolDiscovery` probe-cache entry.
|
|
206
|
+
*/
|
|
207
|
+
const BIOME_TOOL = Tool.named("biome", { prefer: "global" });
|
|
208
|
+
/** Per-stream captured-output ceiling for one Biome run. */
|
|
209
|
+
const MAX_OUTPUT_BYTES = 67108864;
|
|
210
|
+
/**
|
|
211
|
+
* Hard cap so a hung Biome process (password prompt, deadlock, stalled
|
|
212
|
+
* watcher) can't block the MCP server. Expiry fails the run as a
|
|
213
|
+
* `CommandFailedError` of kind `"timeout"`.
|
|
214
|
+
*/
|
|
215
|
+
const BIOME_TIMEOUT = "120 seconds";
|
|
216
|
+
/**
|
|
217
|
+
* Backstop on the write lane, a little above {@link BIOME_TIMEOUT} so the run's
|
|
218
|
+
* own typed timeout fires first; the lane only bounds how long one write may
|
|
219
|
+
* hold the permit, never the wait for it.
|
|
220
|
+
*/
|
|
221
|
+
const WRITE_LANE_TIMEOUT = "125 seconds";
|
|
222
|
+
/**
|
|
223
|
+
* Re-scope a resolved Biome command to the request's `cwd` (a local
|
|
224
|
+
* resolution is scoped to the server's root by its `ExecContext`; paths are
|
|
225
|
+
* relative to `cwd`) and kill it with `SIGKILL` on timeout — core's default
|
|
226
|
+
* `SIGTERM` with no `forceKillAfter` would wait on a child that ignores it.
|
|
227
|
+
*/
|
|
228
|
+
const scoped = (command, cwd) => command._tag === "StandardCommand" ? ChildProcess.make(command.command, command.args, {
|
|
229
|
+
...command.options,
|
|
230
|
+
cwd,
|
|
231
|
+
killSignal: "SIGKILL"
|
|
232
|
+
}) : ChildProcess.setCwd(command, cwd);
|
|
233
|
+
/**
|
|
208
234
|
* Run Biome and return structured diagnostics. When `write`/`unsafe` is set,
|
|
209
235
|
* runs a fix pass first, then a read-only gitlab pass to report what remains.
|
|
210
236
|
*
|
|
211
|
-
* @remarks Resolves the Biome binary
|
|
212
|
-
*
|
|
213
|
-
* {@link McpToolError} member
|
|
214
|
-
* containment tree or when no binary
|
|
215
|
-
* exits with status > 1 (Biome itself failed,
|
|
216
|
-
* found)
|
|
217
|
-
*
|
|
237
|
+
* @remarks Resolves the Biome binary through `ToolDiscovery` (global first,
|
|
238
|
+
* then the project's package manager — {@link BIOME_TOOL}) and runs it with
|
|
239
|
+
* `Run.collect`. Fails with a {@link McpToolError} member: a `ToolRefusal` for
|
|
240
|
+
* a cwd/path outside the containment tree or when no binary resolves,
|
|
241
|
+
* {@link BiomeFailed} when Biome exits with status > 1 (Biome itself failed,
|
|
242
|
+
* vs. status 1 = lint issues found), cannot spawn, times out, or overflows the
|
|
243
|
+
* output cap. The write pass runs on `lane`, one permit per server, so two
|
|
244
|
+
* concurrent write calls never interleave on a tree.
|
|
245
|
+
*
|
|
246
|
+
* @param args - the decoded tool parameters
|
|
247
|
+
* @param fallbackCwd - the server's project directory
|
|
248
|
+
* @param lane - the server's write lane
|
|
218
249
|
*/
|
|
219
|
-
const runBiomeCheck =
|
|
250
|
+
const runBiomeCheck = Effect.fn("biome_check.run")(function* (args, fallbackCwd, lane) {
|
|
220
251
|
const mode = args.mode ?? "check";
|
|
221
252
|
const root = canonicalize(fallbackCwd);
|
|
222
253
|
const cwd = canonicalize(args.cwd ?? fallbackCwd);
|
|
223
|
-
const
|
|
224
|
-
if (
|
|
254
|
+
const contained = yield* resolveContainmentRoot(root, cwd);
|
|
255
|
+
if (Option.isNone(contained)) return yield* ToolRefusal.refuse(`cwd escapes the workspace root: ${ToolFailure.truncate(args.cwd ?? fallbackCwd)}.`, CONTAINMENT_REMEDIATION);
|
|
256
|
+
const containmentRoot = contained.value;
|
|
225
257
|
const within = (abs) => abs === containmentRoot || abs.startsWith(`${containmentRoot}${sep}`);
|
|
226
|
-
const
|
|
258
|
+
const rawPaths = args.paths && args.paths.length > 0 ? args.paths : ["."];
|
|
259
|
+
const paths = [];
|
|
260
|
+
for (const p of rawPaths) {
|
|
227
261
|
const lexical = resolve(cwd, p);
|
|
228
|
-
if (!within(canonicalize(lexical)))
|
|
229
|
-
|
|
230
|
-
}
|
|
262
|
+
if (!within(canonicalize(lexical))) return yield* ToolRefusal.refuse(`path escapes the workspace root: ${ToolFailure.truncate(p)}.`, CONTAINMENT_REMEDIATION);
|
|
263
|
+
paths.push(relative(cwd, lexical) || ".");
|
|
264
|
+
}
|
|
231
265
|
const doWrite = Boolean(args.write || args.unsafe);
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const timeout = 12e4;
|
|
239
|
-
const killSignal = "SIGKILL";
|
|
266
|
+
const biome = yield* (yield* ToolDiscovery).resolve(BIOME_TOOL).pipe(Effect.mapError((error) => error._tag === "ToolNotFoundError" ? ToolRefusal.refuse("Biome not found.", BIOME_REMEDIATION) : ToolRefusal.refuse(`Biome could not be resolved: ${error.message}`, BIOME_REMEDIATION)));
|
|
267
|
+
/** One Biome pass; exit 0/1 is a result, anything else a {@link BiomeFailed}. */
|
|
268
|
+
const pass = (label, argv) => Run.collect(scoped(biome.command(...argv), cwd), {
|
|
269
|
+
timeout: BIOME_TIMEOUT,
|
|
270
|
+
maxOutputBytes: MAX_OUTPUT_BYTES
|
|
271
|
+
}).pipe(Effect.mapError((error) => biomeFailed(`${label} could not run: ${error.message}`, error.exitCode)), Effect.flatMap((output) => output.exitCode > 1 ? Effect.fail(biomeFailed(`${label} failed (exit ${output.exitCode}): ${output.stderr.trim() || "unknown error"}`, output.exitCode)) : Effect.succeed(output)));
|
|
240
272
|
let wrote = false;
|
|
241
273
|
if (doWrite) {
|
|
242
274
|
const writeArgs = [
|
|
243
|
-
...prefix,
|
|
244
275
|
mode,
|
|
245
276
|
"--write",
|
|
246
277
|
...args.unsafe ? ["--unsafe"] : [],
|
|
247
278
|
"--no-errors-on-unmatched",
|
|
248
279
|
...paths
|
|
249
280
|
];
|
|
250
|
-
|
|
251
|
-
cwd,
|
|
252
|
-
encoding: "utf8",
|
|
253
|
-
maxBuffer,
|
|
254
|
-
timeout,
|
|
255
|
-
killSignal
|
|
256
|
-
});
|
|
257
|
-
if (fix.error) throw biomeFailed(`Biome --write could not run: ${fix.error.message}`);
|
|
258
|
-
if ((fix.status ?? 0) > 1) throw biomeFailed(`Biome --write failed (exit ${fix.status}): ${(fix.stderr ?? "").trim() || "unknown error"}`, fix.status ?? void 0);
|
|
281
|
+
yield* lane.run(pass("Biome --write", writeArgs), { timeout: WRITE_LANE_TIMEOUT }).pipe(Effect.catchTag("ExclusiveTimeout", (timeout) => Effect.fail(biomeFailed(`Biome --write did not finish: ${timeout.message}`))));
|
|
259
282
|
wrote = true;
|
|
260
283
|
}
|
|
261
|
-
const
|
|
262
|
-
...prefix,
|
|
284
|
+
const read = yield* pass("Biome", [
|
|
263
285
|
mode,
|
|
264
286
|
"--reporter=gitlab",
|
|
265
287
|
"--no-errors-on-unmatched",
|
|
266
288
|
...paths
|
|
267
|
-
];
|
|
268
|
-
const read = spawnSync(bin, readArgs, {
|
|
269
|
-
cwd,
|
|
270
|
-
encoding: "utf8",
|
|
271
|
-
maxBuffer,
|
|
272
|
-
timeout,
|
|
273
|
-
killSignal
|
|
274
|
-
});
|
|
275
|
-
if (read.error) throw biomeFailed(`Biome could not run: ${read.error.message}`);
|
|
276
|
-
if ((read.status ?? 0) > 1) throw biomeFailed(`Biome failed (exit ${read.status}): ${(read.stderr ?? "").trim() || "unknown error"}`, read.status ?? void 0);
|
|
289
|
+
]);
|
|
277
290
|
return buildBiomeResult({
|
|
278
|
-
diagnostics: parseBiomeGitlab(read.stdout
|
|
291
|
+
diagnostics: parseBiomeGitlab(read.stdout),
|
|
279
292
|
wrote,
|
|
280
293
|
...args.strict !== void 0 ? { strict: args.strict } : {}
|
|
281
294
|
});
|
|
282
|
-
};
|
|
295
|
+
});
|
|
283
296
|
/** Wire parameters for `biome_check`. */
|
|
284
297
|
const BiomeCheckParams = Schema.Struct({
|
|
285
298
|
paths: Schema.optionalKey(Schema.Array(Schema.String).annotate({ description: "Paths to check. Defaults to the whole workspace." })),
|
|
@@ -291,31 +304,30 @@ const BiomeCheckParams = Schema.Struct({
|
|
|
291
304
|
});
|
|
292
305
|
/**
|
|
293
306
|
* The `biome_check` tool value. Mutating when `write`/`unsafe` is set, so it is
|
|
294
|
-
* annotated non-read-only and non-idempotent
|
|
295
|
-
*
|
|
307
|
+
* annotated non-read-only and non-idempotent. `dependencies` lists what the
|
|
308
|
+
* handler yields: `Git` (worktree containment), `ToolDiscovery` (the Biome
|
|
309
|
+
* binary) and core's `ChildProcessSpawner` (`Run.collect`).
|
|
296
310
|
*/
|
|
297
|
-
const biomeCheckTool = Tool.make("biome_check", {
|
|
311
|
+
const biomeCheckTool = Tool$1.make("biome_check", {
|
|
298
312
|
description: "Run Biome over a path and get structured diagnostics back. mode=check (default; lint + format + organize-imports) or mode=lint. Set write=true to apply safe fixes (--write), unsafe=true for unsafe fixes (--write --unsafe). Severities match the project's Biome config (what `biome check` reports); set strict=true to surface project warnings as errors, each marked with its originalSeverity. Prefer this over shelling out to biome; the LSP already covers files you've edited. Returns a typed object in structuredContent (content[] carries the same object as JSON). NOTE: with write/unsafe this tool MUTATES files (git-reversible).",
|
|
299
313
|
parameters: BiomeCheckParams,
|
|
300
314
|
success: BiomeCheckResult,
|
|
301
|
-
failure: McpToolError
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const isMcpToolError = Schema.is(McpToolError);
|
|
315
|
+
failure: McpToolError,
|
|
316
|
+
dependencies: [
|
|
317
|
+
Git,
|
|
318
|
+
ToolDiscovery,
|
|
319
|
+
ChildProcessSpawner.ChildProcessSpawner
|
|
320
|
+
]
|
|
321
|
+
}).annotate(Tool$1.Title, "Biome check").annotate(Tool$1.Readonly, false).annotate(Tool$1.Destructive, false).annotate(Tool$1.Idempotent, false).annotate(Tool$1.OpenWorld, false);
|
|
309
322
|
/**
|
|
310
|
-
* Wire handler: {@link runBiomeCheck}
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
323
|
+
* Wire handler: {@link runBiomeCheck} with its error channel closed over
|
|
324
|
+
* {@link McpToolError}. A defect (a bug in the parser, say) is reported as
|
|
325
|
+
* {@link BiomeFailed} rather than reaching core, which would scrub it to a
|
|
326
|
+
* generic internal-error text.
|
|
327
|
+
*
|
|
328
|
+
* @param fallbackCwd - the server's project directory
|
|
329
|
+
* @param lane - the server's one write lane, built once in `ToolsLayer`
|
|
315
330
|
*/
|
|
316
|
-
const handleBiomeCheck = (fallbackCwd, params) => Effect.
|
|
317
|
-
try: () => runBiomeCheck(params, fallbackCwd),
|
|
318
|
-
catch: (cause) => isMcpToolError(cause) ? cause : biomeFailed(`Biome check failed: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
319
|
-
});
|
|
331
|
+
const handleBiomeCheck = (fallbackCwd, params, lane) => runBiomeCheck(params, fallbackCwd, lane).pipe(Effect.catchDefect((defect) => Effect.fail(biomeFailed(`Biome check failed: ${defect instanceof Error ? defect.message : String(defect)}`))));
|
|
320
332
|
|
|
321
333
|
export { BiomeCheckParams, BiomeCheckResult, BiomeDiagnostic, BiomeSeverity, biomeCheckTool, buildBiomeResult, handleBiomeCheck, parseBiomeGitlab, resolveContainmentRoot, runBiomeCheck };
|